diff --git a/go/pkg/basecamp/doc.go b/go/pkg/basecamp/doc.go index 35219ea2..b191a9ba 100644 --- a/go/pkg/basecamp/doc.go +++ b/go/pkg/basecamp/doc.go @@ -88,20 +88,26 @@ // // # Working with Todos // +// Todo APIs take todolistID or todoID directly; projectID is not required. +// // List todos in a todolist: // -// todos, err := account.Todos().List(ctx, projectID, todolistID, nil) +// todos, err := account.Todos().List(ctx, todolistID, nil) +// +// List completed todos in a todolist: +// +// completed, err := account.Todos().List(ctx, todolistID, &basecamp.TodoListOptions{Status: "completed"}) // // Create a todo: // -// todo, err := account.Todos().Create(ctx, projectID, todolistID, &basecamp.CreateTodoRequest{ +// todo, err := account.Todos().Create(ctx, todolistID, &basecamp.CreateTodoRequest{ // Content: "Ship the feature", // DueOn: "2024-12-31", // }) // // Complete a todo: // -// err := account.Todos().Complete(ctx, projectID, todoID) +// err := account.Todos().Complete(ctx, todoID) // // # Searching // @@ -176,6 +182,6 @@ // acme := client.ForAccount("12345") // initech := client.ForAccount("67890") // -// go func() { acme.Todos().List(ctx, projectID, todolistID, nil) }() +// go func() { acme.Todos().List(ctx, todolistID, nil) }() // go func() { initech.Projects().List(ctx, nil) }() package basecamp diff --git a/go/pkg/basecamp/example_test.go b/go/pkg/basecamp/example_test.go index e681e086..62c42394 100644 --- a/go/pkg/basecamp/example_test.go +++ b/go/pkg/basecamp/example_test.go @@ -134,7 +134,7 @@ func ExampleTodosService_List() { todolistID := int64(789012) - // List all todos in a todolist + // List todos in a todolist todosResult, err := client.ForAccount("12345").Todos().List(ctx, todolistID, nil) if err != nil { log.Fatal(err) @@ -149,6 +149,26 @@ func ExampleTodosService_List() { } } +func ExampleTodosService_List_completed() { + cfg := basecamp.DefaultConfig() + token := &basecamp.StaticTokenProvider{Token: os.Getenv("BASECAMP_TOKEN")} + client := basecamp.NewClient(cfg, token) + + ctx := context.Background() + + todolistID := int64(789012) + + // List completed todos in a todolist + todosResult, err := client.ForAccount("12345").Todos().List(ctx, todolistID, &basecamp.TodoListOptions{Status: "completed"}) + if err != nil { + log.Fatal(err) + } + + for _, t := range todosResult.Todos { + fmt.Printf("[x] %s\n", t.Content) + } +} + func ExampleTodosService_Create() { cfg := basecamp.DefaultConfig() token := &basecamp.StaticTokenProvider{Token: os.Getenv("BASECAMP_TOKEN")} diff --git a/go/pkg/basecamp/todos.go b/go/pkg/basecamp/todos.go index 99bd0d5a..be4e3756 100644 --- a/go/pkg/basecamp/todos.go +++ b/go/pkg/basecamp/todos.go @@ -95,9 +95,10 @@ type Bucket struct { // TodoListOptions specifies options for listing todos. type TodoListOptions struct { - // Status filters by completion status. - // "completed" returns completed todos, "pending" returns pending todos. - // Empty returns all todos. + // Status filters todos by lifecycle or completion status. + // Lifecycle statuses: "active", "archived", "trashed". + // Backwards-compatible completion shorthands: "completed", "pending", "incomplete". + // Empty returns the API default (pending/incomplete todos). Status string // Limit is the maximum number of todos to return. @@ -188,10 +189,26 @@ func (s *TodosService) List(ctx context.Context, todolistID int64, opts *TodoLis ctx = s.client.parent.hooks.OnOperationStart(ctx, op) defer func() { s.client.parent.hooks.OnOperationEnd(ctx, op, err, time.Since(start)) }() - // Build params for generated client + // Build params for generated client. + // The BC3 todos endpoint uses two different query shapes: + // - status=active|archived|trashed for lifecycle state + // - completed=true for completed todos + // Historically this wrapper exposed completion filtering via Status, so we + // keep mapping "completed"/"pending"/"incomplete" here for compatibility. var params *generated.ListTodosParams if opts != nil && opts.Status != "" { - params = &generated.ListTodosParams{Status: opts.Status} + built := &generated.ListTodosParams{} + switch opts.Status { + case "completed": + built.Completed = true + case "pending", "incomplete": + // Omit both params: the API returns pending/incomplete todos by default. + default: + built.Status = opts.Status + } + if built.Status != "" || built.Completed { + params = built + } } // Call generated client for first page (spec-conformant - no manual path construction) diff --git a/go/pkg/basecamp/todos_test.go b/go/pkg/basecamp/todos_test.go index b1a4f9ca..67fe0962 100644 --- a/go/pkg/basecamp/todos_test.go +++ b/go/pkg/basecamp/todos_test.go @@ -495,8 +495,12 @@ func TestTodoListOptions_StatusFilter(t *testing.T) { name string status string }{ + {"active", "active"}, + {"archived", "archived"}, + {"trashed", "trashed"}, {"completed", "completed"}, {"pending", "pending"}, + {"incomplete", "incomplete"}, {"empty", ""}, } @@ -951,6 +955,127 @@ func testTodosServer(t *testing.T, handler http.HandlerFunc) *TodosService { return account.Todos() } +func TestTodosService_ListCompletedFilter(t *testing.T) { + fixture := loadTodosFixture(t, "list.json") + var gotStatus string + var gotCompleted string + var hasStatus bool + var hasCompleted bool + + svc := testTodosServer(t, func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + hasStatus = query.Has("status") + hasCompleted = query.Has("completed") + gotStatus = query.Get("status") + gotCompleted = query.Get("completed") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write(fixture) + }) + + result, err := svc.List(context.Background(), 1069479519, &TodoListOptions{Status: "completed"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(result.Todos) != 2 { + t.Fatalf("expected 2 todos, got %d", len(result.Todos)) + } + if hasStatus { + t.Errorf("expected status query to be omitted, got %q", gotStatus) + } + if !hasCompleted || gotCompleted != "true" { + t.Errorf("expected completed=true, got present=%t value=%q", hasCompleted, gotCompleted) + } +} + +func TestTodosService_ListPendingFilterOmitsQueryParams(t *testing.T) { + fixture := loadTodosFixture(t, "list.json") + var gotStatus string + var gotCompleted string + var hasStatus bool + var hasCompleted bool + + svc := testTodosServer(t, func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + hasStatus = query.Has("status") + hasCompleted = query.Has("completed") + gotStatus = query.Get("status") + gotCompleted = query.Get("completed") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write(fixture) + }) + + _, err := svc.List(context.Background(), 1069479519, &TodoListOptions{Status: "pending"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if hasStatus { + t.Errorf("expected status query to be omitted, got %q", gotStatus) + } + if hasCompleted { + t.Errorf("expected completed query to be omitted, got %q", gotCompleted) + } +} + +func TestTodosService_ListIncompleteFilterOmitsQueryParams(t *testing.T) { + fixture := loadTodosFixture(t, "list.json") + var gotStatus string + var gotCompleted string + var hasStatus bool + var hasCompleted bool + + svc := testTodosServer(t, func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + hasStatus = query.Has("status") + hasCompleted = query.Has("completed") + gotStatus = query.Get("status") + gotCompleted = query.Get("completed") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write(fixture) + }) + + _, err := svc.List(context.Background(), 1069479519, &TodoListOptions{Status: "incomplete"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if hasStatus { + t.Errorf("expected status query to be omitted, got %q", gotStatus) + } + if hasCompleted { + t.Errorf("expected completed query to be omitted, got %q", gotCompleted) + } +} + +func TestTodosService_ListLifecycleStatusFilter(t *testing.T) { + fixture := loadTodosFixture(t, "list.json") + var gotStatus string + var gotCompleted string + var hasCompleted bool + + svc := testTodosServer(t, func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + gotStatus = query.Get("status") + hasCompleted = query.Has("completed") + gotCompleted = query.Get("completed") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write(fixture) + }) + + _, err := svc.List(context.Background(), 1069479519, &TodoListOptions{Status: "archived"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotStatus != "archived" { + t.Errorf("expected status=archived, got %q", gotStatus) + } + if hasCompleted { + t.Errorf("expected completed query to be omitted, got %q", gotCompleted) + } +} + func TestTodosService_Update(t *testing.T) { fixture := loadTodosFixture(t, "get.json") var receivedBody map[string]any