|
|
|
@@ -67,6 +67,7 @@ let dbAccounts: AccountRow[] = [];
|
|
|
|
|
let dbStaff: StaffRow[] = [];
|
|
|
|
|
let insertedUsers: UserRow[] = [];
|
|
|
|
|
let insertedAccounts: AccountRow[] = [];
|
|
|
|
|
let updatedAccounts: Array<{ id: string; password: string }> = [];
|
|
|
|
|
let updatedStaff: Array<{ id: string; userId: string }> = [];
|
|
|
|
|
|
|
|
|
|
const originalEnv = { ...process.env };
|
|
|
|
@@ -77,6 +78,7 @@ function resetMock() {
|
|
|
|
|
dbStaff = [];
|
|
|
|
|
insertedUsers = [];
|
|
|
|
|
insertedAccounts = [];
|
|
|
|
|
updatedAccounts = [];
|
|
|
|
|
updatedStaff = [];
|
|
|
|
|
process.env = { ...originalEnv };
|
|
|
|
|
}
|
|
|
|
@@ -173,7 +175,11 @@ async function seedUatCredentials(
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (existingAccount) {
|
|
|
|
|
// skip — already has credential account
|
|
|
|
|
// Idempotent update: re-hash the current env password and update the stored hash.
|
|
|
|
|
const { hashPassword } = await import("better-auth/crypto");
|
|
|
|
|
const passwordHash = await hashPassword(password);
|
|
|
|
|
existingAccount.password = passwordHash;
|
|
|
|
|
updatedAccounts.push({ id: existingAccount.id, password: passwordHash });
|
|
|
|
|
} else {
|
|
|
|
|
// Use Better-Auth's hashPassword so test helper matches production seed.ts
|
|
|
|
|
const { hashPassword } = await import("better-auth/crypto");
|
|
|
|
@@ -312,9 +318,9 @@ describe("seedUatCredentials — credential provisioning logic", () => {
|
|
|
|
|
expect(updatedStaff).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── AC-5: idempotent — skips when user already exists ───────────────────────
|
|
|
|
|
// ── AC-5: idempotent — does not insert duplicate records ───────────────────
|
|
|
|
|
|
|
|
|
|
it("AC-5: re-running does not duplicate user or account records (idempotent)", async () => {
|
|
|
|
|
it("AC-5: re-running does not insert duplicate user or account records", async () => {
|
|
|
|
|
process.env.SEED_UAT_CUSTOMER_PASSWORD = TEST_PASSWORD;
|
|
|
|
|
|
|
|
|
|
const preExistingUsers: UserRow[] = [
|
|
|
|
@@ -330,25 +336,96 @@ describe("seedUatCredentials — credential provisioning logic", () => {
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// First call — nothing inserted (user + account pre-exist)
|
|
|
|
|
await seedUatCredentials([UAT_ACCOUNTS[2]!], {
|
|
|
|
|
users: preExistingUsers,
|
|
|
|
|
accounts: preExistingAccounts,
|
|
|
|
|
staff: [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// No inserts — user and account already exist
|
|
|
|
|
expect(insertedUsers).toHaveLength(0);
|
|
|
|
|
expect(insertedAccounts).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── AC-5b: password rotation on re-seed ─────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
it("AC-5b: re-running with a new password updates the stored credential hash", async () => {
|
|
|
|
|
const OLD_PASSWORD = "old-password-abc";
|
|
|
|
|
const NEW_PASSWORD = "new-password-xyz";
|
|
|
|
|
process.env.SEED_UAT_CUSTOMER_PASSWORD = NEW_PASSWORD;
|
|
|
|
|
|
|
|
|
|
const preExistingUsers: UserRow[] = [
|
|
|
|
|
{ id: "pre-existing-user", email: "uat-customer@groombook.dev", name: "UAT Customer", emailVerified: true },
|
|
|
|
|
];
|
|
|
|
|
const preExistingAccounts: AccountRow[] = [
|
|
|
|
|
{
|
|
|
|
|
id: "pre-existing-acct",
|
|
|
|
|
accountId: "pre-existing-user",
|
|
|
|
|
providerId: "credential",
|
|
|
|
|
userId: "pre-existing-user",
|
|
|
|
|
password: await hashPassword(OLD_PASSWORD),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Second call — still nothing inserted
|
|
|
|
|
await seedUatCredentials([UAT_ACCOUNTS[2]!], {
|
|
|
|
|
users: preExistingUsers,
|
|
|
|
|
accounts: preExistingAccounts,
|
|
|
|
|
staff: [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// No new records inserted
|
|
|
|
|
expect(insertedUsers).toHaveLength(0);
|
|
|
|
|
expect(insertedAccounts).toHaveLength(0);
|
|
|
|
|
// Password WAS updated to the new env value
|
|
|
|
|
expect(updatedAccounts).toHaveLength(1);
|
|
|
|
|
expect(updatedAccounts[0]!.id).toBe("pre-existing-acct");
|
|
|
|
|
// New hash is valid Better-Auth format (salt:key, each hex)
|
|
|
|
|
const newHashParts = updatedAccounts[0]!.password.split(":");
|
|
|
|
|
expect(Buffer.from(newHashParts[0]!, "hex")).toHaveLength(16);
|
|
|
|
|
expect(Buffer.from(newHashParts[1]!, "hex")).toHaveLength(64);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── AC-8: existing account password IS updated (not frozen at first-seed) ──
|
|
|
|
|
|
|
|
|
|
it("AC-8: re-seeding with a changed password env var updates the stored hash", async () => {
|
|
|
|
|
const ORIGINAL_PASSWORD = "original-password";
|
|
|
|
|
const ROTATED_PASSWORD = "rotated-password-456";
|
|
|
|
|
|
|
|
|
|
process.env.SEED_UAT_CUSTOMER_PASSWORD = ROTATED_PASSWORD;
|
|
|
|
|
|
|
|
|
|
const preExistingUsers: UserRow[] = [
|
|
|
|
|
{ id: "pre-existing-user", email: "uat-customer@groombook.dev", name: "UAT Customer", emailVerified: true },
|
|
|
|
|
];
|
|
|
|
|
// Account was created with the original password on first seed
|
|
|
|
|
const originalHash = await hashPassword(ORIGINAL_PASSWORD);
|
|
|
|
|
const preExistingAccounts: AccountRow[] = [
|
|
|
|
|
{
|
|
|
|
|
id: "pre-existing-acct",
|
|
|
|
|
accountId: "pre-existing-user",
|
|
|
|
|
providerId: "credential",
|
|
|
|
|
userId: "pre-existing-user",
|
|
|
|
|
password: originalHash,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Re-seed with the rotated password env var
|
|
|
|
|
await seedUatCredentials([UAT_ACCOUNTS[2]!], {
|
|
|
|
|
users: preExistingUsers,
|
|
|
|
|
accounts: preExistingAccounts,
|
|
|
|
|
staff: [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// No new user or account created
|
|
|
|
|
expect(insertedUsers).toHaveLength(0);
|
|
|
|
|
expect(insertedAccounts).toHaveLength(0);
|
|
|
|
|
|
|
|
|
|
// The pre-existing account's password WAS updated (not frozen at first-seed).
|
|
|
|
|
// hashPassword uses a random salt so we verify by format + that it is a new,
|
|
|
|
|
// different valid hash from the original.
|
|
|
|
|
const updatedAcct = preExistingAccounts[0]!;
|
|
|
|
|
expect(updatedAcct.password).toBeDefined();
|
|
|
|
|
expect(updatedAcct.password).toMatch(/^[a-f0-9]{32}:[a-f0-9]{128}$/);
|
|
|
|
|
expect(updatedAcct.password).not.toBe(originalHash); // it actually changed
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── AC-6: missing env var skips with warning ────────────────────────────────
|
|
|
|
|