linkwarden_test.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package linkwarden
  4. import (
  5. "encoding/json"
  6. "io"
  7. "net/http"
  8. "net/http/httptest"
  9. "strings"
  10. "testing"
  11. "miniflux.app/v2/internal/model"
  12. )
  13. func TestCreateBookmark(t *testing.T) {
  14. tests := []struct {
  15. name string
  16. baseURL string
  17. apiKey string
  18. collectionID *int64
  19. entryURL string
  20. entryTitle string
  21. serverResponse func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionID *int64)
  22. wantErr bool
  23. errContains string
  24. }{
  25. {
  26. name: "successful bookmark creation without collection",
  27. baseURL: "",
  28. apiKey: "test-api-key",
  29. collectionID: nil,
  30. entryURL: "https://example.com",
  31. entryTitle: "Test Article",
  32. serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
  33. // Verify authorization header
  34. auth := r.Header.Get("Authorization")
  35. if auth != "Bearer test-api-key" {
  36. t.Errorf("Expected Authorization header 'Bearer test-api-key', got %s", auth)
  37. }
  38. // Verify content type
  39. contentType := r.Header.Get("Content-Type")
  40. if contentType != "application/json" {
  41. t.Errorf("Expected Content-Type 'application/json', got %s", contentType)
  42. }
  43. // Parse and verify request
  44. body, _ := io.ReadAll(r.Body)
  45. var req map[string]any
  46. if err := json.Unmarshal(body, &req); err != nil {
  47. t.Errorf("Failed to parse request body: %v", err)
  48. }
  49. // Verify URL
  50. if reqURL := req["url"]; reqURL != "https://example.com" {
  51. t.Errorf("Expected URL 'https://example.com', got %v", reqURL)
  52. }
  53. // Verify title/name
  54. if reqName := req["name"]; reqName != "Test Article" {
  55. t.Errorf("Expected name 'Test Article', got %v", reqName)
  56. }
  57. // Verify collection is not present when nil
  58. if _, ok := req["collection"]; ok {
  59. t.Error("Expected collection field to be omitted when collectionId is nil")
  60. }
  61. // Return success response
  62. w.WriteHeader(http.StatusOK)
  63. json.NewEncoder(w).Encode(map[string]any{
  64. "id": "123",
  65. "url": "https://example.com",
  66. "name": "Test Article",
  67. })
  68. },
  69. wantErr: false,
  70. },
  71. {
  72. name: "successful bookmark creation with collection",
  73. baseURL: "",
  74. apiKey: "test-api-key",
  75. collectionID: model.OptionalNumber(int64(42)),
  76. entryURL: "https://example.com/article",
  77. entryTitle: "Test Article With Collection",
  78. serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionID *int64) {
  79. // Verify authorization header
  80. auth := r.Header.Get("Authorization")
  81. if auth != "Bearer test-api-key" {
  82. t.Errorf("Expected Authorization header 'Bearer test-api-key', got %s", auth)
  83. }
  84. // Parse and verify request
  85. body, _ := io.ReadAll(r.Body)
  86. var req map[string]any
  87. if err := json.Unmarshal(body, &req); err != nil {
  88. t.Errorf("Failed to parse request body: %v", err)
  89. }
  90. // Verify URL
  91. if reqURL := req["url"]; reqURL != "https://example.com/article" {
  92. t.Errorf("Expected URL 'https://example.com/article', got %v", reqURL)
  93. }
  94. // Verify title/name
  95. if reqName := req["name"]; reqName != "Test Article With Collection" {
  96. t.Errorf("Expected name 'Test Article With Collection', got %v", reqName)
  97. }
  98. // Verify collection is present and correct
  99. if collection, ok := req["collection"]; ok {
  100. collectionMap, ok := collection.(map[string]any)
  101. if !ok {
  102. t.Error("Expected collection to be a map")
  103. }
  104. if collectionID, ok := collectionMap["id"]; ok {
  105. // JSON numbers are float64
  106. if collectionIDFloat, ok := collectionID.(float64); !ok || int64(collectionIDFloat) != 42 {
  107. t.Errorf("Expected collection id 42, got %v", collectionID)
  108. }
  109. } else {
  110. t.Error("Expected collection to have 'id' field")
  111. }
  112. } else {
  113. t.Error("Expected collection field to be present when collectionId is set")
  114. }
  115. // Return success response
  116. w.WriteHeader(http.StatusOK)
  117. json.NewEncoder(w).Encode(map[string]any{
  118. "id": "124",
  119. "url": "https://example.com/article",
  120. "name": "Test Article With Collection",
  121. })
  122. },
  123. wantErr: false,
  124. },
  125. {
  126. name: "missing API key",
  127. baseURL: "",
  128. apiKey: "",
  129. collectionID: nil,
  130. entryURL: "https://example.com",
  131. entryTitle: "Test",
  132. serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
  133. // Should not be called
  134. t.Error("Server should not be called when API key is missing")
  135. },
  136. wantErr: true,
  137. errContains: "missing base URL or API key",
  138. },
  139. {
  140. name: "server error",
  141. baseURL: "",
  142. apiKey: "test-api-key",
  143. collectionID: nil,
  144. entryURL: "https://example.com",
  145. entryTitle: "Test",
  146. serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
  147. w.WriteHeader(http.StatusInternalServerError)
  148. w.Write([]byte(`{"error": "Internal server error"}`))
  149. },
  150. wantErr: true,
  151. errContains: "unable to create link: status=500",
  152. },
  153. {
  154. name: "bad request with null collection id error",
  155. baseURL: "",
  156. apiKey: "test-api-key",
  157. collectionID: nil,
  158. entryURL: "https://example.com",
  159. entryTitle: "Test",
  160. serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
  161. w.WriteHeader(http.StatusBadRequest)
  162. w.Write([]byte(`{"response":"Error: Expected number, received null [collection, id]"}`))
  163. },
  164. wantErr: true,
  165. errContains: "unable to create link: status=400",
  166. },
  167. {
  168. name: "unauthorized",
  169. baseURL: "",
  170. apiKey: "invalid-key",
  171. collectionID: nil,
  172. entryURL: "https://example.com",
  173. entryTitle: "Test",
  174. serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
  175. w.WriteHeader(http.StatusUnauthorized)
  176. w.Write([]byte(`{"error": "Unauthorized"}`))
  177. },
  178. wantErr: true,
  179. errContains: "unable to create link: status=401",
  180. },
  181. {
  182. name: "invalid base URL",
  183. baseURL: ":",
  184. apiKey: "test-api-key",
  185. collectionID: nil,
  186. entryURL: "https://example.com",
  187. entryTitle: "Test",
  188. serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
  189. // Should not be called
  190. t.Error("Server should not be called when base URL is invalid")
  191. },
  192. wantErr: true,
  193. errContains: "invalid API endpoint",
  194. },
  195. {
  196. name: "missing base URL",
  197. baseURL: "",
  198. apiKey: "",
  199. collectionID: nil,
  200. entryURL: "https://example.com",
  201. entryTitle: "Test",
  202. serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
  203. // Should not be called
  204. t.Error("Server should not be called when base URL is missing")
  205. },
  206. wantErr: true,
  207. errContains: "missing base URL or API key",
  208. },
  209. {
  210. name: "network connection error",
  211. baseURL: "http://localhost:1", // Invalid port that should fail to connect
  212. apiKey: "test-api-key",
  213. collectionID: nil,
  214. entryURL: "https://example.com",
  215. entryTitle: "Test",
  216. serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
  217. // Should not be called due to connection failure
  218. t.Error("Server should not be called when connection fails")
  219. },
  220. wantErr: true,
  221. errContains: "unable to send request",
  222. },
  223. }
  224. for _, tt := range tests {
  225. t.Run(tt.name, func(t *testing.T) {
  226. // Create test server only if we have a valid apiKey and don't have a custom baseURL for error testing
  227. var server *httptest.Server
  228. if tt.apiKey != "" && tt.baseURL != ":" && tt.baseURL != "http://localhost:1" {
  229. server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  230. tt.serverResponse(w, r, t, tt.collectionID)
  231. }))
  232. defer server.Close()
  233. }
  234. // Use test server URL if baseURL is empty and we have a server
  235. baseURL := tt.baseURL
  236. if baseURL == "" && server != nil {
  237. baseURL = server.URL
  238. }
  239. // Create client
  240. client := NewClient(baseURL, tt.apiKey, tt.collectionID)
  241. // Call CreateBookmark
  242. err := client.CreateBookmark(tt.entryURL, tt.entryTitle)
  243. // Check error
  244. if tt.wantErr {
  245. if err == nil {
  246. t.Error("Expected error, got nil")
  247. } else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
  248. t.Errorf("Expected error to contain '%s', got '%s'", tt.errContains, err.Error())
  249. }
  250. } else {
  251. if err != nil {
  252. t.Errorf("Expected no error, got %v", err)
  253. }
  254. }
  255. })
  256. }
  257. }
  258. func TestNewClient(t *testing.T) {
  259. tests := []struct {
  260. name string
  261. baseURL string
  262. apiKey string
  263. collectionID *int64
  264. }{
  265. {
  266. name: "client without collection",
  267. baseURL: "https://linkwarden.example.com",
  268. apiKey: "test-key",
  269. collectionID: nil,
  270. },
  271. {
  272. name: "client with collection",
  273. baseURL: "https://linkwarden.example.com",
  274. apiKey: "test-key",
  275. collectionID: model.OptionalNumber(int64(123)),
  276. },
  277. }
  278. for _, tt := range tests {
  279. t.Run(tt.name, func(t *testing.T) {
  280. client := NewClient(tt.baseURL, tt.apiKey, tt.collectionID)
  281. if client.baseURL != tt.baseURL {
  282. t.Errorf("Expected baseURL %s, got %s", tt.baseURL, client.baseURL)
  283. }
  284. if client.apiKey != tt.apiKey {
  285. t.Errorf("Expected apiKey %s, got %s", tt.apiKey, client.apiKey)
  286. }
  287. if tt.collectionID == nil {
  288. if client.collectionID != nil {
  289. t.Errorf("Expected collectionId to be nil, got %v", *client.collectionID)
  290. }
  291. } else {
  292. if client.collectionID == nil {
  293. t.Error("Expected collectionId to be set, got nil")
  294. } else if *client.collectionID != *tt.collectionID {
  295. t.Errorf("Expected collectionId %d, got %d", *tt.collectionID, *client.collectionID)
  296. }
  297. }
  298. })
  299. }
  300. }