package api import ( "encoding/json" "net/http" "testing" ) func seedTenantWithOwner(fs *fakeStore) { fs.clients = []ClientRow{{ID: "client-acme", Slug: "acme", Name: "Acme Retail", Active: true}} fs.addUser("owner@acme.com", "correct horse battery", UserRecord{ ID: "u-owner", ClientID: "client-acme", Role: "owner", Active: true, Email: "owner@acme.com", }) } func TestSuspendingACompanyEndsItsSessionsNow(t *testing.T) { s, fs := newServer(t) seedPlatformAdmin(fs) seedTenantWithOwner(fs) owner := login(t, s, "owner@acme.com", "correct horse battery") admin := login(t, s, "root@loyaly.ai", "admin123") rec := do(t, s, "PATCH", "/api/admin/clients/client-acme", admin.Token, map[string]any{"active": false}) if rec.Code != http.StatusOK { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } var out struct { SessionsRevoked int `json:"sessions_revoked"` } _ = json.Unmarshal(rec.Body.Bytes(), &out) if out.SessionsRevoked != 1 { t.Fatalf("expected the owner's one session revoked, got %d", out.SessionsRevoked) } // The owner's token stops working immediately, not at expiry. if rec := do(t, s, "GET", "/api/team", owner.Token, nil); rec.Code != http.StatusUnauthorized { t.Fatalf("suspended tenant's session still works: %d", rec.Code) } } func TestDeletingACompanyIsATwoStepDecision(t *testing.T) { s, fs := newServer(t) seedPlatformAdmin(fs) seedTenantWithOwner(fs) b := &fakeBroker{} s.Broker = b admin := login(t, s, "root@loyaly.ai", "admin123") // Active: refused, whatever the confirmation says. rec := do(t, s, "DELETE", "/api/admin/clients/client-acme", admin.Token, map[string]any{"confirm": "acme"}) if rec.Code != http.StatusConflict { t.Fatalf("deleted an active company: %d %s", rec.Code, rec.Body.String()) } do(t, s, "PATCH", "/api/admin/clients/client-acme", admin.Token, map[string]any{"active": false}) // Suspended but the slug is wrong: refused. rec = do(t, s, "DELETE", "/api/admin/clients/client-acme", admin.Token, map[string]any{"confirm": "acm"}) if rec.Code != http.StatusBadRequest { t.Fatalf("deleted without the slug: %d %s", rec.Code, rec.Body.String()) } rec = do(t, s, "DELETE", "/api/admin/clients/client-acme", admin.Token, map[string]any{"confirm": "acme"}) if rec.Code != http.StatusOK { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } if len(fs.clients) != 0 { t.Fatal("company row survived") } if len(b.deleted) != 1 || b.deleted[0] != "acme.shop1" { t.Fatalf("broker logins not removed: %v", b.deleted) } } func TestAdminResetsTheOwnersPasswordAndItIsShownOnce(t *testing.T) { s, fs := newServer(t) seedPlatformAdmin(fs) seedTenantWithOwner(fs) admin := login(t, s, "root@loyaly.ai", "admin123") rec := do(t, s, "POST", "/api/admin/clients/client-acme/owner-password", admin.Token, nil) if rec.Code != http.StatusOK { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } var out struct{ Email, Password string } _ = json.Unmarshal(rec.Body.Bytes(), &out) if out.Email != "owner@acme.com" || out.Password == "" { t.Fatalf("unexpected result: %s", rec.Body.String()) } if rec := do(t, s, "POST", "/api/auth/login", "", map[string]string{"email": "owner@acme.com", "password": "correct horse battery"}); rec.Code != http.StatusUnauthorized { t.Fatalf("old password still works: %d", rec.Code) } login(t, s, "owner@acme.com", out.Password) } func TestATenantUserCannotReachTheAdminClientRoutes(t *testing.T) { s, fs := newServer(t) seedTenantWithOwner(fs) owner := login(t, s, "owner@acme.com", "correct horse battery") for _, c := range []struct{ method, path string }{ {"PATCH", "/api/admin/clients/client-acme"}, {"POST", "/api/admin/clients/client-acme/owner-password"}, {"DELETE", "/api/admin/clients/client-acme"}, } { if rec := do(t, s, c.method, c.path, owner.Token, map[string]any{"active": false, "confirm": "acme"}); rec.Code != http.StatusNotFound { t.Errorf("%s %s: tenant user got %d, want 404", c.method, c.path, rec.Code) } } } func TestAnOwnerRemovesAnEmptyShopButNotOneWithCameras(t *testing.T) { s, fs := newServer(t) b := &fakeBroker{} s.Broker = b seedTenantWithOwner(fs) fs.sites = []SiteHealth{ {SiteID: "site-empty", Slug: "empty", Name: "Empty"}, {SiteID: siteA, Slug: "chennai", Name: "TeNext Chennai"}, } fs.cameras = []Camera{{ID: "c1", SiteID: siteA, CameraID: "entrance"}} owner := login(t, s, "owner@acme.com", "correct horse battery") if rec := do(t, s, "DELETE", "/api/sites/chennai", owner.Token, nil); rec.Code != http.StatusConflict { t.Fatalf("removed a shop with a camera: %d %s", rec.Code, rec.Body.String()) } if rec := do(t, s, "DELETE", "/api/sites/empty", owner.Token, nil); rec.Code != http.StatusNoContent { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } if len(b.deleted) != 1 { t.Fatalf("broker login not removed: %v", b.deleted) } }