package server_test import ( "net/http" "testing" "gitea.meghsakha.com/platform/tenant-registry/internal/keycloak" "gitea.meghsakha.com/platform/tenant-registry/internal/store" ) func TestCreateTenant_provisionsKeycloak(t *testing.T) { eachStore(t, func(t *testing.T, h *testHarness) { resp, body := h.do("POST", "/v1/tenants", map[string]any{ "slug": "kc-co", "name": "KC Co.", "admin_email": "owner@kc-co.test", "admin_name": "Pat Owner", }) if resp.StatusCode != http.StatusCreated { t.Fatalf("status = %d, body=%s", resp.StatusCode, body) } out := decode[struct { Tenant *store.Tenant `json:"tenant"` InviteURL string `json:"invite_url"` }](t, body) if out.Tenant.Slug != "kc-co" { t.Errorf("slug = %q", out.Tenant.Slug) } if out.InviteURL == "" { t.Error("invite_url missing in response") } // The mock recorded the call. if _, ok := h.kcMock.Orgs[out.Tenant.ID]; !ok { t.Errorf("kc mock did not record org for tenant %s", out.Tenant.ID) } if _, ok := h.kcMock.Users["owner@kc-co.test"]; !ok { t.Error("kc mock did not record user for owner@kc-co.test") } // And we emitted a keycloak.invite_sent audit event. resp, body = h.do("GET", "/v1/audit?action=keycloak.invite_sent&tenant_id="+out.Tenant.ID, nil) if resp.StatusCode != 200 { t.Fatalf("audit list status = %d", resp.StatusCode) } listed := decode[struct { Items []store.AuditEvent `json:"items"` }](t, body) if len(listed.Items) != 1 { t.Errorf("expected 1 invite_sent event, got %d", len(listed.Items)) } }) } func TestCreateTenant_kcFailure_doesNotRollback(t *testing.T) { eachStore(t, func(t *testing.T, h *testHarness) { // Force the mock to fail the next call. h.kcMock.FailNext = keycloak.ErrUnavailable resp, body := h.do("POST", "/v1/tenants", map[string]any{ "slug": "kc-fail", "name": "KC Fail", "admin_email": "x@y.test", }) if resp.StatusCode != http.StatusCreated { t.Fatalf("expected tenant still created despite kc fail; status=%d body=%s", resp.StatusCode, body) } out := decode[struct { Tenant *store.Tenant `json:"tenant"` }](t, body) // Tenant landed in the DB. if out.Tenant.ID == "" { t.Error("tenant id missing") } // And there's a provision_failed audit event for it. _, body = h.do("GET", "/v1/audit?action=keycloak.provision_failed&tenant_id="+out.Tenant.ID, nil) listed := decode[struct { Items []store.AuditEvent `json:"items"` }](t, body) if len(listed.Items) != 1 { t.Errorf("expected 1 provision_failed event, got %d", len(listed.Items)) } }) } func TestKcClaims_returnsCurrentEntitlements(t *testing.T) { eachStore(t, func(t *testing.T, h *testHarness) { resp, body := h.do("POST", "/v1/internal/keycloak/claims", map[string]any{ "tenant_slug": h.tenant.Slug, }) if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, body=%s", resp.StatusCode, body) } got := decode[keycloak.Claims](t, body) if got.TenantID != h.tenant.ID || got.TenantSlug != h.tenant.Slug { t.Errorf("tenant fields off: %+v", got) } if got.Plan != h.tenant.Plan { t.Errorf("plan = %q, want %q", got.Plan, h.tenant.Plan) } if got.TenantStatus != h.tenant.Status { t.Errorf("status = %q, want %q", got.TenantStatus, h.tenant.Status) } // acme is seeded with certifai + compliance entitlements (memory) // or one or zero (postgres, depending on prior subtest ordering). // At minimum the field is present. if got.Products == nil { t.Error("products is nil; should be at least empty slice") } }) } func TestKcClaims_lookupByUserAttrs(t *testing.T) { eachStore(t, func(t *testing.T, h *testHarness) { resp, body := h.do("POST", "/v1/internal/keycloak/claims", map[string]any{ "user_attrs": map[string]string{"tenant_slug": h.tenant.Slug}, }) if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, body=%s", resp.StatusCode, body) } got := decode[keycloak.Claims](t, body) if got.TenantID != h.tenant.ID { t.Errorf("did not resolve via user_attrs; got %+v", got) } }) } func TestKcClaims_missingTenant404(t *testing.T) { eachStore(t, func(t *testing.T, h *testHarness) { resp, _ := h.do("POST", "/v1/internal/keycloak/claims", map[string]any{ "tenant_slug": "nope-nope", }) if resp.StatusCode != http.StatusNotFound { t.Errorf("status = %d, want 404", resp.StatusCode) } }) } func TestKcClaims_requiresInput(t *testing.T) { eachStore(t, func(t *testing.T, h *testHarness) { resp, _ := h.do("POST", "/v1/internal/keycloak/claims", map[string]any{}) if resp.StatusCode != http.StatusBadRequest { t.Errorf("status = %d, want 400", resp.StatusCode) } }) }