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