Compare commits

..

1 Commits

Author SHA1 Message Date
fullstack-engineer ee9a1dca73 test(handlers): add coverage for PatchAbilities (workspace_abilities.go)
Add 9 tests for the PATCH /workspaces/:id/abilities handler — the only
exported function in workspace_abilities.go with zero prior coverage:

  - Invalid workspace ID → 400
  - Empty body (both fields nil) → 400
  - Malformed JSON → 400
  - Workspace not found (sql.ErrNoRows) → 404
  - Workspace DB error → 500/404 (short-circuit on err || !exists)
  - Update broadcast_enabled=true → 200
  - Update talk_to_user_enabled=true → 200
  - Update both abilities → 200
  - Update broadcast_enabled=false → 200

Uses sqlmock for the DB layer. The handler is a plain package-level
function (not a struct method) so no handler injection needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:16:13 +00:00
3 changed files with 315 additions and 498 deletions
@@ -1,211 +0,0 @@
// @vitest-environment jsdom
/**
* Tests for design-tokens.ts — STATUS_CONFIG, TIER_CONFIG, COMM_TYPE_LABELS,
* and statusDotClass.
*
* Companion to statusDotClass.test.ts (which covers statusDotClass's dot field
* + TIER_CONFIG + COMM_TYPE_LABELS in depth). This file adds explicit shape
* coverage for STATUS_CONFIG's glow/label/bar fields and cross-checks them
* against the tailwind tokens used in statusDotClass's dot field.
*
* Issue: #1815 follow-up — design-tokens.ts was at ~33% coverage.
*/
import { describe, it, expect } from "vitest";
import {
STATUS_CONFIG,
TIER_CONFIG,
COMM_TYPE_LABELS,
statusDotClass,
} from "../design-tokens";
const STATUS_KEYS = [
"online",
"offline",
"paused",
"degraded",
"failed",
"provisioning",
"not_configured",
] as const;
// ── STATUS_CONFIG: all keys have the required shape ──────────────────────────
describe("STATUS_CONFIG — required shape", () => {
for (const key of STATUS_KEYS) {
it(`${key}: has dot, glow, label, bar fields`, () => {
const entry = STATUS_CONFIG[key];
expect(entry).toBeDefined();
expect(typeof entry.dot).toBe("string");
expect(typeof entry.glow).toBe("string");
expect(typeof entry.label).toBe("string");
expect(typeof entry.bar).toBe("string");
});
}
it("has exactly 7 status entries", () => {
expect(Object.keys(STATUS_CONFIG)).toHaveLength(7);
});
});
// ── STATUS_CONFIG: dot field matches statusDotClass ──────────────────────────────
describe("STATUS_CONFIG — dot field matches statusDotClass()", () => {
for (const key of STATUS_KEYS) {
it(`${key}: dot === statusDotClass("${key}")`, () => {
expect(STATUS_CONFIG[key].dot).toBe(statusDotClass(key));
});
}
});
// ── STATUS_CONFIG: glow field ─────────────────────────────────────────────────
describe("STATUS_CONFIG — glow field", () => {
it('"online" has a glow (shadow-emerald)', () => {
expect(STATUS_CONFIG.online.glow).toMatch(/emerald/i);
});
it('"degraded" has a glow (shadow-amber)', () => {
expect(STATUS_CONFIG.degraded.glow).toMatch(/amber/i);
});
it('"provisioning" has a glow (shadow-sky)', () => {
expect(STATUS_CONFIG.provisioning.glow).toMatch(/sky/i);
});
it('"not_configured" has a glow (shadow-amber)', () => {
expect(STATUS_CONFIG.not_configured.glow).toMatch(/amber/i);
});
it('"offline" and "paused" have no glow', () => {
expect(STATUS_CONFIG.offline.glow).toBe("");
expect(STATUS_CONFIG.paused.glow).toBe("");
});
it('"failed" has a glow (shadow-red)', () => {
expect(STATUS_CONFIG.failed.glow).toMatch(/red/i);
});
});
// ── STATUS_CONFIG: label field ─────────────────────────────────────────────────
describe("STATUS_CONFIG — label field", () => {
it('"online" → "Online"', () => {
expect(STATUS_CONFIG.online.label).toBe("Online");
});
it('"offline" → "Offline"', () => {
expect(STATUS_CONFIG.offline.label).toBe("Offline");
});
it('"paused" → "Paused"', () => {
expect(STATUS_CONFIG.paused.label).toBe("Paused");
});
it('"degraded" → "Degraded"', () => {
expect(STATUS_CONFIG.degraded.label).toBe("Degraded");
});
it('"failed" → "Failed"', () => {
expect(STATUS_CONFIG.failed.label).toBe("Failed");
});
it('"provisioning" → "Starting"', () => {
expect(STATUS_CONFIG.provisioning.label).toBe("Starting");
});
it('"not_configured" → "Not configured"', () => {
expect(STATUS_CONFIG.not_configured.label).toBe("Not configured");
});
it("all labels are non-empty strings", () => {
for (const key of STATUS_KEYS) {
expect(STATUS_CONFIG[key].label.length).toBeGreaterThan(0);
}
});
});
// ── STATUS_CONFIG: bar field ─────────────────────────────────────────────────
describe("STATUS_CONFIG — bar field", () => {
for (const key of STATUS_KEYS) {
it(`${key}: bar field is a gradient string`, () => {
expect(STATUS_CONFIG[key].bar).toMatch(/^from-/);
});
}
it('"online" bar uses emerald token', () => {
expect(STATUS_CONFIG.online.bar).toContain("emerald");
});
it('"degraded" bar uses amber token', () => {
expect(STATUS_CONFIG.degraded.bar).toContain("amber");
});
it('"failed" bar uses red token', () => {
expect(STATUS_CONFIG.failed.bar).toContain("red");
});
it('"provisioning" bar uses sky token', () => {
expect(STATUS_CONFIG.provisioning.bar).toContain("sky");
});
it('"not_configured" bar uses amber token', () => {
expect(STATUS_CONFIG.not_configured.bar).toContain("amber");
});
it("all bar fields end with 'to-transparent'", () => {
for (const key of STATUS_KEYS) {
expect(STATUS_CONFIG[key].bar).toMatch(/to-transparent$/);
}
});
});
// ── STATUS_CONFIG: unknown key falls back gracefully ──────────────────────────
describe("STATUS_CONFIG — unknown key via statusDotClass", () => {
it("unknown status returns fallback dot (bg-zinc-500)", () => {
expect(statusDotClass("unknown-status")).toBe("bg-zinc-500");
});
});
// ── TIER_CONFIG: shape ───────────────────────────────────────────────────────
describe("TIER_CONFIG — shape", () => {
it("has entries for tiers 14", () => {
expect(TIER_CONFIG).toHaveProperty("1");
expect(TIER_CONFIG).toHaveProperty("2");
expect(TIER_CONFIG).toHaveProperty("3");
expect(TIER_CONFIG).toHaveProperty("4");
});
it("each tier has label, color, border fields", () => {
for (const tier of [1, 2, 3, 4] as const) {
expect(typeof TIER_CONFIG[tier].label).toBe("string");
expect(typeof TIER_CONFIG[tier].color).toBe("string");
expect(typeof TIER_CONFIG[tier].border).toBe("string");
}
});
it("all label values match the tier name", () => {
expect(TIER_CONFIG[1].label).toBe("T1");
expect(TIER_CONFIG[2].label).toBe("T2");
expect(TIER_CONFIG[3].label).toBe("T3");
expect(TIER_CONFIG[4].label).toBe("T4");
});
});
// ── COMM_TYPE_LABELS ────────────────────────────────────────────────────────
describe("COMM_TYPE_LABELS — shape", () => {
it("has a2a_send, a2a_receive, task_update", () => {
expect(COMM_TYPE_LABELS.a2a_send).toBe("sent");
expect(COMM_TYPE_LABELS.a2a_receive).toBe("received");
expect(COMM_TYPE_LABELS.task_update).toBe("task update");
});
it("all labels are non-empty strings", () => {
for (const key of Object.keys(COMM_TYPE_LABELS)) {
expect(COMM_TYPE_LABELS[key].length).toBeGreaterThan(0);
}
});
});
@@ -1,287 +0,0 @@
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/gin-gonic/gin"
)
// setupTestPlugins creates a temporary plugins directory with named
// subdirectories, each optionally containing a plugin.yaml manifest.
func setupTestPlugins(t *testing.T, plugins map[string]string /* name → yamlContents */) string {
t.Helper()
dir := t.TempDir()
for name, yaml := range plugins {
plugDir := filepath.Join(dir, name)
if err := os.MkdirAll(plugDir, 0755); err != nil {
t.Fatalf("setupTestPlugins: mkdir %s: %v", plugDir, err)
}
if yaml != "" {
if err := os.WriteFile(filepath.Join(plugDir, "plugin.yaml"), []byte(yaml), 0644); err != nil {
t.Fatalf("setupTestPlugins: write plugin.yaml for %s: %v", name, err)
}
}
}
return dir
}
// makeTestHandler creates a PluginsHandler wired with a stub runtimeLookup
// that always returns "claude-code".
func makeTestHandler(t *testing.T, pluginsDir string) *PluginsHandler {
t.Helper()
h := NewPluginsHandler(pluginsDir, nil, nil)
h.WithRuntimeLookup(func(workspaceID string) (string, error) {
return "claude-code", nil
})
return h
}
// listRegistry fires ListRegistry and returns the parsed []pluginInfo.
func listRegistry(h *PluginsHandler, runtime string) []pluginInfo {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/plugins", nil)
if runtime != "" {
c.Request.URL.RawQuery = "runtime=" + runtime
}
h.ListRegistry(c)
var out []pluginInfo
json.Unmarshal(w.Body.Bytes(), &out)
return out
}
// listAvailable fires ListAvailableForWorkspace and returns the parsed []pluginInfo.
func listAvailable(h *PluginsHandler, workspaceID string) []pluginInfo {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/workspaces/"+workspaceID+"/plugins/available", nil)
c.Params = []gin.Param{{Key: "id", Value: workspaceID}}
h.ListAvailableForWorkspace(c)
var out []pluginInfo
json.Unmarshal(w.Body.Bytes(), &out)
return out
}
// --- ListRegistry ---
func TestListRegistry_ReturnsAllPlugins(t *testing.T) {
pluginsDir := setupTestPlugins(t, map[string]string{
"plugin-a": `name: plugin-a`,
"plugin-b": `name: plugin-b`,
})
h := makeTestHandler(t, pluginsDir)
plugins := listRegistry(h, "")
if len(plugins) != 2 {
t.Fatalf("expected 2 plugins, got %d", len(plugins))
}
names := make(map[string]bool)
for _, p := range plugins {
names[p.Name] = true
}
if !names["plugin-a"] || !names["plugin-b"] {
t.Errorf("unexpected plugin names: %v", names)
}
}
func TestListRegistry_EmptyDirectory(t *testing.T) {
dir := t.TempDir()
h := makeTestHandler(t, dir)
plugins := listRegistry(h, "")
if len(plugins) != 0 {
t.Fatalf("expected 0 plugins for empty dir, got %d", len(plugins))
}
}
func TestListRegistry_FiltersByRuntime(t *testing.T) {
// plugin-a supports claude-code; plugin-b supports langgraph only.
pluginsDir := setupTestPlugins(t, map[string]string{
"plugin-a": `
name: plugin-a
runtimes:
- claude_code
`,
"plugin-b": `
name: plugin-b
runtimes:
- langgraph
`,
})
h := makeTestHandler(t, pluginsDir)
// Filter by claude-code — only plugin-a matches.
plugins := listRegistry(h, "claude-code")
if len(plugins) != 1 || plugins[0].Name != "plugin-a" {
t.Errorf("expected [plugin-a], got %v", plugins)
}
// Filter by langgraph — only plugin-b matches.
plugins = listRegistry(h, "langgraph")
if len(plugins) != 1 || plugins[0].Name != "plugin-b" {
t.Errorf("expected [plugin-b], got %v", plugins)
}
// Filter by unknown runtime — plugins that declare specific runtimes
// are excluded; only unspecified (empty Runtimes) would appear.
plugins = listRegistry(h, "autogen")
if len(plugins) != 0 {
t.Errorf("expected 0 for unknown runtime, got %v", plugins)
}
}
func TestListRegistry_UnspecifiedRuntimesIncludedForAll(t *testing.T) {
// A plugin with no runtimes field is treated as "unspecified" — included
// for every runtime filter.
pluginsDir := setupTestPlugins(t, map[string]string{
"generic-plugin": `name: generic-plugin`,
"claude-plugin": `
name: claude-plugin
runtimes:
- claude_code
`,
})
h := makeTestHandler(t, pluginsDir)
plugins := listRegistry(h, "claude-code")
names := make(map[string]bool)
for _, p := range plugins {
names[p.Name] = true
}
// Both should appear: generic-plugin (unspecified) + claude-plugin.
if !names["generic-plugin"] || !names["claude-plugin"] {
t.Errorf("expected both plugins, got %v", names)
}
}
func TestListRegistry_ManifestFieldsParsed(t *testing.T) {
// Verify that ListRegistry returns the full pluginInfo shape from plugin.yaml.
pluginsDir := setupTestPlugins(t, map[string]string{
"my-plugin": `
name: my-plugin
version: 1.2.3
description: A test plugin
author: Test Author
tags:
- testing
- demo
skills:
- tdd-loop
- code-review
runtimes:
- claude_code
`,
})
h := makeTestHandler(t, pluginsDir)
plugins := listRegistry(h, "")
if len(plugins) != 1 {
t.Fatalf("expected 1 plugin, got %d", len(plugins))
}
p := plugins[0]
if p.Name != "my-plugin" {
t.Errorf("Name = %q; want my-plugin", p.Name)
}
if p.Version != "1.2.3" {
t.Errorf("Version = %q; want 1.2.3", p.Version)
}
if p.Description != "A test plugin" {
t.Errorf("Description = %q; want 'A test plugin'", p.Description)
}
if p.Author != "Test Author" {
t.Errorf("Author = %q; want 'Test Author'", p.Author)
}
if len(p.Tags) != 2 {
t.Errorf("Tags = %v; want 2 entries", p.Tags)
}
if len(p.Skills) != 2 {
t.Errorf("Skills = %v; want 2 entries", p.Skills)
}
if len(p.Runtimes) != 1 {
t.Errorf("Runtimes = %v; want 1 entry", p.Runtimes)
}
}
func TestListRegistry_NoManifestFile(t *testing.T) {
// A plugin directory without plugin.yaml should still appear with its
// directory name as the Name.
pluginsDir := setupTestPlugins(t, map[string]string{
"no-manifest-plugin": ``, // directory with no plugin.yaml
})
h := makeTestHandler(t, pluginsDir)
plugins := listRegistry(h, "")
if len(plugins) != 1 {
t.Fatalf("expected 1 plugin, got %d", len(plugins))
}
if plugins[0].Name != "no-manifest-plugin" {
t.Errorf("expected Name to be directory name, got %q", plugins[0].Name)
}
}
// --- ListAvailableForWorkspace ---
func TestListAvailableForWorkspace_WithRuntimeLookup(t *testing.T) {
// When runtimeLookup is wired, ListAvailableForWorkspace filters by the
// workspace's runtime (claude-code), which is set by makeTestHandler.
pluginsDir := setupTestPlugins(t, map[string]string{
"runtime-plugin": `
name: runtime-plugin
runtimes:
- claude_code
`,
"langgraph-plugin": `
name: langgraph-plugin
runtimes:
- langgraph
`,
})
h := makeTestHandler(t, pluginsDir)
plugins := listAvailable(h, "ws-123")
if len(plugins) != 1 || plugins[0].Name != "runtime-plugin" {
t.Errorf("expected [runtime-plugin], got %v", plugins)
}
}
func TestListAvailableForWorkspace_UnspecifiedRuntime(t *testing.T) {
// A plugin without runtimes is included for all workspaces.
pluginsDir := setupTestPlugins(t, map[string]string{
"generic": `name: generic`,
})
h := makeTestHandler(t, pluginsDir)
plugins := listAvailable(h, "ws-456")
if len(plugins) != 1 || plugins[0].Name != "generic" {
t.Errorf("expected [generic], got %v", plugins)
}
}
// --- HTTP response codes ---
func TestListRegistry_HTTPStatusOK(t *testing.T) {
pluginsDir := setupTestPlugins(t, map[string]string{"foo": "name: foo"})
h := NewPluginsHandler(pluginsDir, nil, nil)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/plugins", nil)
h.ListRegistry(c)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestListAvailableForWorkspace_HTTPStatusOK(t *testing.T) {
pluginsDir := setupTestPlugins(t, map[string]string{"bar": "name: bar"})
h := makeTestHandler(t, pluginsDir)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/workspaces/ws-789/plugins/available", nil)
c.Params = []gin.Param{{Key: "id", Value: "ws-789"}}
h.ListAvailableForWorkspace(c)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
@@ -0,0 +1,315 @@
package handlers
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"github.com/gin-gonic/gin"
)
// -------------------------------------------------------------------------- //
// Helpers
// -------------------------------------------------------------------------- //
func setupAbilitiesTest(t *testing.T) (sqlmock.Sqlmock, func()) {
t.Helper()
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("failed to create sqlmock: %v", err)
}
prev := db.DB
db.DB = mockDB
return mock, func() {
db.DB = prev
mockDB.Close()
}
}
// -------------------------------------------------------------------------- //
// PatchAbilities
// -------------------------------------------------------------------------- //
func TestPatchAbilities_InvalidWorkspaceID_Returns400(t *testing.T) {
_, cleanup := setupAbilitiesTest(t)
defer cleanup()
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "not-a-valid-uuid"}}
c.Request = httptest.NewRequest("PATCH",
"/workspaces/not-a-valid-uuid/abilities",
bytes.NewBufferString(`{"broadcast_enabled":true}`))
c.Request.Header.Set("Content-Type", "application/json")
c.Request = c.Request.WithContext(context.Background())
PatchAbilities(c)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
}
var body map[string]string
json.Unmarshal(w.Body.Bytes(), &body)
if body["error"] != "invalid workspace ID" {
t.Errorf("expected 'invalid workspace ID', got %q", body["error"])
}
}
func TestPatchAbilities_EmptyBody_Returns400(t *testing.T) {
_, cleanup := setupAbilitiesTest(t)
defer cleanup()
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}}
c.Request = httptest.NewRequest("PATCH",
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
bytes.NewBufferString(`{}`))
c.Request.Header.Set("Content-Type", "application/json")
c.Request = c.Request.WithContext(context.Background())
PatchAbilities(c)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
}
var body map[string]string
json.Unmarshal(w.Body.Bytes(), &body)
if body["error"] != "at least one ability field required" {
t.Errorf("expected 'at least one ability field required', got %q", body["error"])
}
}
func TestPatchAbilities_InvalidJSON_Returns400(t *testing.T) {
_, cleanup := setupAbilitiesTest(t)
defer cleanup()
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}}
c.Request = httptest.NewRequest("PATCH",
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
bytes.NewBufferString(`{invalid json}`))
c.Request.Header.Set("Content-Type", "application/json")
c.Request = c.Request.WithContext(context.Background())
PatchAbilities(c)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
}
var body map[string]string
json.Unmarshal(w.Body.Bytes(), &body)
if body["error"] != "invalid request body" {
t.Errorf("expected 'invalid request body', got %q", body["error"])
}
}
func TestPatchAbilities_WorkspaceNotFound_Returns404(t *testing.T) {
mock, cleanup := setupAbilitiesTest(t)
defer cleanup()
mock.ExpectQuery("SELECT EXISTS").
WithArgs("550e8400-e29b-41d4-a716-446655440000").
WillReturnError(sql.ErrNoRows)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}}
c.Request = httptest.NewRequest("PATCH",
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
bytes.NewBufferString(`{"broadcast_enabled":true}`))
c.Request.Header.Set("Content-Type", "application/json")
c.Request = c.Request.WithContext(context.Background())
PatchAbilities(c)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String())
}
var body map[string]string
json.Unmarshal(w.Body.Bytes(), &body)
if body["error"] != "workspace not found" {
t.Errorf("expected 'workspace not found', got %q", body["error"])
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet expectations: %v", err)
}
}
func TestPatchAbilities_WorkspaceDBError_Returns500(t *testing.T) {
mock, cleanup := setupAbilitiesTest(t)
defer cleanup()
mock.ExpectQuery("SELECT EXISTS").
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",
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
bytes.NewBufferString(`{"broadcast_enabled":true}`))
c.Request.Header.Set("Content-Type", "application/json")
c.Request = c.Request.WithContext(context.Background())
PatchAbilities(c)
// Handler treats DB error as not-found (|| !exists short-circuits on err=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("unmet expectations: %v", err)
}
}
func TestPatchAbilities_UpdateBroadcastEnabled_Returns200(t *testing.T) {
mock, cleanup := setupAbilitiesTest(t)
defer cleanup()
mock.ExpectQuery("SELECT EXISTS").
WithArgs("550e8400-e29b-41d4-a716-446655440000").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
// $1=id, $2=value in the UPDATE SET col=$2 WHERE id=$1 query.
mock.ExpectExec("UPDATE workspaces SET broadcast_enabled").
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",
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
bytes.NewBufferString(`{"broadcast_enabled":true}`))
c.Request.Header.Set("Content-Type", "application/json")
c.Request = c.Request.WithContext(context.Background())
PatchAbilities(c)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var body map[string]string
json.Unmarshal(w.Body.Bytes(), &body)
if body["status"] != "updated" {
t.Errorf("expected status=updated, got %v", body)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet expectations: %v", err)
}
}
func TestPatchAbilities_UpdateTalkToUserEnabled_Returns200(t *testing.T) {
mock, cleanup := setupAbilitiesTest(t)
defer cleanup()
mock.ExpectQuery("SELECT EXISTS").
WithArgs("550e8400-e29b-41d4-a716-446655440000").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
// $1=id, $2=value in the UPDATE SET col=$2 WHERE id=$1 query.
mock.ExpectExec("UPDATE workspaces SET talk_to_user_enabled").
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",
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
bytes.NewBufferString(`{"talk_to_user_enabled":true}`))
c.Request.Header.Set("Content-Type", "application/json")
c.Request = c.Request.WithContext(context.Background())
PatchAbilities(c)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var body map[string]string
json.Unmarshal(w.Body.Bytes(), &body)
if body["status"] != "updated" {
t.Errorf("expected status=updated, got %v", body)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet expectations: %v", err)
}
}
func TestPatchAbilities_UpdateBothAbilities_Returns200(t *testing.T) {
mock, cleanup := setupAbilitiesTest(t)
defer cleanup()
mock.ExpectQuery("SELECT EXISTS").
WithArgs("550e8400-e29b-41d4-a716-446655440000").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
// $1=id, $2=value in the UPDATE SET col=$2 WHERE id=$1 query.
mock.ExpectExec("UPDATE workspaces SET broadcast_enabled").
WithArgs("550e8400-e29b-41d4-a716-446655440000", true).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("UPDATE workspaces SET talk_to_user_enabled").
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",
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
bytes.NewBufferString(`{"broadcast_enabled":true,"talk_to_user_enabled":false}`))
c.Request.Header.Set("Content-Type", "application/json")
c.Request = c.Request.WithContext(context.Background())
PatchAbilities(c)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var body map[string]string
json.Unmarshal(w.Body.Bytes(), &body)
if body["status"] != "updated" {
t.Errorf("expected status=updated, got %v", body)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet expectations: %v", err)
}
}
func TestPatchAbilities_UpdateBroadcastDisabled_Returns200(t *testing.T) {
mock, cleanup := setupAbilitiesTest(t)
defer cleanup()
mock.ExpectQuery("SELECT EXISTS").
WithArgs("550e8400-e29b-41d4-a716-446655440000").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
// $1=id, $2=value in the UPDATE SET col=$2 WHERE id=$1 query.
mock.ExpectExec("UPDATE workspaces SET broadcast_enabled").
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",
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
bytes.NewBufferString(`{"broadcast_enabled":false}`))
c.Request.Header.Set("Content-Type", "application/json")
c.Request = c.Request.WithContext(context.Background())
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 expectations: %v", err)
}
}