164 lines
4.2 KiB
Go
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)
|
||
|
|
}
|
||
|
|
}
|