Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/db/seed/SeedService.test.ts
T

713 lines
30 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
type SeedService = typeof import("./SeedService").default;
const dbMock = {
$queryRaw: vi.fn(async () => [{ marked: true }]),
user: {
findUnique: vi.fn(async ({ where }: { where: { email: string } }) => ({ id: `user-${where.email}`, active: true })),
create: vi.fn(),
update: vi.fn(async ({ where, data }: { where: { id: string }; data: { active: boolean } }) => ({ id: where.id, active: data.active })),
},
adminUser: {
findUnique: vi.fn(async () => ({ userId: "admin-id" })),
create: vi.fn(),
},
tenant: {
findUnique: vi.fn(async ({ where, include }: { where: { slug: string }; include?: unknown }) => ({
id: `tenant-${where.slug}`,
slug: where.slug,
name: where.slug,
...(include ? { subscription: { id: `subscription-${where.slug}`, lastInvoiceExternalId: null, lastPaymentExternalId: null, products: [] } } : {}),
})),
create: vi.fn(),
update: vi.fn(),
},
tenantInboundAddress: {
findFirst: vi.fn(),
create: vi.fn(),
},
tenantSubscription: {
findUnique: vi.fn(async ({ where }: { where: { tenantId: string } }) => ({ id: `subscription-${where.tenantId}` })),
create: vi.fn(),
},
tenantUser: {
findFirst: vi.fn(),
create: vi.fn(),
},
subscriptionProduct: {
findFirst: vi.fn(async ({ where }: { where: { billingProviderId: string } }) => ({ id: `product-${where.billingProviderId}` })),
},
tenantSubscriptionProduct: {
create: vi.fn(),
},
tenantProductConfiguration: {
upsert: vi.fn(),
},
tenantFeatureOverride: {
upsert: vi.fn(),
},
entity: {
aggregate: vi.fn(async () => ({ _max: { order: 0 } })),
findMany: vi.fn(async () => []),
create: vi.fn(async ({ data }: { data: { name: string } }) => ({ id: `entity-${data.name}`, ...data })),
updateMany: vi.fn(),
},
entityWebhook: {
create: vi.fn(),
},
property: {
create: vi.fn(async ({ data }: { data: { name: string } }) => ({ id: `property-${data.name}`, ...data })),
},
propertyOption: {
deleteMany: vi.fn(),
create: vi.fn(),
},
propertyAttribute: {
deleteMany: vi.fn(),
create: vi.fn(),
},
permission: {
findMany: vi.fn(async ({ where }: { where: { name: { in: string[] } } }) => where.name.in.map((name) => ({ id: `permission-${name}`, name }))),
},
role: {
findFirst: vi.fn(),
create: vi.fn(async ({ data }: { data: { name: string } }) => ({ id: `role-${data.name}` })),
},
rolePermission: {
findFirst: vi.fn(),
create: vi.fn(),
},
userRole: {
findFirst: vi.fn(),
create: vi.fn(),
},
knowledgeBase: {
upsert: vi.fn(async ({ create }: { create: { slug: string } }) => ({ id: `kb-${create.slug}` })),
},
knowledgeBaseCategory: {
upsert: vi.fn(async ({ create }: { create: { slug: string } }) => ({ id: `category-${create.slug}` })),
},
knowledgeBaseArticle: {
upsert: vi.fn(),
},
supportTicket: {
upsert: vi.fn(),
},
app: {
upsert: vi.fn(),
},
automationTemplate: {
findFirst: vi.fn(async () => null),
create: vi.fn(),
update: vi.fn(),
},
bookingPage: {
upsert: vi.fn(async ({ create }: { create: { slug: string; tenantId: string } }) => ({ id: `booking-page-${create.tenantId}-${create.slug}` })),
},
bookingPageProvider: {
upsert: vi.fn(),
},
availabilityRule: {
upsert: vi.fn(),
},
availabilityException: {
upsert: vi.fn(),
},
appointment: {
upsert: vi.fn(),
},
calendarEvent: {
upsert: vi.fn(),
deleteMany: vi.fn(),
},
};
const hashMock = vi.fn(async () => "hashed-password");
const genSaltMock = vi.fn(async () => "generated-salt");
const seedRolesAndPermissionsMock = vi.fn();
const seedStandardEntitlementsMock = vi.fn();
const seedStagingDomainDemoMock = vi.fn();
const originalEnv = { ...process.env };
const loggerInfo = vi.fn();
const loggerWarn = vi.fn();
const loggerError = vi.fn();
const loggerDebug = vi.fn();
vi.mock("~/utils/logger.server", () => ({
createScopedLogger: () => ({ info: loggerInfo, warn: loggerWarn, error: loggerError, debug: loggerDebug }),
}));
vi.mock("bcryptjs", () => ({ default: { genSalt: genSaltMock, hash: hashMock } }));
vi.mock("@hivegrid/db/entitlements-seed.server", () => ({
getEntitlementPlanBillingProviderId: ({ id }: { id: string }) => `hivegrid_entitlement_plan_${id}`,
seedStandardEntitlements: seedStandardEntitlementsMock,
}));
vi.mock("@hivegrid/db/seed.server", async () => {
const actual = await vi.importActual<typeof import("@hivegrid/db/seed.server")>("@hivegrid/db/seed.server");
return actual;
});
vi.mock("~/utils/services/emailService", () => ({ getAvailableTenantInboundAddress: vi.fn(async () => "seed@example.test") }));
vi.mock("~/utils/services/rolesAndPermissionsService", () => ({ seedRolesAndPermissions: seedRolesAndPermissionsMock }));
vi.mock("~/utils/db.server", () => ({ db: dbMock }));
vi.mock("./stagingDomainDemoSeed.server", () => ({ seedStagingDomainDemo: seedStagingDomainDemoMock }));
async function loadSeedService(): Promise<SeedService> {
vi.resetModules();
const module = await import("./SeedService");
return module.default;
}
describe("SeedService", () => {
beforeEach(() => {
vi.clearAllMocks();
dbMock.user.findUnique.mockImplementation(async ({ where }: { where: { email: string } }) => ({ id: `user-${where.email}`, active: true }));
dbMock.user.update.mockImplementation(async ({ where, data }: { where: { id: string }; data: { active: boolean } }) => ({ id: where.id, active: data.active }));
process.env = { ...originalEnv };
delete process.env.SEED_ADMIN_EMAIL;
delete process.env.SEED_ADMIN_PASSWORD;
delete process.env.SEED_DEMO_DATA;
delete process.env.ALLOW_STAGING_DEMO_SEED;
delete process.env.SEED_DEMO_PRO_MANAGED;
delete process.env.LOCAL_DEV_CREDENTIALS;
// These unit cases exercise explicit seed environments. The dedicated
// local-credential contract suite covers the fail-closed CI branch.
delete process.env.CI;
delete process.env.SENTRY_ENVIRONMENT;
delete process.env.RLS_RUNTIME_MODE;
delete process.env.ADMIN_APP_HOST;
process.env.NODE_ENV = "test";
});
it("rejects default admin credentials in production before DB writes", async () => {
process.env.NODE_ENV = "production";
const consoleLog = vi.spyOn(console, "log").mockImplementation(() => undefined);
const SeedService = await loadSeedService();
await expect(SeedService.seed()).rejects.toThrow("default seed credentials");
expect(dbMock.user.findUnique).not.toHaveBeenCalled();
expect(dbMock.user.create).not.toHaveBeenCalled();
expect(seedRolesAndPermissionsMock).not.toHaveBeenCalled();
expect(seedStandardEntitlementsMock).not.toHaveBeenCalled();
expect(JSON.stringify(consoleLog.mock.calls)).not.toContain("password");
});
it("uses safe admin credential overrides in production", async () => {
process.env.NODE_ENV = "production";
process.env.SEED_ADMIN_EMAIL = "owner@example.com";
process.env.SEED_ADMIN_PASSWORD = "safe-production-password";
const consoleLog = vi.spyOn(console, "log").mockImplementation(() => undefined);
const SeedService = await loadSeedService();
await SeedService.seed();
expect(dbMock.user.findUnique).toHaveBeenCalledWith({ where: { email: "owner@example.com" } });
expect(dbMock.user.findUnique).not.toHaveBeenCalledWith({ where: { email: "john.doe@company.com" } });
expect(dbMock.user.findUnique).not.toHaveBeenCalledWith({ where: { email: "luna.davis@company.com" } });
expect(dbMock.tenant.findUnique).not.toHaveBeenCalled();
expect(genSaltMock).toHaveBeenCalledWith(10);
expect(hashMock).toHaveBeenCalledWith("safe-production-password", "generated-salt");
expect(seedRolesAndPermissionsMock).toHaveBeenCalledWith({
adminEmail: "owner@example.com",
diagnostics: expect.any(Function),
});
expect(loggerInfo).toHaveBeenCalledWith(
expect.objectContaining({ op: "db-seed", phase: "start", nodeEnv: "production" }),
"DB seed event",
);
expect(loggerInfo).toHaveBeenCalledWith(
expect.objectContaining({ op: "db-seed", phase: "success", nodeEnv: "production" }),
"DB seed event",
);
// The new logger redacts admin email behind a SHA-256 fingerprint. The
// raw email must never appear in any logger arg.
expect(JSON.stringify(loggerInfo.mock.calls)).not.toContain("owner@example.com");
expect(JSON.stringify(loggerInfo.mock.calls)).not.toContain("safe-production-password");
expect(JSON.stringify(consoleLog.mock.calls)).not.toContain("safe-production-password");
});
it("keeps the existing seed override path when the canonical local launcher flag is absent", async () => {
process.env.SEED_ADMIN_EMAIL = "owner@example.com";
process.env.SEED_ADMIN_PASSWORD = "safe-development-password";
const SeedService = await loadSeedService();
await SeedService.seed();
expect(dbMock.user.findUnique).toHaveBeenCalledWith({ where: { email: "owner@example.com" } });
expect(dbMock.user.findUnique).not.toHaveBeenCalledWith({ where: { email: "admin@email.com" } });
expect(hashMock).toHaveBeenCalledWith("safe-development-password", "generated-salt");
expect(dbMock.user.update).not.toHaveBeenCalled();
expect(dbMock.$queryRaw).not.toHaveBeenCalled();
});
it("restores canonical seed credentials only through the verified local launcher contract", async () => {
process.env.NODE_ENV = "development";
process.env.LOCAL_DEV_CREDENTIALS = "1";
process.env.DATABASE_URL = "postgresql://local:local@127.0.0.1:5432/hivegrid";
process.env.ADMIN_APP_HOST = "admin.localhost:3000";
process.env.SEED_ADMIN_EMAIL = "owner@example.com";
process.env.SEED_ADMIN_PASSWORD = "safe-production-password";
dbMock.user.findUnique.mockResolvedValueOnce({ id: "local-admin", active: false } as never);
dbMock.user.update.mockResolvedValueOnce({ id: "local-admin", active: true } as never);
const SeedService = await loadSeedService();
await SeedService.seed();
expect(dbMock.user.findUnique).toHaveBeenCalledWith({ where: { email: "admin@email.com" } });
expect(dbMock.user.findUnique).not.toHaveBeenCalledWith({ where: { email: "owner@example.com" } });
expect(hashMock).toHaveBeenCalledWith("password", "generated-salt");
expect(dbMock.$queryRaw).toHaveBeenCalledOnce();
expect(dbMock.user.update).toHaveBeenCalledWith({
where: { id: "local-admin" },
data: { passwordHash: "hashed-password", active: true },
});
});
it("fails before seed work when the local launcher flag targets a remote database", async () => {
process.env.NODE_ENV = "development";
process.env.LOCAL_DEV_CREDENTIALS = "1";
process.env.DATABASE_URL = "postgresql://app:secret@staging-db:5432/hivegrid";
process.env.ADMIN_APP_HOST = "admin.localhost:3000";
const SeedService = await loadSeedService();
await expect(SeedService.seed()).rejects.toThrow("loopback PostgreSQL");
expect(dbMock.user.findUnique).not.toHaveBeenCalled();
expect(dbMock.user.create).not.toHaveBeenCalled();
expect(hashMock).not.toHaveBeenCalled();
expect(seedRolesAndPermissionsMock).not.toHaveBeenCalled();
expect(dbMock.$queryRaw).not.toHaveBeenCalled();
});
it("fails before seed work when a loopback database lacks the disposable-local marker", async () => {
process.env.NODE_ENV = "development";
process.env.LOCAL_DEV_CREDENTIALS = "1";
process.env.DATABASE_URL = "postgresql://local:local@127.0.0.1:5432/hivegrid";
process.env.ADMIN_APP_HOST = "admin.localhost:3000";
dbMock.$queryRaw.mockResolvedValueOnce([{ marked: false }]);
const SeedService = await loadSeedService();
await expect(SeedService.seed()).rejects.toThrow("database marked as disposable local development data");
expect(dbMock.user.findUnique).not.toHaveBeenCalled();
expect(dbMock.user.create).not.toHaveBeenCalled();
expect(hashMock).not.toHaveBeenCalled();
expect(seedRolesAndPermissionsMock).not.toHaveBeenCalled();
});
it("creates seed-managed users as active", async () => {
dbMock.user.findUnique.mockResolvedValueOnce(null as never);
dbMock.user.create.mockResolvedValueOnce({ id: "new-admin", active: true } as never);
const SeedService = await loadSeedService();
await SeedService.seed();
expect(dbMock.user.create).toHaveBeenCalledWith({
data: expect.objectContaining({ active: true }),
});
});
it("rejects blank admin credential overrides in production before DB writes", async () => {
process.env.NODE_ENV = "production";
process.env.SEED_ADMIN_EMAIL = "";
process.env.SEED_ADMIN_PASSWORD = "safe-production-password";
const SeedService = await loadSeedService();
await expect(SeedService.seed()).rejects.toThrow("missing seed credentials");
expect(dbMock.user.findUnique).not.toHaveBeenCalled();
expect(dbMock.user.create).not.toHaveBeenCalled();
expect(seedRolesAndPermissionsMock).not.toHaveBeenCalled();
expect(seedStandardEntitlementsMock).not.toHaveBeenCalled();
});
it("refuses to promote an existing non-admin user during production seed", async () => {
process.env.NODE_ENV = "production";
process.env.SEED_ADMIN_EMAIL = "owner@example.com";
process.env.SEED_ADMIN_PASSWORD = "safe-production-password";
dbMock.user.findUnique.mockResolvedValueOnce({ id: "existing-user", email: "owner@example.com" } as never);
dbMock.adminUser.findUnique.mockResolvedValueOnce(null as never);
const SeedService = await loadSeedService();
await expect(SeedService.seed()).rejects.toThrow("Production seed refused to promote an existing non-admin user");
expect(dbMock.adminUser.create).not.toHaveBeenCalled();
expect(seedRolesAndPermissionsMock).not.toHaveBeenCalled();
expect(seedStandardEntitlementsMock).not.toHaveBeenCalled();
});
it("skips local demo users and tenants unless explicitly enabled", async () => {
const SeedService = await loadSeedService();
vi.spyOn(console, "log").mockImplementation(() => undefined);
await SeedService.seed();
expect(dbMock.user.findUnique).toHaveBeenCalledWith({ where: { email: "admin@email.com" } });
expect(dbMock.user.findUnique).not.toHaveBeenCalledWith({ where: { email: "john.doe@company.com" } });
expect(dbMock.user.findUnique).not.toHaveBeenCalledWith({ where: { email: "luna.davis@company.com" } });
expect(dbMock.tenant.findUnique).not.toHaveBeenCalled();
expect(loggerInfo).not.toHaveBeenCalledWith(
expect.objectContaining({ op: "seed.createDemoUsers" }),
expect.anything(),
);
});
it("seeds local demo users and tenants only when explicitly enabled outside production", async () => {
process.env.SEED_DEMO_DATA = "true";
const SeedService = await loadSeedService();
vi.spyOn(console, "log").mockImplementation(() => undefined);
await SeedService.seed();
expect(dbMock.user.findUnique).toHaveBeenCalledWith({ where: { email: "admin@email.com" } });
expect(dbMock.user.findUnique).toHaveBeenCalledWith({ where: { email: "john.doe@company.com" } });
expect(dbMock.user.findUnique).toHaveBeenCalledWith({ where: { email: "luna.davis@company.com" } });
expect(dbMock.tenant.findUnique).toHaveBeenCalledWith({ where: { slug: "demo-starter-crm" } });
expect(dbMock.tenant.findUnique).toHaveBeenCalledWith({ where: { slug: "demo-pro-crm" } });
expect(dbMock.tenant.findUnique).toHaveBeenCalledWith({ where: { slug: "demo-business-crm" } });
expect(dbMock.subscriptionProduct.findFirst).toHaveBeenCalledWith({
where: { billingProviderId: "hivegrid_entitlement_plan_starter" },
select: { id: true },
});
expect(dbMock.subscriptionProduct.findFirst).toHaveBeenCalledWith({
where: { billingProviderId: "hivegrid_entitlement_plan_pro" },
select: { id: true },
});
expect(dbMock.subscriptionProduct.findFirst).toHaveBeenCalledWith({
where: { billingProviderId: "hivegrid_entitlement_plan_business" },
select: { id: true },
});
expect(dbMock.tenantSubscriptionProduct.create).toHaveBeenCalledTimes(3);
// R-P6 (DoD 1 + 6): the legacy entity-builder CRM seed
// (ensureProductCoreCrmEntities) was removed from the seed path; demo
// tenants now receive the (currently empty) SHK custom-field template
// instead, so no `company`/`contact`/`opportunity` Entity rows are created.
expect(dbMock.entity.create).not.toHaveBeenCalled();
expect(dbMock.tenantProductConfiguration.upsert).toHaveBeenCalledTimes(3);
expect(dbMock.knowledgeBase.upsert).toHaveBeenCalledTimes(3);
expect(dbMock.supportTicket.upsert).toHaveBeenCalledTimes(6);
expect(seedStagingDomainDemoMock).toHaveBeenCalledOnce();
expect(seedStagingDomainDemoMock).toHaveBeenCalledWith({
tenantId: "tenant-demo-business-crm",
ownerUserId: "user-john.doe@company.com",
});
// AK-P16a/b: booking + calendar demo data per demo tenant (2 pages, 2
// providers each, Mo-Fr rules x 2 providers, 2 exceptions, 4 appointments,
// 3 internal mirrors + 1 external event; the cancelled appointment clears
// its mirror instead).
expect(dbMock.bookingPage.upsert).toHaveBeenCalledTimes(6);
expect(dbMock.bookingPageProvider.upsert).toHaveBeenCalledTimes(12);
expect(dbMock.availabilityRule.upsert).toHaveBeenCalledTimes(30);
expect(dbMock.availabilityException.upsert).toHaveBeenCalledTimes(6);
expect(dbMock.appointment.upsert).toHaveBeenCalledTimes(12);
expect(dbMock.calendarEvent.upsert).toHaveBeenCalledTimes(12);
expect(dbMock.calendarEvent.deleteMany).toHaveBeenCalledTimes(3);
expect(dbMock.bookingPage.upsert).toHaveBeenCalledWith(
expect.objectContaining({
where: { tenantId_slug: { tenantId: "tenant-demo-starter-crm", slug: "beratungstermin" } },
create: expect.objectContaining({ isDemoData: true, isPublished: true }),
}),
);
expect(dbMock.appointment.upsert).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: "seed-demo-starter-crm-appt-cancelled-consult" },
create: expect.objectContaining({ status: "cancelled", isDemoData: true, internalCalendarEventId: null }),
}),
);
expect(loggerInfo).toHaveBeenCalledWith(
expect.objectContaining({ op: "seed.createDemoUsers", count: 2 }),
"Creating CRM SaaS demo users with tenants",
);
});
it("keeps the managed demo-pro tenant outside the legacy boot demo seed", async () => {
process.env.SEED_DEMO_DATA = "true";
process.env.SEED_DEMO_PRO_MANAGED = "true";
const SeedService = await loadSeedService();
vi.spyOn(console, "log").mockImplementation(() => undefined);
await SeedService.seed();
expect(dbMock.tenant.findUnique).toHaveBeenCalledWith({ where: { slug: "demo-starter-crm" } });
expect(dbMock.tenant.findUnique).toHaveBeenCalledWith({ where: { slug: "demo-business-crm" } });
expect(dbMock.tenant.findUnique).not.toHaveBeenCalledWith(expect.objectContaining({ where: { slug: "demo-pro-crm" } }));
expect(dbMock.tenantUser.create).toHaveBeenCalledTimes(6);
expect(dbMock.subscriptionProduct.findFirst).toHaveBeenCalledWith({
where: { billingProviderId: "hivegrid_entitlement_plan_starter" },
select: { id: true },
});
expect(dbMock.subscriptionProduct.findFirst).not.toHaveBeenCalledWith({
where: { billingProviderId: "hivegrid_entitlement_plan_pro" },
select: { id: true },
});
expect(dbMock.subscriptionProduct.findFirst).toHaveBeenCalledWith({
where: { billingProviderId: "hivegrid_entitlement_plan_business" },
select: { id: true },
});
expect(dbMock.tenantSubscriptionProduct.create).toHaveBeenCalledTimes(2);
expect(dbMock.tenantProductConfiguration.upsert).toHaveBeenCalledTimes(2);
expect(dbMock.knowledgeBase.upsert).toHaveBeenCalledTimes(2);
expect(dbMock.supportTicket.upsert).toHaveBeenCalledTimes(4);
expect(dbMock.bookingPage.upsert).toHaveBeenCalledTimes(4);
expect(dbMock.bookingPageProvider.upsert).toHaveBeenCalledTimes(8);
expect(dbMock.availabilityRule.upsert).toHaveBeenCalledTimes(20);
expect(dbMock.availabilityException.upsert).toHaveBeenCalledTimes(4);
expect(dbMock.appointment.upsert).toHaveBeenCalledTimes(8);
expect(dbMock.calendarEvent.upsert).toHaveBeenCalledTimes(8);
expect(dbMock.calendarEvent.deleteMany).toHaveBeenCalledTimes(2);
expect(seedStagingDomainDemoMock).toHaveBeenCalledOnce();
expect(seedStagingDomainDemoMock).toHaveBeenCalledWith({
tenantId: "tenant-demo-business-crm",
ownerUserId: "user-john.doe@company.com",
});
expect(seedRolesAndPermissionsMock).toHaveBeenCalledWith({
adminEmail: "admin@email.com",
diagnostics: expect.any(Function),
excludedTenantSlugs: ["demo-pro-crm"],
});
});
it("does not reassign a demo tenant plan after self-service checkout", async () => {
process.env.SEED_DEMO_DATA = "true";
dbMock.tenant.findUnique.mockImplementation(async ({ where, include }: { where: { slug: string }; include?: unknown }) => ({
id: `tenant-${where.slug}`,
slug: where.slug,
name: where.slug,
...(include
? {
subscription: {
id: `subscription-${where.slug}`,
lastInvoiceExternalId: where.slug === "demo-pro-crm" ? "hgco_paid_checkout" : null,
lastPaymentExternalId: where.slug === "demo-pro-crm" ? "hgco_paid_checkout" : null,
products: [],
},
}
: {}),
}));
const SeedService = await loadSeedService();
await SeedService.seed();
expect(dbMock.subscriptionProduct.findFirst).toHaveBeenCalledWith({
where: { billingProviderId: "hivegrid_entitlement_plan_starter" },
select: { id: true },
});
expect(dbMock.subscriptionProduct.findFirst).not.toHaveBeenCalledWith({
where: { billingProviderId: "hivegrid_entitlement_plan_pro" },
select: { id: true },
});
expect(dbMock.subscriptionProduct.findFirst).toHaveBeenCalledWith({
where: { billingProviderId: "hivegrid_entitlement_plan_business" },
select: { id: true },
});
});
it("refuses demo data in production without the explicit staging override", async () => {
process.env.NODE_ENV = "production";
process.env.SEED_DEMO_DATA = "true";
process.env.SEED_ADMIN_EMAIL = "owner@example.com";
process.env.SEED_ADMIN_PASSWORD = "safe-production-password";
const SeedService = await loadSeedService();
await expect(SeedService.seed()).rejects.toThrow("ALLOW_STAGING_DEMO_SEED=true");
expect(dbMock.user.findUnique).not.toHaveBeenCalled();
expect(seedRolesAndPermissionsMock).not.toHaveBeenCalled();
expect(seedStandardEntitlementsMock).not.toHaveBeenCalled();
});
it("allows explicit staging demo data while production runtime safety stays opt-in", async () => {
process.env.NODE_ENV = "production";
process.env.SEED_DEMO_DATA = "true";
process.env.ALLOW_STAGING_DEMO_SEED = "true";
process.env.SEED_ADMIN_EMAIL = "owner@example.com";
process.env.SEED_ADMIN_PASSWORD = "safe-production-password";
vi.spyOn(console, "log").mockImplementation(() => undefined);
const SeedService = await loadSeedService();
dbMock.user.findUnique
.mockResolvedValueOnce({ id: "admin-user", active: true } as never)
.mockResolvedValueOnce({ id: "demo-user-1", active: false } as never)
.mockResolvedValueOnce({ id: "demo-user-2", active: false } as never);
await SeedService.seed();
expect(dbMock.user.findUnique).toHaveBeenCalledWith({ where: { email: "john.doe@company.com" } });
expect(dbMock.tenant.findUnique).toHaveBeenCalledWith({ where: { slug: "demo-starter-crm" } });
expect(dbMock.tenantProductConfiguration.upsert).toHaveBeenCalled();
expect(dbMock.user.update).toHaveBeenCalledTimes(2);
expect(dbMock.user.update).toHaveBeenCalledWith({
where: { id: "demo-user-1" },
data: { active: true },
});
expect(dbMock.user.update).toHaveBeenCalledWith({
where: { id: "demo-user-2" },
data: { active: true },
});
expect(loggerInfo).toHaveBeenCalledWith(
expect.objectContaining({ op: "db-seed", phase: "start", nodeEnv: "staging-demo" }),
"DB seed event",
);
expect(JSON.stringify(loggerInfo.mock.calls)).not.toContain("owner@example.com");
});
it("seeds standard entitlements once after roles and permissions", async () => {
const SeedService = await loadSeedService();
const calls: string[] = [];
seedRolesAndPermissionsMock.mockImplementation(async () => {
calls.push("roles");
});
seedStandardEntitlementsMock.mockImplementation(async () => {
calls.push("entitlements");
return {
planCount: 3,
featureCount: 9,
createdProducts: 3,
updatedProducts: 0,
deletedFeatures: 0,
createdFeatures: 9,
};
});
await SeedService.seed();
expect(seedRolesAndPermissionsMock).toHaveBeenCalledWith({
adminEmail: "admin@email.com",
diagnostics: expect.any(Function),
});
expect(seedStandardEntitlementsMock).toHaveBeenCalledOnce();
expect(seedStandardEntitlementsMock).toHaveBeenCalledWith({
client: dbMock,
includeCustomerPortalCommercialProducts: true,
includeEmailMarketingCommercialProducts: true,
diagnostics: expect.any(Function),
});
expect(calls).toEqual(["roles", "entitlements"]);
});
it("upserts the integration catalog into the global App table (R-P5)", async () => {
const SeedService = await loadSeedService();
const { INTEGRATION_CATALOG } = await import("@hivegrid/integrations");
await SeedService.seed();
expect(dbMock.app.upsert).toHaveBeenCalledTimes(INTEGRATION_CATALOG.length);
expect(dbMock.app.upsert).toHaveBeenCalledWith(
expect.objectContaining({
where: { key: "sample-data-sync" },
create: expect.objectContaining({ key: "sample-data-sync", title: "integrations.app.sampleDataSync.title", connectionType: "rest_api", isActive: true }),
update: expect.objectContaining({ title: "integrations.app.sampleDataSync.title" }),
})
);
});
it("logs redacted DB seed failure diagnostics without raw secret errors", async () => {
process.env.SEED_ADMIN_EMAIL = "owner@example.com";
process.env.SEED_ADMIN_PASSWORD = "safe-secret-password";
const SeedService = await loadSeedService();
const consoleLog = vi.spyOn(console, "log").mockImplementation(() => undefined);
const originalError = new Error("failed with safe-secret-password token sk_live_secret");
seedRolesAndPermissionsMock.mockRejectedValueOnce(originalError);
await expect(SeedService.seed()).rejects.toBe(originalError);
expect(loggerInfo).toHaveBeenCalledWith(
expect.objectContaining({ op: "db-seed", phase: "failure", nodeEnv: "test" }),
"DB seed event",
);
expect(JSON.stringify(loggerInfo.mock.calls)).not.toContain("owner@example.com");
expect(JSON.stringify(loggerInfo.mock.calls)).not.toContain("safe-secret-password");
expect(JSON.stringify(loggerInfo.mock.calls)).not.toContain("sk_live_secret");
expect(JSON.stringify(consoleLog.mock.calls)).not.toContain("safe-secret-password");
expect(JSON.stringify(consoleLog.mock.calls)).not.toContain("sk_live_secret");
});
it("logs redacted roles and permissions seed diagnostics from the helper", async () => {
const SeedService = await loadSeedService();
vi.spyOn(console, "log").mockImplementation(() => undefined);
seedRolesAndPermissionsMock.mockImplementation(async ({ diagnostics }) => {
await diagnostics?.({
phase: "success",
adminRoleCount: 1,
appRoleCount: 4,
defaultPermissionCount: 120,
dynamicEntityPermissionCount: 3,
adminUserAssignmentCount: 1,
tenantUserAssignmentCount: 8,
password: "password",
token: "sk_live_secret",
});
});
await SeedService.seed();
expect(loggerInfo).toHaveBeenCalledWith(
expect.objectContaining({
op: "seed.rolesAndPermissions",
phase: "success",
adminRoleCount: 1,
appRoleCount: 4,
defaultPermissionCount: 120,
dynamicEntityPermissionCount: 3,
adminUserAssignmentCount: 1,
tenantUserAssignmentCount: 8,
}),
"Roles and permissions seed event",
);
// The helper-supplied `password` and `token` fields must be dropped
// before they hit the logger.
expect(JSON.stringify(loggerInfo.mock.calls)).not.toContain("password");
expect(JSON.stringify(loggerInfo.mock.calls)).not.toContain("sk_live_secret");
});
it("logs redacted entitlements seed diagnostics from the helper", async () => {
const SeedService = await loadSeedService();
vi.spyOn(console, "log").mockImplementation(() => undefined);
seedStandardEntitlementsMock.mockImplementation(async ({ diagnostics }) => {
await diagnostics?.({
phase: "failure",
planCount: 3,
featureCount: 9,
createdProducts: 1,
updatedProducts: 2,
deletedFeatures: 4,
createdFeatures: 5,
planId: "team",
error: {
name: "Error",
message: "Failed for [redacted]",
},
});
return {
planCount: 3,
featureCount: 9,
createdProducts: 1,
updatedProducts: 2,
deletedFeatures: 4,
createdFeatures: 5,
};
});
await SeedService.seed();
expect(loggerInfo).toHaveBeenCalledWith(
expect.objectContaining({
op: "seed.entitlements",
phase: "failure",
planCount: 3,
featureCount: 9,
createdProducts: 1,
updatedProducts: 2,
deletedFeatures: 4,
createdFeatures: 5,
planId: "team",
err: expect.objectContaining({ name: "Error", message: "Failed for [redacted]" }),
}),
"Entitlements seed event",
);
expect(JSON.stringify(loggerInfo.mock.calls)).not.toContain("sk_live_");
expect(JSON.stringify(loggerInfo.mock.calls)).not.toContain("cus_secret");
});
});