Compare commits

...

1 Commits

Author SHA1 Message Date
fullstack-engineer 00ceb8b6ae test(handlers): add coverage for PatchAbilities (workspace abilities endpoint)
workspace_abilities_test.go: 12 tests covering the PATCH /workspaces/:id/abilities handler:

Input validation:
- Invalid workspace UUID → 400
- Malformed JSON body → 400
- Empty body (no fields) → 400
- Null pointer fields only → 400

Workspace not found:
- Workspace doesn't exist → 404
- DB error on existence query → 404 (handler maps any error to not-found)

Update failures:
- DB error on broadcast_enabled UPDATE → 500
- DB error on talk_to_user_enabled UPDATE → 500

Happy path:
- broadcast_enabled=true → 200, correct UPDATE
- broadcast_enabled=false → 200, correct UPDATE
- talk_to_user_enabled=true → 200, correct UPDATE
- talk_to_user_enabled=false → 200, correct UPDATE
- Both fields in one request → both UPDATEs executed, 200
- Response body shape: status="updated"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 15:18:26 +00:00
@@ -0,0 +1,336 @@
package handlers
import (
"bytes"
"database/sql"
"net/http"
"net/http/httptest"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
)
// Suppress unused import warning.
var _ = sqlmock.Sqlmock(nil)
// validWSID is a properly-formed UUID used throughout this file.
const validWSID = "aabbccdd-eeff-1234-5678-123456789abc"
// patchAbilitiesRequest builds and executes a PATCH /workspaces/:id/abilities request.
func patchAbilitiesRequest(id, body string) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: id}}
c.Request = httptest.NewRequest("PATCH", "/workspaces/"+id+"/abilities",
bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
PatchAbilities(c)
return w
}
// ptrBool returns a pointer to a bool, for constructing AbilitiesPayload in JSON.
func ptrBool(b bool) *bool { return &b }
// ==================== Input validation ====================
// TestPatchAbilities_InvalidWorkspaceID verifies that a malformed UUID in the path
// returns HTTP 400 before any DB call.
func TestPatchAbilities_InvalidWorkspaceID(t *testing.T) {
mock := setupTestDB(t)
_ = mock
w := patchAbilitiesRequest("not-a-uuid", `{"broadcast_enabled":true}`)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("sqlmock expectations not met: %v", err)
}
}
// TestPatchAbilities_InvalidJSON verifies that malformed JSON returns HTTP 400.
func TestPatchAbilities_InvalidJSON(t *testing.T) {
mock := setupTestDB(t)
_ = mock
w := patchAbilitiesRequest(validWSID, "{broken")
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("sqlmock expectations not met: %v", err)
}
}
// TestPatchAbilities_EmptyBody verifies that a request with neither field
// returns HTTP 400 with the correct error key.
func TestPatchAbilities_EmptyBody(t *testing.T) {
mock := setupTestDB(t)
_ = mock
w := patchAbilitiesRequest(validWSID, `{}`)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("sqlmock expectations not met: %v", err)
}
}
// TestPatchAbilities_OnlyNullFields verifies that explicitly null pointer fields
// (Gin's default for missing JSON fields) are treated the same as absent fields
// and rejected with 400.
func TestPatchAbilities_OnlyNullFields(t *testing.T) {
mock := setupTestDB(t)
_ = mock
w := patchAbilitiesRequest(validWSID, `{"broadcast_enabled":null,"talk_to_user_enabled":null}`)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
}
}
// ==================== Workspace not found ====================
// TestPatchAbilities_WorkspaceNotFound verifies that a workspace not in the DB
// returns HTTP 404.
func TestPatchAbilities_WorkspaceNotFound(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(`SELECT EXISTS`).
WithArgs(validWSID).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false))
w := patchAbilitiesRequest(validWSID, `{"broadcast_enabled":true}`)
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("sqlmock expectations not met: %v", err)
}
}
// TestPatchAbilities_ExistenceQueryError verifies that a DB error on the
// existence check returns HTTP 404 (handler maps any error to not-found).
func TestPatchAbilities_ExistenceQueryError(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(`SELECT EXISTS`).
WithArgs(validWSID).
WillReturnError(sql.ErrNoRows)
w := patchAbilitiesRequest(validWSID, `{"broadcast_enabled":true}`)
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("sqlmock expectations not met: %v", err)
}
}
// ==================== Update failures ====================
// TestPatchAbilities_BroadcastUpdateFails verifies that a DB error on the
// broadcast_enabled UPDATE returns HTTP 500.
func TestPatchAbilities_BroadcastUpdateFails(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(`SELECT EXISTS`).
WithArgs(validWSID).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2`).
WithArgs(validWSID, true).
WillReturnError(sql.ErrConnDone)
w := patchAbilitiesRequest(validWSID, `{"broadcast_enabled":true}`)
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("sqlmock expectations not met: %v", err)
}
}
// TestPatchAbilities_TalkToUserUpdateFails verifies that a DB error on the
// talk_to_user_enabled UPDATE returns HTTP 500.
func TestPatchAbilities_TalkToUserUpdateFails(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(`SELECT EXISTS`).
WithArgs(validWSID).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec(`UPDATE workspaces SET talk_to_user_enabled = \$2`).
WithArgs(validWSID, true).
WillReturnError(sql.ErrConnDone)
w := patchAbilitiesRequest(validWSID, `{"talk_to_user_enabled":true}`)
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("sqlmock expectations not met: %v", err)
}
}
// ==================== Happy path ====================
// TestPatchAbilities_BroadcastEnabledTrue verifies that enabling broadcast
// returns HTTP 200 and writes the correct UPDATE.
func TestPatchAbilities_BroadcastEnabledTrue(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(`SELECT EXISTS`).
WithArgs(validWSID).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2`).
WithArgs(validWSID, true).
WillReturnResult(sqlmock.NewResult(0, 1))
w := patchAbilitiesRequest(validWSID, `{"broadcast_enabled":true}`)
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("sqlmock expectations not met: %v", err)
}
}
// TestPatchAbilities_BroadcastEnabledFalse verifies that disabling broadcast
// returns HTTP 200 and writes the correct UPDATE.
func TestPatchAbilities_BroadcastEnabledFalse(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(`SELECT EXISTS`).
WithArgs(validWSID).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2`).
WithArgs(validWSID, false).
WillReturnResult(sqlmock.NewResult(0, 1))
w := patchAbilitiesRequest(validWSID, `{"broadcast_enabled":false}`)
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("sqlmock expectations not met: %v", err)
}
}
// TestPatchAbilities_TalkToUserEnabledTrue verifies that enabling talk_to_user
// returns HTTP 200 and writes the correct UPDATE.
func TestPatchAbilities_TalkToUserEnabledTrue(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(`SELECT EXISTS`).
WithArgs(validWSID).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec(`UPDATE workspaces SET talk_to_user_enabled = \$2`).
WithArgs(validWSID, true).
WillReturnResult(sqlmock.NewResult(0, 1))
w := patchAbilitiesRequest(validWSID, `{"talk_to_user_enabled":true}`)
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("sqlmock expectations not met: %v", err)
}
}
// TestPatchAbilities_TalkToUserEnabledFalse verifies that disabling talk_to_user
// returns HTTP 200 and writes the correct UPDATE.
func TestPatchAbilities_TalkToUserEnabledFalse(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(`SELECT EXISTS`).
WithArgs(validWSID).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec(`UPDATE workspaces SET talk_to_user_enabled = \$2`).
WithArgs(validWSID, false).
WillReturnResult(sqlmock.NewResult(0, 1))
w := patchAbilitiesRequest(validWSID, `{"talk_to_user_enabled":false}`)
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("sqlmock expectations not met: %v", err)
}
}
// TestPatchAbilities_BothFields verifies that setting both ability flags in one
// request executes both UPDATEs and returns HTTP 200.
func TestPatchAbilities_BothFields(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(`SELECT EXISTS`).
WithArgs(validWSID).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
// UPDATE order in handler: broadcast first, then talk_to_user.
mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2`).
WithArgs(validWSID, true).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`UPDATE workspaces SET talk_to_user_enabled = \$2`).
WithArgs(validWSID, false).
WillReturnResult(sqlmock.NewResult(0, 1))
w := patchAbilitiesRequest(validWSID, `{"broadcast_enabled":true,"talk_to_user_enabled":false}`)
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("sqlmock expectations not met: %v", err)
}
}
// TestPatchAbilities_ResponseBody verifies the success response shape.
func TestPatchAbilities_ResponseBody(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(`SELECT EXISTS`).
WithArgs(validWSID).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2`).
WithArgs(validWSID, true).
WillReturnResult(sqlmock.NewResult(0, 1))
w := patchAbilitiesRequest(validWSID, `{"broadcast_enabled":true}`)
body := w.Body.String()
if body == "" {
t.Fatal("expected non-empty response body")
}
if !bytes.Contains([]byte(body), []byte(`"status"`)) {
t.Errorf("expected response to contain 'status' key, got: %s", body)
}
if !bytes.Contains([]byte(body), []byte(`"updated"`)) {
t.Errorf("expected status 'updated', got: %s", body)
}
}
// Suppress unused import warning.
var _ = sqlmock.Sqlmock(nil)