feat: add category write operations (create, update, remove, get-by-name)
Add CreateCategory, UpdateCategory, RemoveCategory, and GetCategoryByName methods to the client, with BoardScope wrappers for project-scoped operations. Includes comprehensive tests for all new methods. Closes kanboard-4n3
This commit is contained in:
parent
56da2e0fdc
commit
17896d80e7
5 changed files with 350 additions and 2 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"worktree_root": "/home/oli/Dev/kanboard-api",
|
||||
"last_export_commit": "a381c0dc51d778382edd71d5a406199e0a5a532b",
|
||||
"last_export_time": "2026-01-28T12:09:21.47967978+01:00",
|
||||
"last_export_commit": "12c1660d94b1daa7d0e4407480a0d3d75f5816a8",
|
||||
"last_export_time": "2026-01-28T12:09:27.757995829+01:00",
|
||||
"jsonl_hash": "e0d7c3244033c584f1bf6c013c22b66723e2595dcbe1bf20da7deb9ed6f3e68d"
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
{"id":"kanboard-1cb","title":"Investigate MoveTaskPosition generic error message","description":"## Context\n\nThis is an investigation task to determine if a problem in the `hqcli` tool (which uses this library) has its root cause here.\n\nThe `kb ticket move` command in hqcli fails with an unhelpful error:\n\n```\n❯ dist/hqcli kb ticket move 3765\n2026/01/27 11:02:43 Fehler beim Verschieben des Tickets: moveTaskPosition: failed to move task 3765\n```\n\n## Library Behavior\n\nLooking at `tasks.go:131-150`, the `MoveTaskPosition` function:\n\n```go\nfunc (c *Client) MoveTaskPosition(...) error {\n // ...\n var success bool\n if err := c.call(ctx, \"moveTaskPosition\", params, \u0026success); err != nil {\n return fmt.Errorf(\"moveTaskPosition: %w\", err)\n }\n\n if !success {\n return fmt.Errorf(\"moveTaskPosition: failed to move task %d\", taskID) // \u003c-- generic error\n }\n return nil\n}\n```\n\nWhen the API returns `false`, the error message is completely generic with no actionable information.\n\n## Investigation Needed\n\n1. **API Response Analysis**: What does Kanboard actually return when `moveTaskPosition` fails?\n - Does it include an error message in the JSON-RPC response?\n - Is the `false` result the only indication of failure?\n\n2. **Possible Causes for API returning false**:\n - Invalid column/project combination\n - Permission issues\n - Task doesn't exist\n - Position 0 might have different semantics than documented\n\n3. **Documentation Check**: [Kanboard API docs](https://docs.kanboard.org/v1/api/task_procedures/#movetaskposition)\n\n## Potential Improvements\n\nIf the API provides no additional details, consider:\n- Adding debug logging to show the full API response\n- Checking if the task/column/project exist before calling\n- Providing a more descriptive error: \"moveTaskPosition returned false - verify task exists, column belongs to project, and user has permission\"\n\n## Acceptance Criteria\n\n- [ ] Root cause identified (is it library issue or hqcli usage issue?)\n- [ ] If library issue: improve error message or error handling\n- [ ] Document findings","status":"closed","priority":2,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-27T11:05:16.089428488+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T11:11:58.26724838+01:00","closed_at":"2026-01-27T11:11:58.26724838+01:00","close_reason":"Closed"}
|
||||
{"id":"kanboard-1ov","title":"Add GetColorList for available task colors","description":"## Context\n\nTask colors are already partially supported in the library:\n- `ColorID` field exists in the `Task` type\n- `WithColor()` works for task creation via `TaskParams`\n- `SetColor()` works for task updates via `TaskUpdateParams`\n\nHowever, there's no way to query the available colors from the Kanboard instance.\n\n## Missing Feature\n\nThe Kanboard API provides `getColorList` in the [Application Procedures](https://docs.kanboard.org/v1/api/application_procedures/):\n\n```\nRequest:\n{\"jsonrpc\": \"2.0\", \"method\": \"getColorList\", \"id\": 1}\n\nResponse:\n{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"result\": {\n \"yellow\": \"Yellow\",\n \"blue\": \"Blue\",\n \"green\": \"Green\",\n ...\n }\n}\n```\n\n## Implementation\n\nCreate a new file `application.go` with:\n\n```go\n// GetColorList returns the available task colors.\n// Returns a map of color_id to display name.\nfunc (c *Client) GetColorList(ctx context.Context) (map[string]string, error) {\n var result map[string]string\n if err := c.call(ctx, \"getColorList\", nil, \u0026result); err != nil {\n return nil, fmt.Errorf(\"getColorList: %w\", err)\n }\n return result, nil\n}\n```\n\n## Files to Create/Modify\n\n- `application.go` - New file with `GetColorList()`\n- `application_test.go` - Tests for the new function\n\n## Acceptance Criteria\n\n- [ ] `GetColorList()` returns map of color_id to display name\n- [ ] Works with standard Kanboard color set\n- [ ] Tests written and passing","status":"closed","priority":3,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-27T12:11:00.218309278+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T12:13:06.411067375+01:00","closed_at":"2026-01-27T12:13:06.411067375+01:00","close_reason":"Closed"}
|
||||
{"id":"kanboard-3pe","title":"Add DateMoved timestamp field to Task struct","description":"The Task struct in `types.go` is missing the `date_moved` timestamp that Kanboard returns when a task is moved between columns or swimlanes.\n\n## Background\n\nKanboard tracks when a task was last moved with the `date_moved` field. This timestamp is returned in API responses but is currently not captured by the Go client's Task struct.\n\n## Implementation\n\nAdd the following field to the Task struct in `types.go`:\n\n```go\nDateMoved Timestamp `json:\"date_moved\"`\n```\n\nThis should be added alongside the other date fields (after `DateDue` for consistency with the grouping of timestamp fields).\n\n## Acceptance Criteria\n\n- [ ] `DateMoved` field added to `Task` struct in `types.go`\n- [ ] Field uses `Timestamp` type with JSON tag `json:\"date_moved\"`\n- [ ] All existing tests pass\n- [ ] Field is properly handled in all task-related operations (read/unmarshal)","status":"closed","priority":2,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T12:05:50.949698791+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T12:09:01.530912406+01:00","closed_at":"2026-01-28T12:09:01.530912406+01:00","close_reason":"Closed"}
|
||||
{"id":"kanboard-4n3","title":"Implement category write operations","description":"Implement the missing category write operations from the Kanboard API: `createCategory`, `updateCategory`, and `removeCategory`. Additionally, add a client-side `getCategoryByName` helper.\n\nReference: https://docs.kanboard.org/v1/api/category_procedures/\n\n## Details\n\nCurrently only read operations exist (`GetAllCategories`, `GetCategory` in `categories.go`). The following need to be added:\n\n### createCategory\n- Parameters: `project_id` (int), `name` (string), optional `color_id` (string)\n- Returns: category ID on success\n\n### updateCategory\n- Parameters: `id` (int), `name` (string), optional `color_id` (string)\n- Returns: bool\n\n### removeCategory\n- Parameters: `category_id` (int)\n- Returns: bool\n\n### getCategoryByName (client-side helper)\n- Since `getAllCategories` returns the full array, implement `GetCategoryByName(projectID, name)` as a client-side filter over `GetAllCategories` rather than a separate RPC call (unless a server-side procedure exists).\n- Return `ErrCategoryNotFound` if no match.\n\n## Acceptance Criteria\n- [ ] `CreateCategory(projectID, name, opts...)` implemented with functional options for color_id\n- [ ] `UpdateCategory(id, name, opts...)` implemented with functional options for color_id\n- [ ] `RemoveCategory(categoryID)` implemented\n- [ ] `GetCategoryByName(projectID, name)` implemented (client-side filter)\n- [ ] All new methods have corresponding unit tests in `categories_test.go`\n- [ ] BoardScope wrappers added where appropriate\n- [ ] Follow existing code patterns and conventions from the project","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-29T09:12:59.169475651+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-29T09:20:09.394074185+01:00","closed_at":"2026-01-29T09:20:09.394074185+01:00","close_reason":"Closed"}
|
||||
{"id":"kanboard-7es","title":"JSON-RPC Request-ID: Zufälligen Wert statt fester 1 verwenden","description":"## Kontext\n\nIn jedem JSON-RPC Request an die Kanboard-API wird im Root-Objekt ein Feld `id` mitgeliefert. Dieses dient dazu, bei asynchroner Kommunikation Request und Response einander zuordnen zu können – die API liefert diese ID in der Antwort zurück.\n\n**Aktuell:** Die ID ist fest auf `1` gesetzt.\n\n## Anforderung\n\n1. **Wenn keine ID von außen gesetzt wird:** Die Library soll intern einen zufälligen Wert generieren\n2. **API-Dokumentation prüfen:** Welche Werte sind erlaubt? Welche Größenordnung? (vermutlich Integer)\n3. **Signatur beibehalten:** Die öffentliche API der Library-Funktionen soll unverändert bleiben\n4. **Interne Generierung:** Die Library bestimmt selbst einen zufälligen Wert\n\n## Implementierungshinweise\n\n- Prüfen: Kanboard JSON-RPC Dokumentation bezüglich erlaubter ID-Werte\n- Vermutlich: `int64` oder `int32` Range\n- Zufallsgenerator: `math/rand` mit Seed oder `crypto/rand` für bessere Verteilung\n- Ggf. bestehende `requestIDCounter` in `jsonrpc.go` (Zeile 40) anpassen oder ersetzen\n\n## Beispiel\n\n**Vorher (immer gleich):**\n```json\n{\"jsonrpc\": \"2.0\", \"method\": \"getTask\", \"id\": 1, \"params\": {...}}\n```\n\n**Nachher (zufällig):**\n```json\n{\"jsonrpc\": \"2.0\", \"method\": \"getTask\", \"id\": 847291536, \"params\": {...}}\n```\n\n## Referenz\n\n- Datei: `jsonrpc.go`\n- Zeile 17: `ID int64 \\`json:\"id\"\\``\n- Zeile 40: `requestIDCounter` (existiert bereits)","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-23T17:44:51.566737509+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T10:21:06.415129879+01:00","closed_at":"2026-01-27T10:21:06.415129879+01:00","close_reason":"Closed"}
|
||||
{"id":"kanboard-9wa","title":"Support custom authentication header name","description":"## Description\n\nKanboard supports using an alternative HTTP header for authentication when the server has specific configuration requirements.\n\nCurrently, authentication uses the standard `Authorization` header via Go's `SetBasicAuth()`. This needs to be configurable so users can specify a custom header name (e.g., `X-API-Auth`).\n\n## Requirements\n\n- Add an optional configuration parameter for custom auth header name\n- Default to standard `Authorization` header if not specified\n- When custom header is set, use that header name instead of `Authorization`\n- The header value format should remain the same (Basic Auth base64-encoded credentials)\n\n## Acceptance Criteria\n\n- [ ] New client configuration method (e.g., `WithAuthHeader(headerName string)`)\n- [ ] Default behavior unchanged when no custom header specified\n- [ ] Custom header name is used when configured\n- [ ] Works with both API token and basic auth\n- [ ] Tests cover default and custom header scenarios\n\n## Reference\n\nKanboard API documentation: \"You can use an alternative HTTP header for authentication if your server has a very specific configuration.\"","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-23T18:08:31.507616093+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-23T18:26:50.40804952+01:00","closed_at":"2026-01-23T18:26:50.40804952+01:00","close_reason":"Closed"}
|
||||
{"id":"kanboard-a7x","title":"Handle URLs already ending in /jsonrpc.php","description":"## Context\n\nThe `NewClient()` function in `client.go` always appends `/jsonrpc.php` to the base URL. This causes issues when users pass a URL that already includes the endpoint path.\n\n## Problem\n\nIf a user calls:\n```go\nclient := kanboard.NewClient(\"https://example.com/jsonrpc.php\")\n```\n\nThe resulting endpoint becomes `https://example.com/jsonrpc.php/jsonrpc.php`, which fails.\n\n## Solution\n\nModify `NewClient()` to detect and handle URLs that already end in `/jsonrpc.php`:\n\n```go\nfunc NewClient(baseURL string) *Client {\n // Ensure no trailing slash\n baseURL = strings.TrimSuffix(baseURL, \"/\")\n\n // Handle URLs that already include /jsonrpc.php\n endpoint := baseURL\n if !strings.HasSuffix(baseURL, \"/jsonrpc.php\") {\n endpoint = baseURL + \"/jsonrpc.php\"\n }\n\n c := \u0026Client{\n baseURL: baseURL,\n endpoint: endpoint,\n }\n // ... rest unchanged\n}\n```\n\n## Files to Modify\n\n- `client.go` - Update `NewClient()` to check for existing `/jsonrpc.php` suffix\n\n## Acceptance Criteria\n\n- [ ] `NewClient(\"https://example.com\")` → endpoint `https://example.com/jsonrpc.php`\n- [ ] `NewClient(\"https://example.com/\")` → endpoint `https://example.com/jsonrpc.php`\n- [ ] `NewClient(\"https://example.com/jsonrpc.php\")` → endpoint `https://example.com/jsonrpc.php`\n- [ ] `NewClient(\"https://example.com/kanboard/jsonrpc.php\")` → endpoint `https://example.com/kanboard/jsonrpc.php`\n- [ ] Tests written and passing","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-27T10:25:51.077352962+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T10:27:15.189568683+01:00","closed_at":"2026-01-27T10:27:15.189568683+01:00","close_reason":"Closed"}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,16 @@ func (b *BoardScope) CreateTask(ctx context.Context, req CreateTaskRequest) (*Ta
|
|||
return b.client.CreateTask(ctx, req)
|
||||
}
|
||||
|
||||
// CreateCategory creates a new category in the project and returns its ID.
|
||||
func (b *BoardScope) CreateCategory(ctx context.Context, name string, colorID string) (int, error) {
|
||||
return b.client.CreateCategory(ctx, b.projectID, name, colorID)
|
||||
}
|
||||
|
||||
// GetCategoryByName returns a category by name within the project.
|
||||
func (b *BoardScope) GetCategoryByName(ctx context.Context, name string) (*Category, error) {
|
||||
return b.client.GetCategoryByName(ctx, b.projectID, name)
|
||||
}
|
||||
|
||||
// CreateTaskFromParams creates a new task in the project using TaskParams.
|
||||
// This provides a fluent interface for task creation.
|
||||
func (b *BoardScope) CreateTaskFromParams(ctx context.Context, params *TaskParams) (*Task, error) {
|
||||
|
|
|
|||
|
|
@ -33,3 +33,80 @@ func (c *Client) GetCategory(ctx context.Context, categoryID int) (*Category, er
|
|||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateCategory creates a new category and returns its ID.
|
||||
func (c *Client) CreateCategory(ctx context.Context, projectID int, name string, colorID string) (int, error) {
|
||||
params := map[string]interface{}{
|
||||
"project_id": projectID,
|
||||
"name": name,
|
||||
}
|
||||
if colorID != "" {
|
||||
params["color_id"] = colorID
|
||||
}
|
||||
|
||||
var result IntOrFalse
|
||||
if err := c.call(ctx, "createCategory", params, &result); err != nil {
|
||||
return 0, fmt.Errorf("createCategory: %w", err)
|
||||
}
|
||||
|
||||
if int(result) == 0 {
|
||||
return 0, fmt.Errorf("createCategory: failed to create category %q", name)
|
||||
}
|
||||
|
||||
return int(result), nil
|
||||
}
|
||||
|
||||
// UpdateCategory updates a category's name and optionally its color.
|
||||
func (c *Client) UpdateCategory(ctx context.Context, categoryID int, name string, colorID string) error {
|
||||
params := map[string]interface{}{
|
||||
"id": categoryID,
|
||||
"name": name,
|
||||
}
|
||||
if colorID != "" {
|
||||
params["color_id"] = colorID
|
||||
}
|
||||
|
||||
var result bool
|
||||
if err := c.call(ctx, "updateCategory", params, &result); err != nil {
|
||||
return fmt.Errorf("updateCategory: %w", err)
|
||||
}
|
||||
|
||||
if !result {
|
||||
return fmt.Errorf("updateCategory: failed to update category %d", categoryID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveCategory deletes a category.
|
||||
func (c *Client) RemoveCategory(ctx context.Context, categoryID int) error {
|
||||
params := map[string]int{"category_id": categoryID}
|
||||
|
||||
var result bool
|
||||
if err := c.call(ctx, "removeCategory", params, &result); err != nil {
|
||||
return fmt.Errorf("removeCategory: %w", err)
|
||||
}
|
||||
|
||||
if !result {
|
||||
return fmt.Errorf("removeCategory: failed to remove category %d", categoryID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCategoryByName returns a category by name within a project.
|
||||
// Returns ErrCategoryNotFound if no category matches.
|
||||
func (c *Client) GetCategoryByName(ctx context.Context, projectID int, name string) (*Category, error) {
|
||||
categories, err := c.GetAllCategories(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range categories {
|
||||
if categories[i].Name == name {
|
||||
return &categories[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: category %q in project %d", ErrCategoryNotFound, name, projectID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,6 +148,266 @@ func TestClient_GetCategory_NotFound(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_CreateCategory(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 != "createCategory" {
|
||||
t.Errorf("expected method=createCategory, 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["name"].(string) != "Bug" {
|
||||
t.Errorf("expected name=Bug, got %v", params["name"])
|
||||
}
|
||||
if params["color_id"].(string) != "red" {
|
||||
t.Errorf("expected color_id=red, got %v", params["color_id"])
|
||||
}
|
||||
|
||||
resp := JSONRPCResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: req.ID,
|
||||
Result: json.RawMessage(`42`),
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL).WithAPIToken("test-token")
|
||||
|
||||
id, err := client.CreateCategory(context.Background(), 1, "Bug", "red")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if id != 42 {
|
||||
t.Errorf("expected id=42, got %d", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_CreateCategory_NoColor(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req JSONRPCRequest
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
params := req.Params.(map[string]any)
|
||||
if _, ok := params["color_id"]; ok {
|
||||
t.Error("expected color_id to be absent")
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
id, err := client.CreateCategory(context.Background(), 1, "Bug", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if id != 10 {
|
||||
t.Errorf("expected id=10, got %d", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_CreateCategory_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.CreateCategory(context.Background(), 1, "Bug", "")
|
||||
if err == nil {
|
||||
t.Fatal("expected error on failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_UpdateCategory(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 != "updateCategory" {
|
||||
t.Errorf("expected method=updateCategory, got %s", req.Method)
|
||||
}
|
||||
|
||||
params := req.Params.(map[string]any)
|
||||
if params["id"].(float64) != 5 {
|
||||
t.Errorf("expected id=5, got %v", params["id"])
|
||||
}
|
||||
if params["name"].(string) != "Feature" {
|
||||
t.Errorf("expected name=Feature, got %v", params["name"])
|
||||
}
|
||||
if params["color_id"].(string) != "blue" {
|
||||
t.Errorf("expected color_id=blue, got %v", params["color_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.UpdateCategory(context.Background(), 5, "Feature", "blue")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_UpdateCategory_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.UpdateCategory(context.Background(), 5, "Feature", "")
|
||||
if err == nil {
|
||||
t.Fatal("expected error on failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_RemoveCategory(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 != "removeCategory" {
|
||||
t.Errorf("expected method=removeCategory, got %s", req.Method)
|
||||
}
|
||||
|
||||
params := req.Params.(map[string]any)
|
||||
if params["category_id"].(float64) != 3 {
|
||||
t.Errorf("expected category_id=3, got %v", params["category_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.RemoveCategory(context.Background(), 3)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_RemoveCategory_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.RemoveCategory(context.Background(), 3)
|
||||
if err == nil {
|
||||
t.Fatal("expected error on failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetCategoryByName(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(`[
|
||||
{"id": "1", "name": "Bug", "project_id": "1", "color_id": "red"},
|
||||
{"id": "2", "name": "Feature", "project_id": "1", "color_id": "blue"}
|
||||
]`),
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL).WithAPIToken("test-token")
|
||||
|
||||
cat, err := client.GetCategoryByName(context.Background(), 1, "Feature")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if cat.Name != "Feature" {
|
||||
t.Errorf("expected name=Feature, got %s", cat.Name)
|
||||
}
|
||||
if int(cat.ID) != 2 {
|
||||
t.Errorf("expected id=2, got %d", cat.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetCategoryByName_NotFound(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")
|
||||
|
||||
_, err := client.GetCategoryByName(context.Background(), 1, "Nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-existent category")
|
||||
}
|
||||
if !errors.Is(err, ErrCategoryNotFound) {
|
||||
t.Errorf("expected ErrCategoryNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetAllCategories_ContextCanceled(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
select {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue