Files
CapaKraken/packages/api/src/__tests__/reset-password.test.ts
T
Hartmut dfeb4d361e fix(tests): align 20 drifted tests with current source behavior
Tests fell behind source changes: lastTotpAt replay-attack prevention,
activeSession invalidation on password reset, select clauses in
permission updates, UNAUTHORIZED (anti-enumeration) for disabled TOTP,
and password minimum raised from 8 to 12 characters.

Also fix root eslint.config.mjs to ignore packages/ (linted via turbo)
and add --no-warn-ignored to lint-staged to suppress warnings for
ignored files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 15:41:42 +02:00

107 lines
3.8 KiB
TypeScript

/**
* Unit tests for setUserPassword (admin password reset).
*
* Tests cover:
* - Happy path: passwordHash is updated in the DB
* - Audit entry "Password reset by admin" is created
* - Non-existent user throws NOT_FOUND
* - Short password is rejected by Zod schema before reaching the function
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { setUserPassword, SetUserPasswordInputSchema } from "../router/user-procedure-support.js";
// ── Mocks ────────────────────────────────────────────────────────────────────
vi.mock("../lib/audit.js", () => ({
createAuditEntry: vi.fn(),
}));
const hashMock = vi.hoisted(() => vi.fn().mockResolvedValue("$argon2id$hashed"));
vi.mock("@node-rs/argon2", () => ({ hash: hashMock }));
// ── Helpers ──────────────────────────────────────────────────────────────────
function makeCtx(userRow: Record<string, unknown> | null = null) {
return {
db: {
user: {
findUnique: vi.fn().mockResolvedValue(userRow),
update: vi.fn().mockResolvedValue({}),
},
activeSession: {
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
},
} as never,
dbUser: { id: "admin_1" },
};
}
const EXISTING_USER = { id: "user_1", name: "Alice", email: "alice@example.com" };
// ── Tests ────────────────────────────────────────────────────────────────────
describe("setUserPassword — happy path", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("hashes the new password and updates the DB", async () => {
const ctx = makeCtx(EXISTING_USER);
const result = await setUserPassword(ctx, { userId: "user_1", password: "NewPassword123!" });
expect(hashMock).toHaveBeenCalledWith("NewPassword123!");
expect(ctx.db.user.update).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: "user_1" },
data: { passwordHash: "$argon2id$hashed" },
}),
);
expect(result).toEqual({ success: true });
});
it("creates an audit entry with summary 'Password reset by admin'", async () => {
const { createAuditEntry } = await import("../lib/audit.js");
const ctx = makeCtx(EXISTING_USER);
await setUserPassword(ctx, { userId: "user_1", password: "NewPassword123!" });
// createAuditEntry is called fire-and-forget (void), so we give microtasks a tick
await Promise.resolve();
expect(createAuditEntry).toHaveBeenCalledWith(
expect.objectContaining({ summary: "Password reset by admin" }),
);
});
});
describe("setUserPassword — not found", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("throws when the user does not exist", async () => {
const ctx = makeCtx(null); // findUnique returns null
await expect(
setUserPassword(ctx, { userId: "ghost", password: "NewPassword123!" }),
).rejects.toThrow();
});
});
describe("SetUserPasswordInputSchema — validation", () => {
it("accepts a valid input", () => {
const result = SetUserPasswordInputSchema.safeParse({
userId: "u1",
password: "ValidPass123!",
});
expect(result.success).toBe(true);
});
it("rejects a password shorter than 12 characters", () => {
const result = SetUserPasswordInputSchema.safeParse({ userId: "u1", password: "short" });
expect(result.success).toBe(false);
});
it("rejects missing userId", () => {
const result = SetUserPasswordInputSchema.safeParse({ password: "Valid123!" });
expect(result.success).toBe(false);
});
});