Implement MoveToNextColumn and MoveToPreviousColumn
- MoveToNextColumn: moves task to next column in workflow - MoveToPreviousColumn: moves task to previous column - Gets columns sorted by position, finds current, moves to adjacent - Returns ErrAlreadyInLastColumn when at workflow end - Returns ErrAlreadyInFirstColumn when at workflow start - Handles column gaps (non-sequential positions) - Comprehensive test coverage for all edge cases
This commit is contained in:
parent
527d27b73f
commit
4907a7caad
3 changed files with 285 additions and 1 deletions
|
|
@ -18,7 +18,7 @@
|
||||||
{"id":"kanboard-api-l9b","title":"Implement Column API methods","description":"Implement direct API methods for column operations.\n\n## Methods to implement\n- GetColumns(ctx, projectID int) ([]Column, error) - getColumns\n- GetColumn(ctx, columnID int) (*Column, error) - getColumn\n\n## Files to create\n- columns.go\n\n## Acceptance criteria\n- Columns returned sorted by position\n- Returns ErrColumnNotFound when appropriate","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:16.23369865+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:23:40.255170515+01:00","closed_at":"2026-01-15T18:23:40.255170515+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-l9b","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:42:53.022754817+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-l9b","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:42:53.094484504+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"kanboard-api-l9b","title":"Implement Column API methods","description":"Implement direct API methods for column operations.\n\n## Methods to implement\n- GetColumns(ctx, projectID int) ([]Column, error) - getColumns\n- GetColumn(ctx, columnID int) (*Column, error) - getColumn\n\n## Files to create\n- columns.go\n\n## Acceptance criteria\n- Columns returned sorted by position\n- Returns ErrColumnNotFound when appropriate","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:16.23369865+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:23:40.255170515+01:00","closed_at":"2026-01-15T18:23:40.255170515+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-l9b","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:42:53.022754817+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-l9b","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:42:53.094484504+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"kanboard-api-ob2","title":"Add MIT license","description":"Add MIT license file to the repository.\n\n## Files to create\n- LICENSE\n\n## Content\nStandard MIT license text with appropriate copyright holder.\n\n## Acceptance criteria\n- Valid MIT license\n- Proper copyright attribution","status":"open","priority":3,"issue_type":"chore","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:53.955909551+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:53.955909551+01:00"}
|
{"id":"kanboard-api-ob2","title":"Add MIT license","description":"Add MIT license file to the repository.\n\n## Files to create\n- LICENSE\n\n## Content\nStandard MIT license text with appropriate copyright holder.\n\n## Acceptance criteria\n- Valid MIT license\n- Proper copyright attribution","status":"open","priority":3,"issue_type":"chore","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:53.955909551+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:53.955909551+01:00"}
|
||||||
{"id":"kanboard-api-s7k","title":"Implement error types and handling","description":"Implement comprehensive error types for the Kanboard API client.\n\n## Sentinel Errors\n- ErrConnectionFailed, ErrTimeout (network)\n- ErrUnauthorized, ErrForbidden (auth)\n- ErrNotFound, ErrProjectNotFound, ErrTaskNotFound, ErrColumnNotFound, ErrCommentNotFound (resources)\n- ErrAlreadyInLastColumn, ErrAlreadyInFirstColumn, ErrTaskClosed, ErrTaskOpen (logic)\n- ErrEmptyTitle, ErrInvalidProjectID (validation)\n- ErrAPIError (API)\n\n## APIError struct\n- Code int\n- Message string\n- Error() string method\n\n## Helper functions\n- IsNotFound(err error) bool\n- IsUnauthorized(err error) bool\n- IsAPIError(err error) bool\n\n## Files to create\n- errors.go\n\n## Acceptance criteria\n- All errors properly support errors.Is/errors.As\n- Error wrapping with %w for context preservation","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:54.484116412+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:13:03.195687148+01:00","closed_at":"2026-01-15T18:13:03.195687148+01:00","close_reason":"Closed"}
|
{"id":"kanboard-api-s7k","title":"Implement error types and handling","description":"Implement comprehensive error types for the Kanboard API client.\n\n## Sentinel Errors\n- ErrConnectionFailed, ErrTimeout (network)\n- ErrUnauthorized, ErrForbidden (auth)\n- ErrNotFound, ErrProjectNotFound, ErrTaskNotFound, ErrColumnNotFound, ErrCommentNotFound (resources)\n- ErrAlreadyInLastColumn, ErrAlreadyInFirstColumn, ErrTaskClosed, ErrTaskOpen (logic)\n- ErrEmptyTitle, ErrInvalidProjectID (validation)\n- ErrAPIError (API)\n\n## APIError struct\n- Code int\n- Message string\n- Error() string method\n\n## Helper functions\n- IsNotFound(err error) bool\n- IsUnauthorized(err error) bool\n- IsAPIError(err error) bool\n\n## Files to create\n- errors.go\n\n## Acceptance criteria\n- All errors properly support errors.Is/errors.As\n- Error wrapping with %w for context preservation","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:54.484116412+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:13:03.195687148+01:00","closed_at":"2026-01-15T18:13:03.195687148+01:00","close_reason":"Closed"}
|
||||||
{"id":"kanboard-api-t36","title":"Implement MoveToNextColumn with column logic","description":"Implement TaskScope.MoveToNextColumn with automatic column position logic.\n\n## Method to implement\n- TaskScope.MoveToNextColumn(ctx) error\n\n## Workflow\n1. Get task via getTask\n2. Get columns for task's project via getColumns\n3. Sort columns by position\n4. Find current column position\n5. If last column: return ErrAlreadyInLastColumn\n6. Move to next column via moveTaskPosition\n\n## Files to modify\n- task_scope.go\n\n## Acceptance criteria\n- Correctly identifies next column by position\n- Returns ErrAlreadyInLastColumn for last column\n- Handles column gaps (positions 1, 3, 5 etc.)","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:29.900647957+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:29.900647957+01:00","dependencies":[{"issue_id":"kanboard-api-t36","depends_on_id":"kanboard-api-una","type":"blocks","created_at":"2026-01-15T17:45:32.256714732+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-t36","depends_on_id":"kanboard-api-l9b","type":"blocks","created_at":"2026-01-15T17:45:32.319227723+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"kanboard-api-t36","title":"Implement MoveToNextColumn with column logic","description":"Implement TaskScope.MoveToNextColumn with automatic column position logic.\n\n## Method to implement\n- TaskScope.MoveToNextColumn(ctx) error\n\n## Workflow\n1. Get task via getTask\n2. Get columns for task's project via getColumns\n3. Sort columns by position\n4. Find current column position\n5. If last column: return ErrAlreadyInLastColumn\n6. Move to next column via moveTaskPosition\n\n## Files to modify\n- task_scope.go\n\n## Acceptance criteria\n- Correctly identifies next column by position\n- Returns ErrAlreadyInLastColumn for last column\n- Handles column gaps (positions 1, 3, 5 etc.)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:29.900647957+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:41:56.137534164+01:00","closed_at":"2026-01-15T18:41:56.137534164+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-t36","depends_on_id":"kanboard-api-una","type":"blocks","created_at":"2026-01-15T17:45:32.256714732+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-t36","depends_on_id":"kanboard-api-l9b","type":"blocks","created_at":"2026-01-15T17:45:32.319227723+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"kanboard-api-ukc","title":"Set up Mage build system","description":"Set up Mage build system for development automation.\n\n## Directory structure\n```\nmagefiles/\n├── go.mod\n├── go.sum\n└── magefile.go\n```\n\n## Targets to implement\n- Test() - Run all tests with race detection and coverage\n- Coverage() - Run tests and open HTML coverage report\n- Lint() - Run golangci-lint\n- Build() - Verify module compiles\n\n## Files to create\n- magefiles/go.mod\n- magefiles/magefile.go\n\n## Acceptance criteria\n- `mage test` runs tests with -race and -coverprofile\n- `mage lint` runs golangci-lint\n- `mage build` verifies compilation","status":"open","priority":2,"issue_type":"chore","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:30.260235504+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:30.260235504+01:00"}
|
{"id":"kanboard-api-ukc","title":"Set up Mage build system","description":"Set up Mage build system for development automation.\n\n## Directory structure\n```\nmagefiles/\n├── go.mod\n├── go.sum\n└── magefile.go\n```\n\n## Targets to implement\n- Test() - Run all tests with race detection and coverage\n- Coverage() - Run tests and open HTML coverage report\n- Lint() - Run golangci-lint\n- Build() - Verify module compiles\n\n## Files to create\n- magefiles/go.mod\n- magefiles/magefile.go\n\n## Acceptance criteria\n- `mage test` runs tests with -race and -coverprofile\n- `mage lint` runs golangci-lint\n- `mage build` verifies compilation","status":"open","priority":2,"issue_type":"chore","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:30.260235504+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:30.260235504+01:00"}
|
||||||
{"id":"kanboard-api-uls","title":"Implement Client struct with fluent configuration","description":"Implement the main Client struct with fluent builder pattern for configuration.\n\n## Requirements\n- Client struct with baseURL, httpClient, auth, logger, requestID\n- NewClient(baseURL string) constructor\n- Fluent configuration methods:\n - WithAPIToken(token string) *Client\n - WithBasicAuth(username, password string) *Client\n - WithHTTPClient(client *http.Client) *Client\n - WithTimeout(timeout time.Duration) *Client\n - WithLogger(logger *slog.Logger) *Client\n- Default timeout of 30 seconds\n- Thread-safe for concurrent use\n\n## Files to create\n- client.go\n\n## Acceptance criteria\n- Client is immutable after configuration\n- Thread-safe concurrent usage\n- Configurable HTTP client and logger","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:54.051926732+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:14:16.530243958+01:00","closed_at":"2026-01-15T18:14:16.530243958+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-uls","depends_on_id":"kanboard-api-2g1","type":"blocks","created_at":"2026-01-15T17:42:48.457362117+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-uls","depends_on_id":"kanboard-api-k33","type":"blocks","created_at":"2026-01-15T17:42:48.576622508+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-uls","depends_on_id":"kanboard-api-s7k","type":"blocks","created_at":"2026-01-15T17:42:48.749342195+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"kanboard-api-uls","title":"Implement Client struct with fluent configuration","description":"Implement the main Client struct with fluent builder pattern for configuration.\n\n## Requirements\n- Client struct with baseURL, httpClient, auth, logger, requestID\n- NewClient(baseURL string) constructor\n- Fluent configuration methods:\n - WithAPIToken(token string) *Client\n - WithBasicAuth(username, password string) *Client\n - WithHTTPClient(client *http.Client) *Client\n - WithTimeout(timeout time.Duration) *Client\n - WithLogger(logger *slog.Logger) *Client\n- Default timeout of 30 seconds\n- Thread-safe for concurrent use\n\n## Files to create\n- client.go\n\n## Acceptance criteria\n- Client is immutable after configuration\n- Thread-safe concurrent usage\n- Configurable HTTP client and logger","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:54.051926732+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:14:16.530243958+01:00","closed_at":"2026-01-15T18:14:16.530243958+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-uls","depends_on_id":"kanboard-api-2g1","type":"blocks","created_at":"2026-01-15T17:42:48.457362117+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-uls","depends_on_id":"kanboard-api-k33","type":"blocks","created_at":"2026-01-15T17:42:48.576622508+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-uls","depends_on_id":"kanboard-api-s7k","type":"blocks","created_at":"2026-01-15T17:42:48.749342195+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"kanboard-api-una","title":"Implement TaskScope fluent builder (core methods)","description":"Implement TaskScope for fluent task-scoped operations (core methods).\n\n## Requirements\n- TaskScope struct with client and taskID\n- Client.Task(taskID int) *TaskScope method\n- Core TaskScope methods:\n - Get(ctx) (*Task, error)\n - Close(ctx) error\n - Open(ctx) error\n - MoveToColumn(ctx, columnID int) error\n - MoveToProject(ctx, projectID int) error\n - Update(ctx, params *TaskUpdateParams) error\n\n## Files to create\n- task_scope.go\n\n## Example usage\n```go\ntask, _ := client.Task(123).Get(ctx)\nclient.Task(123).Close(ctx)\nclient.Task(123).Update(ctx, kanboard.NewTaskUpdate().SetTitle(\"New\"))\n```\n\n## Acceptance criteria\n- All methods delegate to direct Client methods\n- Proper error propagation","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:41.173930396+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:34:32.369975567+01:00","closed_at":"2026-01-15T18:34:32.369975567+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-una","depends_on_id":"kanboard-api-91a","type":"blocks","created_at":"2026-01-15T17:43:31.198935686+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-una","depends_on_id":"kanboard-api-5fb","type":"blocks","created_at":"2026-01-15T17:43:31.261453756+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"kanboard-api-una","title":"Implement TaskScope fluent builder (core methods)","description":"Implement TaskScope for fluent task-scoped operations (core methods).\n\n## Requirements\n- TaskScope struct with client and taskID\n- Client.Task(taskID int) *TaskScope method\n- Core TaskScope methods:\n - Get(ctx) (*Task, error)\n - Close(ctx) error\n - Open(ctx) error\n - MoveToColumn(ctx, columnID int) error\n - MoveToProject(ctx, projectID int) error\n - Update(ctx, params *TaskUpdateParams) error\n\n## Files to create\n- task_scope.go\n\n## Example usage\n```go\ntask, _ := client.Task(123).Get(ctx)\nclient.Task(123).Close(ctx)\nclient.Task(123).Update(ctx, kanboard.NewTaskUpdate().SetTitle(\"New\"))\n```\n\n## Acceptance criteria\n- All methods delegate to direct Client methods\n- Proper error propagation","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:41.173930396+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:34:32.369975567+01:00","closed_at":"2026-01-15T18:34:32.369975567+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-una","depends_on_id":"kanboard-api-91a","type":"blocks","created_at":"2026-01-15T17:43:31.198935686+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-una","depends_on_id":"kanboard-api-5fb","type":"blocks","created_at":"2026-01-15T17:43:31.261453756+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,86 @@ func (t *TaskScope) MoveToColumn(ctx context.Context, columnID int) error {
|
||||||
return t.client.MoveTaskPosition(ctx, int(task.ProjectID), t.taskID, columnID, 0, int(task.SwimlaneID))
|
return t.client.MoveTaskPosition(ctx, int(task.ProjectID), t.taskID, columnID, 0, int(task.SwimlaneID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoveToNextColumn moves the task to the next column in the workflow.
|
||||||
|
// Columns are ordered by their position field.
|
||||||
|
// Returns ErrAlreadyInLastColumn if the task is already in the last column.
|
||||||
|
func (t *TaskScope) MoveToNextColumn(ctx context.Context) error {
|
||||||
|
// Get task to find current column and project
|
||||||
|
task, err := t.Get(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all columns for the project (already sorted by position)
|
||||||
|
columns, err := t.client.GetColumns(ctx, int(task.ProjectID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(columns) == 0 {
|
||||||
|
return ErrAlreadyInLastColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the current column index
|
||||||
|
currentColumnID := int(task.ColumnID)
|
||||||
|
currentIndex := -1
|
||||||
|
for i, col := range columns {
|
||||||
|
if int(col.ID) == currentColumnID {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If current column not found or is the last column
|
||||||
|
if currentIndex == -1 || currentIndex >= len(columns)-1 {
|
||||||
|
return ErrAlreadyInLastColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the next column
|
||||||
|
nextColumn := columns[currentIndex+1]
|
||||||
|
return t.client.MoveTaskPosition(ctx, int(task.ProjectID), t.taskID, int(nextColumn.ID), 0, int(task.SwimlaneID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveToPreviousColumn moves the task to the previous column in the workflow.
|
||||||
|
// Columns are ordered by their position field.
|
||||||
|
// Returns ErrAlreadyInFirstColumn if the task is already in the first column.
|
||||||
|
func (t *TaskScope) MoveToPreviousColumn(ctx context.Context) error {
|
||||||
|
// Get task to find current column and project
|
||||||
|
task, err := t.Get(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all columns for the project (already sorted by position)
|
||||||
|
columns, err := t.client.GetColumns(ctx, int(task.ProjectID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(columns) == 0 {
|
||||||
|
return ErrAlreadyInFirstColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the current column index
|
||||||
|
currentColumnID := int(task.ColumnID)
|
||||||
|
currentIndex := -1
|
||||||
|
for i, col := range columns {
|
||||||
|
if int(col.ID) == currentColumnID {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If current column not found or is the first column
|
||||||
|
if currentIndex <= 0 {
|
||||||
|
return ErrAlreadyInFirstColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the previous column
|
||||||
|
prevColumn := columns[currentIndex-1]
|
||||||
|
return t.client.MoveTaskPosition(ctx, int(task.ProjectID), t.taskID, int(prevColumn.ID), 0, int(task.SwimlaneID))
|
||||||
|
}
|
||||||
|
|
||||||
// MoveToProject moves the task to a different project.
|
// MoveToProject moves the task to a different project.
|
||||||
func (t *TaskScope) MoveToProject(ctx context.Context, projectID int) error {
|
func (t *TaskScope) MoveToProject(ctx context.Context, projectID int) error {
|
||||||
return t.client.MoveTaskToProject(ctx, t.taskID, projectID)
|
return t.client.MoveTaskToProject(ctx, t.taskID, projectID)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package kanboard
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -180,6 +181,209 @@ func TestTaskScope_MoveToColumn(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTaskScope_MoveToNextColumn(t *testing.T) {
|
||||||
|
callCount := 0
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req JSONRPCRequest
|
||||||
|
json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
callCount++
|
||||||
|
switch callCount {
|
||||||
|
case 1:
|
||||||
|
// First call: getTask
|
||||||
|
resp := JSONRPCResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
|
ID: req.ID,
|
||||||
|
Result: json.RawMessage(`{"id": "42", "project_id": "1", "column_id": "2", "swimlane_id": "0", "is_active": "1"}`),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
case 2:
|
||||||
|
// Second call: getColumns
|
||||||
|
if req.Method != "getColumns" {
|
||||||
|
t.Errorf("expected method=getColumns, got %s", req.Method)
|
||||||
|
}
|
||||||
|
// Return columns in position order
|
||||||
|
resp := JSONRPCResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
|
ID: req.ID,
|
||||||
|
Result: json.RawMessage(`[
|
||||||
|
{"id": "1", "title": "Backlog", "position": "1", "project_id": "1"},
|
||||||
|
{"id": "2", "title": "In Progress", "position": "2", "project_id": "1"},
|
||||||
|
{"id": "3", "title": "Done", "position": "3", "project_id": "1"}
|
||||||
|
]`),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
case 3:
|
||||||
|
// Third call: moveTaskPosition
|
||||||
|
if req.Method != "moveTaskPosition" {
|
||||||
|
t.Errorf("expected method=moveTaskPosition, got %s", req.Method)
|
||||||
|
}
|
||||||
|
params := req.Params.(map[string]any)
|
||||||
|
if params["column_id"].(float64) != 3 {
|
||||||
|
t.Errorf("expected column_id=3 (Done), got %v", params["column_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.Task(42).MoveToNextColumn(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaskScope_MoveToNextColumn_AlreadyInLastColumn(t *testing.T) {
|
||||||
|
callCount := 0
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req JSONRPCRequest
|
||||||
|
json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
callCount++
|
||||||
|
switch callCount {
|
||||||
|
case 1:
|
||||||
|
// Task is already in column 3 (Done)
|
||||||
|
resp := JSONRPCResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
|
ID: req.ID,
|
||||||
|
Result: json.RawMessage(`{"id": "42", "project_id": "1", "column_id": "3", "swimlane_id": "0", "is_active": "1"}`),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
case 2:
|
||||||
|
// getColumns
|
||||||
|
resp := JSONRPCResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
|
ID: req.ID,
|
||||||
|
Result: json.RawMessage(`[
|
||||||
|
{"id": "1", "title": "Backlog", "position": "1", "project_id": "1"},
|
||||||
|
{"id": "2", "title": "In Progress", "position": "2", "project_id": "1"},
|
||||||
|
{"id": "3", "title": "Done", "position": "3", "project_id": "1"}
|
||||||
|
]`),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
default:
|
||||||
|
t.Error("moveTaskPosition should not be called when already in last column")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL).WithAPIToken("test-token")
|
||||||
|
|
||||||
|
err := client.Task(42).MoveToNextColumn(context.Background())
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for task already in last column")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, ErrAlreadyInLastColumn) {
|
||||||
|
t.Errorf("expected ErrAlreadyInLastColumn, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaskScope_MoveToPreviousColumn(t *testing.T) {
|
||||||
|
callCount := 0
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req JSONRPCRequest
|
||||||
|
json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
callCount++
|
||||||
|
switch callCount {
|
||||||
|
case 1:
|
||||||
|
// Task in column 2 (In Progress)
|
||||||
|
resp := JSONRPCResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
|
ID: req.ID,
|
||||||
|
Result: json.RawMessage(`{"id": "42", "project_id": "1", "column_id": "2", "swimlane_id": "0", "is_active": "1"}`),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
case 2:
|
||||||
|
// getColumns
|
||||||
|
resp := JSONRPCResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
|
ID: req.ID,
|
||||||
|
Result: json.RawMessage(`[
|
||||||
|
{"id": "1", "title": "Backlog", "position": "1", "project_id": "1"},
|
||||||
|
{"id": "2", "title": "In Progress", "position": "2", "project_id": "1"},
|
||||||
|
{"id": "3", "title": "Done", "position": "3", "project_id": "1"}
|
||||||
|
]`),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
case 3:
|
||||||
|
// moveTaskPosition
|
||||||
|
params := req.Params.(map[string]any)
|
||||||
|
if params["column_id"].(float64) != 1 {
|
||||||
|
t.Errorf("expected column_id=1 (Backlog), got %v", params["column_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.Task(42).MoveToPreviousColumn(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaskScope_MoveToPreviousColumn_AlreadyInFirstColumn(t *testing.T) {
|
||||||
|
callCount := 0
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req JSONRPCRequest
|
||||||
|
json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
callCount++
|
||||||
|
switch callCount {
|
||||||
|
case 1:
|
||||||
|
// Task is already in column 1 (Backlog)
|
||||||
|
resp := JSONRPCResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
|
ID: req.ID,
|
||||||
|
Result: json.RawMessage(`{"id": "42", "project_id": "1", "column_id": "1", "swimlane_id": "0", "is_active": "1"}`),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
case 2:
|
||||||
|
// getColumns
|
||||||
|
resp := JSONRPCResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
|
ID: req.ID,
|
||||||
|
Result: json.RawMessage(`[
|
||||||
|
{"id": "1", "title": "Backlog", "position": "1", "project_id": "1"},
|
||||||
|
{"id": "2", "title": "In Progress", "position": "2", "project_id": "1"},
|
||||||
|
{"id": "3", "title": "Done", "position": "3", "project_id": "1"}
|
||||||
|
]`),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
default:
|
||||||
|
t.Error("moveTaskPosition should not be called when already in first column")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL).WithAPIToken("test-token")
|
||||||
|
|
||||||
|
err := client.Task(42).MoveToPreviousColumn(context.Background())
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for task already in first column")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, ErrAlreadyInFirstColumn) {
|
||||||
|
t.Errorf("expected ErrAlreadyInFirstColumn, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTaskScope_MoveToProject(t *testing.T) {
|
func TestTaskScope_MoveToProject(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req JSONRPCRequest
|
var req JSONRPCRequest
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue