1package systemviewsbridge
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "testing"
8 "time"
9
10 "github.com/bnema/dumber/internal/application/dto"
11 "github.com/bnema/dumber/internal/application/port"
12)
13
14func TestBuildMessageEnvelope(t *testing.T) {
15 t.Parallel()
16
17 payload := struct {
18 RequestID string `json:"requestId"`
19 Limit int `json:"limit"`
20 }{RequestID: "req-1", Limit: 25}
21
22 got, err := buildMessageEnvelope("history_timeline", payload)
23 if err != nil {
24 t.Fatalf("buildMessageEnvelope() error = %v", err)
25 }
26
27 var msg port.WebUIMessage
28 if err := json.Unmarshal(got, &msg); err != nil {
29 t.Fatalf("unmarshal envelope: %v", err)
30 }
31
32 if msg.Type != "history_timeline" {
33 t.Fatalf("msg.Type = %q, want %q", msg.Type, "history_timeline")
34 }
35
36 if gotPayload := string(msg.Payload); gotPayload != `{"requestId":"req-1","limit":25}` {
37 t.Fatalf("msg.Payload = %s, want %s", gotPayload, `{"requestId":"req-1","limit":25}`)
38 }
39}
40
41func TestClientSendPrefersNativeTransport(t *testing.T) {
42 t.Parallel()
43
44 native := &fakeTransport{available: true, response: []byte(`{"transport":"native"}`)}
45 fetch := &fakeTransport{available: true, response: []byte(`{"transport":"fetch"}`)}
46
47 client := NewClient(native, fetch)
48 got, err := client.Send(context.Background(), "favorite_list", struct {
49 RequestID string `json:"requestId"`
50 }{RequestID: "req-2"})
51 if err != nil {
52 t.Fatalf("Send() error = %v", err)
53 }
54
55 if string(got) != `{"transport":"native"}` {
56 t.Fatalf("Send() = %s, want %s", got, `{"transport":"native"}`)
57 }
58
59 if !native.called {
60 t.Fatal("native transport was not used")
61 }
62 if fetch.called {
63 t.Fatal("fetch transport was used unexpectedly")
64 }
65
66 var msg port.WebUIMessage
67 if err := json.Unmarshal(native.last, &msg); err != nil {
68 t.Fatalf("unmarshal sent envelope: %v", err)
69 }
70 if msg.Type != "favorite_list" {
71 t.Fatalf("sent type = %q, want %q", msg.Type, "favorite_list")
72 }
73}
74
75func TestClientSendFallsBackToFetchTransport(t *testing.T) {
76 t.Parallel()
77
78 native := &fakeTransport{available: false, response: []byte(`{"transport":"native"}`)}
79 fetch := &fakeTransport{available: true, response: []byte(`{"transport":"fetch"}`)}
80
81 client := NewClient(native, fetch)
82 got, err := client.Send(context.Background(), "favorite_list", struct {
83 RequestID string `json:"requestId"`
84 }{RequestID: "req-3"})
85 if err != nil {
86 t.Fatalf("Send() error = %v", err)
87 }
88
89 if string(got) != `{"transport":"fetch"}` {
90 t.Fatalf("Send() = %s, want %s", got, `{"transport":"fetch"}`)
91 }
92
93 if native.called {
94 t.Fatal("native transport was used unexpectedly")
95 }
96 if !fetch.called {
97 t.Fatal("fetch transport was not used")
98 }
99}
100
101func TestClientTimelineDecodesEntries(t *testing.T) {
102 t.Parallel()
103
104 native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-9","success":true,"data":[{"id":1,"url":"https://example.com","title":"Example"}]}`)}
105 client := NewClient(native, nil)
106
107 entries, err := client.Timeline(context.Background(), 25, 0)
108 if err != nil {
109 t.Fatalf("Timeline() error = %v", err)
110 }
111 if len(entries) != 1 {
112 t.Fatalf("Timeline() len = %d, want 1", len(entries))
113 }
114 if entries[0].URL != "https://example.com" || entries[0].Title != "Example" {
115 t.Fatalf("Timeline() entry = %+v", entries[0])
116 }
117
118 var msg port.WebUIMessage
119 if err := json.Unmarshal(native.last, &msg); err != nil {
120 t.Fatalf("unmarshal sent envelope: %v", err)
121 }
122 if msg.Type != "history_timeline" {
123 t.Fatalf("sent type = %q, want %q", msg.Type, "history_timeline")
124 }
125 if gotPayload := string(msg.Payload); gotPayload == "" {
126 t.Fatal("sent payload was empty")
127 }
128}
129
130func TestClientTimelineWindowOmitsZeroCursor(t *testing.T) {
131 t.Parallel()
132
133 native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-window","success":true}`)}
134 client := NewClient(native, nil)
135
136 _, err := client.TimelineWindow(context.Background(), time.Time{}, "")
137 if err != nil {
138 t.Fatalf("TimelineWindow() error = %v", err)
139 }
140
141 var msg port.WebUIMessage
142 if err := json.Unmarshal(native.last, &msg); err != nil {
143 t.Fatalf("unmarshal sent envelope: %v", err)
144 }
145 if msg.Type != "history_timeline_window" {
146 t.Fatalf("sent type = %q, want %q", msg.Type, "history_timeline_window")
147 }
148 var payload map[string]any
149 if err := json.Unmarshal(msg.Payload, &payload); err != nil {
150 t.Fatalf("unmarshal payload: %v", err)
151 }
152 if _, ok := payload["before"]; ok {
153 t.Fatalf("zero TimelineWindow cursor should be omitted, payload = %s", msg.Payload)
154 }
155}
156
157func TestClientDeleteRangeRejectsEmptyRange(t *testing.T) {
158 t.Parallel()
159
160 transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-range-empty","success":true}`)}
161 client := NewClient(transport, nil)
162
163 err := client.DeleteRange(context.Background(), " ")
164 if err == nil {
165 t.Fatal("DeleteRange() error = nil, want validation error")
166 }
167 if transport.called {
168 t.Fatal("DeleteRange() sent a request for an invalid range")
169 }
170}
171
172func TestClientDeleteRangeSendsRange(t *testing.T) {
173 t.Parallel()
174
175 native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-10","success":true}`)}
176 client := NewClient(native, nil)
177
178 if err := client.DeleteRange(context.Background(), "week"); err != nil {
179 t.Fatalf("DeleteRange() error = %v", err)
180 }
181
182 var msg port.WebUIMessage
183 if err := json.Unmarshal(native.last, &msg); err != nil {
184 t.Fatalf("unmarshal sent envelope: %v", err)
185 }
186 if msg.Type != "history_delete_range" {
187 t.Fatalf("sent type = %q, want %q", msg.Type, "history_delete_range")
188 }
189}
190
191func TestClientListDecodesFavorites(t *testing.T) {
192 t.Parallel()
193
194 native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-11","success":true,"data":[{"id":1,"url":"https://example.com","title":"Example"}]}`)}
195 client := NewClient(native, nil)
196
197 favorites, err := client.List(context.Background())
198 if err != nil {
199 t.Fatalf("List() error = %v", err)
200 }
201 if len(favorites) != 1 {
202 t.Fatalf("List() len = %d, want 1", len(favorites))
203 }
204 if favorites[0].URL != "https://example.com" || favorites[0].Title != "Example" {
205 t.Fatalf("List() favorite = %+v", favorites[0])
206 }
207
208 var msg port.WebUIMessage
209 if err := json.Unmarshal(native.last, &msg); err != nil {
210 t.Fatalf("unmarshal sent envelope: %v", err)
211 }
212 if msg.Type != "favorite_list" {
213 t.Fatalf("sent type = %q, want %q", msg.Type, "favorite_list")
214 }
215}
216
217func TestClientListFoldersDecodesFolders(t *testing.T) {
218 t.Parallel()
219
220 native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-12","success":true,"data":[{"id":9,"name":"Read Later"}]}`)}
221 client := NewClient(native, nil)
222
223 folders, err := client.ListFolders(context.Background())
224 if err != nil {
225 t.Fatalf("ListFolders() error = %v", err)
226 }
227 if len(folders) != 1 {
228 t.Fatalf("ListFolders() len = %d, want 1", len(folders))
229 }
230 if folders[0].Name != "Read Later" {
231 t.Fatalf("ListFolders() folder = %+v", folders[0])
232 }
233
234 var msg port.WebUIMessage
235 if err := json.Unmarshal(native.last, &msg); err != nil {
236 t.Fatalf("unmarshal sent envelope: %v", err)
237 }
238 if msg.Type != "folder_list" {
239 t.Fatalf("sent type = %q, want %q", msg.Type, "folder_list")
240 }
241}
242
243func TestClientListTagsDecodesTags(t *testing.T) {
244 t.Parallel()
245
246 native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-13","success":true,"data":[{"id":5,"name":"Go","color":"#00ff00"}]}`)}
247 client := NewClient(native, nil)
248
249 tags, err := client.ListTags(context.Background())
250 if err != nil {
251 t.Fatalf("ListTags() error = %v", err)
252 }
253 if len(tags) != 1 {
254 t.Fatalf("ListTags() len = %d, want 1", len(tags))
255 }
256 if tags[0].Name != "Go" || tags[0].Color != "#00ff00" {
257 t.Fatalf("ListTags() tag = %+v", tags[0])
258 }
259
260 var msg port.WebUIMessage
261 if err := json.Unmarshal(native.last, &msg); err != nil {
262 t.Fatalf("unmarshal sent envelope: %v", err)
263 }
264 if msg.Type != "tag_list" {
265 t.Fatalf("sent type = %q, want %q", msg.Type, "tag_list")
266 }
267}
268
269func TestClientCurrentAndDefaultDecodeConfigPayload(t *testing.T) {
270 t.Parallel()
271
272 resp := []byte(`{"requestId":"req-14","success":true,"data":{"engine_type":"webkit","default_search_engine":"DuckDuckGo"}}`)
273
274 currentClient := NewClient(&fakeTransport{available: true, response: resp}, nil)
275 current, err := currentClient.Current(context.Background())
276 if err != nil {
277 t.Fatalf("Current() error = %v", err)
278 }
279 if current.EngineType != "webkit" || current.DefaultSearchEngine != "DuckDuckGo" {
280 t.Fatalf("Current() = %+v", current)
281 }
282
283 var msg port.WebUIMessage
284 if unmarshalErr := json.Unmarshal(currentClient.native.(*fakeTransport).last, &msg); unmarshalErr != nil {
285 t.Fatalf("unmarshal sent envelope: %v", unmarshalErr)
286 }
287 if msg.Type != "/api/config" {
288 t.Fatalf("sent type = %q, want %q", msg.Type, "/api/config")
289 }
290
291 defaultClient := NewClient(&fakeTransport{available: true, response: resp}, nil)
292 defaultCfg, err := defaultClient.Default(context.Background())
293 if err != nil {
294 t.Fatalf("Default() error = %v", err)
295 }
296 if defaultCfg.EngineType != "webkit" || defaultCfg.DefaultSearchEngine != "DuckDuckGo" {
297 t.Fatalf("Default() = %+v", defaultCfg)
298 }
299
300 if err := json.Unmarshal(defaultClient.native.(*fakeTransport).last, &msg); err != nil {
301 t.Fatalf("unmarshal sent envelope: %v", err)
302 }
303 if msg.Type != "/api/config/default" {
304 t.Fatalf("sent type = %q, want %q", msg.Type, "/api/config/default")
305 }
306}
307
308func TestClientConfigActionsUseExpectedMessageTypes(t *testing.T) {
309 t.Parallel()
310
311 tests := []struct {
312 name string
313 call func(*testing.T, *Client) error
314 want string
315 payload any
316 }{
317 {
318 name: "save config",
319 call: func(_ *testing.T, client *Client) error {
320 return client.Save(context.Background(), dto.WebUIConfig{DefaultSearchEngine: "DuckDuckGo"})
321 },
322 want: "save_config",
323 payload: dto.WebUIConfig{DefaultSearchEngine: "DuckDuckGo"},
324 },
325 {
326 name: "get keybindings",
327 call: func(t *testing.T, client *Client) error {
328 got, err := client.GetKeybindings(context.Background())
329 if err != nil {
330 return err
331 }
332 if len(got.Groups) != 1 || got.Groups[0].Mode != "default" {
333 t.Fatalf("GetKeybindings() = %#v", got)
334 }
335 return nil
336 },
337 want: "get_keybindings",
338 },
339 {
340 name: "set keybinding",
341 call: func(t *testing.T, client *Client) error {
342 got, err := client.SetKeybinding(context.Background(), port.SetKeybindingRequest{RequestID: "req-1", Mode: "default", Action: "open", Keys: []string{"ctrl+o"}})
343 if err != nil {
344 return err
345 }
346 if len(got.Conflicts) != 0 {
347 t.Fatalf("SetKeybinding() = %#v", got)
348 }
349 return nil
350 },
351 want: "set_keybinding",
352 payload: port.SetKeybindingRequest{RequestID: "req-1", Mode: "default", Action: "open", Keys: []string{"ctrl+o"}},
353 },
354 {
355 name: "reset keybinding",
356 call: func(_ *testing.T, client *Client) error {
357 return client.ResetKeybinding(context.Background(), port.ResetKeybindingRequest{RequestID: "req-2", Mode: "default", Action: "open"})
358 },
359 want: "reset_keybinding",
360 payload: port.ResetKeybindingRequest{RequestID: "req-2", Mode: "default", Action: "open"},
361 },
362 {
363 name: "reset all keybindings",
364 call: func(_ *testing.T, client *Client) error {
365 return client.ResetAllKeybindings(context.Background())
366 },
367 want: "reset_all_keybindings",
368 },
369 }
370
371 for _, tt := range tests {
372 t.Run(tt.name, func(t *testing.T) {
373 t.Parallel()
374
375 transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-15","success":true,"data":{"status":"success","conflicts":[]}}`)}
376 if tt.name == "save config" || tt.name == "reset keybinding" || tt.name == "reset all keybindings" {
377 transport.response = []byte(`{"requestId":"req-15","success":true}`)
378 }
379 if tt.name == "get keybindings" {
380 transport.response = []byte(`{"requestId":"req-15","success":true,"data":{"groups":[{"mode":"default","display_name":"Default","bindings":[{"action":"open","description":"Open","keys":["ctrl+o"],"default_keys":["ctrl+o"],"is_custom":false}]}]}}`)
381 }
382 client := NewClient(transport, nil)
383
384 if err := tt.call(t, client); err != nil {
385 t.Fatalf("call() error = %v", err)
386 }
387
388 var msg port.WebUIMessage
389 if err := json.Unmarshal(transport.last, &msg); err != nil {
390 t.Fatalf("unmarshal sent envelope: %v", err)
391 }
392 if msg.Type != tt.want {
393 t.Fatalf("sent type = %q, want %q", msg.Type, tt.want)
394 }
395 if tt.payload != nil && len(msg.Payload) == 0 {
396 t.Fatal("sent payload was empty")
397 }
398 })
399 }
400}
401
402func TestClientRequestUsesDefaultTimeoutWhenCallerHasNoDeadline(t *testing.T) {
403 t.Parallel()
404
405 transport := &blockingTransport{available: true}
406 client := NewClient(transport, nil, WithRequestTimeout(time.Millisecond))
407
408 _, err := client.Timeline(context.Background(), 25, 0)
409 if !errors.Is(err, context.DeadlineExceeded) {
410 t.Fatalf("Timeline() error = %v, want context deadline exceeded", err)
411 }
412 if !transport.called {
413 t.Fatal("transport was not called")
414 }
415}
416
417func TestClientCreateFolderIncludesParentID(t *testing.T) {
418 t.Parallel()
419
420 parentID := int64(42)
421 transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-16","success":true,"data":{"id":9,"name":"Nested","icon":"📁","parent_id":42}}`)}
422 client := NewClient(transport, nil)
423
424 folder, err := client.CreateFolder(context.Background(), "Nested", " 📁 ", &parentID)
425 if err != nil {
426 t.Fatalf("CreateFolder() error = %v", err)
427 }
428 if folder == nil {
429 t.Fatal("CreateFolder() returned nil folder")
430 }
431 if folder.Name != "Nested" {
432 t.Fatalf("CreateFolder() folder name = %q, want %q", folder.Name, "Nested")
433 }
434 if folder.Icon != "📁" {
435 t.Fatalf("CreateFolder() folder icon = %q, want 📁", folder.Icon)
436 }
437 if folder.ParentID == nil || int64(*folder.ParentID) != parentID {
438 t.Fatalf("CreateFolder() folder parent_id = %#v, want %d", folder.ParentID, parentID)
439 }
440
441 var msg port.WebUIMessage
442 if err := json.Unmarshal(transport.last, &msg); err != nil {
443 t.Fatalf("unmarshal sent envelope: %v", err)
444 }
445 if msg.Type != "folder_create" {
446 t.Fatalf("sent type = %q, want %q", msg.Type, "folder_create")
447 }
448
449 var payload struct {
450 RequestID string `json:"requestId"`
451 Name string `json:"name"`
452 Icon *string `json:"icon"`
453 ParentID *int64 `json:"parent_id"`
454 }
455 if err := json.Unmarshal(msg.Payload, &payload); err != nil {
456 t.Fatalf("unmarshal sent payload: %v", err)
457 }
458 if payload.ParentID == nil || *payload.ParentID != parentID {
459 t.Fatalf("sent parent_id = %#v, want %d", payload.ParentID, parentID)
460 }
461 if payload.Name != "Nested" {
462 t.Fatalf("sent name = %q, want %q", payload.Name, "Nested")
463 }
464 if payload.Icon == nil || *payload.Icon != "📁" {
465 t.Fatalf("sent icon = %#v, want 📁", payload.Icon)
466 }
467 if payload.RequestID == "" {
468 t.Fatal("sent requestId was empty")
469 }
470}
471
472func TestClientDomainStatsRejectsNegativeLimit(t *testing.T) {
473 t.Parallel()
474
475 transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-17","success":true,"data":[]}`)}
476 client := NewClient(transport, nil)
477
478 _, err := client.DomainStats(context.Background(), -1)
479 if err == nil {
480 t.Fatal("DomainStats() error = nil, want validation error")
481 }
482 if transport.called {
483 t.Fatal("DomainStats() sent a request for a negative limit")
484 }
485}
486
487func TestClientDeleteDomainRejectsEmptyDomain(t *testing.T) {
488 t.Parallel()
489
490 transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-domain-empty","success":true}`)}
491 client := NewClient(transport, nil)
492
493 err := client.DeleteDomain(context.Background(), " ")
494 if err == nil {
495 t.Fatal("DeleteDomain() error = nil, want validation error")
496 }
497 if transport.called {
498 t.Fatal("DeleteDomain() sent a request for an empty domain")
499 }
500}
501
502func TestClientUpdateFolderTrimsIcon(t *testing.T) {
503 t.Parallel()
504
505 transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-18","success":true}`)}
506 client := NewClient(transport, nil)
507
508 if err := client.UpdateFolder(context.Background(), 9, "Read", " 📚 "); err != nil {
509 t.Fatalf("UpdateFolder() error = %v", err)
510 }
511
512 var msg port.WebUIMessage
513 if err := json.Unmarshal(transport.last, &msg); err != nil {
514 t.Fatalf("unmarshal sent envelope: %v", err)
515 }
516 if msg.Type != "folder_update" {
517 t.Fatalf("sent type = %q, want folder_update", msg.Type)
518 }
519 var payload struct {
520 Icon *string `json:"icon"`
521 }
522 if err := json.Unmarshal(msg.Payload, &payload); err != nil {
523 t.Fatalf("unmarshal sent payload: %v", err)
524 }
525 if payload.Icon == nil || *payload.Icon != "📚" {
526 t.Fatalf("sent icon = %#v, want 📚", payload.Icon)
527 }
528}
529
530func TestClientUpdateFolderOmitsBlankIcon(t *testing.T) {
531 t.Parallel()
532
533 transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-19","success":true}`)}
534 client := NewClient(transport, nil)
535
536 if err := client.UpdateFolder(context.Background(), 9, "Read", " "); err != nil {
537 t.Fatalf("UpdateFolder() error = %v", err)
538 }
539
540 var msg port.WebUIMessage
541 if err := json.Unmarshal(transport.last, &msg); err != nil {
542 t.Fatalf("unmarshal sent envelope: %v", err)
543 }
544 var payload struct {
545 Icon *string `json:"icon"`
546 }
547 if err := json.Unmarshal(msg.Payload, &payload); err != nil {
548 t.Fatalf("unmarshal sent payload: %v", err)
549 }
550 if payload.Icon != nil {
551 t.Fatalf("sent icon = %#v, want nil", payload.Icon)
552 }
553}
554
555func TestClientDeleteEntryRejectsInvalidID(t *testing.T) {
556 t.Parallel()
557
558 transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-20","success":true}`)}
559 client := NewClient(transport, nil)
560
561 err := client.DeleteEntry(context.Background(), 0)
562 if err == nil {
563 t.Fatal("DeleteEntry() error = nil, want validation error")
564 }
565 if transport.called {
566 t.Fatal("DeleteEntry() sent a request for an invalid id")
567 }
568}
569
570func TestClientTagActionsTrimFields(t *testing.T) {
571 t.Parallel()
572
573 createTransport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-21","success":true,"data":{"id":5,"name":"Go","color":"#00add8"}}`)}
574 client := NewClient(createTransport, nil)
575 if _, err := client.CreateTag(context.Background(), " Go ", " #00add8 "); err != nil {
576 t.Fatalf("CreateTag() error = %v", err)
577 }
578 var msg port.WebUIMessage
579 if err := json.Unmarshal(createTransport.last, &msg); err != nil {
580 t.Fatalf("unmarshal create envelope: %v", err)
581 }
582 var createPayload struct {
583 Name string `json:"name"`
584 Color *string `json:"color"`
585 }
586 if err := json.Unmarshal(msg.Payload, &createPayload); err != nil {
587 t.Fatalf("unmarshal create payload: %v", err)
588 }
589 if createPayload.Name != "Go" || createPayload.Color == nil || *createPayload.Color != "#00add8" {
590 t.Fatalf("CreateTag payload = %#v", createPayload)
591 }
592
593 updateTransport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-22","success":true}`)}
594 client = NewClient(updateTransport, nil)
595 if err := client.UpdateTag(context.Background(), 5, " Go ", " "); err != nil {
596 t.Fatalf("UpdateTag() error = %v", err)
597 }
598 if err := json.Unmarshal(updateTransport.last, &msg); err != nil {
599 t.Fatalf("unmarshal update envelope: %v", err)
600 }
601 var updatePayload struct {
602 Name *string `json:"name"`
603 Color *string `json:"color"`
604 }
605 if err := json.Unmarshal(msg.Payload, &updatePayload); err != nil {
606 t.Fatalf("unmarshal update payload: %v", err)
607 }
608 if updatePayload.Name == nil || *updatePayload.Name != "Go" || updatePayload.Color != nil {
609 t.Fatalf("UpdateTag payload = %#v", updatePayload)
610 }
611}
612
613type fakeTransport struct {
614 available bool
615 called bool
616 last []byte
617 response []byte
618}
619
620func (t *fakeTransport) Available() bool { return t.available }
621
622func (t *fakeTransport) Send(_ context.Context, body []byte) ([]byte, error) {
623 t.called = true
624 t.last = append(t.last[:0], body...)
625 return t.response, nil
626}
627
628type blockingTransport struct {
629 available bool
630 called bool
631}
632
633func (t *blockingTransport) Available() bool { return t.available }
634
635func (t *blockingTransport) Send(ctx context.Context, _ []byte) ([]byte, error) {
636 t.called = true
637 <-ctx.Done()
638 return nil, ctx.Err()
639}
640
641func TestClientRejectsInvalidFavoriteMutationIDs(t *testing.T) {
642 t.Parallel()
643
644 transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req","success":true}`)}
645 client := NewClient(transport, nil)
646
647 checks := []struct {
648 name string
649 call func() error
650 }{
651 {name: "delete favorite", call: func() error { return client.DeleteFavorite(context.Background(), 0) }},
652 {name: "set shortcut", call: func() error { return client.SetShortcut(context.Background(), 0, nil) }},
653 {name: "set folder", call: func() error { return client.SetFolder(context.Background(), 0, nil) }},
654 {name: "create folder", call: func() error { _, err := client.CreateFolder(context.Background(), " ", "", nil); return err }},
655 {name: "update folder id", call: func() error { return client.UpdateFolder(context.Background(), 0, "Folder", "") }},
656 {name: "update folder name", call: func() error { return client.UpdateFolder(context.Background(), 1, " ", "") }},
657 {name: "delete folder", call: func() error { return client.DeleteFolder(context.Background(), 0) }},
658 {name: "create tag", call: func() error { _, err := client.CreateTag(context.Background(), " ", ""); return err }},
659 {name: "update tag", call: func() error { return client.UpdateTag(context.Background(), 0, "Tag", "") }},
660 {name: "update tag name", call: func() error { return client.UpdateTag(context.Background(), 1, " ", "") }},
661 {name: "delete tag", call: func() error { return client.DeleteTag(context.Background(), 0) }},
662 {name: "assign favorite", call: func() error { return client.AssignTag(context.Background(), 0, 1) }},
663 {name: "assign tag", call: func() error { return client.AssignTag(context.Background(), 1, 0) }},
664 {name: "remove favorite", call: func() error { return client.RemoveTag(context.Background(), 0, 1) }},
665 {name: "remove tag", call: func() error { return client.RemoveTag(context.Background(), 1, 0) }},
666 }
667 for _, check := range checks {
668 t.Run(check.name, func(t *testing.T) {
669 if err := check.call(); err == nil {
670 t.Fatal("error = nil, want validation error")
671 }
672 })
673 }
674 if transport.called {
675 t.Fatal("transport called for invalid favorite mutation IDs")
676 }
677}
678
679func TestClientRejectsNegativeHistoryLimits(t *testing.T) {
680 t.Parallel()
681
682 client := NewClient(&fakeTransport{available: true}, nil)
683
684 if _, err := client.Timeline(context.Background(), -1, 0); err == nil {
685 t.Fatal("Timeline() error = nil, want error")
686 }
687 if _, err := client.Timeline(context.Background(), 1, -1); err == nil {
688 t.Fatal("Timeline() error = nil, want error")
689 }
690 if _, err := client.TimelineByDomain(context.Background(), "", 1, 0); err == nil {
691 t.Fatal("TimelineByDomain() error = nil, want error")
692 }
693 if _, err := client.TimelineByDomain(context.Background(), "example.com", -1, 0); err == nil {
694 t.Fatal("TimelineByDomain() error = nil, want error")
695 }
696 if _, err := client.TimelineByDomain(context.Background(), "example.com", 1, -1); err == nil {
697 t.Fatal("TimelineByDomain() error = nil, want error")
698 }
699 if _, err := client.Search(context.Background(), "example", -1); err == nil {
700 t.Fatal("Search() error = nil, want error")
701 }
702}