Quoted-printable still rewrote token= as token=3D in the raw MIME, which some clients never decode. Distinguish a missing token from a real expiry, and keep links valid for 60 minutes. Co-authored-by: Cursor <cursoragent@cursor.com>
92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
import { NextRequest } from "next/server";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const isDatabaseConfiguredMock = vi.fn();
|
|
const getSessionPepperMock = vi.fn();
|
|
const hashSessionTokenMock = vi.fn();
|
|
const findUniqueMock = vi.fn();
|
|
const deleteMock = vi.fn();
|
|
const upsertMock = vi.fn();
|
|
const createSessionForUserMock = vi.fn();
|
|
const setSessionCookieMock = vi.fn();
|
|
|
|
vi.mock("../../lib/server/env", () => ({
|
|
isDatabaseConfigured: () => isDatabaseConfiguredMock(),
|
|
getSessionPepper: () => getSessionPepperMock(),
|
|
}));
|
|
|
|
vi.mock("../../lib/server/hash", () => ({
|
|
hashSessionToken: (...args: unknown[]) => hashSessionTokenMock(...args),
|
|
}));
|
|
|
|
vi.mock("../../lib/server/session", () => ({
|
|
createSessionForUser: (...args: unknown[]) =>
|
|
createSessionForUserMock(...args),
|
|
setSessionCookie: (...args: unknown[]) => setSessionCookieMock(...args),
|
|
}));
|
|
|
|
vi.mock("../../lib/server/db", () => ({
|
|
prisma: {
|
|
magicLinkToken: {
|
|
findUnique: (...args: unknown[]) => findUniqueMock(...args),
|
|
delete: (...args: unknown[]) => deleteMock(...args),
|
|
},
|
|
user: {
|
|
upsert: (...args: unknown[]) => upsertMock(...args),
|
|
},
|
|
},
|
|
}));
|
|
|
|
import { GET } from "../../app/api/auth/magic-link/verify/route";
|
|
|
|
beforeEach(() => {
|
|
isDatabaseConfiguredMock.mockReset();
|
|
getSessionPepperMock.mockReset();
|
|
hashSessionTokenMock.mockReset();
|
|
findUniqueMock.mockReset();
|
|
deleteMock.mockReset();
|
|
upsertMock.mockReset();
|
|
createSessionForUserMock.mockReset();
|
|
setSessionCookieMock.mockReset();
|
|
|
|
isDatabaseConfiguredMock.mockReturnValue(true);
|
|
getSessionPepperMock.mockReturnValue("pepper");
|
|
hashSessionTokenMock.mockReturnValue("token-hash");
|
|
});
|
|
|
|
function getWithToken(token: string) {
|
|
return new NextRequest(
|
|
`https://x.test/api/auth/magic-link/verify?token=${encodeURIComponent(token)}`,
|
|
);
|
|
}
|
|
|
|
describe("GET /api/auth/magic-link/verify", () => {
|
|
it("redirects with invalid_link when the token is missing", async () => {
|
|
const res = await GET(
|
|
new NextRequest("https://x.test/api/auth/magic-link/verify"),
|
|
);
|
|
expect(res.status).toBe(307);
|
|
expect(res.headers.get("location")).toContain("error=invalid_link");
|
|
});
|
|
|
|
it("redirects with invalid_link when no row matches", async () => {
|
|
findUniqueMock.mockResolvedValue(null);
|
|
const res = await GET(getWithToken("a-token-value-long-enough"));
|
|
expect(res.status).toBe(307);
|
|
expect(res.headers.get("location")).toContain("error=invalid_link");
|
|
});
|
|
|
|
it("redirects with expired_link when the row is past expiresAt", async () => {
|
|
findUniqueMock.mockResolvedValue({
|
|
id: "row-1",
|
|
email: "a@b.c",
|
|
expiresAt: new Date(Date.now() - 1000),
|
|
nextPath: null,
|
|
draftPayload: null,
|
|
});
|
|
const res = await GET(getWithToken("a-token-value-long-enough"));
|
|
expect(res.status).toBe(307);
|
|
expect(res.headers.get("location")).toContain("error=expired_link");
|
|
});
|
|
});
|