Files
buzz-sheet/lib/auth/sudloh.test.ts
T
gunshiz 4b16941e11
CI / Verify (push) Successful in 1m31s
CI / Build immutable images and deploy (push) Successful in 2m16s
fix(auth) : keep Guide sessions beyond Sudloh token expiry
2026-10-06 18:00:42 +07:00

81 lines
3.4 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const rows: unknown[][] = [];
const updateUser = vi.fn(async () => undefined);
vi.mock("server-only", () => ({}));
vi.mock("@/db", () => ({ getDb: () => ({
select: () => ({ from: () => ({ where: () => ({ limit: async () => rows.shift() ?? [] }) }) }),
update: () => ({ set: () => ({ where: updateUser }) }),
}) }));
const { refreshLinkedSudlohProfile } = await import("./sudloh");
const account = { accountId: "sub-1", accessToken: "access-1",
accessTokenExpiresAt: new Date(Date.now() + 60 * 60_000) };
beforeEach(() => {
rows.length = 0;
vi.clearAllMocks();
process.env.SUDLOH_OIDC_ISSUER = "https://account.test/api/auth";
process.env.SUDLOH_OIDC_CLIENT_ID = "client";
process.env.SUDLOH_OIDC_CLIENT_SECRET = "secret";
});
function provider(active: boolean, profileSub = "sub-1") {
return vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
const url = String(input);
if (url.endsWith("openid-configuration")) return Response.json({
issuer: "https://account.test/api/auth",
introspection_endpoint: "https://account.test/api/auth/oauth2/introspect",
userinfo_endpoint: "https://account.test/api/auth/oauth2/userinfo",
});
if (url.endsWith("introspect")) return Response.json({ active, sub: "sub-1",
exp: Math.floor(Date.now() / 1000) + 3600 });
if (url.endsWith("userinfo")) return Response.json({ sub: profileSub, name: "New Name",
email: "[email protected]", email_verified: true, picture: "https://account.test/avatar.png" });
throw new Error(`unexpected URL: ${url}`);
});
}
describe("Sudloh profile refresh", () => {
it("requires a new Sudloh sign-in after the access token expires", async () => {
rows.push([{ ...account, accessTokenExpiresAt: new Date(Date.now() - 1000) }]);
await expect(refreshLinkedSudlohProfile("user-1"))
.rejects.toMatchObject({ status: 401, message: "sudloh-sign-in-required" });
});
it("requires a new Sudloh sign-in when the token is revoked", async () => {
const fetchMock = provider(false);
rows.push([account]);
await expect(refreshLinkedSudlohProfile("user-1"))
.rejects.toMatchObject({ status: 401, message: "sudloh-sign-in-required" });
fetchMock.mockRestore();
});
it("rejects UserInfo for another subject", async () => {
const fetchMock = provider(true, "someone-else");
rows.push([account]);
await expect(refreshLinkedSudlohProfile("user-1"))
.rejects.toMatchObject({ status: 503 });
expect(updateUser).not.toHaveBeenCalled();
fetchMock.mockRestore();
});
it("reports a verified Sudloh email that conflicts with another Guide account", async () => {
const fetchMock = provider(true);
rows.push([account], [{ name: "Old Name", email: "[email protected]", emailVerified: true, image: null }]);
updateUser.mockRejectedValueOnce({ cause: { code: "23505" } });
await expect(refreshLinkedSudlohProfile("user-1"))
.rejects.toMatchObject({ status: 409, message: "sudloh-email-conflict" });
fetchMock.mockRestore();
});
it("updates a linked profile while its token is active", async () => {
const fetchMock = provider(true);
rows.push([account], [{ name: "Old Name", email: "[email protected]", emailVerified: true, image: null }]);
await refreshLinkedSudlohProfile("user-1");
expect(updateUser).toHaveBeenCalledOnce();
fetchMock.mockRestore();
});
});