From 0bb7d2d7354e706816ea6b0b7f4082687a5a6fb7 Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Wed, 14 Jan 2026 13:42:05 +0100 Subject: [PATCH] Add unit tests for Checklists Create checklists_test.go with comprehensive tests: - TestChecklists_List: list all checklists - TestChecklists_ListArchived: list with archived filter - TestChecklists_Get: get single checklist by ID - TestChecklists_Get_NotFound: handle 404 error - TestChecklists_Create: create new checklist - TestChecklists_Update: update checklist name - TestChecklists_Delete: delete checklist Add testdata/checklists/ fixtures: - list.json: sample checklist list - list_archived.json: archived checklists - single.json: single checklist response All 7 tests pass using httptest.Server mocking. Closes checkvist-api-347 --- .beads/issues.jsonl | 2 +- checklists_test.go | 260 +++++++++++++++++++++++++ testdata/checklists/list.json | 24 +++ testdata/checklists/list_archived.json | 13 ++ testdata/checklists/single.json | 11 ++ 5 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 checklists_test.go create mode 100644 testdata/checklists/list.json create mode 100644 testdata/checklists/list_archived.json create mode 100644 testdata/checklists/single.json diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 0fa83b7..2a15ee4 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -3,7 +3,7 @@ {"id":"checkvist-api-1ki","title":"Write GoDoc examples","description":"Create example_test.go with runnable examples:\n- Example_basicUsage\n- ExampleNewClient\n- ExampleClient_Authenticate\n- ExampleChecklistService_List\n- ExampleTaskService_Create\n- ExampleNewTask (builder pattern)\n- ExampleNewFilter (filter builder)\n- ExampleFilter_Apply\nAll examples should be runnable and appear in GoDoc.","status":"open","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.443075101+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:31:38.443075101+01:00","dependencies":[{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:14.730667505+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:15.039931257+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:15.388830036+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-1ze","title":"Implement client-side Filter builder","description":"Create filter.go with Filter builder (since Checkvist API has no server-side filtering):\n- NewFilter(tasks []Task) *Filter\n- WithTag(tag string) *Filter\n- WithTags(tags ...string) *Filter (AND logic)\n- WithStatus(status TaskStatus) *Filter\n- WithDueBefore(t time.Time) *Filter\n- WithDueAfter(t time.Time) *Filter\n- WithDueOn(t time.Time) *Filter\n- WithOverdue() *Filter\n- WithSearch(query string) *Filter (searches in Content)\n- Apply() []Task\nPerformance target: \u003c10ms for 1000+ tasks\nFilters are combined with AND logic.","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.24379077+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:30:56.24379077+01:00","dependencies":[{"issue_id":"checkvist-api-1ze","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.543121262+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-1zf","title":"Write unit tests for Filter","description":"Create filter_test.go with tests:\n- TestFilter_WithTag\n- TestFilter_WithMultipleTags\n- TestFilter_WithStatus\n- TestFilter_WithDueBefore\n- TestFilter_WithDueAfter\n- TestFilter_WithDueOn\n- TestFilter_WithOverdue\n- TestFilter_WithSearch\n- TestFilter_Combined\n- TestFilter_Performance_1000Tasks\nUse table-driven tests.","status":"open","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.136650557+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:31:38.136650557+01:00","dependencies":[{"issue_id":"checkvist-api-1zf","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:14.4378645+01:00","created_by":"Oliver Jakoubek"}]} -{"id":"checkvist-api-347","title":"Write unit tests for Checklists","description":"Create checklists_test.go with tests:\n- TestChecklists_List\n- TestChecklists_ListArchived\n- TestChecklists_Get\n- TestChecklists_Get_NotFound\n- TestChecklists_Create\n- TestChecklists_Update\n- TestChecklists_Delete\nUse table-driven tests. Create testdata/checklists/ fixtures.","status":"open","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.243525209+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:31:37.243525209+01:00","dependencies":[{"issue_id":"checkvist-api-347","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:13.512887479+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-347","title":"Write unit tests for Checklists","description":"Create checklists_test.go with tests:\n- TestChecklists_List\n- TestChecklists_ListArchived\n- TestChecklists_Get\n- TestChecklists_Get_NotFound\n- TestChecklists_Create\n- TestChecklists_Update\n- TestChecklists_Delete\nUse table-driven tests. Create testdata/checklists/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.243525209+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:41:58.076714398+01:00","closed_at":"2026-01-14T13:41:58.076714398+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-347","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:13.512887479+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-47y","title":"Quality \u0026 Documentation","description":"Phase 4: Complete unit tests, GoDoc examples, README, and CHANGELOG. Target \u003e80% test coverage. All public functions documented with examples.","status":"open","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.655509387+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:31:36.655509387+01:00"} {"id":"checkvist-api-5ab","title":"Implement Note operations","description":"Create notes.go with NoteService:\n- client.Notes(checklistID, taskID) returns NoteService\n- List(ctx) ([]Note, error) - GET /checklists/{id}/tasks/{task_id}/comments.json\n- Create(ctx, comment string) (*Note, error) - POST /checklists/{id}/tasks/{task_id}/comments.json\n- Update(ctx, noteID, comment string) (*Note, error) - PUT /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\n- Delete(ctx, noteID) error - DELETE /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\nContext support for all methods.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:54.268124634+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:40:31.06124818+01:00","closed_at":"2026-01-14T13:40:31.06124818+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-5ab","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:32:55.843507717+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-5wr","title":"Initialize Go module and project structure","description":"Create go.mod with module path code.beautifulmachines.dev/jakoubek/checkvist-api. Set up flat package structure with placeholder files: client.go, checklists.go, tasks.go, notes.go, filter.go, models.go, errors.go, options.go. Create magefiles/ directory with separate go.mod for Mage build targets. Add LICENSE (MIT) file.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:06.285510329+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:43:45.392753825+01:00","closed_at":"2026-01-14T12:43:45.392753825+01:00","close_reason":"Closed"} diff --git a/checklists_test.go b/checklists_test.go new file mode 100644 index 0000000..f8469a5 --- /dev/null +++ b/checklists_test.go @@ -0,0 +1,260 @@ +package checkvist + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestChecklists_List(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/auth/login.json": + json.NewEncoder(w).Encode(map[string]string{"token": "test-token"}) + case "/checklists.json": + if r.URL.Query().Get("archived") != "" { + t.Error("unexpected archived parameter in List") + } + w.Write(loadFixture(t, "testdata/checklists/list.json")) + default: + t.Errorf("unexpected path: %s", r.URL.Path) + } + })) + defer server.Close() + + client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL)) + checklists, err := client.Checklists().List(context.Background()) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(checklists) != 2 { + t.Fatalf("expected 2 checklists, got %d", len(checklists)) + } + if checklists[0].ID != 1 { + t.Errorf("expected ID 1, got %d", checklists[0].ID) + } + if checklists[0].Name != "My First Checklist" { + t.Errorf("expected name 'My First Checklist', got %s", checklists[0].Name) + } + if checklists[1].TaskCount != 25 { + t.Errorf("expected TaskCount 25, got %d", checklists[1].TaskCount) + } +} + +func TestChecklists_ListArchived(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/auth/login.json": + json.NewEncoder(w).Encode(map[string]string{"token": "test-token"}) + case "/checklists.json": + if r.URL.Query().Get("archived") != "true" { + t.Error("expected archived=true parameter") + } + w.Write(loadFixture(t, "testdata/checklists/list_archived.json")) + default: + t.Errorf("unexpected path: %s", r.URL.Path) + } + })) + defer server.Close() + + client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL)) + checklists, err := client.Checklists().ListWithOptions(context.Background(), ListOptions{Archived: true}) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(checklists) != 1 { + t.Fatalf("expected 1 checklist, got %d", len(checklists)) + } + if !checklists[0].Archived { + t.Error("expected checklist to be archived") + } +} + +func TestChecklists_Get(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/auth/login.json": + json.NewEncoder(w).Encode(map[string]string{"token": "test-token"}) + case "/checklists/1.json": + w.Write(loadFixture(t, "testdata/checklists/single.json")) + default: + t.Errorf("unexpected path: %s", r.URL.Path) + } + })) + defer server.Close() + + client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL)) + checklist, err := client.Checklists().Get(context.Background(), 1) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if checklist.ID != 1 { + t.Errorf("expected ID 1, got %d", checklist.ID) + } + if checklist.Name != "My First Checklist" { + t.Errorf("expected name 'My First Checklist', got %s", checklist.Name) + } +} + +func TestChecklists_Get_NotFound(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/auth/login.json": + json.NewEncoder(w).Encode(map[string]string{"token": "test-token"}) + case "/checklists/999.json": + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"error": "Checklist not found"}`)) + default: + t.Errorf("unexpected path: %s", r.URL.Path) + } + })) + defer server.Close() + + client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL)) + _, err := client.Checklists().Get(context.Background(), 999) + + if err == nil { + t.Fatal("expected error, got nil") + } + if !errors.Is(err, ErrNotFound) { + t.Errorf("expected ErrNotFound, got %v", err) + } +} + +func TestChecklists_Create(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/auth/login.json": + json.NewEncoder(w).Encode(map[string]string{"token": "test-token"}) + case "/checklists.json": + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + + var req createChecklistRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("failed to decode request: %v", err) + } + if req.Name != "New Checklist" { + t.Errorf("expected name 'New Checklist', got %s", req.Name) + } + + response := Checklist{ + ID: 42, + Name: req.Name, + Public: false, + Archived: false, + UpdatedAt: time.Now(), + } + json.NewEncoder(w).Encode(response) + default: + t.Errorf("unexpected path: %s", r.URL.Path) + } + })) + defer server.Close() + + client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL)) + checklist, err := client.Checklists().Create(context.Background(), "New Checklist") + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if checklist.ID != 42 { + t.Errorf("expected ID 42, got %d", checklist.ID) + } + if checklist.Name != "New Checklist" { + t.Errorf("expected name 'New Checklist', got %s", checklist.Name) + } +} + +func TestChecklists_Update(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/auth/login.json": + json.NewEncoder(w).Encode(map[string]string{"token": "test-token"}) + case "/checklists/1.json": + if r.Method != http.MethodPut { + t.Errorf("expected PUT, got %s", r.Method) + } + + var req updateChecklistRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("failed to decode request: %v", err) + } + if req.Name != "Updated Name" { + t.Errorf("expected name 'Updated Name', got %s", req.Name) + } + + response := Checklist{ + ID: 1, + Name: req.Name, + UpdatedAt: time.Now(), + } + json.NewEncoder(w).Encode(response) + default: + t.Errorf("unexpected path: %s", r.URL.Path) + } + })) + defer server.Close() + + client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL)) + checklist, err := client.Checklists().Update(context.Background(), 1, "Updated Name") + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if checklist.Name != "Updated Name" { + t.Errorf("expected name 'Updated Name', got %s", checklist.Name) + } +} + +func TestChecklists_Delete(t *testing.T) { + var deleteCalled bool + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/auth/login.json": + json.NewEncoder(w).Encode(map[string]string{"token": "test-token"}) + case "/checklists/1.json": + if r.Method != http.MethodDelete { + t.Errorf("expected DELETE, got %s", r.Method) + } + deleteCalled = true + w.WriteHeader(http.StatusOK) + default: + t.Errorf("unexpected path: %s", r.URL.Path) + } + })) + defer server.Close() + + client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL)) + err := client.Checklists().Delete(context.Background(), 1) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !deleteCalled { + t.Error("expected DELETE to be called") + } +} diff --git a/testdata/checklists/list.json b/testdata/checklists/list.json new file mode 100644 index 0000000..38d5ad7 --- /dev/null +++ b/testdata/checklists/list.json @@ -0,0 +1,24 @@ +[ + { + "id": 1, + "name": "My First Checklist", + "public": false, + "archived": false, + "read_only": false, + "task_count": 10, + "task_completed": 3, + "tags_as_text": "", + "updated_at": "2026-01-14T10:00:00Z" + }, + { + "id": 2, + "name": "Work Tasks", + "public": true, + "archived": false, + "read_only": false, + "task_count": 25, + "task_completed": 12, + "tags_as_text": "work, important", + "updated_at": "2026-01-13T15:30:00Z" + } +] diff --git a/testdata/checklists/list_archived.json b/testdata/checklists/list_archived.json new file mode 100644 index 0000000..79471df --- /dev/null +++ b/testdata/checklists/list_archived.json @@ -0,0 +1,13 @@ +[ + { + "id": 3, + "name": "Old Project", + "public": false, + "archived": true, + "read_only": false, + "task_count": 50, + "task_completed": 50, + "tags_as_text": "", + "updated_at": "2025-06-01T09:00:00Z" + } +] diff --git a/testdata/checklists/single.json b/testdata/checklists/single.json new file mode 100644 index 0000000..46ad7a5 --- /dev/null +++ b/testdata/checklists/single.json @@ -0,0 +1,11 @@ +{ + "id": 1, + "name": "My First Checklist", + "public": false, + "archived": false, + "read_only": false, + "task_count": 10, + "task_completed": 3, + "tags_as_text": "", + "updated_at": "2026-01-14T10:00:00Z" +}