package kanboard import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestClient_Task(t *testing.T) { client := NewClient("http://example.com").WithAPIToken("test") scope := client.Task(42) if scope == nil { t.Fatal("expected non-nil TaskScope") } if scope.taskID != 42 { t.Errorf("expected taskID=42, got %d", scope.taskID) } if scope.client != client { t.Error("expected TaskScope to reference the same client") } } func TestTaskScope_Get(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 != "getTask" { t.Errorf("expected method=getTask, 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": "42", "title": "Test Task", "project_id": "1", "is_active": "1"}`), } json.NewEncoder(w).Encode(resp) })) defer server.Close() client := NewClient(server.URL).WithAPIToken("test-token") task, err := client.Task(42).Get(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if int(task.ID) != 42 { t.Errorf("expected ID=42, got %d", task.ID) } if task.Title != "Test Task" { t.Errorf("expected title='Test Task', got %s", task.Title) } } func TestTaskScope_Close(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 != "closeTask" { t.Errorf("expected method=closeTask, 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(`true`), } json.NewEncoder(w).Encode(resp) })) defer server.Close() client := NewClient(server.URL).WithAPIToken("test-token") err := client.Task(42).Close(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestTaskScope_Open(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 != "openTask" { t.Errorf("expected method=openTask, 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(`true`), } json.NewEncoder(w).Encode(resp) })) defer server.Close() client := NewClient(server.URL).WithAPIToken("test-token") err := client.Task(42).Open(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestTaskScope_MoveToColumn(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++ if callCount == 1 { // First call: getTask to get project_id and swimlane_id if req.Method != "getTask" { t.Errorf("expected method=getTask, got %s", req.Method) } resp := JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(`{"id": "42", "title": "Test Task", "project_id": "1", "column_id": "1", "swimlane_id": "0", "is_active": "1"}`), } json.NewEncoder(w).Encode(resp) } else { // Second call: moveTaskPosition 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) != 3 { t.Errorf("expected column_id=3, got %v", params["column_id"]) } if params["position"].(float64) != 0 { t.Errorf("expected position=0, got %v", params["position"]) } 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).MoveToColumn(context.Background(), 3) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestTaskScope_MoveToProject(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.Task(42).MoveToProject(context.Background(), 5) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestTaskScope_Update(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 != "updateTask" { t.Errorf("expected method=updateTask, got %s", req.Method) } params := req.Params.(map[string]any) if params["id"].(float64) != 42 { t.Errorf("expected id=42, got %v", params["id"]) } if params["title"] != "Updated Title" { t.Errorf("expected title='Updated Title', got %v", params["title"]) } 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).Update(context.Background(), NewTaskUpdate().SetTitle("Updated Title")) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestTaskScope_Chaining(t *testing.T) { // Verify we can chain operations on the same task client := NewClient("http://example.com").WithAPIToken("test") // These should all return the same underlying taskID scope1 := client.Task(42) scope2 := client.Task(42) if scope1.taskID != scope2.taskID { t.Error("expected same taskID for same task scope") } } func TestTaskScope_GetTags(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 != "getTaskTags" { t.Errorf("expected method=getTaskTags, 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(`{"1": "urgent", "2": "backend"}`), } json.NewEncoder(w).Encode(resp) })) defer server.Close() client := NewClient(server.URL).WithAPIToken("test-token") tags, err := client.Task(42).GetTags(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(tags) != 2 { t.Errorf("expected 2 tags, got %d", len(tags)) } if tags[1] != "urgent" { t.Errorf("expected tags[1]='urgent', got %s", tags[1]) } } func TestTaskScope_SetTags(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++ if callCount == 1 { // First call: getTask to get project_id resp := JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(`{"id": "42", "title": "Test", "project_id": "1", "is_active": "1"}`), } json.NewEncoder(w).Encode(resp) } else { // Second call: setTaskTags if req.Method != "setTaskTags" { t.Errorf("expected method=setTaskTags, 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"]) } 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).SetTags(context.Background(), "urgent", "review") if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestTaskScope_ClearTags(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++ if callCount == 1 { // First call: getTask resp := JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(`{"id": "42", "title": "Test", "project_id": "1", "is_active": "1"}`), } json.NewEncoder(w).Encode(resp) } else { // Second call: setTaskTags with empty array if req.Method != "setTaskTags" { t.Errorf("expected method=setTaskTags, got %s", req.Method) } params := req.Params.(map[string]any) tags, ok := params["tags"].([]any) if !ok { // Tags might be nil if passed as nil slice tags = []any{} } if len(tags) != 0 { t.Errorf("expected empty tags array, got %v", tags) } 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).ClearTags(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestTaskScope_AddTag(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", "title": "Test", "project_id": "1", "is_active": "1"}`), } json.NewEncoder(w).Encode(resp) case 2: // Second call: getTaskTags resp := JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(`{"1": "existing"}`), } json.NewEncoder(w).Encode(resp) case 3: // Third call: setTaskTags if req.Method != "setTaskTags" { t.Errorf("expected method=setTaskTags, got %s", req.Method) } params := req.Params.(map[string]any) tags := params["tags"].([]any) if len(tags) != 2 { t.Errorf("expected 2 tags, got %d", len(tags)) } // Check new tag is present hasNew := false for _, tag := range tags { if tag == "new-tag" { hasNew = true } } if !hasNew { t.Error("expected 'new-tag' in tags") } 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).AddTag(context.Background(), "new-tag") if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestTaskScope_AddTag_Idempotent(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", "title": "Test", "project_id": "1", "is_active": "1"}`), } json.NewEncoder(w).Encode(resp) case 2: // Second call: getTaskTags - tag already exists resp := JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(`{"1": "existing-tag"}`), } json.NewEncoder(w).Encode(resp) default: // Should not reach setTaskTags t.Error("setTaskTags should not be called when tag already exists") } })) defer server.Close() client := NewClient(server.URL).WithAPIToken("test-token") // Adding tag that already exists - should be no-op err := client.Task(42).AddTag(context.Background(), "existing-tag") if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestTaskScope_RemoveTag(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", "title": "Test", "project_id": "1", "is_active": "1"}`), } json.NewEncoder(w).Encode(resp) case 2: // Second call: getTaskTags resp := JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(`{"1": "keep", "2": "remove-me"}`), } json.NewEncoder(w).Encode(resp) case 3: // Third call: setTaskTags if req.Method != "setTaskTags" { t.Errorf("expected method=setTaskTags, got %s", req.Method) } params := req.Params.(map[string]any) tags := params["tags"].([]any) if len(tags) != 1 { t.Errorf("expected 1 tag after removal, got %d", len(tags)) } // Check removed tag is not present for _, tag := range tags { if tag == "remove-me" { t.Error("'remove-me' should have been filtered out") } } 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).RemoveTag(context.Background(), "remove-me") if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestTaskScope_RemoveTag_Idempotent(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", "title": "Test", "project_id": "1", "is_active": "1"}`), } json.NewEncoder(w).Encode(resp) case 2: // Second call: getTaskTags - tag doesn't exist resp := JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(`{"1": "other-tag"}`), } json.NewEncoder(w).Encode(resp) default: // Should not reach setTaskTags t.Error("setTaskTags should not be called when tag doesn't exist") } })) defer server.Close() client := NewClient(server.URL).WithAPIToken("test-token") // Removing tag that doesn't exist - should be no-op err := client.Task(42).RemoveTag(context.Background(), "nonexistent") if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestTaskScope_HasTag_True(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(`{"1": "urgent", "2": "backend"}`), } json.NewEncoder(w).Encode(resp) })) defer server.Close() client := NewClient(server.URL).WithAPIToken("test-token") hasTag, err := client.Task(42).HasTag(context.Background(), "urgent") if err != nil { t.Fatalf("unexpected error: %v", err) } if !hasTag { t.Error("expected HasTag to return true") } } func TestTaskScope_HasTag_False(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(`{"1": "urgent"}`), } json.NewEncoder(w).Encode(resp) })) defer server.Close() client := NewClient(server.URL).WithAPIToken("test-token") hasTag, err := client.Task(42).HasTag(context.Background(), "nonexistent") if err != nil { t.Fatalf("unexpected error: %v", err) } if hasTag { t.Error("expected HasTag to return false") } }