package handlers import ( "context" "encoding/json" "net/http" "testing" "github.com/gin-gonic/gin" authpkg "armature/auth" "armature/config" "armature/database" "armature/middleware" "armature/store" postgres "armature/store/postgres" sqlite "armature/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") } }