2026-02-21 15:22:50 +00:00
|
|
|
package forge
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-04-02 07:46:05 +00:00
|
|
|
"fmt"
|
2026-03-26 18:00:20 +00:00
|
|
|
json "github.com/goccy/go-json"
|
2026-02-21 15:22:50 +00:00
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"testing"
|
2026-03-26 13:27:06 +00:00
|
|
|
|
|
|
|
|
core "dappco.re/go/core"
|
2026-02-21 15:22:50 +00:00
|
|
|
)
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestClient_Get_Good(t *testing.T) {
|
2026-02-21 15:22:50 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodGet {
|
|
|
|
|
t.Errorf("expected GET, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if r.Header.Get("Authorization") != "token test-token" {
|
|
|
|
|
t.Errorf("missing auth header")
|
|
|
|
|
}
|
|
|
|
|
if r.URL.Path != "/api/v1/user" {
|
|
|
|
|
t.Errorf("wrong path: %s", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{"login": "virgil"})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
c := NewClient(srv.URL, "test-token")
|
|
|
|
|
var out map[string]string
|
|
|
|
|
err := c.Get(context.Background(), "/api/v1/user", &out)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if out["login"] != "virgil" {
|
|
|
|
|
t.Errorf("got login=%q", out["login"])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestClient_Post_Good(t *testing.T) {
|
2026-02-21 15:22:50 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodPost {
|
|
|
|
|
t.Errorf("expected POST, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
var body map[string]string
|
|
|
|
|
json.NewDecoder(r.Body).Decode(&body)
|
|
|
|
|
if body["name"] != "test-repo" {
|
|
|
|
|
t.Errorf("wrong body: %v", body)
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
|
|
|
json.NewEncoder(w).Encode(map[string]any{"id": 1, "name": "test-repo"})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
c := NewClient(srv.URL, "test-token")
|
|
|
|
|
body := map[string]string{"name": "test-repo"}
|
|
|
|
|
var out map[string]any
|
|
|
|
|
err := c.Post(context.Background(), "/api/v1/orgs/core/repos", body, &out)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if out["name"] != "test-repo" {
|
|
|
|
|
t.Errorf("got name=%v", out["name"])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 06:51:49 +00:00
|
|
|
func TestClient_PostRaw_Good(t *testing.T) {
|
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodPost {
|
|
|
|
|
t.Errorf("expected POST, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Path; got != "/api/v1/markdown" {
|
|
|
|
|
t.Errorf("wrong path: %s", got)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("X-RateLimit-Limit", "100")
|
|
|
|
|
w.Header().Set("X-RateLimit-Remaining", "98")
|
|
|
|
|
w.Header().Set("X-RateLimit-Reset", "1700000001")
|
|
|
|
|
w.Write([]byte("<p>Hello</p>"))
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
c := NewClient(srv.URL, "test-token")
|
|
|
|
|
body := map[string]string{"text": "Hello"}
|
|
|
|
|
got, err := c.PostRaw(context.Background(), "/api/v1/markdown", body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if string(got) != "<p>Hello</p>" {
|
|
|
|
|
t.Errorf("got body=%q", string(got))
|
|
|
|
|
}
|
|
|
|
|
rl := c.RateLimit()
|
|
|
|
|
if rl.Limit != 100 || rl.Remaining != 98 || rl.Reset != 1700000001 {
|
|
|
|
|
t.Fatalf("unexpected rate limit: %+v", rl)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestClient_Delete_Good(t *testing.T) {
|
2026-02-21 15:22:50 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodDelete {
|
|
|
|
|
t.Errorf("expected DELETE, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
c := NewClient(srv.URL, "test-token")
|
|
|
|
|
err := c.Delete(context.Background(), "/api/v1/repos/core/test")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 06:51:49 +00:00
|
|
|
func TestClient_GetRaw_Good(t *testing.T) {
|
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodGet {
|
|
|
|
|
t.Errorf("expected GET, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Path; got != "/api/v1/signing-key.gpg" {
|
|
|
|
|
t.Errorf("wrong path: %s", got)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("X-RateLimit-Limit", "60")
|
|
|
|
|
w.Header().Set("X-RateLimit-Remaining", "59")
|
|
|
|
|
w.Header().Set("X-RateLimit-Reset", "1700000002")
|
|
|
|
|
w.Write([]byte("key-data"))
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
c := NewClient(srv.URL, "test-token")
|
|
|
|
|
got, err := c.GetRaw(context.Background(), "/api/v1/signing-key.gpg")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if string(got) != "key-data" {
|
|
|
|
|
t.Errorf("got body=%q", string(got))
|
|
|
|
|
}
|
|
|
|
|
rl := c.RateLimit()
|
|
|
|
|
if rl.Limit != 60 || rl.Remaining != 59 || rl.Reset != 1700000002 {
|
|
|
|
|
t.Fatalf("unexpected rate limit: %+v", rl)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestClient_ServerError_Bad(t *testing.T) {
|
2026-02-21 15:22:50 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{"message": "internal error"})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
c := NewClient(srv.URL, "test-token")
|
|
|
|
|
err := c.Get(context.Background(), "/api/v1/user", nil)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected error")
|
|
|
|
|
}
|
|
|
|
|
var apiErr *APIError
|
2026-03-26 13:27:06 +00:00
|
|
|
if !core.As(err, &apiErr) {
|
2026-02-21 15:22:50 +00:00
|
|
|
t.Fatalf("expected APIError, got %T", err)
|
|
|
|
|
}
|
|
|
|
|
if apiErr.StatusCode != 500 {
|
|
|
|
|
t.Errorf("got status=%d", apiErr.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestClient_NotFound_Bad(t *testing.T) {
|
2026-02-21 15:22:50 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{"message": "not found"})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
c := NewClient(srv.URL, "test-token")
|
|
|
|
|
err := c.Get(context.Background(), "/api/v1/repos/x/y", nil)
|
|
|
|
|
if !IsNotFound(err) {
|
|
|
|
|
t.Fatalf("expected not found, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestClient_ContextCancellation_Good(t *testing.T) {
|
2026-02-21 15:22:50 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
<-r.Context().Done()
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
c := NewClient(srv.URL, "test-token")
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
cancel() // cancel immediately
|
|
|
|
|
err := c.Get(ctx, "/api/v1/user", nil)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected error from cancelled context")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestClient_Options_Good(t *testing.T) {
|
2026-02-21 15:22:50 +00:00
|
|
|
c := NewClient("https://forge.lthn.ai", "tok",
|
|
|
|
|
WithUserAgent("go-forge/1.0"),
|
|
|
|
|
)
|
|
|
|
|
if c.userAgent != "go-forge/1.0" {
|
|
|
|
|
t.Errorf("got user agent=%q", c.userAgent)
|
|
|
|
|
}
|
2026-04-02 07:29:00 +00:00
|
|
|
if got := c.UserAgent(); got != "go-forge/1.0" {
|
|
|
|
|
t.Errorf("got UserAgent()=%q", got)
|
|
|
|
|
}
|
2026-02-21 15:22:50 +00:00
|
|
|
}
|
fix(dx): update CLAUDE.md and add tests for untested critical paths
- Fix CLAUDE.md: "zero dependencies" → documents go-io/go-log deps
- Add coding standards for coreerr.E() and go-io usage
- Add tests for APIError.Error(), IsConflict, IsForbidden, WithHTTPClient,
RateLimit, Forge.Client(), Resource.Iter() (happy, error, early-break)
- Coverage: 64.5% → 66.8% (+2.3%)
- No fmt.Errorf, errors.New, or os.ReadFile/WriteFile violations found
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 08:11:30 +00:00
|
|
|
|
2026-04-02 07:31:56 +00:00
|
|
|
func TestClient_HasToken_Good(t *testing.T) {
|
|
|
|
|
c := NewClient("https://forge.lthn.ai", "tok")
|
|
|
|
|
if !c.HasToken() {
|
|
|
|
|
t.Fatal("expected HasToken to report configured token")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestClient_HasToken_Bad(t *testing.T) {
|
|
|
|
|
c := NewClient("https://forge.lthn.ai", "")
|
|
|
|
|
if c.HasToken() {
|
|
|
|
|
t.Fatal("expected HasToken to report missing token")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 09:00:15 +00:00
|
|
|
func TestClient_NilSafeAccessors(t *testing.T) {
|
|
|
|
|
var c *Client
|
|
|
|
|
if got := c.BaseURL(); got != "" {
|
|
|
|
|
t.Fatalf("got BaseURL()=%q, want empty string", got)
|
|
|
|
|
}
|
|
|
|
|
if got := c.RateLimit(); got != (RateLimit{}) {
|
|
|
|
|
t.Fatalf("got RateLimit()=%#v, want zero value", got)
|
|
|
|
|
}
|
|
|
|
|
if got := c.UserAgent(); got != "" {
|
|
|
|
|
t.Fatalf("got UserAgent()=%q, want empty string", got)
|
|
|
|
|
}
|
|
|
|
|
if got := c.HTTPClient(); got != nil {
|
|
|
|
|
t.Fatal("expected HTTPClient() to return nil")
|
|
|
|
|
}
|
|
|
|
|
if got := c.HasToken(); got {
|
|
|
|
|
t.Fatal("expected HasToken() to report false")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestClient_WithHTTPClient_Good(t *testing.T) {
|
fix(dx): update CLAUDE.md and add tests for untested critical paths
- Fix CLAUDE.md: "zero dependencies" → documents go-io/go-log deps
- Add coding standards for coreerr.E() and go-io usage
- Add tests for APIError.Error(), IsConflict, IsForbidden, WithHTTPClient,
RateLimit, Forge.Client(), Resource.Iter() (happy, error, early-break)
- Coverage: 64.5% → 66.8% (+2.3%)
- No fmt.Errorf, errors.New, or os.ReadFile/WriteFile violations found
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 08:11:30 +00:00
|
|
|
custom := &http.Client{}
|
|
|
|
|
c := NewClient("https://forge.lthn.ai", "tok", WithHTTPClient(custom))
|
|
|
|
|
if c.httpClient != custom {
|
|
|
|
|
t.Error("expected custom HTTP client to be set")
|
|
|
|
|
}
|
2026-04-02 07:35:37 +00:00
|
|
|
if got := c.HTTPClient(); got != custom {
|
|
|
|
|
t.Error("expected HTTPClient() to return the configured HTTP client")
|
|
|
|
|
}
|
fix(dx): update CLAUDE.md and add tests for untested critical paths
- Fix CLAUDE.md: "zero dependencies" → documents go-io/go-log deps
- Add coding standards for coreerr.E() and go-io usage
- Add tests for APIError.Error(), IsConflict, IsForbidden, WithHTTPClient,
RateLimit, Forge.Client(), Resource.Iter() (happy, error, early-break)
- Coverage: 64.5% → 66.8% (+2.3%)
- No fmt.Errorf, errors.New, or os.ReadFile/WriteFile violations found
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 08:11:30 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-02 07:46:05 +00:00
|
|
|
func TestClient_String_Good(t *testing.T) {
|
|
|
|
|
c := NewClient("https://forge.lthn.ai", "tok", WithUserAgent("go-forge/1.0"))
|
|
|
|
|
got := fmt.Sprint(c)
|
|
|
|
|
want := `forge.Client{baseURL="https://forge.lthn.ai", token=set, userAgent="go-forge/1.0"}`
|
|
|
|
|
if got != want {
|
|
|
|
|
t.Fatalf("got %q, want %q", got, want)
|
|
|
|
|
}
|
|
|
|
|
if got := c.String(); got != want {
|
|
|
|
|
t.Fatalf("got String()=%q, want %q", got, want)
|
|
|
|
|
}
|
2026-04-02 07:48:37 +00:00
|
|
|
if got := fmt.Sprintf("%#v", c); got != want {
|
|
|
|
|
t.Fatalf("got GoString=%q, want %q", got, want)
|
|
|
|
|
}
|
2026-04-02 07:46:05 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestAPIError_Error_Good(t *testing.T) {
|
fix(dx): update CLAUDE.md and add tests for untested critical paths
- Fix CLAUDE.md: "zero dependencies" → documents go-io/go-log deps
- Add coding standards for coreerr.E() and go-io usage
- Add tests for APIError.Error(), IsConflict, IsForbidden, WithHTTPClient,
RateLimit, Forge.Client(), Resource.Iter() (happy, error, early-break)
- Coverage: 64.5% → 66.8% (+2.3%)
- No fmt.Errorf, errors.New, or os.ReadFile/WriteFile violations found
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 08:11:30 +00:00
|
|
|
e := &APIError{StatusCode: 404, Message: "not found", URL: "/api/v1/repos/x/y"}
|
|
|
|
|
got := e.Error()
|
|
|
|
|
want := "forge: /api/v1/repos/x/y 404: not found"
|
|
|
|
|
if got != want {
|
|
|
|
|
t.Errorf("got %q, want %q", got, want)
|
|
|
|
|
}
|
2026-04-02 08:04:08 +00:00
|
|
|
if got := e.String(); got != want {
|
|
|
|
|
t.Errorf("got String()=%q, want %q", got, want)
|
|
|
|
|
}
|
|
|
|
|
if got := fmt.Sprint(e); got != want {
|
|
|
|
|
t.Errorf("got fmt.Sprint=%q, want %q", got, want)
|
|
|
|
|
}
|
|
|
|
|
if got := fmt.Sprintf("%#v", e); got != want {
|
|
|
|
|
t.Errorf("got GoString=%q, want %q", got, want)
|
|
|
|
|
}
|
fix(dx): update CLAUDE.md and add tests for untested critical paths
- Fix CLAUDE.md: "zero dependencies" → documents go-io/go-log deps
- Add coding standards for coreerr.E() and go-io usage
- Add tests for APIError.Error(), IsConflict, IsForbidden, WithHTTPClient,
RateLimit, Forge.Client(), Resource.Iter() (happy, error, early-break)
- Coverage: 64.5% → 66.8% (+2.3%)
- No fmt.Errorf, errors.New, or os.ReadFile/WriteFile violations found
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 08:11:30 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestIsConflict_Match_Good(t *testing.T) {
|
fix(dx): update CLAUDE.md and add tests for untested critical paths
- Fix CLAUDE.md: "zero dependencies" → documents go-io/go-log deps
- Add coding standards for coreerr.E() and go-io usage
- Add tests for APIError.Error(), IsConflict, IsForbidden, WithHTTPClient,
RateLimit, Forge.Client(), Resource.Iter() (happy, error, early-break)
- Coverage: 64.5% → 66.8% (+2.3%)
- No fmt.Errorf, errors.New, or os.ReadFile/WriteFile violations found
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 08:11:30 +00:00
|
|
|
err := &APIError{StatusCode: http.StatusConflict, Message: "conflict", URL: "/test"}
|
|
|
|
|
if !IsConflict(err) {
|
|
|
|
|
t.Error("expected IsConflict to return true for 409")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestIsConflict_NotConflict_Bad(t *testing.T) {
|
fix(dx): update CLAUDE.md and add tests for untested critical paths
- Fix CLAUDE.md: "zero dependencies" → documents go-io/go-log deps
- Add coding standards for coreerr.E() and go-io usage
- Add tests for APIError.Error(), IsConflict, IsForbidden, WithHTTPClient,
RateLimit, Forge.Client(), Resource.Iter() (happy, error, early-break)
- Coverage: 64.5% → 66.8% (+2.3%)
- No fmt.Errorf, errors.New, or os.ReadFile/WriteFile violations found
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 08:11:30 +00:00
|
|
|
err := &APIError{StatusCode: http.StatusNotFound, Message: "not found", URL: "/test"}
|
|
|
|
|
if IsConflict(err) {
|
|
|
|
|
t.Error("expected IsConflict to return false for 404")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestIsForbidden_NotForbidden_Bad(t *testing.T) {
|
fix(dx): update CLAUDE.md and add tests for untested critical paths
- Fix CLAUDE.md: "zero dependencies" → documents go-io/go-log deps
- Add coding standards for coreerr.E() and go-io usage
- Add tests for APIError.Error(), IsConflict, IsForbidden, WithHTTPClient,
RateLimit, Forge.Client(), Resource.Iter() (happy, error, early-break)
- Coverage: 64.5% → 66.8% (+2.3%)
- No fmt.Errorf, errors.New, or os.ReadFile/WriteFile violations found
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 08:11:30 +00:00
|
|
|
err := &APIError{StatusCode: http.StatusNotFound, Message: "not found", URL: "/test"}
|
|
|
|
|
if IsForbidden(err) {
|
|
|
|
|
t.Error("expected IsForbidden to return false for 404")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestClient_RateLimit_Good(t *testing.T) {
|
fix(dx): update CLAUDE.md and add tests for untested critical paths
- Fix CLAUDE.md: "zero dependencies" → documents go-io/go-log deps
- Add coding standards for coreerr.E() and go-io usage
- Add tests for APIError.Error(), IsConflict, IsForbidden, WithHTTPClient,
RateLimit, Forge.Client(), Resource.Iter() (happy, error, early-break)
- Coverage: 64.5% → 66.8% (+2.3%)
- No fmt.Errorf, errors.New, or os.ReadFile/WriteFile violations found
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 08:11:30 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("X-RateLimit-Limit", "100")
|
|
|
|
|
w.Header().Set("X-RateLimit-Remaining", "99")
|
|
|
|
|
w.Header().Set("X-RateLimit-Reset", "1700000000")
|
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{"login": "test"})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
c := NewClient(srv.URL, "tok")
|
|
|
|
|
var out map[string]string
|
|
|
|
|
if err := c.Get(context.Background(), "/api/v1/user", &out); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rl := c.RateLimit()
|
|
|
|
|
if rl.Limit != 100 {
|
|
|
|
|
t.Errorf("got limit=%d, want 100", rl.Limit)
|
|
|
|
|
}
|
|
|
|
|
if rl.Remaining != 99 {
|
|
|
|
|
t.Errorf("got remaining=%d, want 99", rl.Remaining)
|
|
|
|
|
}
|
|
|
|
|
if rl.Reset != 1700000000 {
|
|
|
|
|
t.Errorf("got reset=%d, want 1700000000", rl.Reset)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestClient_Forbidden_Bad(t *testing.T) {
|
fix(dx): update CLAUDE.md and add tests for untested critical paths
- Fix CLAUDE.md: "zero dependencies" → documents go-io/go-log deps
- Add coding standards for coreerr.E() and go-io usage
- Add tests for APIError.Error(), IsConflict, IsForbidden, WithHTTPClient,
RateLimit, Forge.Client(), Resource.Iter() (happy, error, early-break)
- Coverage: 64.5% → 66.8% (+2.3%)
- No fmt.Errorf, errors.New, or os.ReadFile/WriteFile violations found
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 08:11:30 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.WriteHeader(http.StatusForbidden)
|
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{"message": "forbidden"})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
c := NewClient(srv.URL, "tok")
|
|
|
|
|
err := c.Get(context.Background(), "/api/v1/admin", nil)
|
|
|
|
|
if !IsForbidden(err) {
|
|
|
|
|
t.Fatalf("expected forbidden, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestClient_Conflict_Bad(t *testing.T) {
|
fix(dx): update CLAUDE.md and add tests for untested critical paths
- Fix CLAUDE.md: "zero dependencies" → documents go-io/go-log deps
- Add coding standards for coreerr.E() and go-io usage
- Add tests for APIError.Error(), IsConflict, IsForbidden, WithHTTPClient,
RateLimit, Forge.Client(), Resource.Iter() (happy, error, early-break)
- Coverage: 64.5% → 66.8% (+2.3%)
- No fmt.Errorf, errors.New, or os.ReadFile/WriteFile violations found
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 08:11:30 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.WriteHeader(http.StatusConflict)
|
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{"message": "already exists"})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
c := NewClient(srv.URL, "tok")
|
|
|
|
|
err := c.Post(context.Background(), "/api/v1/repos", map[string]string{"name": "dup"}, nil)
|
|
|
|
|
if !IsConflict(err) {
|
|
|
|
|
t.Fatalf("expected conflict, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|