package bookstack import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" ) func testClient(t *testing.T, handler http.HandlerFunc) *Client { t.Helper() srv := httptest.NewServer(handler) t.Cleanup(srv.Close) c, err := NewClient(Config{ BaseURL: srv.URL, TokenID: "test-id", TokenSecret: "test-secret", HTTPClient: srv.Client(), }) if err != nil { t.Fatalf("NewClient: %v", err) } return c } func TestDo_AuthHeader(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { got := r.Header.Get("Authorization") want := "Token test-id:test-secret" if got != want { t.Errorf("Authorization = %q, want %q", got, want) } w.WriteHeader(http.StatusOK) }) err := c.do(context.Background(), http.MethodGet, "/api/test", nil, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestDo_GET_JSON(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("method = %s, want GET", r.Method) } if r.Header.Get("Accept") != "application/json" { t.Error("missing Accept: application/json") } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"name": "Test Book"}) }) var result struct { Name string `json:"name"` } err := c.do(context.Background(), http.MethodGet, "/api/books/1", nil, &result) if err != nil { t.Fatalf("unexpected error: %v", err) } if result.Name != "Test Book" { t.Errorf("Name = %q, want %q", result.Name, "Test Book") } } func TestDo_POST_JSON(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("method = %s, want POST", r.Method) } if r.Header.Get("Content-Type") != "application/json" { t.Error("missing Content-Type: application/json") } var body map[string]string json.NewDecoder(r.Body).Decode(&body) if body["name"] != "New Book" { t.Errorf("body name = %q, want %q", body["name"], "New Book") } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]any{"id": 1, "name": "New Book"}) }) reqBody := map[string]string{"name": "New Book"} var result struct { ID int `json:"id"` Name string `json:"name"` } err := c.do(context.Background(), http.MethodPost, "/api/books", reqBody, &result) if err != nil { t.Fatalf("unexpected error: %v", err) } if result.ID != 1 { t.Errorf("ID = %d, want 1", result.ID) } } func TestDo_APIError(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(map[string]any{ "error": map[string]string{ "code": "not_found", "message": "Book not found", }, }) }) err := c.do(context.Background(), http.MethodGet, "/api/books/999", nil, nil) if err == nil { t.Fatal("expected error") } var apiErr *APIError if !errors.As(err, &apiErr) { t.Fatalf("expected APIError, got %T", err) } if apiErr.StatusCode != 404 { t.Errorf("StatusCode = %d, want 404", apiErr.StatusCode) } if apiErr.Code != "not_found" { t.Errorf("Code = %q, want %q", apiErr.Code, "not_found") } if !errors.Is(err, ErrNotFound) { t.Error("expected errors.Is(err, ErrNotFound) to be true") } } func TestDo_ContextCancellation(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) ctx, cancel := context.WithCancel(context.Background()) cancel() err := c.do(ctx, http.MethodGet, "/api/test", nil, nil) if err == nil { t.Fatal("expected error from cancelled context") } } func TestDo_NonJSONError(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("internal error")) }) err := c.do(context.Background(), http.MethodGet, "/api/test", nil, nil) if err == nil { t.Fatal("expected error") } var apiErr *APIError if !errors.As(err, &apiErr) { t.Fatalf("expected APIError, got %T", err) } if apiErr.Message != "Internal Server Error" { t.Errorf("Message = %q, want fallback status text", apiErr.Message) } }