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) + } +}