Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions go/pkg/basecamp/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
//
Expand Down Expand Up @@ -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
22 changes: 21 additions & 1 deletion go/pkg/basecamp/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")}
Expand Down
27 changes: 22 additions & 5 deletions go/pkg/basecamp/todos.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
125 changes: 125 additions & 0 deletions go/pkg/basecamp/todos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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", ""},
}

Expand Down Expand Up @@ -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
Expand Down
Loading