bookstack-api/http_test.go
Oliver Jakoubek 4875540f21 feat(bookstack-api-8ea): implement HTTP helper and request building
Implement do() method on Client with auth header, JSON marshaling,
error parsing into APIError, and context support. Add httptest-based
unit tests.
2026-01-30 09:48:01 +01:00

164 lines
4.2 KiB
Go

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)
}
}