Implement Task search and move methods

- SearchTasks: search tasks using Kanboard query syntax
- MoveTaskPosition: move task to specific position in column/swimlane
- MoveTaskToProject: move task to different project
- Comprehensive test coverage for all methods
This commit is contained in:
Oliver Jakoubek 2026-01-15 18:28:11 +01:00
commit c2a107054f
3 changed files with 247 additions and 1 deletions

View file

@ -11,7 +11,7 @@
{"id":"kanboard-api-apl","title":"Implement Project API methods","description":"Implement direct API methods for project/board operations.\n\n## Methods to implement\n- GetAllProjects(ctx) ([]Project, error) - getAllProjects\n- GetProjectByID(ctx, projectID int) (*Project, error) - getProjectById\n- GetProjectByName(ctx, name string) (*Project, error) - getProjectByName (Nice-to-have)\n\n## Files to create\n- projects.go\n\n## Acceptance criteria\n- All methods use context for cancellation\n- Proper error handling and wrapping\n- Returns ErrProjectNotFound when appropriate","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:15.864764497+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:21:36.226187777+01:00","closed_at":"2026-01-15T18:21:36.226187777+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-apl","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:42:52.850751716+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-apl","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:42:52.946556814+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-apl","title":"Implement Project API methods","description":"Implement direct API methods for project/board operations.\n\n## Methods to implement\n- GetAllProjects(ctx) ([]Project, error) - getAllProjects\n- GetProjectByID(ctx, projectID int) (*Project, error) - getProjectById\n- GetProjectByName(ctx, name string) (*Project, error) - getProjectByName (Nice-to-have)\n\n## Files to create\n- projects.go\n\n## Acceptance criteria\n- All methods use context for cancellation\n- Proper error handling and wrapping\n- Returns ErrProjectNotFound when appropriate","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:15.864764497+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:21:36.226187777+01:00","closed_at":"2026-01-15T18:21:36.226187777+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-apl","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:42:52.850751716+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-apl","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:42:52.946556814+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"kanboard-api-c3i","title":"Create README.md with documentation","description":"Create comprehensive README.md documentation in English.\n\n## Sections to include\n- Project overview and features\n- Installation instructions\n- Quick start example\n- API documentation overview\n- Authentication methods (API token, user/password)\n- Fluent API examples\n- Direct API examples\n- Error handling\n- Thread-safety notes\n- Tag operations warning (non-atomic)\n- License (MIT)\n\n## Example code to include\n```go\n// Client creation\nclient := kanboard.NewClient(\"https://kanboard.example.com\").\n WithAPIToken(\"my-token\").\n WithTimeout(30 * time.Second)\n\n// Fluent API\ntask, _ := client.Board(1).CreateTask(ctx,\n kanboard.NewTask(\"My Task\").WithDescription(\"Details\"))\n\n// Direct API\ntasks, _ := client.GetAllTasks(ctx, 1, kanboard.StatusActive)\n```\n\n## Files to create\n- README.md\n\n## Acceptance criteria\n- Clear installation instructions\n- Working code examples\n- Documents all major features","status":"open","priority":2,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:53.228407343+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:53.228407343+01:00","dependencies":[{"issue_id":"kanboard-api-c3i","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:46:55.445847253+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-c3i","title":"Create README.md with documentation","description":"Create comprehensive README.md documentation in English.\n\n## Sections to include\n- Project overview and features\n- Installation instructions\n- Quick start example\n- API documentation overview\n- Authentication methods (API token, user/password)\n- Fluent API examples\n- Direct API examples\n- Error handling\n- Thread-safety notes\n- Tag operations warning (non-atomic)\n- License (MIT)\n\n## Example code to include\n```go\n// Client creation\nclient := kanboard.NewClient(\"https://kanboard.example.com\").\n WithAPIToken(\"my-token\").\n WithTimeout(30 * time.Second)\n\n// Fluent API\ntask, _ := client.Board(1).CreateTask(ctx,\n kanboard.NewTask(\"My Task\").WithDescription(\"Details\"))\n\n// Direct API\ntasks, _ := client.GetAllTasks(ctx, 1, kanboard.StatusActive)\n```\n\n## Files to create\n- README.md\n\n## Acceptance criteria\n- Clear installation instructions\n- Working code examples\n- Documents all major features","status":"open","priority":2,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:53.228407343+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:53.228407343+01:00","dependencies":[{"issue_id":"kanboard-api-c3i","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:46:55.445847253+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"kanboard-api-cyc","title":"Implement entity structs (types.go)","description":"Implement all entity structs for Kanboard API responses.\n\n## Structs to implement\n- Project (ID, Name, Description, IsActive, Token, LastModified, etc.)\n- Task (ID, Title, Description, dates, ColorID, ProjectID, ColumnID, etc.)\n- Column (ID, Title, Position, ProjectID, TaskLimit, Description)\n- Category (ID, Name, ProjectID, ColorID)\n- Comment (ID, TaskID, UserID, DateCreation, Content, Username, etc.)\n- TaskLink (ID, LinkID, TaskID, OppositeTaskID, Label, Title)\n- TaskFile (ID, Name, Path, IsImage, TaskID, DateCreation, UserID, Size)\n- Tag (ID, Name, ProjectID, ColorID)\n- TaskStatus enum (StatusActive, StatusInactive)\n\n## Request structs\n- CreateTaskRequest\n- UpdateTaskRequest (with pointer fields for optional values)\n\n## Files to create\n- types.go\n\n## Acceptance criteria\n- All JSON tags match Kanboard API\n- Optional fields use pointers with omitempty\n- Timestamp fields use custom Timestamp type","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:55.484472208+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:18:40.163015715+01:00","closed_at":"2026-01-15T18:18:40.163015715+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-cyc","depends_on_id":"kanboard-api-25y","type":"blocks","created_at":"2026-01-15T17:42:48.385166815+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-cyc","title":"Implement entity structs (types.go)","description":"Implement all entity structs for Kanboard API responses.\n\n## Structs to implement\n- Project (ID, Name, Description, IsActive, Token, LastModified, etc.)\n- Task (ID, Title, Description, dates, ColorID, ProjectID, ColumnID, etc.)\n- Column (ID, Title, Position, ProjectID, TaskLimit, Description)\n- Category (ID, Name, ProjectID, ColorID)\n- Comment (ID, TaskID, UserID, DateCreation, Content, Username, etc.)\n- TaskLink (ID, LinkID, TaskID, OppositeTaskID, Label, Title)\n- TaskFile (ID, Name, Path, IsImage, TaskID, DateCreation, UserID, Size)\n- Tag (ID, Name, ProjectID, ColorID)\n- TaskStatus enum (StatusActive, StatusInactive)\n\n## Request structs\n- CreateTaskRequest\n- UpdateTaskRequest (with pointer fields for optional values)\n\n## Files to create\n- types.go\n\n## Acceptance criteria\n- All JSON tags match Kanboard API\n- Optional fields use pointers with omitempty\n- Timestamp fields use custom Timestamp type","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:55.484472208+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:18:40.163015715+01:00","closed_at":"2026-01-15T18:18:40.163015715+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-cyc","depends_on_id":"kanboard-api-25y","type":"blocks","created_at":"2026-01-15T17:42:48.385166815+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"kanboard-api-fue","title":"Implement Task search and move methods","description":"Implement task search and movement API methods.\n\n## Methods to implement\n- SearchTasks(ctx, projectID int, query string) ([]Task, error) - searchTasks\n- MoveTaskPosition(ctx, projectID, taskID, columnID, position, swimlaneID int) error - moveTaskPosition\n- MoveTaskToProject(ctx, taskID, projectID int) error - moveTaskToProject\n\n## Files to create\n- tasks.go (extend)\n\n## Acceptance criteria\n- Search supports Kanboard query syntax\n- MoveTaskPosition handles column and position\n- Proper error handling for invalid moves","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:17.380817511+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:35:17.380817511+01:00","dependencies":[{"issue_id":"kanboard-api-fue","depends_on_id":"kanboard-api-91a","type":"blocks","created_at":"2026-01-15T17:42:53.59290181+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-fue","title":"Implement Task search and move methods","description":"Implement task search and movement API methods.\n\n## Methods to implement\n- SearchTasks(ctx, projectID int, query string) ([]Task, error) - searchTasks\n- MoveTaskPosition(ctx, projectID, taskID, columnID, position, swimlaneID int) error - moveTaskPosition\n- MoveTaskToProject(ctx, taskID, projectID int) error - moveTaskToProject\n\n## Files to create\n- tasks.go (extend)\n\n## Acceptance criteria\n- Search supports Kanboard query syntax\n- MoveTaskPosition handles column and position\n- Proper error handling for invalid moves","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:17.380817511+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:28:05.809359198+01:00","closed_at":"2026-01-15T18:28:05.809359198+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-fue","depends_on_id":"kanboard-api-91a","type":"blocks","created_at":"2026-01-15T17:42:53.59290181+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"kanboard-api-h4u","title":"Implement Comment API methods","description":"Implement direct API methods for comment operations.\n\n## Methods to implement\n- GetAllComments(ctx, taskID int) ([]Comment, error) - getAllComments\n- CreateComment(ctx, taskID, userID int, content string) (*Comment, error) - createComment\n- UpdateComment(ctx, commentID int, content string) error - updateComment\n- RemoveComment(ctx, commentID int) error - removeComment\n\n## TaskScope methods to add\n- AddComment(ctx, content string) (*Comment, error)\n- GetComments(ctx) ([]Comment, error)\n\n## Files to create\n- comments.go\n- task_scope.go (extend)\n\n## Acceptance criteria\n- CreateComment returns the created comment\n- Returns ErrCommentNotFound when appropriate","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:08.156950163+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:08.156950163+01:00","dependencies":[{"issue_id":"kanboard-api-h4u","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:43:48.898295911+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-h4u","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:49.058259216+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-h4u","title":"Implement Comment API methods","description":"Implement direct API methods for comment operations.\n\n## Methods to implement\n- GetAllComments(ctx, taskID int) ([]Comment, error) - getAllComments\n- CreateComment(ctx, taskID, userID int, content string) (*Comment, error) - createComment\n- UpdateComment(ctx, commentID int, content string) error - updateComment\n- RemoveComment(ctx, commentID int) error - removeComment\n\n## TaskScope methods to add\n- AddComment(ctx, content string) (*Comment, error)\n- GetComments(ctx) ([]Comment, error)\n\n## Files to create\n- comments.go\n- task_scope.go (extend)\n\n## Acceptance criteria\n- CreateComment returns the created comment\n- Returns ErrCommentNotFound when appropriate","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:08.156950163+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:08.156950163+01:00","dependencies":[{"issue_id":"kanboard-api-h4u","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:43:48.898295911+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-h4u","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:49.058259216+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"kanboard-api-k33","title":"Implement authentication system","description":"Implement HTTP Basic Auth support for both API token and user/password authentication.\n\n## Requirements\n- Authenticator interface for auth strategies\n- API Token auth: HTTP Basic with username `jsonrpc` and API token as password\n- User/Password auth: HTTP Basic with username and password\n- WithAPIToken(token string) fluent method\n- WithBasicAuth(username, password string) fluent method\n- Secure handling - no credential storage/logging\n\n## Files to create\n- auth.go\n\n## Acceptance criteria\n- Both auth methods work correctly\n- Credentials properly encoded in Authorization header\n- No sensitive data logged","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:53.631074781+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:11:42.58778642+01:00","closed_at":"2026-01-15T18:11:42.58778642+01:00","close_reason":"Closed"} {"id":"kanboard-api-k33","title":"Implement authentication system","description":"Implement HTTP Basic Auth support for both API token and user/password authentication.\n\n## Requirements\n- Authenticator interface for auth strategies\n- API Token auth: HTTP Basic with username `jsonrpc` and API token as password\n- User/Password auth: HTTP Basic with username and password\n- WithAPIToken(token string) fluent method\n- WithBasicAuth(username, password string) fluent method\n- Secure handling - no credential storage/logging\n\n## Files to create\n- auth.go\n\n## Acceptance criteria\n- Both auth methods work correctly\n- Credentials properly encoded in Authorization header\n- No sensitive data logged","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:53.631074781+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:11:42.58778642+01:00","closed_at":"2026-01-15T18:11:42.58778642+01:00","close_reason":"Closed"}
{"id":"kanboard-api-l72","title":"Implement global task search with parallel execution","description":"Implement cross-project task search with parallel execution.\n\n## Method to implement\n- SearchTasksGlobally(ctx, query string) ([]Task, error)\n\n## Workflow\n1. Get all projects via getAllProjects\n2. Execute searchTasks for each project in parallel (errgroup)\n3. Aggregate and return all results\n4. Respect context cancellation\n\n## Implementation details\n- Use golang.org/x/sync/errgroup for parallel execution\n- Results channel to collect tasks\n- Handle errors from any project search\n- Context propagation for cancellation\n\n## Files to modify\n- tasks.go\n\n## Acceptance criteria\n- Parallel execution improves performance\n- Context cancellation stops all goroutines\n- Single project failure cancels remaining searches","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:29.533649255+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:29.533649255+01:00","dependencies":[{"issue_id":"kanboard-api-l72","depends_on_id":"kanboard-api-apl","type":"blocks","created_at":"2026-01-15T17:45:32.11473352+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-l72","depends_on_id":"kanboard-api-fue","type":"blocks","created_at":"2026-01-15T17:45:32.178153305+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-l72","title":"Implement global task search with parallel execution","description":"Implement cross-project task search with parallel execution.\n\n## Method to implement\n- SearchTasksGlobally(ctx, query string) ([]Task, error)\n\n## Workflow\n1. Get all projects via getAllProjects\n2. Execute searchTasks for each project in parallel (errgroup)\n3. Aggregate and return all results\n4. Respect context cancellation\n\n## Implementation details\n- Use golang.org/x/sync/errgroup for parallel execution\n- Results channel to collect tasks\n- Handle errors from any project search\n- Context propagation for cancellation\n\n## Files to modify\n- tasks.go\n\n## Acceptance criteria\n- Parallel execution improves performance\n- Context cancellation stops all goroutines\n- Single project failure cancels remaining searches","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:29.533649255+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:29.533649255+01:00","dependencies":[{"issue_id":"kanboard-api-l72","depends_on_id":"kanboard-api-apl","type":"blocks","created_at":"2026-01-15T17:45:32.11473352+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-l72","depends_on_id":"kanboard-api-fue","type":"blocks","created_at":"2026-01-15T17:45:32.178153305+01:00","created_by":"Oliver Jakoubek"}]}

View file

@ -100,3 +100,61 @@ func (c *Client) OpenTask(ctx context.Context, taskID int) error {
return nil return nil
} }
// SearchTasks searches for tasks in a project using Kanboard's query syntax.
// The query supports filters like: status:open, assignee:me, color:red, etc.
func (c *Client) SearchTasks(ctx context.Context, projectID int, query string) ([]Task, error) {
params := map[string]any{
"project_id": projectID,
"query": query,
}
var result []Task
if err := c.call(ctx, "searchTasks", params, &result); err != nil {
return nil, fmt.Errorf("searchTasks: %w", err)
}
return result, nil
}
// MoveTaskPosition moves a task to a specific position within a column and swimlane.
// Use position=1 for first position, position=0 to append at end.
func (c *Client) MoveTaskPosition(ctx context.Context, projectID, taskID, columnID, position, swimlaneID int) error {
params := map[string]int{
"project_id": projectID,
"task_id": taskID,
"column_id": columnID,
"position": position,
"swimlane_id": swimlaneID,
}
var success bool
if err := c.call(ctx, "moveTaskPosition", params, &success); err != nil {
return fmt.Errorf("moveTaskPosition: %w", err)
}
if !success {
return fmt.Errorf("moveTaskPosition: failed to move task %d", taskID)
}
return nil
}
// MoveTaskToProject moves a task to a different project.
func (c *Client) MoveTaskToProject(ctx context.Context, taskID, projectID int) error {
params := map[string]int{
"task_id": taskID,
"project_id": projectID,
}
var success bool
if err := c.call(ctx, "moveTaskToProject", params, &success); err != nil {
return fmt.Errorf("moveTaskToProject: %w", err)
}
if !success {
return fmt.Errorf("moveTaskToProject: failed to move task %d to project %d", taskID, projectID)
}
return nil
}

View file

@ -508,3 +508,191 @@ func TestClient_GetTask_ContextCanceled(t *testing.T) {
t.Fatal("expected error due to canceled context") t.Fatal("expected error due to canceled context")
} }
} }
func TestClient_SearchTasks(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
if req.Method != "searchTasks" {
t.Errorf("expected method=searchTasks, got %s", req.Method)
}
params := req.Params.(map[string]any)
if params["project_id"].(float64) != 1 {
t.Errorf("expected project_id=1, got %v", params["project_id"])
}
if params["query"] != "status:open assignee:me" {
t.Errorf("expected query='status:open assignee:me', got %v", params["query"])
}
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: json.RawMessage(`[
{"id": "1", "title": "Task One", "project_id": "1", "is_active": "1"},
{"id": "2", "title": "Task Two", "project_id": "1", "is_active": "1"}
]`),
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("test-token")
tasks, err := client.SearchTasks(context.Background(), 1, "status:open assignee:me")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(tasks) != 2 {
t.Errorf("expected 2 tasks, got %d", len(tasks))
}
}
func TestClient_SearchTasks_Empty(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: json.RawMessage(`[]`),
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("test-token")
tasks, err := client.SearchTasks(context.Background(), 1, "title:nonexistent")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(tasks) != 0 {
t.Errorf("expected 0 tasks, got %d", len(tasks))
}
}
func TestClient_MoveTaskPosition(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
if req.Method != "moveTaskPosition" {
t.Errorf("expected method=moveTaskPosition, got %s", req.Method)
}
params := req.Params.(map[string]any)
if params["project_id"].(float64) != 1 {
t.Errorf("expected project_id=1, got %v", params["project_id"])
}
if params["task_id"].(float64) != 42 {
t.Errorf("expected task_id=42, got %v", params["task_id"])
}
if params["column_id"].(float64) != 2 {
t.Errorf("expected column_id=2, got %v", params["column_id"])
}
if params["position"].(float64) != 1 {
t.Errorf("expected position=1, got %v", params["position"])
}
if params["swimlane_id"].(float64) != 0 {
t.Errorf("expected swimlane_id=0, got %v", params["swimlane_id"])
}
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: json.RawMessage(`true`),
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("test-token")
err := client.MoveTaskPosition(context.Background(), 1, 42, 2, 1, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestClient_MoveTaskPosition_Failure(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: json.RawMessage(`false`),
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("test-token")
err := client.MoveTaskPosition(context.Background(), 1, 42, 999, 1, 0)
if err == nil {
t.Fatal("expected error for failed move")
}
}
func TestClient_MoveTaskToProject(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
if req.Method != "moveTaskToProject" {
t.Errorf("expected method=moveTaskToProject, got %s", req.Method)
}
params := req.Params.(map[string]any)
if params["task_id"].(float64) != 42 {
t.Errorf("expected task_id=42, got %v", params["task_id"])
}
if params["project_id"].(float64) != 5 {
t.Errorf("expected project_id=5, got %v", params["project_id"])
}
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: json.RawMessage(`true`),
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("test-token")
err := client.MoveTaskToProject(context.Background(), 42, 5)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestClient_MoveTaskToProject_Failure(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: json.RawMessage(`false`),
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("test-token")
err := client.MoveTaskToProject(context.Background(), 42, 999)
if err == nil {
t.Fatal("expected error for failed move")
}
}