- Fix 6 chat-extension manifests: "name" → "title" (was blocking install) - Add "type": "surface" to hello-dashboard, icd-test-runner, sdk-test-runner - Add dormant status to packages schema (SQLite + Postgres) - Installer auto-detects requires: ["chat"] → sets dormant + disabled - Enable endpoint returns 409 for dormant packages - Relax dependency check: pending_review libraries allowed - Admin UI: dormant badge, disabled Enable button, Dormant stat card - 4 new handler tests (143 total passing) - Docker audit: all 16 packages install correctly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
233 lines
6.9 KiB
Go
233 lines
6.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
authpkg "switchboard-core/auth"
|
|
"switchboard-core/config"
|
|
"switchboard-core/database"
|
|
"switchboard-core/middleware"
|
|
"switchboard-core/store"
|
|
postgres "switchboard-core/store/postgres"
|
|
sqlite "switchboard-core/store/sqlite"
|
|
)
|
|
|
|
// ── Dormant Test Harness ──────────────────────
|
|
|
|
type dormantHarness struct {
|
|
*testHarness
|
|
adminToken string
|
|
adminID string
|
|
stores store.Stores
|
|
pkgH *PackageHandler
|
|
}
|
|
|
|
func setupDormantHarness(t *testing.T) *dormantHarness {
|
|
t.Helper()
|
|
database.RequireTestDB(t)
|
|
database.TruncateAll(t)
|
|
|
|
cfg := &config.Config{
|
|
JWTSecret: testJWTSecret,
|
|
BasePath: "",
|
|
}
|
|
|
|
var stores store.Stores
|
|
if database.IsSQLite() {
|
|
stores = sqlite.NewStores(database.TestDB)
|
|
} else {
|
|
stores = postgres.NewStores(database.TestDB)
|
|
}
|
|
userCache := middleware.NewUserStatusCache()
|
|
|
|
r := gin.New()
|
|
api := r.Group("/api/v1")
|
|
|
|
auth := NewAuthHandler(cfg, stores, nil, authpkg.NewBuiltinProvider())
|
|
api.POST("/auth/register", auth.Register)
|
|
|
|
pkgH := NewPackageHandler(stores)
|
|
|
|
// Protected (authenticated) routes
|
|
protected := api.Group("")
|
|
protected.Use(middleware.Auth(cfg, stores.Users, userCache))
|
|
protected.GET("/surfaces", pkgH.ListEnabledSurfaces)
|
|
|
|
// Admin routes
|
|
admin := api.Group("/admin")
|
|
admin.Use(middleware.Auth(cfg, stores.Users, userCache), middleware.RequireAdmin(stores))
|
|
admin.GET("/packages", pkgH.ListPackages)
|
|
admin.PUT("/packages/:id/enable", pkgH.EnablePackage)
|
|
admin.PUT("/packages/:id/disable", pkgH.DisablePackage)
|
|
|
|
// Seed admin user
|
|
adminID := seedInsertReturningID(t,
|
|
`INSERT INTO users (username, email, password_hash, handle, auth_source) VALUES ($1, $2, $3, $4, $5) RETURNING id`,
|
|
"dorm-admin", "dorm-admin@test.com", "$2a$10$test", "dorm-admin", "builtin",
|
|
)
|
|
database.SeedEveryoneGroupMember(t, adminID)
|
|
database.SeedAdminsGroupMember(t, adminID)
|
|
adminToken := makeToken(adminID, "dorm-admin@test.com", "admin")
|
|
|
|
return &dormantHarness{
|
|
testHarness: &testHarness{router: r, t: t},
|
|
adminToken: adminToken,
|
|
adminID: adminID,
|
|
stores: stores,
|
|
pkgH: pkgH,
|
|
}
|
|
}
|
|
|
|
// seedPackage inserts a package directly into the DB for testing.
|
|
func (h *dormantHarness) seedPackage(id, title, pkgType, status string, enabled bool, manifest map[string]any) {
|
|
h.t.Helper()
|
|
ctx := context.Background()
|
|
|
|
manifestJSON, _ := json.Marshal(manifest)
|
|
|
|
q := dialectSQL(`INSERT INTO packages (id, title, type, version, tier, manifest, enabled, status, source)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`)
|
|
|
|
enabledInt := 0
|
|
if enabled {
|
|
enabledInt = 1
|
|
}
|
|
|
|
_, err := database.TestDB.ExecContext(ctx, q,
|
|
id, title, pkgType, "1.0.0", "browser", string(manifestJSON), enabledInt, status, "extension",
|
|
)
|
|
if err != nil {
|
|
h.t.Fatalf("seedPackage(%s): %v", id, err)
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════
|
|
// Tests
|
|
// ═══════════════════════════════════════════════
|
|
|
|
// TestSetStatus_Dormant verifies the DB CHECK constraint accepts 'dormant'.
|
|
func TestSetStatus_Dormant(t *testing.T) {
|
|
h := setupDormantHarness(t)
|
|
|
|
h.seedPackage("status-test", "Status Test", "surface", "active", true, map[string]any{
|
|
"id": "status-test", "title": "Status Test", "type": "surface",
|
|
})
|
|
|
|
err := h.stores.Packages.SetStatus(context.Background(), "status-test", "dormant")
|
|
if err != nil {
|
|
t.Fatalf("SetStatus(dormant) failed: %v", err)
|
|
}
|
|
|
|
pkg, err := h.stores.Packages.Get(context.Background(), "status-test")
|
|
if err != nil || pkg == nil {
|
|
t.Fatalf("Get after SetStatus failed: %v", err)
|
|
}
|
|
if pkg.Status != "dormant" {
|
|
t.Errorf("status = %q, want %q", pkg.Status, "dormant")
|
|
}
|
|
}
|
|
|
|
// TestEnableDormant_Blocked verifies the enable endpoint rejects dormant packages.
|
|
func TestEnableDormant_Blocked(t *testing.T) {
|
|
h := setupDormantHarness(t)
|
|
|
|
h.seedPackage("dormant-ext", "Dormant Extension", "extension", "dormant", false, map[string]any{
|
|
"id": "dormant-ext", "title": "Dormant Extension", "type": "extension", "requires": []string{"chat"},
|
|
})
|
|
|
|
w := h.request("PUT", "/api/v1/admin/packages/dormant-ext/enable", h.adminToken, nil)
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("enable dormant: want 409, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
decode(t, w, &resp)
|
|
errMsg, _ := resp["error"].(string)
|
|
if errMsg == "" {
|
|
t.Fatal("expected error message in response")
|
|
}
|
|
}
|
|
|
|
// TestDormantNotInSurfaces verifies dormant packages are excluded from the surfaces list.
|
|
func TestDormantNotInSurfaces(t *testing.T) {
|
|
h := setupDormantHarness(t)
|
|
|
|
// Active surface — should appear
|
|
h.seedPackage("active-surface", "Active Surface", "surface", "active", true, map[string]any{
|
|
"id": "active-surface", "title": "Active Surface", "type": "surface",
|
|
"route": "/s/active-surface",
|
|
})
|
|
// Dormant surface — should NOT appear
|
|
h.seedPackage("dormant-surface", "Dormant Surface", "full", "dormant", false, map[string]any{
|
|
"id": "dormant-surface", "title": "Dormant Surface", "type": "full",
|
|
"route": "/s/dormant-surface", "requires": []string{"chat"},
|
|
})
|
|
|
|
w := h.request("GET", "/api/v1/surfaces", h.adminToken, nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("surfaces list: want 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
decode(t, w, &resp)
|
|
data, _ := resp["data"].([]interface{})
|
|
|
|
for _, item := range data {
|
|
m, _ := item.(map[string]interface{})
|
|
if m["id"] == "dormant-surface" {
|
|
t.Fatal("dormant surface should not appear in surfaces list")
|
|
}
|
|
}
|
|
|
|
found := false
|
|
for _, item := range data {
|
|
m, _ := item.(map[string]interface{})
|
|
if m["id"] == "active-surface" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("active surface should appear in surfaces list")
|
|
}
|
|
}
|
|
|
|
// TestDormantInAdminList verifies dormant packages still appear in admin package list.
|
|
func TestDormantInAdminList(t *testing.T) {
|
|
h := setupDormantHarness(t)
|
|
|
|
h.seedPackage("vis-active", "Visible Active", "surface", "active", true, map[string]any{
|
|
"id": "vis-active", "title": "Visible Active", "type": "surface",
|
|
})
|
|
h.seedPackage("vis-dormant", "Visible Dormant", "extension", "dormant", false, map[string]any{
|
|
"id": "vis-dormant", "title": "Visible Dormant", "type": "extension", "requires": []string{"chat"},
|
|
})
|
|
|
|
w := h.request("GET", "/api/v1/admin/packages", h.adminToken, nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("admin list: want 200, got %d", w.Code)
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
decode(t, w, &resp)
|
|
data, _ := resp["data"].([]interface{})
|
|
|
|
ids := map[string]bool{}
|
|
for _, item := range data {
|
|
m, _ := item.(map[string]interface{})
|
|
id, _ := m["id"].(string)
|
|
ids[id] = true
|
|
}
|
|
|
|
if !ids["vis-active"] {
|
|
t.Error("active package missing from admin list")
|
|
}
|
|
if !ids["vis-dormant"] {
|
|
t.Error("dormant package missing from admin list")
|
|
}
|
|
}
|