mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
test(auth): give the MFA fixtures a Bearer transport
The disable-route suite only ever supplied a cookie-shaped context, which is why the token-id-as-session-id defect was invisible to it. It now covers both arms and asserts the credential shape handed to the session revoker, plus that the elevation is spent before the teardown and not spent when the factor code is wrong.
This commit is contained in:
1 parent
4cd0adc46d
commit
f1c977a7a7
4 files changed
+75
-2
No files matched your search
@@ -55,8 +55,10 @@ function req() {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requireMfaManagementAuth).mockResolvedValue({
|
||||
transport: "cookie",
|
||||
user: { id: "u1" },
|
||||
session: { id: "sess-current" },
|
||||
commitElevation: vi.fn().mockResolvedValue(undefined),
|
||||
} as never);
|
||||
vi.mocked(verifyMfaFactor).mockResolvedValue({ ok: true } as never);
|
||||
vi.mocked(prisma.$transaction).mockImplementation(async (cb: unknown) => {
|
||||
@@ -74,7 +76,68 @@ describe("POST /api/auth/me/mfa/disable", () => {
|
||||
it("revokes other sessions + trusted devices (via destroyOtherSessions), keeping the current one", async () => {
|
||||
const res = await POST(req());
|
||||
expect(res.status).toBe(200);
|
||||
expect(destroyOtherSessions).toHaveBeenCalledWith("u1", "sess-current");
|
||||
expect(destroyOtherSessions).toHaveBeenCalledWith("u1", {
|
||||
kind: "session",
|
||||
sessionId: "sess-current",
|
||||
});
|
||||
});
|
||||
|
||||
it("spares the CALLER's own device login on the Bearer path", async () => {
|
||||
// A Bearer caller has no session row. The earlier revision passed
|
||||
// `session.id` — an ApiToken id on this arm — into a session-scoped query,
|
||||
// so the "keep the current one" exclusion excluded nothing: every browser
|
||||
// session went AND the caller's own refresh token was revoked, logging the
|
||||
// app out at its next rotation. A cookie-shaped fixture could never see it.
|
||||
vi.mocked(requireMfaManagementAuth).mockResolvedValue({
|
||||
transport: "bearer",
|
||||
user: { id: "u1" },
|
||||
apiTokenId: "token-row-1",
|
||||
accessTokenHash: "hash-of-caller-access-token",
|
||||
commitElevation: vi.fn().mockResolvedValue(undefined),
|
||||
} as never);
|
||||
|
||||
const res = await POST(req());
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(destroyOtherSessions).toHaveBeenCalledWith("u1", {
|
||||
kind: "accessToken",
|
||||
accessTokenHash: "hash-of-caller-access-token",
|
||||
});
|
||||
});
|
||||
|
||||
it("spends the elevation before tearing the factor down", async () => {
|
||||
const commitElevation = vi.fn().mockResolvedValue(undefined);
|
||||
vi.mocked(requireMfaManagementAuth).mockResolvedValue({
|
||||
transport: "bearer",
|
||||
user: { id: "u1" },
|
||||
apiTokenId: "token-row-1",
|
||||
accessTokenHash: "h",
|
||||
commitElevation,
|
||||
} as never);
|
||||
|
||||
await POST(req());
|
||||
|
||||
expect(commitElevation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does NOT spend the elevation when the factor code is wrong", async () => {
|
||||
const commitElevation = vi.fn().mockResolvedValue(undefined);
|
||||
vi.mocked(requireMfaManagementAuth).mockResolvedValue({
|
||||
transport: "bearer",
|
||||
user: { id: "u1" },
|
||||
apiTokenId: "token-row-1",
|
||||
accessTokenHash: "h",
|
||||
commitElevation,
|
||||
} as never);
|
||||
vi.mocked(verifyMfaFactor).mockResolvedValue({
|
||||
ok: false,
|
||||
replay: false,
|
||||
} as never);
|
||||
|
||||
const res = await POST(req());
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
expect(commitElevation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects a wrong current factor without revoking anything", async () => {
|
||||
|
||||
@@ -57,7 +57,10 @@ function verifyRequest(body: Record<string, unknown>): NextRequest {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(requireMfaManagementAuth).mockResolvedValue({
|
||||
transport: "cookie",
|
||||
user: USER,
|
||||
session: { id: "sess-1" },
|
||||
commitElevation: vi.fn().mockResolvedValue(undefined),
|
||||
} as never);
|
||||
vi.mocked(checkRateLimit).mockResolvedValue({ allowed: true } as never);
|
||||
});
|
||||
|
||||
@@ -157,7 +157,10 @@ describe("DELETE /api/auth/me/sessions (sign out everywhere)", () => {
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.data.sessionsRevoked).toBe(3);
|
||||
expect(destroyOtherSessions).toHaveBeenCalledWith("user-1", "sess-current");
|
||||
expect(destroyOtherSessions).toHaveBeenCalledWith("user-1", {
|
||||
kind: "session",
|
||||
sessionId: "sess-current",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -205,6 +205,10 @@ export async function handleIdempotencyCleanup(
|
||||
* but table space. It lives here rather than fire-and-forget on the mint request
|
||||
* because a cross-tenant unbounded DELETE has no business competing with a
|
||||
* user's request for a pool connection.
|
||||
*
|
||||
* The delete lives here rather than in `@/lib/auth/step-up` because it has to
|
||||
* run on the WORKER Prisma client; a second copy of the predicate in the auth
|
||||
* module would be dead code with a licence to drift.
|
||||
*/
|
||||
export async function handleStepUpElevationCleanup(
|
||||
jobs: Job<StepUpElevationCleanupPayload>[],
|
||||
|
||||
Reference in new issue
Block a user