Skip to content

Commit ea86da2

Browse files
committed
feat(linkwarden): update linkwardenRequest to use pointer for collection and add tests for CreateBookmark
1 parent b0ec482 commit ea86da2

File tree

2 files changed

+345
-4
lines changed

2 files changed

+345
-4
lines changed

internal/integration/linkwarden/linkwarden.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ type linkwardenCollection struct {
2929
}
3030

3131
type linkwardenRequest struct {
32-
URL string `json:"url"`
33-
Name string `json:"name"`
34-
Collection linkwardenCollection `json:"collection,omitempty"`
32+
URL string `json:"url"`
33+
Name string `json:"name"`
34+
Collection *linkwardenCollection `json:"collection,omitempty"`
3535
}
3636

3737
func NewClient(baseURL, apiKey string, collectionId *int64) *Client {
@@ -54,7 +54,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
5454
}
5555

5656
if c.collectionId != nil {
57-
payload.Collection = linkwardenCollection{Id: c.collectionId}
57+
payload.Collection = &linkwardenCollection{Id: c.collectionId}
5858
}
5959

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

0 commit comments

Comments
 (0)