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>
This commit is contained in:
2026-04-10 15:41:42 +02:00
parent 9bd3781c03
commit dfeb4d361e
11 changed files with 711 additions and 545 deletions
@@ -21,23 +21,33 @@ const totpValidateMock = vi.hoisted(() => vi.fn<() => number | null>());
vi.mock("otpauth", () => {
class Secret {
base32: string;
constructor() { this.base32 = "TESTBASE32SECRET"; }
static fromBase32(v: string) { return v; }
constructor() {
this.base32 = "TESTBASE32SECRET";
}
static fromBase32(v: string) {
return v;
}
}
class TOTP {
validate(_args: { token: string; window: number }) { return totpValidateMock(); }
toString() { return "otpauth://totp/CapaKraken:test@example.com?secret=TESTBASE32SECRET"; }
validate(_args: { token: string; window: number }) {
return totpValidateMock();
}
toString() {
return "otpauth://totp/CapaKraken:test@example.com?secret=TESTBASE32SECRET";
}
}
return { Secret, TOTP };
});
// ─── rate-limit mock ──────────────────────────────────────────────────────────
// Default: rate limit allows all requests. Override in specific tests.
const totpRateLimiterMock = vi.hoisted(() => vi.fn(async (_key: string) => ({
allowed: true,
remaining: 9,
resetAt: new Date(Date.now() + 30_000),
})));
const totpRateLimiterMock = vi.hoisted(() =>
vi.fn(async (_key: string) => ({
allowed: true,
remaining: 9,
resetAt: new Date(Date.now() + 30_000),
})),
);
vi.mock("../middleware/rate-limit.js", () => ({
totpRateLimiter: totpRateLimiterMock,
@@ -85,6 +95,7 @@ function makePublicCtx(dbOverrides: Record<string, unknown> = {}) {
db: {
user: {
findUnique: vi.fn(),
update: vi.fn().mockResolvedValue({}),
...((dbOverrides.user as object | undefined) ?? {}),
},
},
@@ -94,7 +105,9 @@ function makePublicCtx(dbOverrides: Record<string, unknown> = {}) {
// ─── generateTotpSecret ───────────────────────────────────────────────────────
describe("generateTotpSecret", () => {
beforeEach(() => { vi.clearAllMocks(); });
beforeEach(() => {
vi.clearAllMocks();
});
it("writes the base32 secret to the user record", async () => {
const ctx = makeSelfServiceCtx();
@@ -134,14 +147,13 @@ describe("verifyAndEnableTotp", () => {
const ctx = makeSelfServiceCtx({
user: { findUnique: vi.fn().mockResolvedValue(baseUser) },
});
const result = await verifyAndEnableTotp(
ctx as Parameters<typeof verifyAndEnableTotp>[0],
{ token: "123456" },
);
const result = await verifyAndEnableTotp(ctx as Parameters<typeof verifyAndEnableTotp>[0], {
token: "123456",
});
expect(result).toEqual({ enabled: true });
expect(ctx.db.user.update).toHaveBeenCalledWith({
where: { id: "user_1" },
data: { totpEnabled: true },
data: { totpEnabled: true, lastTotpAt: expect.any(Date) },
});
});
@@ -178,7 +190,9 @@ describe("verifyAndEnableTotp", () => {
const ctx = makeSelfServiceCtx({
user: { findUnique: vi.fn().mockResolvedValue(baseUser) },
});
await verifyAndEnableTotp(ctx as Parameters<typeof verifyAndEnableTotp>[0], { token: "123456" });
await verifyAndEnableTotp(ctx as Parameters<typeof verifyAndEnableTotp>[0], {
token: "123456",
});
// Audit entry is fire-and-forget; wait one tick
await new Promise((r) => setTimeout(r, 0));
expect(ctx.db.auditLog.create).toHaveBeenCalled();
@@ -191,7 +205,11 @@ describe("verifyTotp", () => {
beforeEach(() => {
vi.clearAllMocks();
totpValidateMock.mockReset();
totpRateLimiterMock.mockResolvedValue({ allowed: true, remaining: 9, resetAt: new Date(Date.now() + 30_000) });
totpRateLimiterMock.mockResolvedValue({
allowed: true,
remaining: 9,
resetAt: new Date(Date.now() + 30_000),
});
});
const mfaUser = { id: "user_1", totpSecret: "TESTBASE32SECRET", totpEnabled: true };
@@ -214,13 +232,13 @@ describe("verifyTotp", () => {
).rejects.toThrow(new TRPCError({ code: "UNAUTHORIZED", message: "Invalid TOTP token." }));
});
it("throws BAD_REQUEST when user does not have MFA enabled", async () => {
it("throws UNAUTHORIZED when user does not have MFA enabled (prevents user enumeration)", async () => {
const ctx = makePublicCtx({
user: { findUnique: vi.fn().mockResolvedValue({ ...mfaUser, totpEnabled: false }) },
});
await expect(
verifyTotp(ctx as Parameters<typeof verifyTotp>[0], { userId: "user_1", token: "123456" }),
).rejects.toThrow(new TRPCError({ code: "BAD_REQUEST", message: "TOTP is not enabled for this user." }));
).rejects.toThrow(new TRPCError({ code: "UNAUTHORIZED", message: "Invalid TOTP token." }));
});
it("throws BAD_REQUEST when user has no TOTP secret (inconsistent state)", async () => {
@@ -233,11 +251,20 @@ describe("verifyTotp", () => {
});
it("throws TOO_MANY_REQUESTS when rate limit is exceeded", async () => {
totpRateLimiterMock.mockResolvedValue({ allowed: false, remaining: 0, resetAt: new Date(Date.now() + 30_000) });
totpRateLimiterMock.mockResolvedValue({
allowed: false,
remaining: 0,
resetAt: new Date(Date.now() + 30_000),
});
const ctx = makePublicCtx({ user: { findUnique: vi.fn().mockResolvedValue(mfaUser) } });
await expect(
verifyTotp(ctx as Parameters<typeof verifyTotp>[0], { userId: "user_1", token: "123456" }),
).rejects.toThrow(new TRPCError({ code: "TOO_MANY_REQUESTS", message: "Too many TOTP attempts. Please wait before trying again." }));
).rejects.toThrow(
new TRPCError({
code: "TOO_MANY_REQUESTS",
message: "Too many TOTP attempts. Please wait before trying again.",
}),
);
});
it("does not check the token when rate limit is exceeded", async () => {
@@ -253,7 +280,10 @@ describe("verifyTotp", () => {
it("calls the rate limiter with the userId as key", async () => {
totpValidateMock.mockReturnValue(0);
const ctx = makePublicCtx({ user: { findUnique: vi.fn().mockResolvedValue(mfaUser) } });
await verifyTotp(ctx as Parameters<typeof verifyTotp>[0], { userId: "user_1", token: "123456" });
await verifyTotp(ctx as Parameters<typeof verifyTotp>[0], {
userId: "user_1",
token: "123456",
});
expect(totpRateLimiterMock).toHaveBeenCalledWith("user_1");
});
});
@@ -261,7 +291,9 @@ describe("verifyTotp", () => {
// ─── getCurrentMfaStatus ──────────────────────────────────────────────────────
describe("getCurrentMfaStatus", () => {
beforeEach(() => { vi.clearAllMocks(); });
beforeEach(() => {
vi.clearAllMocks();
});
it("returns totpEnabled: true when MFA is active", async () => {
const ctx = makeSelfServiceCtx({