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:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user