test(handlers): org_scope + workspace_abilities coverage (#1312, clean extract) #2114

Merged
devops-engineer merged 1 commits from test/org-scope-abilities-coverage-clean into main 2026-06-02 00:47:51 +00:00
2 changed files with 391 additions and 0 deletions
@@ -0,0 +1,191 @@
package handlers
// Sqlmock-backed coverage for org_scope.go (orgRootID + sameOrg).
// Security-critical path — cross-tenant isolation (#1953).
import (
"context"
"errors"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/db"
)
// ---------- orgRootID ----------
func TestOrgRootID_HappyPath_NonRoot(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
// CTE walks: ws-child → ws-parent → org-root (parent_id IS NULL)
mock.ExpectQuery(`WITH RECURSIVE org_chain`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow(wsUUID3))
root, err := orgRootID(context.Background(), db.DB, wsUUID1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if root != wsUUID3 {
t.Errorf("root=%q, want %q", root, wsUUID3)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet: %v", err)
}
}
func TestOrgRootID_WorkspaceIsRoot(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
// One-row chain: the workspace itself is the org root.
mock.ExpectQuery(`WITH RECURSIVE org_chain`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow(wsUUID1))
root, err := orgRootID(context.Background(), db.DB, wsUUID1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if root != wsUUID1 {
t.Errorf("root=%q, want %q", root, wsUUID1)
}
}
func TestOrgRootID_NoRows(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`WITH RECURSIVE org_chain`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"root_id"}))
_, err := orgRootID(context.Background(), db.DB, wsUUID1)
if !errors.Is(err, errNoOrgRoot) {
t.Fatalf("expected errNoOrgRoot, got %v", err)
}
}
func TestOrgRootID_DBError(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`WITH RECURSIVE org_chain`).
WithArgs(wsUUID1).
WillReturnError(errors.New("conn lost"))
_, err := orgRootID(context.Background(), db.DB, wsUUID1)
if err == nil || errors.Is(err, errNoOrgRoot) {
t.Fatalf("expected DB error, got %v", err)
}
}
func TestOrgRootID_EmptyRoot(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
// Row present but root is empty string → treated as not-found.
mock.ExpectQuery(`WITH RECURSIVE org_chain`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow(""))
_, err := orgRootID(context.Background(), db.DB, wsUUID1)
if !errors.Is(err, errNoOrgRoot) {
t.Fatalf("expected errNoOrgRoot for empty root, got %v", err)
}
}
// ---------- sameOrg ----------
func TestSameOrg_SameWorkspace(t *testing.T) {
// Fast path: identical IDs are same-org without touching DB.
mock, cleanup := withMockDB(t)
defer cleanup()
ok, err := sameOrg(context.Background(), db.DB, wsUUID1, wsUUID1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Error("same workspace must be same-org")
}
// No DB expectations → proves short-circuit.
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("DB was touched despite short-circuit: %v", err)
}
}
func TestSameOrg_SameOrg(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`WITH RECURSIVE org_chain`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow(wsUUID3))
mock.ExpectQuery(`WITH RECURSIVE org_chain`).
WithArgs(wsUUID2).
WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow(wsUUID3))
ok, err := sameOrg(context.Background(), db.DB, wsUUID1, wsUUID2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Error("expected same-org")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet: %v", err)
}
}
func TestSameOrg_DifferentOrg(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`WITH RECURSIVE org_chain`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow(wsUUID3))
mock.ExpectQuery(`WITH RECURSIVE org_chain`).
WithArgs(wsUUID2).
WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow("org-b"))
ok, err := sameOrg(context.Background(), db.DB, wsUUID1, wsUUID2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ok {
t.Error("expected different-org")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet: %v", err)
}
}
func TestSameOrg_OrgRootFails(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`WITH RECURSIVE org_chain`).
WithArgs(wsUUID1).
WillReturnError(errors.New("conn lost"))
_, err := sameOrg(context.Background(), db.DB, wsUUID1, wsUUID2)
if err == nil {
t.Fatal("expected error when orgRootID fails")
}
}
func TestSameOrg_OrgRootNotFound(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`WITH RECURSIVE org_chain`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"root_id"}))
_, err := sameOrg(context.Background(), db.DB, wsUUID1, wsUUID2)
if !errors.Is(err, errNoOrgRoot) {
t.Fatalf("expected errNoOrgRoot, got %v", err)
}
}
@@ -0,0 +1,200 @@
package handlers
// Sqlmock-backed coverage for workspace_abilities.go (PatchAbilities).
// Closes #1312 — handler was at 0% coverage.
import (
"bytes"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
)
func patchAbilitiesReq(t *testing.T, wsID string, body string) *httptest.ResponseRecorder {
t.Helper()
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: wsID}}
c.Request = httptest.NewRequest("PATCH", "/workspaces/"+wsID+"/abilities", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
PatchAbilities(c)
return w
}
// ---------- Validation errors ----------
func TestPatchAbilities_InvalidWorkspaceID(t *testing.T) {
w := patchAbilitiesReq(t, "not-a-uuid", `{"broadcast_enabled":true}`)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String())
}
}
func TestPatchAbilities_InvalidJSON(t *testing.T) {
w := patchAbilitiesReq(t, wsUUID1, `not json`)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String())
}
}
func TestPatchAbilities_EmptyBody(t *testing.T) {
w := patchAbilitiesReq(t, wsUUID1, `{}`)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String())
}
}
// ---------- Not found ----------
func TestPatchAbilities_WorkspaceNotFound(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false))
w := patchAbilitiesReq(t, wsUUID1, `{"broadcast_enabled":true}`)
if w.Code != http.StatusNotFound {
t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet: %v", err)
}
}
func TestPatchAbilities_ExistsQueryError(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`).
WithArgs(wsUUID1).
WillReturnError(errors.New("conn refused"))
w := patchAbilitiesReq(t, wsUUID1, `{"broadcast_enabled":true}`)
if w.Code != http.StatusNotFound {
t.Fatalf("expected 404 on exists query error, got %d: %s", w.Code, w.Body.String())
}
}
// ---------- Happy paths ----------
func TestPatchAbilities_BroadcastOnly(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2, updated_at = now\(\) WHERE id = \$1`).
WithArgs(wsUUID1, true).
WillReturnResult(sqlmock.NewResult(0, 1))
w := patchAbilitiesReq(t, wsUUID1, `{"broadcast_enabled":true}`)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet: %v", err)
}
}
func TestPatchAbilities_TalkToUserOnly(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec(`UPDATE workspaces SET talk_to_user_enabled = \$2, updated_at = now\(\) WHERE id = \$1`).
WithArgs(wsUUID1, false).
WillReturnResult(sqlmock.NewResult(0, 1))
w := patchAbilitiesReq(t, wsUUID1, `{"talk_to_user_enabled":false}`)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet: %v", err)
}
}
func TestPatchAbilities_BothFields(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2, updated_at = now\(\) WHERE id = \$1`).
WithArgs(wsUUID1, true).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`UPDATE workspaces SET talk_to_user_enabled = \$2, updated_at = now\(\) WHERE id = \$1`).
WithArgs(wsUUID1, true).
WillReturnResult(sqlmock.NewResult(0, 1))
w := patchAbilitiesReq(t, wsUUID1, `{"broadcast_enabled":true,"talk_to_user_enabled":true}`)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet: %v", err)
}
}
// ---------- DB errors on update ----------
func TestPatchAbilities_BroadcastUpdateError(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2, updated_at = now\(\) WHERE id = \$1`).
WithArgs(wsUUID1, true).
WillReturnError(errors.New("disk full"))
w := patchAbilitiesReq(t, wsUUID1, `{"broadcast_enabled":true}`)
if w.Code != http.StatusInternalServerError {
t.Fatalf("expected 500, got %d: %s", w.Code, w.Body.String())
}
}
func TestPatchAbilities_TalkToUserUpdateError(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec(`UPDATE workspaces SET talk_to_user_enabled = \$2, updated_at = now\(\) WHERE id = \$1`).
WithArgs(wsUUID1, false).
WillReturnError(errors.New("disk full"))
w := patchAbilitiesReq(t, wsUUID1, `{"talk_to_user_enabled":false}`)
if w.Code != http.StatusInternalServerError {
t.Fatalf("expected 500, got %d: %s", w.Code, w.Body.String())
}
}
func TestPatchAbilities_BothFields_BroadcastFails(t *testing.T) {
mock, cleanup := withMockDB(t)
defer cleanup()
mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`).
WithArgs(wsUUID1).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2, updated_at = now\(\) WHERE id = \$1`).
WithArgs(wsUUID1, true).
WillReturnError(errors.New("disk full"))
w := patchAbilitiesReq(t, wsUUID1, `{"broadcast_enabled":true,"talk_to_user_enabled":true}`)
if w.Code != http.StatusInternalServerError {
t.Fatalf("expected 500, got %d: %s", w.Code, w.Body.String())
}
}