package wikijs import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" ) func newTestClient(t *testing.T, handler http.HandlerFunc) *Client { t.Helper() srv := httptest.NewServer(handler) t.Cleanup(srv.Close) client, err := New(srv.URL, "test-key", WithUserAgent("wikijs-mcp/test")) if err != nil { t.Fatalf("New() error = %v", err) } return client } func decodeRequest(t *testing.T, r *http.Request) graphqlRequest { t.Helper() var req graphqlRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Errorf("decode request: %v", err) } return req } func writeJSON(t *testing.T, w http.ResponseWriter, status int, body string) { t.Helper() w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if _, err := w.Write([]byte(body)); err != nil { t.Errorf("write response: %v", err) } } func TestNewNormalizesURL(t *testing.T) { tests := []struct { base string wantBase string wantEndpoint string }{ {base: "https://wiki.example.com", wantBase: "https://wiki.example.com", wantEndpoint: "https://wiki.example.com/graphql"}, {base: "https://wiki.example.com/", wantBase: "https://wiki.example.com", wantEndpoint: "https://wiki.example.com/graphql"}, {base: "https://wiki.example.com/graphql", wantBase: "https://wiki.example.com", wantEndpoint: "https://wiki.example.com/graphql"}, {base: "https://wiki.example.com/graphql/", wantBase: "https://wiki.example.com", wantEndpoint: "https://wiki.example.com/graphql"}, {base: "https://wiki.example.com/wiki", wantBase: "https://wiki.example.com/wiki", wantEndpoint: "https://wiki.example.com/wiki/graphql"}, } for _, tt := range tests { t.Run(tt.base, func(t *testing.T) { c, err := New(tt.base, "key") if err != nil { t.Fatalf("New() error = %v", err) } if c.baseURL != tt.wantBase { t.Errorf("baseURL = %q, want %q", c.baseURL, tt.wantBase) } if c.endpoint != tt.wantEndpoint { t.Errorf("endpoint = %q, want %q", c.endpoint, tt.wantEndpoint) } }) } } func TestNewRejectsInvalidURL(t *testing.T) { for _, raw := range []string{"", "wiki.example.com", "ftp://wiki.example.com", "/relative", "https://wiki.example.com/?a=1", "https://wiki.example.com/#x"} { if _, err := New(raw, "key"); err == nil { t.Errorf("New(%q) error = nil, want error", raw) } } } func TestPageByID(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { if got, want := r.Header.Get("Authorization"), "Bearer test-key"; got != want { t.Errorf("Authorization = %q, want %q", got, want) } if got, want := r.Header.Get("User-Agent"), "wikijs-mcp/test"; got != want { t.Errorf("User-Agent = %q, want %q", got, want) } if got, want := r.Header.Get("Content-Type"), "application/json"; got != want { t.Errorf("Content-Type = %q, want %q", got, want) } req := decodeRequest(t, r) if !strings.Contains(req.Query, "single(id: $id)") { t.Errorf("query does not contain single query: %s", req.Query) } if got := req.Variables["id"]; got != float64(42) { t.Errorf("variables[id] = %v, want 42", got) } writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"single":{ "id":42,"path":"/dev/go","title":"Go","description":"desc", "tags":[{"tag":"go","title":"Go"}],"content":"body", "createdAt":"2025-01-01T00:00:00Z","updatedAt":"2025-01-02T00:00:00Z", "locale":"de"}}}}`) }) page, err := client.PageByID(context.Background(), 42) if err != nil { t.Fatalf("PageByID() error = %v", err) } if page == nil { t.Fatal("PageByID() = nil, want page") } if page.ID != 42 || page.Title != "Go" { t.Errorf("page = %+v", page) } if got, want := page.URL, client.baseURL+"/de/dev/go"; got != want { t.Errorf("URL = %q, want %q", got, want) } if len(page.Tags) != 1 || page.Tags[0].Tag != "go" { t.Errorf("Tags = %+v", page.Tags) } } func TestPageByIDEnglishHasNoLocalePrefix(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"single":{ "id":1,"path":"/start","title":"Start","description":"", "tags":null,"content":"","createdAt":"","updatedAt":"","locale":"en"}}}}`) }) page, err := client.PageByID(context.Background(), 1) if err != nil { t.Fatalf("PageByID() error = %v", err) } if got, want := page.URL, client.baseURL+"/start"; got != want { t.Errorf("URL = %q, want %q", got, want) } if page.Tags == nil { t.Error("Tags = nil, want empty slice") } } func TestPageByIDNotFound(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"single":null}}}`) }) page, err := client.PageByID(context.Background(), 99) if err != nil { t.Fatalf("PageByID() error = %v", err) } if page != nil { t.Errorf("PageByID() = %+v, want nil", page) } } func TestPageByPath(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { req := decodeRequest(t, r) if !strings.Contains(req.Query, "singleByPath") { t.Errorf("query = %s", req.Query) } if got := req.Variables["path"]; got != "/start" { t.Errorf("variables[path] = %v", got) } if got := req.Variables["locale"]; got != "de-DE" { t.Errorf("variables[locale] = %v", got) } writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"singleByPath":{ "id":7,"path":"/start","title":"Start","description":"desc", "tags":[],"content":"body","createdAt":"c","updatedAt":"u","locale":"de-DE"}}}}`) }) page, err := client.PageByPath(context.Background(), "/start", "de-DE") if err != nil { t.Fatalf("PageByPath() error = %v", err) } if got, want := page.URL, client.baseURL+"/de-DE/start"; got != want { t.Errorf("URL = %q, want %q", got, want) } } func TestSearchPages(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { req := decodeRequest(t, r) if got := req.Variables["query"]; got != "mcp" { t.Errorf("variables[query] = %v", got) } if got := req.Variables["path"]; got != "/dev" { t.Errorf("variables[path] = %v", got) } if _, ok := req.Variables["locale"]; ok { t.Errorf("variables[locale] = %v, want omitted", req.Variables["locale"]) } writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"search":{ "results":[ {"title":"A","description":"a","path":"/a","locale":"en"}, null, {"title":"B","description":"b","path":"/b","locale":"de"} ], "suggestions":["s1"],"totalHits":3}}}}`) }) res, err := client.SearchPages(context.Background(), "mcp", "/dev", "") if err != nil { t.Fatalf("SearchPages() error = %v", err) } if got, want := res.TotalHits, 3; got != want { t.Errorf("TotalHits = %d, want %d", got, want) } if len(res.Results) != 2 { t.Fatalf("Results = %d, want 2", len(res.Results)) } if got, want := res.Results[0].URL, client.baseURL+"/a"; got != want { t.Errorf("URL = %q, want %q", got, want) } if got, want := res.Results[1].URL, client.baseURL+"/de/b"; got != want { t.Errorf("URL = %q, want %q", got, want) } if len(res.Suggestions) != 1 || res.Suggestions[0] != "s1" { t.Errorf("Suggestions = %v", res.Suggestions) } } func TestSearchPagesEmpty(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"search":null}}}`) }) res, err := client.SearchPages(context.Background(), "nothing", "", "") if err != nil { t.Fatalf("SearchPages() error = %v", err) } if res.Results == nil || res.Suggestions == nil { t.Errorf("empty slices must not be nil: %+v", res) } if res.TotalHits != 0 { t.Errorf("TotalHits = %d, want 0", res.TotalHits) } } func TestListPages(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { req := decodeRequest(t, r) if got := req.Variables["limit"]; got != float64(10) { t.Errorf("variables[limit] = %v", got) } if got := req.Variables["locale"]; got != "de" { t.Errorf("variables[locale] = %v", got) } tags, ok := req.Variables["tags"].([]any) if !ok || len(tags) != 2 || tags[0] != "go" { t.Errorf("variables[tags] = %v", req.Variables["tags"]) } writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"list":[ {"id":1,"path":"/a","locale":"de","title":"A","description":"a", "createdAt":"c","updatedAt":"u","tags":["go"]}, null, {"id":2,"path":"/b","locale":"en","title":"B","description":"b", "createdAt":"c","updatedAt":"u","tags":null} ]}}}`) }) limit := 10 pages, err := client.ListPages(context.Background(), ListOptions{ Limit: &limit, Locale: "de", Tags: []string{"go", "mcp"}, }) if err != nil { t.Fatalf("ListPages() error = %v", err) } if len(pages) != 2 { t.Fatalf("ListPages() returned %d pages, want 2", len(pages)) } if got, want := pages[0].URL, client.baseURL+"/de/a"; got != want { t.Errorf("URL = %q, want %q", got, want) } if pages[1].Tags == nil { t.Error("Tags = nil, want empty slice") } } func TestListPagesOmitsEmptyFilters(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { req := decodeRequest(t, r) for _, key := range []string{"limit", "locale", "tags"} { if _, ok := req.Variables[key]; ok { t.Errorf("variables[%s] present, want omitted", key) } } writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"list":[]}}}`) }) if _, err := client.ListPages(context.Background(), ListOptions{}); err != nil { t.Fatalf("ListPages() error = %v", err) } } func TestCreatePage(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { req := decodeRequest(t, r) if !strings.Contains(req.Query, "mutation CreatePage") { t.Errorf("query = %s", req.Query) } if got := req.Variables["tags"].([]any); len(got) != 1 || got[0] != "go" { t.Errorf("variables[tags] = %v", req.Variables["tags"]) } if got := req.Variables["isPublished"]; got != true { t.Errorf("variables[isPublished] = %v", got) } writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"create":{"responseResult":{ "succeeded":true,"errorCode":0,"slug":"new-page","message":"Page created"}}}}}`) }) res, err := client.CreatePage(context.Background(), CreatePageInput{ Title: "New", Content: "body", Description: "desc", Path: "new-page", Editor: "markdown", IsPublished: true, Locale: "en", Tags: []string{"go"}, }) if err != nil { t.Fatalf("CreatePage() error = %v", err) } if !res.Succeeded || res.Message != "Page created" || res.Slug != "new-page" { t.Errorf("result = %+v", res) } } func TestUpdatePageFailureIsReturned(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"update":{"responseResult":{ "succeeded":false,"errorCode":6001,"slug":"","message":"Page not found"}}}}}`) }) res, err := client.UpdatePage(context.Background(), UpdatePageInput{ID: 1}) if err != nil { t.Fatalf("UpdatePage() error = %v", err) } if res.Succeeded { t.Error("Succeeded = true, want false") } if res.ErrorCode != 6001 || res.Message != "Page not found" { t.Errorf("result = %+v", res) } } func TestGraphQLErrors(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, http.StatusOK, `{"errors":[{"message":"Unauthorized"},{"message":"Forbidden"}]}`) }) _, err := client.PageByID(context.Background(), 1) if err == nil { t.Fatal("PageByID() error = nil, want error") } var apiErr *APIError if !errors.As(err, &apiErr) { t.Fatalf("error type = %T, want *APIError", err) } if got := apiErr.Error(); !strings.Contains(got, "Unauthorized") || !strings.Contains(got, "Forbidden") { t.Errorf("error = %q", got) } } func TestHTTPError(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, "server exploded", http.StatusInternalServerError) }) _, err := client.PageByID(context.Background(), 1) if err == nil { t.Fatal("PageByID() error = nil, want error") } var apiErr *APIError if !errors.As(err, &apiErr) { t.Fatalf("error type = %T, want *APIError", err) } if apiErr.Status != http.StatusInternalServerError { t.Errorf("Status = %d, want 500", apiErr.Status) } if !strings.Contains(err.Error(), "server exploded") { t.Errorf("error = %q", err) } } func TestMissingData(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, http.StatusOK, `{"data":null}`) }) _, err := client.PageByID(context.Background(), 1) if err == nil { t.Fatal("PageByID() error = nil, want error") } } func TestContextCancellation(t *testing.T) { client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { t.Error("handler must not be called") }) ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := client.PageByID(ctx, 1); err == nil { t.Fatal("PageByID() error = nil, want context error") } }