From ae3908caca912fc84782f6ea648a701b207854ac Mon Sep 17 00:00:00 2001 From: Molecule AI Fullstack Engineer Date: Sat, 16 May 2026 07:21:46 +0000 Subject: [PATCH] =?UTF-8?q?test(handlers):=20add=20PatchAbilities=20covera?= =?UTF-8?q?ge=20=E2=80=94=20workspace=5Fabilities.go=200%=20=E2=86=92=2010?= =?UTF-8?q?0%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1312. 10 test cases exercising every path in PatchAbilities: - Invalid workspace ID → 400 - Invalid JSON body → 400 - No ability fields → 400 "at least one ability field required" - Workspace not found → 404 - Workspace exists query error → 404 (!exists) - Success: broadcast_enabled only → 200 - Success: talk_to_user_enabled only → 200 - Success: both fields → 200 (both updates issued) - DB error on broadcast update → 500 - DB error on talk_to_user update → 500 PatchAbilities: 0% → 100%. Package: 69.1% → 69.4%. Co-Authored-By: Claude Opus 4.7 --- .../handlers/workspace_abilities_test.go | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 workspace-server/internal/handlers/workspace_abilities_test.go diff --git a/workspace-server/internal/handlers/workspace_abilities_test.go b/workspace-server/internal/handlers/workspace_abilities_test.go new file mode 100644 index 000000000..d266a14b2 --- /dev/null +++ b/workspace-server/internal/handlers/workspace_abilities_test.go @@ -0,0 +1,259 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/gin-gonic/gin" +) + +// workspace_abilities_test.go — coverage for PatchAbilities (workspace_abilities.go). + +func TestPatchAbilities_InvalidWorkspaceID_Returns400(t *testing.T) { + setupTestDB(t) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "not-a-uuid"}} + c.Request = httptest.NewRequest("PATCH", "/", bytes.NewBufferString(`{}`)) + c.Request.Header.Set("Content-Type", "application/json") + + PatchAbilities(c) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestPatchAbilities_InvalidJSON_Returns400(t *testing.T) { + setupTestDB(t) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}} + c.Request = httptest.NewRequest("PATCH", "/", bytes.NewBufferString(`{invalid`)) + c.Request.Header.Set("Content-Type", "application/json") + + PatchAbilities(c) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestPatchAbilities_NoAbilityFields_Returns400(t *testing.T) { + setupTestDB(t) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}} + c.Request = httptest.NewRequest("PATCH", "/", bytes.NewBufferString(`{}`)) + c.Request.Header.Set("Content-Type", "application/json") + + PatchAbilities(c) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) + } + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["error"] != "at least one ability field required" { + t.Errorf("expected 'at least one ability field required', got %v", resp["error"]) + } +} + +func TestPatchAbilities_WorkspaceNotFound_Returns404(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs("550e8400-e29b-41d4-a716-446655440000"). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false)) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}} + c.Request = httptest.NewRequest("PATCH", "/", bytes.NewBufferString(`{"broadcast_enabled":true}`)) + c.Request.Header.Set("Content-Type", "application/json") + + PatchAbilities(c) + + if w.Code != http.StatusNotFound { + t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestPatchAbilities_WorkspaceExistsQueryError_Returns404(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs("550e8400-e29b-41d4-a716-446655440000"). + WillReturnError(errors.New("connection refused")) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}} + c.Request = httptest.NewRequest("PATCH", "/", bytes.NewBufferString(`{"broadcast_enabled":true}`)) + c.Request.Header.Set("Content-Type", "application/json") + + PatchAbilities(c) + + if w.Code != http.StatusNotFound { + t.Errorf("expected 404 (exists-query error → !exists), got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestPatchAbilities_Success_BroadcastEnabled(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs("550e8400-e29b-41d4-a716-446655440000"). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + + mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2, updated_at = now\(\) WHERE id = \$1`). + WithArgs("550e8400-e29b-41d4-a716-446655440000", true). + WillReturnResult(sqlmock.NewResult(0, 1)) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}} + c.Request = httptest.NewRequest("PATCH", "/", bytes.NewBufferString(`{"broadcast_enabled":true}`)) + c.Request.Header.Set("Content-Type", "application/json") + + PatchAbilities(c) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["status"] != "updated" { + t.Errorf("expected status=updated, got %v", resp) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestPatchAbilities_Success_TalkToUserEnabled(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs("550e8400-e29b-41d4-a716-446655440000"). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + + mock.ExpectExec(`UPDATE workspaces SET talk_to_user_enabled = \$2, updated_at = now\(\) WHERE id = \$1`). + WithArgs("550e8400-e29b-41d4-a716-446655440000", false). + WillReturnResult(sqlmock.NewResult(0, 1)) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}} + c.Request = httptest.NewRequest("PATCH", "/", bytes.NewBufferString(`{"talk_to_user_enabled":false}`)) + c.Request.Header.Set("Content-Type", "application/json") + + PatchAbilities(c) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestPatchAbilities_Success_BothFields(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs("550e8400-e29b-41d4-a716-446655440000"). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + + mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2, updated_at = now\(\) WHERE id = \$1`). + WithArgs("550e8400-e29b-41d4-a716-446655440000", true). + WillReturnResult(sqlmock.NewResult(0, 1)) + + mock.ExpectExec(`UPDATE workspaces SET talk_to_user_enabled = \$2, updated_at = now\(\) WHERE id = \$1`). + WithArgs("550e8400-e29b-41d4-a716-446655440000", true). + WillReturnResult(sqlmock.NewResult(0, 1)) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}} + c.Request = httptest.NewRequest("PATCH", "/", bytes.NewBufferString(`{"broadcast_enabled":true,"talk_to_user_enabled":true}`)) + c.Request.Header.Set("Content-Type", "application/json") + + PatchAbilities(c) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestPatchAbilities_DBErrorOnBroadcastUpdate_Returns500(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs("550e8400-e29b-41d4-a716-446655440000"). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + + mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2, updated_at = now\(\) WHERE id = \$1`). + WithArgs("550e8400-e29b-41d4-a716-446655440000", true). + WillReturnError(errors.New("connection refused")) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}} + c.Request = httptest.NewRequest("PATCH", "/", bytes.NewBufferString(`{"broadcast_enabled":true}`)) + c.Request.Header.Set("Content-Type", "application/json") + + PatchAbilities(c) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestPatchAbilities_DBErrorOnTalkToUserUpdate_Returns500(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs("550e8400-e29b-41d4-a716-446655440000"). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + + // Broadcast update succeeds (nil in body → skipped). + mock.ExpectExec(`UPDATE workspaces SET talk_to_user_enabled = \$2, updated_at = now\(\) WHERE id = \$1`). + WithArgs("550e8400-e29b-41d4-a716-446655440000", false). + WillReturnError(errors.New("write failed")) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}} + c.Request = httptest.NewRequest("PATCH", "/", bytes.NewBufferString(`{"talk_to_user_enabled":false}`)) + c.Request.Header.Set("Content-Type", "application/json") + + PatchAbilities(c) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} -- 2.52.0