client_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. package wikijs
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "net/http"
  7. "net/http/httptest"
  8. "strings"
  9. "testing"
  10. )
  11. func newTestClient(t *testing.T, handler http.HandlerFunc) *Client {
  12. t.Helper()
  13. srv := httptest.NewServer(handler)
  14. t.Cleanup(srv.Close)
  15. client, err := New(srv.URL, "test-key", WithUserAgent("wikijs-mcp/test"))
  16. if err != nil {
  17. t.Fatalf("New() error = %v", err)
  18. }
  19. return client
  20. }
  21. func decodeRequest(t *testing.T, r *http.Request) graphqlRequest {
  22. t.Helper()
  23. var req graphqlRequest
  24. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  25. t.Errorf("decode request: %v", err)
  26. }
  27. return req
  28. }
  29. func writeJSON(t *testing.T, w http.ResponseWriter, status int, body string) {
  30. t.Helper()
  31. w.Header().Set("Content-Type", "application/json")
  32. w.WriteHeader(status)
  33. if _, err := w.Write([]byte(body)); err != nil {
  34. t.Errorf("write response: %v", err)
  35. }
  36. }
  37. func TestNewNormalizesURL(t *testing.T) {
  38. tests := []struct {
  39. base string
  40. wantBase string
  41. wantEndpoint string
  42. }{
  43. {base: "https://wiki.example.com", wantBase: "https://wiki.example.com", wantEndpoint: "https://wiki.example.com/graphql"},
  44. {base: "https://wiki.example.com/", wantBase: "https://wiki.example.com", wantEndpoint: "https://wiki.example.com/graphql"},
  45. {base: "https://wiki.example.com/graphql", wantBase: "https://wiki.example.com", wantEndpoint: "https://wiki.example.com/graphql"},
  46. {base: "https://wiki.example.com/graphql/", wantBase: "https://wiki.example.com", wantEndpoint: "https://wiki.example.com/graphql"},
  47. {base: "https://wiki.example.com/wiki", wantBase: "https://wiki.example.com/wiki", wantEndpoint: "https://wiki.example.com/wiki/graphql"},
  48. }
  49. for _, tt := range tests {
  50. t.Run(tt.base, func(t *testing.T) {
  51. c, err := New(tt.base, "key")
  52. if err != nil {
  53. t.Fatalf("New() error = %v", err)
  54. }
  55. if c.baseURL != tt.wantBase {
  56. t.Errorf("baseURL = %q, want %q", c.baseURL, tt.wantBase)
  57. }
  58. if c.endpoint != tt.wantEndpoint {
  59. t.Errorf("endpoint = %q, want %q", c.endpoint, tt.wantEndpoint)
  60. }
  61. })
  62. }
  63. }
  64. func TestNewRejectsInvalidURL(t *testing.T) {
  65. for _, raw := range []string{"", "wiki.example.com", "ftp://wiki.example.com", "/relative", "https://wiki.example.com/?a=1", "https://wiki.example.com/#x"} {
  66. if _, err := New(raw, "key"); err == nil {
  67. t.Errorf("New(%q) error = nil, want error", raw)
  68. }
  69. }
  70. }
  71. func TestPageByID(t *testing.T) {
  72. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  73. if got, want := r.Header.Get("Authorization"), "Bearer test-key"; got != want {
  74. t.Errorf("Authorization = %q, want %q", got, want)
  75. }
  76. if got, want := r.Header.Get("User-Agent"), "wikijs-mcp/test"; got != want {
  77. t.Errorf("User-Agent = %q, want %q", got, want)
  78. }
  79. if got, want := r.Header.Get("Content-Type"), "application/json"; got != want {
  80. t.Errorf("Content-Type = %q, want %q", got, want)
  81. }
  82. req := decodeRequest(t, r)
  83. if !strings.Contains(req.Query, "single(id: $id)") {
  84. t.Errorf("query does not contain single query: %s", req.Query)
  85. }
  86. if got := req.Variables["id"]; got != float64(42) {
  87. t.Errorf("variables[id] = %v, want 42", got)
  88. }
  89. writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"single":{
  90. "id":42,"path":"/dev/go","title":"Go","description":"desc",
  91. "tags":[{"tag":"go","title":"Go"}],"content":"body",
  92. "createdAt":"2025-01-01T00:00:00Z","updatedAt":"2025-01-02T00:00:00Z",
  93. "locale":"de"}}}}`)
  94. })
  95. page, err := client.PageByID(context.Background(), 42)
  96. if err != nil {
  97. t.Fatalf("PageByID() error = %v", err)
  98. }
  99. if page == nil {
  100. t.Fatal("PageByID() = nil, want page")
  101. }
  102. if page.ID != 42 || page.Title != "Go" {
  103. t.Errorf("page = %+v", page)
  104. }
  105. if got, want := page.URL, client.baseURL+"/de/dev/go"; got != want {
  106. t.Errorf("URL = %q, want %q", got, want)
  107. }
  108. if len(page.Tags) != 1 || page.Tags[0].Tag != "go" {
  109. t.Errorf("Tags = %+v", page.Tags)
  110. }
  111. }
  112. func TestPageByIDEnglishHasNoLocalePrefix(t *testing.T) {
  113. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  114. writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"single":{
  115. "id":1,"path":"/start","title":"Start","description":"",
  116. "tags":null,"content":"","createdAt":"","updatedAt":"","locale":"en"}}}}`)
  117. })
  118. page, err := client.PageByID(context.Background(), 1)
  119. if err != nil {
  120. t.Fatalf("PageByID() error = %v", err)
  121. }
  122. if got, want := page.URL, client.baseURL+"/start"; got != want {
  123. t.Errorf("URL = %q, want %q", got, want)
  124. }
  125. if page.Tags == nil {
  126. t.Error("Tags = nil, want empty slice")
  127. }
  128. }
  129. func TestPageByIDNotFound(t *testing.T) {
  130. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  131. writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"single":null}}}`)
  132. })
  133. page, err := client.PageByID(context.Background(), 99)
  134. if err != nil {
  135. t.Fatalf("PageByID() error = %v", err)
  136. }
  137. if page != nil {
  138. t.Errorf("PageByID() = %+v, want nil", page)
  139. }
  140. }
  141. func TestPageByPath(t *testing.T) {
  142. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  143. req := decodeRequest(t, r)
  144. if !strings.Contains(req.Query, "singleByPath") {
  145. t.Errorf("query = %s", req.Query)
  146. }
  147. if got := req.Variables["path"]; got != "/start" {
  148. t.Errorf("variables[path] = %v", got)
  149. }
  150. if got := req.Variables["locale"]; got != "de-DE" {
  151. t.Errorf("variables[locale] = %v", got)
  152. }
  153. writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"singleByPath":{
  154. "id":7,"path":"/start","title":"Start","description":"desc",
  155. "tags":[],"content":"body","createdAt":"c","updatedAt":"u","locale":"de-DE"}}}}`)
  156. })
  157. page, err := client.PageByPath(context.Background(), "/start", "de-DE")
  158. if err != nil {
  159. t.Fatalf("PageByPath() error = %v", err)
  160. }
  161. if got, want := page.URL, client.baseURL+"/de-DE/start"; got != want {
  162. t.Errorf("URL = %q, want %q", got, want)
  163. }
  164. }
  165. func TestSearchPages(t *testing.T) {
  166. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  167. req := decodeRequest(t, r)
  168. if got := req.Variables["query"]; got != "mcp" {
  169. t.Errorf("variables[query] = %v", got)
  170. }
  171. if got := req.Variables["path"]; got != "/dev" {
  172. t.Errorf("variables[path] = %v", got)
  173. }
  174. if _, ok := req.Variables["locale"]; ok {
  175. t.Errorf("variables[locale] = %v, want omitted", req.Variables["locale"])
  176. }
  177. writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"search":{
  178. "results":[
  179. {"title":"A","description":"a","path":"/a","locale":"en"},
  180. null,
  181. {"title":"B","description":"b","path":"/b","locale":"de"}
  182. ],
  183. "suggestions":["s1"],"totalHits":3}}}}`)
  184. })
  185. res, err := client.SearchPages(context.Background(), "mcp", "/dev", "")
  186. if err != nil {
  187. t.Fatalf("SearchPages() error = %v", err)
  188. }
  189. if got, want := res.TotalHits, 3; got != want {
  190. t.Errorf("TotalHits = %d, want %d", got, want)
  191. }
  192. if len(res.Results) != 2 {
  193. t.Fatalf("Results = %d, want 2", len(res.Results))
  194. }
  195. if got, want := res.Results[0].URL, client.baseURL+"/a"; got != want {
  196. t.Errorf("URL = %q, want %q", got, want)
  197. }
  198. if got, want := res.Results[1].URL, client.baseURL+"/de/b"; got != want {
  199. t.Errorf("URL = %q, want %q", got, want)
  200. }
  201. if len(res.Suggestions) != 1 || res.Suggestions[0] != "s1" {
  202. t.Errorf("Suggestions = %v", res.Suggestions)
  203. }
  204. }
  205. func TestSearchPagesEmpty(t *testing.T) {
  206. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  207. writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"search":null}}}`)
  208. })
  209. res, err := client.SearchPages(context.Background(), "nothing", "", "")
  210. if err != nil {
  211. t.Fatalf("SearchPages() error = %v", err)
  212. }
  213. if res.Results == nil || res.Suggestions == nil {
  214. t.Errorf("empty slices must not be nil: %+v", res)
  215. }
  216. if res.TotalHits != 0 {
  217. t.Errorf("TotalHits = %d, want 0", res.TotalHits)
  218. }
  219. }
  220. func TestListPages(t *testing.T) {
  221. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  222. req := decodeRequest(t, r)
  223. if got := req.Variables["limit"]; got != float64(10) {
  224. t.Errorf("variables[limit] = %v", got)
  225. }
  226. if got := req.Variables["locale"]; got != "de" {
  227. t.Errorf("variables[locale] = %v", got)
  228. }
  229. tags, ok := req.Variables["tags"].([]any)
  230. if !ok || len(tags) != 2 || tags[0] != "go" {
  231. t.Errorf("variables[tags] = %v", req.Variables["tags"])
  232. }
  233. writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"list":[
  234. {"id":1,"path":"/a","locale":"de","title":"A","description":"a",
  235. "createdAt":"c","updatedAt":"u","tags":["go"]},
  236. null,
  237. {"id":2,"path":"/b","locale":"en","title":"B","description":"b",
  238. "createdAt":"c","updatedAt":"u","tags":null}
  239. ]}}}`)
  240. })
  241. limit := 10
  242. pages, err := client.ListPages(context.Background(), ListOptions{
  243. Limit: &limit,
  244. Locale: "de",
  245. Tags: []string{"go", "mcp"},
  246. })
  247. if err != nil {
  248. t.Fatalf("ListPages() error = %v", err)
  249. }
  250. if len(pages) != 2 {
  251. t.Fatalf("ListPages() returned %d pages, want 2", len(pages))
  252. }
  253. if got, want := pages[0].URL, client.baseURL+"/de/a"; got != want {
  254. t.Errorf("URL = %q, want %q", got, want)
  255. }
  256. if pages[1].Tags == nil {
  257. t.Error("Tags = nil, want empty slice")
  258. }
  259. }
  260. func TestListPagesOmitsEmptyFilters(t *testing.T) {
  261. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  262. req := decodeRequest(t, r)
  263. for _, key := range []string{"limit", "locale", "tags"} {
  264. if _, ok := req.Variables[key]; ok {
  265. t.Errorf("variables[%s] present, want omitted", key)
  266. }
  267. }
  268. writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"list":[]}}}`)
  269. })
  270. if _, err := client.ListPages(context.Background(), ListOptions{}); err != nil {
  271. t.Fatalf("ListPages() error = %v", err)
  272. }
  273. }
  274. func TestCreatePage(t *testing.T) {
  275. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  276. req := decodeRequest(t, r)
  277. if !strings.Contains(req.Query, "mutation CreatePage") {
  278. t.Errorf("query = %s", req.Query)
  279. }
  280. if got := req.Variables["tags"].([]any); len(got) != 1 || got[0] != "go" {
  281. t.Errorf("variables[tags] = %v", req.Variables["tags"])
  282. }
  283. if got := req.Variables["isPublished"]; got != true {
  284. t.Errorf("variables[isPublished] = %v", got)
  285. }
  286. writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"create":{"responseResult":{
  287. "succeeded":true,"errorCode":0,"slug":"new-page","message":"Page created"}}}}}`)
  288. })
  289. res, err := client.CreatePage(context.Background(), CreatePageInput{
  290. Title: "New",
  291. Content: "body",
  292. Description: "desc",
  293. Path: "new-page",
  294. Editor: "markdown",
  295. IsPublished: true,
  296. Locale: "en",
  297. Tags: []string{"go"},
  298. })
  299. if err != nil {
  300. t.Fatalf("CreatePage() error = %v", err)
  301. }
  302. if !res.Succeeded || res.Message != "Page created" || res.Slug != "new-page" {
  303. t.Errorf("result = %+v", res)
  304. }
  305. }
  306. func TestUpdatePageFailureIsReturned(t *testing.T) {
  307. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  308. writeJSON(t, w, http.StatusOK, `{"data":{"pages":{"update":{"responseResult":{
  309. "succeeded":false,"errorCode":6001,"slug":"","message":"Page not found"}}}}}`)
  310. })
  311. res, err := client.UpdatePage(context.Background(), UpdatePageInput{ID: 1})
  312. if err != nil {
  313. t.Fatalf("UpdatePage() error = %v", err)
  314. }
  315. if res.Succeeded {
  316. t.Error("Succeeded = true, want false")
  317. }
  318. if res.ErrorCode != 6001 || res.Message != "Page not found" {
  319. t.Errorf("result = %+v", res)
  320. }
  321. }
  322. func TestGraphQLErrors(t *testing.T) {
  323. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  324. writeJSON(t, w, http.StatusOK, `{"errors":[{"message":"Unauthorized"},{"message":"Forbidden"}]}`)
  325. })
  326. _, err := client.PageByID(context.Background(), 1)
  327. if err == nil {
  328. t.Fatal("PageByID() error = nil, want error")
  329. }
  330. var apiErr *APIError
  331. if !errors.As(err, &apiErr) {
  332. t.Fatalf("error type = %T, want *APIError", err)
  333. }
  334. if got := apiErr.Error(); !strings.Contains(got, "Unauthorized") || !strings.Contains(got, "Forbidden") {
  335. t.Errorf("error = %q", got)
  336. }
  337. }
  338. func TestHTTPError(t *testing.T) {
  339. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  340. http.Error(w, "server exploded", http.StatusInternalServerError)
  341. })
  342. _, err := client.PageByID(context.Background(), 1)
  343. if err == nil {
  344. t.Fatal("PageByID() error = nil, want error")
  345. }
  346. var apiErr *APIError
  347. if !errors.As(err, &apiErr) {
  348. t.Fatalf("error type = %T, want *APIError", err)
  349. }
  350. if apiErr.Status != http.StatusInternalServerError {
  351. t.Errorf("Status = %d, want 500", apiErr.Status)
  352. }
  353. if !strings.Contains(err.Error(), "server exploded") {
  354. t.Errorf("error = %q", err)
  355. }
  356. }
  357. func TestMissingData(t *testing.T) {
  358. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  359. writeJSON(t, w, http.StatusOK, `{"data":null}`)
  360. })
  361. _, err := client.PageByID(context.Background(), 1)
  362. if err == nil {
  363. t.Fatal("PageByID() error = nil, want error")
  364. }
  365. }
  366. func TestContextCancellation(t *testing.T) {
  367. client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
  368. t.Error("handler must not be called")
  369. })
  370. ctx, cancel := context.WithCancel(context.Background())
  371. cancel()
  372. if _, err := client.PageByID(ctx, 1); err == nil {
  373. t.Fatal("PageByID() error = nil, want context error")
  374. }
  375. }