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

452 lines
18 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
type SeedService = typeof import("./SeedService").default;
const dbMock = {
user: {
findUnique: vi.fn(async ({ where }: { where: { email: string } }) => ({ id: `user-${where.email}` })),
create: vi.fn(),
},
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}`, 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: { stripeId: string } }) => ({ id: `product-${where.stripeId}` })),
},
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(),
},
};
const hashMock = vi.fn(async () => "hashed-password");
const genSaltMock = vi.fn(async () => "generated-salt");
const seedRolesAndPermissionsMock = vi.fn();
const seedStandardEntitlementsMock = 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("@toolctrl/db/entitlements-seed.server", () => ({
getEntitlementPlanStripeId: ({ id }: { id: string }) => `toolctrl_entitlement_plan_${id}`,
seedStandardEntitlements: seedStandardEntitlementsMock,
}));
vi.mock("@toolctrl/db/seed.server", async () => {
const actual = await vi.importActual<typeof import("@toolctrl/db/seed.server")>("@toolctrl/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 }));
async function loadSeedService(): Promise<SeedService> {
vi.resetModules();
const module = await import("./SeedService");
return module.default;
}
describe("SeedService", () => {
beforeEach(() => {
vi.clearAllMocks();
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;
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("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();
const consoleLog = 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();
const consoleLog = 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: { stripeId: "toolctrl_entitlement_plan_starter" },
select: { id: true },
});
expect(dbMock.subscriptionProduct.findFirst).toHaveBeenCalledWith({
where: { stripeId: "toolctrl_entitlement_plan_pro" },
select: { id: true },
});
expect(dbMock.subscriptionProduct.findFirst).toHaveBeenCalledWith({
where: { stripeId: "toolctrl_entitlement_plan_business" },
select: { id: true },
});
expect(dbMock.tenantSubscriptionProduct.create).toHaveBeenCalledTimes(3);
expect(dbMock.entity.findMany).toHaveBeenCalledWith({
where: { name: { in: ["company", "contact", "opportunity"] } },
select: { id: true, name: true, type: true },
});
expect(dbMock.entity.create).toHaveBeenCalledWith(expect.objectContaining({ data: expect.objectContaining({ name: "company", type: "all" }) }));
expect(dbMock.entity.create).toHaveBeenCalledWith(expect.objectContaining({ data: expect.objectContaining({ name: "contact", type: "all" }) }));
expect(dbMock.entity.create).toHaveBeenCalledWith(expect.objectContaining({ data: expect.objectContaining({ name: "opportunity", type: "all" }) }));
expect(dbMock.entity.create).not.toHaveBeenCalledWith(expect.objectContaining({ data: expect.objectContaining({ name: "submission" }) }));
expect(dbMock.tenantProductConfiguration.upsert).toHaveBeenCalledTimes(3);
expect(dbMock.knowledgeBase.upsert).toHaveBeenCalledTimes(3);
expect(dbMock.supportTicket.upsert).toHaveBeenCalledTimes(6);
expect(loggerInfo).toHaveBeenCalledWith(
expect.objectContaining({ op: "seed.createDemoUsers", count: 2 }),
"Creating CRM SaaS demo users with tenants",
);
});
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";
const consoleLog = vi.spyOn(console, "log").mockImplementation(() => undefined);
const SeedService = await loadSeedService();
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(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,
diagnostics: expect.any(Function),
});
expect(calls).toEqual(["roles", "entitlements"]);
});
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();
const consoleLog = 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();
const consoleLog = 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");
});
});