package main import ( "encoding/base64" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "strconv" "strings" "testing" ) func newTestClient(t *testing.T, h http.HandlerFunc) (*Client, *httptest.Server) { t.Helper() srv := httptest.NewServer(h) t.Cleanup(srv.Close) c := &Client{ BaseURL: srv.URL, Key: "test-key", HTTP: srv.Client(), } return c, srv } func TestBasicAuthHeader(t *testing.T) { want := "Basic " + base64.StdEncoding.EncodeToString([]byte("test-key:")) var got string c, _ := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { got = r.Header.Get("Authorization") w.WriteHeader(200) fmt.Fprint(w, `{}`) }) if err := c.get(apiPrefix+"/accounts/1", nil, nil); err != nil { t.Fatalf("get: %v", err) } if got != want { t.Errorf("Authorization header:\n got=%q\nwant=%q", got, want) } } func TestGetErrorFormatting(t *testing.T) { c, _ := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(401) fmt.Fprint(w, `forbidden`) }) err := c.get(apiPrefix+"/accounts/1", nil, nil) if err == nil { t.Fatal("expected error") } msg := err.Error() for _, want := range []string{"GET ", "401", "forbidden", apiPrefix + "/accounts/1"} { if !strings.Contains(msg, want) { t.Errorf("error %q missing %q", msg, want) } } } // TestPathIDEncoding verifies IDs containing '/' are encoded per spec // (e.g. john/doe -> john%2Fdoe) and the server sees the encoded form. func TestPathIDEncoding(t *testing.T) { var sawPath string c, _ := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { sawPath = r.URL.EscapedPath() w.WriteHeader(200) fmt.Fprint(w, `{"id":"john/doe","name":"x"}`) }) path := apiPrefix + "/accounts/" + pathID("john/doe") if err := c.get(path, nil, nil); err != nil { t.Fatalf("get: %v", err) } want := apiPrefix + "/accounts/john%2Fdoe" if sawPath != want { t.Errorf("server saw path %q, want %q", sawPath, want) } } // TestListAllPaginates walks 3 pages and concatenates rows. func TestListAllPaginates(t *testing.T) { c, _ := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { q, _ := url.ParseQuery(r.URL.RawQuery) page, _ := strconv.Atoi(q.Get("page")) if page == 0 { page = 1 } // 3 pages, 2 items each. const pageCount = 3 if page > pageCount { fmt.Fprintf(w, `{"data":[],"paginator":{"page":%d,"page_count":%d,"limit":2,"total_count":6}}`, page, pageCount) return } a := (page-1)*2 + 1 b := a + 1 fmt.Fprintf(w, `{"data":[{"id":"%d","name":"n%d"},{"id":"%d","name":"n%d"}],"paginator":{"page":%d,"page_count":%d,"limit":2,"total_count":6}}`, a, a, b, b, page, pageCount) }) items, p, err := c.listAll("/accounts", 0, decodeListPage) if err != nil { t.Fatalf("listAll: %v", err) } if len(items) != 6 { t.Errorf("got %d items, want 6", len(items)) } // Verify last item is what we expect and ordering is preserved. var last Account if err := json.Unmarshal(items[5], &last); err != nil { t.Fatalf("decode last: %v", err) } if last.ID != "6" || last.Name != "n6" { t.Errorf("last item = %+v, want id=6 name=n6", last) } if p.TotalCount != 6 { t.Errorf("synthetic paginator total_count = %d, want 6", p.TotalCount) } } func TestListAllStopsOnEmptyPage(t *testing.T) { calls := 0 c, _ := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { calls++ // page_count=0 means we should stop immediately on empty data. fmt.Fprint(w, `{"data":[],"paginator":{"page":1,"page_count":0,"limit":0,"total_count":0}}`) }) items, _, err := c.listAll("/accounts", 0, decodeListPage) if err != nil { t.Fatalf("listAll: %v", err) } if len(items) != 0 { t.Errorf("got %d items, want 0", len(items)) } if calls != 1 { t.Errorf("hit server %d times, want 1", calls) } } func TestValidateLimit(t *testing.T) { cases := []struct { limit int wantErr bool }{ {0, false}, {100, false}, {500, false}, {1000, false}, {99, true}, {1001, true}, {-1, true}, } for _, tc := range cases { err := validateLimit(tc.limit) if (err != nil) != tc.wantErr { t.Errorf("validateLimit(%d) err=%v, wantErr=%v", tc.limit, err, tc.wantErr) } } }