Implement Link API methods
- GetAllTaskLinks: retrieve all links for a task - CreateTaskLink: create link between two tasks - RemoveTaskLink: delete a task link - TaskScope.GetLinks and LinkTo for fluent API - Link types include "blocks", "is blocked by", etc. - Comprehensive test coverage
This commit is contained in:
parent
4907a7caad
commit
39ff0a90c8
4 changed files with 336 additions and 1 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
{"id":"kanboard-api-25y","title":"Implement Timestamp type with JSON handling","description":"Implement custom Timestamp type that handles Kanboard's Unix timestamp format.\n\n## Requirements\n- Timestamp struct wrapping time.Time\n- UnmarshalJSON supporting:\n - Unix timestamps as integers\n - Empty strings and \"0\" as zero time\n - Zero value (0) as zero time\n- MarshalJSON returning Unix timestamp or 0 for zero time\n\n## Files to create\n- timestamp.go\n\n## Acceptance criteria\n- Correctly parses integer Unix timestamps\n- Handles empty strings and \"0\" strings\n- Zero time marshals to 0\n- Non-zero time marshals to Unix timestamp","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:55.0044989+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:15:27.299644047+01:00","closed_at":"2026-01-15T18:15:27.299644047+01:00","close_reason":"Closed"}
|
{"id":"kanboard-api-25y","title":"Implement Timestamp type with JSON handling","description":"Implement custom Timestamp type that handles Kanboard's Unix timestamp format.\n\n## Requirements\n- Timestamp struct wrapping time.Time\n- UnmarshalJSON supporting:\n - Unix timestamps as integers\n - Empty strings and \"0\" as zero time\n - Zero value (0) as zero time\n- MarshalJSON returning Unix timestamp or 0 for zero time\n\n## Files to create\n- timestamp.go\n\n## Acceptance criteria\n- Correctly parses integer Unix timestamps\n- Handles empty strings and \"0\" strings\n- Zero time marshals to 0\n- Non-zero time marshals to Unix timestamp","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:55.0044989+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:15:27.299644047+01:00","closed_at":"2026-01-15T18:15:27.299644047+01:00","close_reason":"Closed"}
|
||||||
{"id":"kanboard-api-2g1","title":"Implement JSON-RPC client foundation","description":"Implement the core JSON-RPC 2.0 client for Kanboard API communication.\n\n## Requirements\n- JSONRPCRequest struct with jsonrpc, method, id, params fields\n- JSONRPCResponse struct with jsonrpc, id, result, error fields \n- JSONRPCError struct with code and message\n- Generic `call` method to send requests and parse responses\n- Automatic `/jsonrpc.php` path appending to baseURL\n- Thread-safe request ID generation via atomic.Int64\n\n## Files to create\n- jsonrpc.go\n\n## Acceptance criteria\n- All JSON-RPC structs properly marshal/unmarshal\n- Request IDs increment atomically\n- Supports subdirectory installations (e.g. /kanboard/jsonrpc.php)","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:53.232007312+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:10:29.68466887+01:00","closed_at":"2026-01-15T18:10:29.68466887+01:00","close_reason":"Closed"}
|
{"id":"kanboard-api-2g1","title":"Implement JSON-RPC client foundation","description":"Implement the core JSON-RPC 2.0 client for Kanboard API communication.\n\n## Requirements\n- JSONRPCRequest struct with jsonrpc, method, id, params fields\n- JSONRPCResponse struct with jsonrpc, id, result, error fields \n- JSONRPCError struct with code and message\n- Generic `call` method to send requests and parse responses\n- Automatic `/jsonrpc.php` path appending to baseURL\n- Thread-safe request ID generation via atomic.Int64\n\n## Files to create\n- jsonrpc.go\n\n## Acceptance criteria\n- All JSON-RPC structs properly marshal/unmarshal\n- Request IDs increment atomically\n- Supports subdirectory installations (e.g. /kanboard/jsonrpc.php)","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:53.232007312+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:10:29.68466887+01:00","closed_at":"2026-01-15T18:10:29.68466887+01:00","close_reason":"Closed"}
|
||||||
{"id":"kanboard-api-2ze","title":"Implement BoardScope fluent builder","description":"Implement BoardScope for fluent project-scoped operations.\n\n## Requirements\n- BoardScope struct with client and projectID\n- Client.Board(projectID int) *BoardScope method\n- BoardScope methods:\n - GetColumns(ctx) ([]Column, error)\n - GetCategories(ctx) ([]Category, error)\n - GetTasks(ctx, status TaskStatus) ([]Task, error)\n - SearchTasks(ctx, query string) ([]Task, error)\n - CreateTask(ctx, task *TaskParams) (*Task, error)\n\n## Files to create\n- board_scope.go\n\n## Example usage\n```go\ncolumns, _ := client.Board(1).GetColumns(ctx)\ntask, _ := client.Board(1).CreateTask(ctx, kanboard.NewTask(\"Title\"))\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:40.044649709+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:29:29.648709021+01:00","closed_at":"2026-01-15T18:29:29.648709021+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-apl","type":"blocks","created_at":"2026-01-15T17:43:30.81063282+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-l9b","type":"blocks","created_at":"2026-01-15T17:43:30.874964284+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-0fz","type":"blocks","created_at":"2026-01-15T17:43:30.939377116+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-91a","type":"blocks","created_at":"2026-01-15T17:43:31.005026627+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"kanboard-api-2ze","title":"Implement BoardScope fluent builder","description":"Implement BoardScope for fluent project-scoped operations.\n\n## Requirements\n- BoardScope struct with client and projectID\n- Client.Board(projectID int) *BoardScope method\n- BoardScope methods:\n - GetColumns(ctx) ([]Column, error)\n - GetCategories(ctx) ([]Category, error)\n - GetTasks(ctx, status TaskStatus) ([]Task, error)\n - SearchTasks(ctx, query string) ([]Task, error)\n - CreateTask(ctx, task *TaskParams) (*Task, error)\n\n## Files to create\n- board_scope.go\n\n## Example usage\n```go\ncolumns, _ := client.Board(1).GetColumns(ctx)\ntask, _ := client.Board(1).CreateTask(ctx, kanboard.NewTask(\"Title\"))\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:40.044649709+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:29:29.648709021+01:00","closed_at":"2026-01-15T18:29:29.648709021+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-apl","type":"blocks","created_at":"2026-01-15T17:43:30.81063282+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-l9b","type":"blocks","created_at":"2026-01-15T17:43:30.874964284+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-0fz","type":"blocks","created_at":"2026-01-15T17:43:30.939377116+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-91a","type":"blocks","created_at":"2026-01-15T17:43:31.005026627+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"kanboard-api-3dc","title":"Implement Link API methods","description":"Implement direct API methods for task link operations.\n\n## Methods to implement\n- GetAllTaskLinks(ctx, taskID int) ([]TaskLink, error) - getAllTaskLinks\n- CreateTaskLink(ctx, taskID, oppositeTaskID, linkID int) (int, error) - createTaskLink\n- RemoveTaskLink(ctx, taskLinkID int) error - removeTaskLink (Nice-to-have)\n\n## TaskScope methods to add\n- GetLinks(ctx) ([]TaskLink, error)\n- LinkTo(ctx, oppositeTaskID, linkID int) error\n\n## Files to create\n- links.go\n- task_scope.go (extend)\n\n## Acceptance criteria\n- CreateTaskLink returns the link ID\n- Links include related task information","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:09.328552773+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:09.328552773+01:00","dependencies":[{"issue_id":"kanboard-api-3dc","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:43:49.785710003+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-3dc","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:49.886111429+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"kanboard-api-3dc","title":"Implement Link API methods","description":"Implement direct API methods for task link operations.\n\n## Methods to implement\n- GetAllTaskLinks(ctx, taskID int) ([]TaskLink, error) - getAllTaskLinks\n- CreateTaskLink(ctx, taskID, oppositeTaskID, linkID int) (int, error) - createTaskLink\n- RemoveTaskLink(ctx, taskLinkID int) error - removeTaskLink (Nice-to-have)\n\n## TaskScope methods to add\n- GetLinks(ctx) ([]TaskLink, error)\n- LinkTo(ctx, oppositeTaskID, linkID int) error\n\n## Files to create\n- links.go\n- task_scope.go (extend)\n\n## Acceptance criteria\n- CreateTaskLink returns the link ID\n- Links include related task information","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:09.328552773+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:43:29.707624597+01:00","closed_at":"2026-01-15T18:43:29.707624597+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-3dc","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:43:49.785710003+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-3dc","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:49.886111429+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"kanboard-api-5fb","title":"Implement TaskUpdateParams builder","description":"Implement TaskUpdateParams for fluent task update configuration.\n\n## Requirements\n- TaskUpdateParams struct with pointer fields\n- NewTaskUpdate() *TaskUpdateParams constructor\n- Fluent setter methods:\n - SetTitle(title string) *TaskUpdateParams\n - SetDescription(desc string) *TaskUpdateParams\n - SetColor(colorID string) *TaskUpdateParams\n - SetOwner(ownerID int) *TaskUpdateParams\n - SetCategory(categoryID int) *TaskUpdateParams\n - SetPriority(priority int) *TaskUpdateParams\n - SetDueDate(date time.Time) *TaskUpdateParams\n- Internal method to convert to UpdateTaskRequest\n\n## Files to create\n- task_update_params.go\n\n## Example usage\n```go\nparams := kanboard.NewTaskUpdate().\n SetTitle(\"New Title\").\n SetPriority(2)\n```\n\n## Acceptance criteria\n- Only set fields are included in update request\n- All setters return *TaskUpdateParams for chaining","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:40.814955926+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:33:14.78349244+01:00","closed_at":"2026-01-15T18:33:14.78349244+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-5fb","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:31.134402453+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"kanboard-api-5fb","title":"Implement TaskUpdateParams builder","description":"Implement TaskUpdateParams for fluent task update configuration.\n\n## Requirements\n- TaskUpdateParams struct with pointer fields\n- NewTaskUpdate() *TaskUpdateParams constructor\n- Fluent setter methods:\n - SetTitle(title string) *TaskUpdateParams\n - SetDescription(desc string) *TaskUpdateParams\n - SetColor(colorID string) *TaskUpdateParams\n - SetOwner(ownerID int) *TaskUpdateParams\n - SetCategory(categoryID int) *TaskUpdateParams\n - SetPriority(priority int) *TaskUpdateParams\n - SetDueDate(date time.Time) *TaskUpdateParams\n- Internal method to convert to UpdateTaskRequest\n\n## Files to create\n- task_update_params.go\n\n## Example usage\n```go\nparams := kanboard.NewTaskUpdate().\n SetTitle(\"New Title\").\n SetPriority(2)\n```\n\n## Acceptance criteria\n- Only set fields are included in update request\n- All setters return *TaskUpdateParams for chaining","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:40.814955926+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:33:14.78349244+01:00","closed_at":"2026-01-15T18:33:14.78349244+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-5fb","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:31.134402453+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"kanboard-api-70e","title":"Add GoDoc documentation to all public APIs","description":"Add comprehensive GoDoc comments to all exported types and functions.\n\n## Requirements\n- Package-level documentation in doc.go\n- All exported types documented\n- All exported functions documented with examples\n- All exported constants/variables documented\n\n## Documentation standards\n- Start with the name being documented\n- Include usage examples where helpful\n- Document error conditions\n- Note thread-safety characteristics\n\n## Files to create/modify\n- doc.go (package documentation)\n- All .go files with exported symbols\n\n## Acceptance criteria\n- `go doc` produces clean output\n- All exports have documentation\n- Examples are runnable","status":"open","priority":2,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:54.342657373+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:54.342657373+01:00","dependencies":[{"issue_id":"kanboard-api-70e","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:46:55.508211397+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"kanboard-api-70e","title":"Add GoDoc documentation to all public APIs","description":"Add comprehensive GoDoc comments to all exported types and functions.\n\n## Requirements\n- Package-level documentation in doc.go\n- All exported types documented\n- All exported functions documented with examples\n- All exported constants/variables documented\n\n## Documentation standards\n- Start with the name being documented\n- Include usage examples where helpful\n- Document error conditions\n- Note thread-safety characteristics\n\n## Files to create/modify\n- doc.go (package documentation)\n- All .go files with exported symbols\n\n## Acceptance criteria\n- `go doc` produces clean output\n- All exports have documentation\n- Examples are runnable","status":"open","priority":2,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:54.342657373+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:54.342657373+01:00","dependencies":[{"issue_id":"kanboard-api-70e","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:46:55.508211397+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"kanboard-api-91a","title":"Implement Task API methods (CRUD)","description":"Implement direct API methods for core task operations.\n\n## Methods to implement\n- GetTask(ctx, taskID int) (*Task, error) - getTask\n- GetAllTasks(ctx, projectID int, status TaskStatus) ([]Task, error) - getAllTasks\n- CreateTask(ctx, req CreateTaskRequest) (*Task, error) - createTask\n- UpdateTask(ctx, req UpdateTaskRequest) error - updateTask\n- CloseTask(ctx, taskID int) error - closeTask\n- OpenTask(ctx, taskID int) error - openTask\n\n## Files to create\n- tasks.go\n\n## Acceptance criteria\n- CreateTask returns the created task\n- UpdateTask supports partial updates via pointer fields\n- Proper error types (ErrTaskNotFound, ErrTaskClosed, etc.)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:16.9962543+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:26:49.591860359+01:00","closed_at":"2026-01-15T18:26:49.591860359+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-91a","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:42:53.291442735+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-91a","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:42:53.434459365+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"kanboard-api-91a","title":"Implement Task API methods (CRUD)","description":"Implement direct API methods for core task operations.\n\n## Methods to implement\n- GetTask(ctx, taskID int) (*Task, error) - getTask\n- GetAllTasks(ctx, projectID int, status TaskStatus) ([]Task, error) - getAllTasks\n- CreateTask(ctx, req CreateTaskRequest) (*Task, error) - createTask\n- UpdateTask(ctx, req UpdateTaskRequest) error - updateTask\n- CloseTask(ctx, taskID int) error - closeTask\n- OpenTask(ctx, taskID int) error - openTask\n\n## Files to create\n- tasks.go\n\n## Acceptance criteria\n- CreateTask returns the created task\n- UpdateTask supports partial updates via pointer fields\n- Proper error types (ErrTaskNotFound, ErrTaskClosed, etc.)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:16.9962543+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:26:49.591860359+01:00","closed_at":"2026-01-15T18:26:49.591860359+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-91a","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:42:53.291442735+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-91a","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:42:53.434459365+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
|
|
|
||||||
56
links.go
Normal file
56
links.go
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
package kanboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetAllTaskLinks returns all links for a task.
|
||||||
|
func (c *Client) GetAllTaskLinks(ctx context.Context, taskID int) ([]TaskLink, error) {
|
||||||
|
params := map[string]int{"task_id": taskID}
|
||||||
|
|
||||||
|
var result []TaskLink
|
||||||
|
if err := c.call(ctx, "getAllTaskLinks", params, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("getAllTaskLinks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTaskLink creates a link between two tasks.
|
||||||
|
// The linkID specifies the type of relationship (e.g., "blocks", "is blocked by").
|
||||||
|
// Returns the ID of the created link.
|
||||||
|
func (c *Client) CreateTaskLink(ctx context.Context, taskID, oppositeTaskID, linkID int) (int, error) {
|
||||||
|
params := map[string]int{
|
||||||
|
"task_id": taskID,
|
||||||
|
"opposite_task_id": oppositeTaskID,
|
||||||
|
"link_id": linkID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var result int
|
||||||
|
if err := c.call(ctx, "createTaskLink", params, &result); err != nil {
|
||||||
|
return 0, fmt.Errorf("createTaskLink: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result == 0 {
|
||||||
|
return 0, fmt.Errorf("createTaskLink: failed to create link")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveTaskLink deletes a task link.
|
||||||
|
func (c *Client) RemoveTaskLink(ctx context.Context, taskLinkID int) error {
|
||||||
|
params := map[string]int{"task_link_id": taskLinkID}
|
||||||
|
|
||||||
|
var success bool
|
||||||
|
if err := c.call(ctx, "removeTaskLink", params, &success); err != nil {
|
||||||
|
return fmt.Errorf("removeTaskLink: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
return fmt.Errorf("removeTaskLink: delete failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
267
links_test.go
Normal file
267
links_test.go
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
package kanboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClient_GetAllTaskLinks(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 != "getAllTaskLinks" {
|
||||||
|
t.Errorf("expected method=getAllTaskLinks, 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"])
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := JSONRPCResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
|
ID: req.ID,
|
||||||
|
Result: json.RawMessage(`[
|
||||||
|
{"id": "1", "link_id": "2", "task_id": "42", "opposite_task_id": "100", "label": "blocks", "title": "Blocked Task"},
|
||||||
|
{"id": "2", "link_id": "3", "task_id": "42", "opposite_task_id": "101", "label": "is duplicated by", "title": "Duplicate Task"}
|
||||||
|
]`),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL).WithAPIToken("test-token")
|
||||||
|
|
||||||
|
links, err := client.GetAllTaskLinks(context.Background(), 42)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(links) != 2 {
|
||||||
|
t.Errorf("expected 2 links, got %d", len(links))
|
||||||
|
}
|
||||||
|
if links[0].Label != "blocks" {
|
||||||
|
t.Errorf("expected label='blocks', got %s", links[0].Label)
|
||||||
|
}
|
||||||
|
if int(links[0].OppositeTaskID) != 100 {
|
||||||
|
t.Errorf("expected opposite_task_id=100, got %d", links[0].OppositeTaskID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_GetAllTaskLinks_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")
|
||||||
|
|
||||||
|
links, err := client.GetAllTaskLinks(context.Background(), 42)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(links) != 0 {
|
||||||
|
t.Errorf("expected 0 links, got %d", len(links))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_CreateTaskLink(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 != "createTaskLink" {
|
||||||
|
t.Errorf("expected method=createTaskLink, 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["opposite_task_id"].(float64) != 100 {
|
||||||
|
t.Errorf("expected opposite_task_id=100, got %v", params["opposite_task_id"])
|
||||||
|
}
|
||||||
|
if params["link_id"].(float64) != 2 {
|
||||||
|
t.Errorf("expected link_id=2, got %v", params["link_id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := JSONRPCResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
|
ID: req.ID,
|
||||||
|
Result: json.RawMessage(`10`),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL).WithAPIToken("test-token")
|
||||||
|
|
||||||
|
linkID, err := client.CreateTaskLink(context.Background(), 42, 100, 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if linkID != 10 {
|
||||||
|
t.Errorf("expected linkID=10, got %d", linkID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_CreateTaskLink_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(`0`),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL).WithAPIToken("test-token")
|
||||||
|
|
||||||
|
_, err := client.CreateTaskLink(context.Background(), 42, 100, 2)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for failed link creation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_RemoveTaskLink(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 != "removeTaskLink" {
|
||||||
|
t.Errorf("expected method=removeTaskLink, got %s", req.Method)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := req.Params.(map[string]any)
|
||||||
|
if params["task_link_id"].(float64) != 5 {
|
||||||
|
t.Errorf("expected task_link_id=5, got %v", params["task_link_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.RemoveTaskLink(context.Background(), 5)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_RemoveTaskLink_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.RemoveTaskLink(context.Background(), 5)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for failed delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaskScope_GetLinks(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 != "getAllTaskLinks" {
|
||||||
|
t.Errorf("expected method=getAllTaskLinks, 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"])
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := JSONRPCResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
|
ID: req.ID,
|
||||||
|
Result: json.RawMessage(`[{"id": "1", "link_id": "2", "task_id": "42", "opposite_task_id": "100", "label": "blocks"}]`),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL).WithAPIToken("test-token")
|
||||||
|
|
||||||
|
links, err := client.Task(42).GetLinks(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(links) != 1 {
|
||||||
|
t.Errorf("expected 1 link, got %d", len(links))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaskScope_LinkTo(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 != "createTaskLink" {
|
||||||
|
t.Errorf("expected method=createTaskLink, 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["opposite_task_id"].(float64) != 100 {
|
||||||
|
t.Errorf("expected opposite_task_id=100, got %v", params["opposite_task_id"])
|
||||||
|
}
|
||||||
|
if params["link_id"].(float64) != 2 {
|
||||||
|
t.Errorf("expected link_id=2, got %v", params["link_id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := JSONRPCResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
|
ID: req.ID,
|
||||||
|
Result: json.RawMessage(`10`),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL).WithAPIToken("test-token")
|
||||||
|
|
||||||
|
err := client.Task(42).LinkTo(context.Background(), 100, 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -254,3 +254,15 @@ func (t *TaskScope) GetComments(ctx context.Context) ([]Comment, error) {
|
||||||
func (t *TaskScope) AddComment(ctx context.Context, userID int, content string) (*Comment, error) {
|
func (t *TaskScope) AddComment(ctx context.Context, userID int, content string) (*Comment, error) {
|
||||||
return t.client.CreateComment(ctx, t.taskID, userID, content)
|
return t.client.CreateComment(ctx, t.taskID, userID, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLinks returns all links for this task.
|
||||||
|
func (t *TaskScope) GetLinks(ctx context.Context) ([]TaskLink, error) {
|
||||||
|
return t.client.GetAllTaskLinks(ctx, t.taskID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkTo creates a link from this task to another task.
|
||||||
|
// The linkID specifies the type of relationship (e.g., "blocks", "is blocked by").
|
||||||
|
func (t *TaskScope) LinkTo(ctx context.Context, oppositeTaskID, linkID int) error {
|
||||||
|
_, err := t.client.CreateTaskLink(ctx, t.taskID, oppositeTaskID, linkID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue