import { beforeEach, describe, expect, it, vi } from "vitest"; const { hashLocalPassword, queryDatabase, transactionQuery, verifyLocalPassword, withDatabaseTransaction, state } = vi.hoisted(() => { const state = { user: {} as Record, transactionTail: Promise.resolve() as Promise }; const transactionQuery = vi.fn(async (text: string, values: readonly unknown[] = []) => { if (text.includes("SELECT * FROM platform_users")) return { rows: [{ ...state.user }], rowCount: 1 }; if (text.includes("SET failed_login_count=$2")) { state.user.failed_login_count = values[1]; state.user.locked_until = values[2]; state.user.updated_at = values[3]; return { rows: [], rowCount: 1 }; } if (text.includes("SET failed_login_count=0")) { state.user.failed_login_count = 0; state.user.locked_until = null; state.user.last_login_at = values[1]; state.user.updated_at = values[1]; return { rows: [{ ...state.user }], rowCount: 1 }; } if (text.includes("SET password_hash=$2")) { state.user.password_hash = values[1]; state.user.password_salt = values[2]; state.user.session_version = Number(state.user.session_version) + 1; state.user.updated_at = values[3]; return { rows: [{ ...state.user }], rowCount: 1 }; } throw new Error(`Unexpected SQL: ${text}`); }); const withDatabaseTransaction = vi.fn((callback: (client: { query: typeof transactionQuery }) => Promise) => { const run = state.transactionTail.then(() => callback({ query: transactionQuery })); state.transactionTail = run.catch(() => undefined); return run; }); return { hashLocalPassword: vi.fn(async (password: string) => ({ hash: `hash:${password}`, salt: `salt:${password}` })), queryDatabase: vi.fn(), transactionQuery, verifyLocalPassword: vi.fn(async (password: string, hash: string) => hash === `hash:${password}`), withDatabaseTransaction, state }; }); vi.mock("@/lib/server/database", () => ({ isPostgresBackend: () => true, queryDatabase, withDatabaseTransaction })); vi.mock("@/lib/server/data-store", () => ({ reassignOwnerData: vi.fn() })); vi.mock("@/lib/server/auth/password", () => ({ hashLocalPassword, verifyLocalPassword })); import { authenticatePlatformUser, changeOwnPassword } from "@/lib/server/account-store"; describe("PostgreSQL account authentication", () => { beforeEach(() => { queryDatabase.mockReset(); transactionQuery.mockClear(); withDatabaseTransaction.mockClear(); hashLocalPassword.mockClear(); verifyLocalPassword.mockClear(); state.transactionTail = Promise.resolve(); state.user = { id: "user-1", phone: "13800138000", display_name: "Concurrent user", role: "super_admin", organization_id: null, status: "active", password_hash: "hash", password_salt: "salt", failed_login_count: 0, locked_until: null, session_version: 1, last_login_at: null, legacy_subject: null, created_at: new Date("2026-08-12T00:00:00.000Z"), updated_at: new Date("2026-08-12T00:00:00.000Z") }; }); it("allows only one concurrent password change using the same current password", async () => { state.user.password_hash = "hash:current-password"; state.user.password_salt = "salt:current-password"; const attempts = await Promise.allSettled([ changeOwnPassword("user-1", "current-password", "next-password-one"), changeOwnPassword("user-1", "current-password", "next-password-two") ]); expect(attempts[0]).toMatchObject({ status: "fulfilled", value: { sessionVersion: 2 } }); expect(attempts[1]).toMatchObject({ status: "rejected", reason: { status: 400 } }); expect(state.user.password_hash).toBe("hash:next-password-one"); expect(state.user.session_version).toBe(2); expect(withDatabaseTransaction).toHaveBeenCalledTimes(2); expect(transactionQuery.mock.calls.filter(([sql]) => String(sql).includes("FOR UPDATE"))).toHaveLength(2); expect(transactionQuery.mock.calls.filter(([sql]) => String(sql).includes("session_version=session_version+1"))).toHaveLength(1); expect(hashLocalPassword).toHaveBeenCalledTimes(1); expect(queryDatabase).not.toHaveBeenCalled(); }); it("serializes concurrent failures on the user row and locks on the fifth attempt", async () => { const attempts = await Promise.allSettled( Array.from({ length: 5 }, () => authenticatePlatformUser("138 0013 8000", "wrong-password")) ); expect(attempts.map((attempt) => attempt.status === "rejected" && attempt.reason.status)).toEqual([401, 401, 401, 401, 423]); expect(state.user.failed_login_count).toBe(0); expect(new Date(String(state.user.locked_until)).getTime()).toBeGreaterThan(Date.now()); expect(withDatabaseTransaction).toHaveBeenCalledTimes(5); expect(transactionQuery.mock.calls.filter(([sql]) => String(sql).includes("FOR UPDATE"))).toHaveLength(5); expect(queryDatabase).not.toHaveBeenCalled(); await expect(authenticatePlatformUser("13800138000", "wrong-password")).rejects.toMatchObject({ status: 423 }); expect(transactionQuery.mock.calls.filter(([sql]) => String(sql).includes("SET failed_login_count=$2"))).toHaveLength(5); }); });