mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 18:23:47 +10:00
161 lines
6.4 KiB
TypeScript
161 lines
6.4 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { createProcedureClient } from "@orpc/server";
|
|
import { env } from "@reactive-resume/env/server";
|
|
|
|
const authMock = vi.hoisted(() => ({
|
|
api: {
|
|
getSession: vi.fn(),
|
|
verifyApiKey: vi.fn(),
|
|
},
|
|
}));
|
|
const verifyOAuthTokenMock = vi.hoisted(() => vi.fn());
|
|
|
|
const dbMock = vi.hoisted(() => ({
|
|
select: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@reactive-resume/auth/config", () => ({
|
|
auth: authMock,
|
|
verifyOAuthToken: verifyOAuthTokenMock,
|
|
}));
|
|
vi.mock("@reactive-resume/db/client", () => ({ db: dbMock }));
|
|
vi.mock("@reactive-resume/db/schema", () => ({ user: { __table: "user" } }));
|
|
vi.mock("drizzle-orm", () => ({ eq: () => "EQ" }));
|
|
|
|
const { resolveUserFromRequestHeaders, resolveAuthenticationFromRequestHeaders, protectedProcedure } =
|
|
await import("./context");
|
|
|
|
const setupDbResolves = (userResult: unknown) => {
|
|
dbMock.select.mockReturnValueOnce({
|
|
from: () => ({
|
|
where: () => ({
|
|
limit: () => Promise.resolve(userResult ? [userResult] : []),
|
|
}),
|
|
}),
|
|
});
|
|
};
|
|
|
|
const reset = () => {
|
|
authMock.api.getSession.mockReset();
|
|
authMock.api.verifyApiKey.mockReset();
|
|
verifyOAuthTokenMock.mockReset();
|
|
dbMock.select.mockReset();
|
|
};
|
|
|
|
describe("resolveUserFromRequestHeaders", () => {
|
|
it("falls back to a valid bearer token when an API key is invalid", async () => {
|
|
reset();
|
|
authMock.api.verifyApiKey.mockResolvedValueOnce({ valid: false });
|
|
verifyOAuthTokenMock.mockResolvedValueOnce({ sub: "bearer-user" });
|
|
setupDbResolves({ id: "bearer-user" });
|
|
const user = await resolveUserFromRequestHeaders(
|
|
new Headers({ "x-api-key": "expired-key", authorization: "Bearer valid-token" }),
|
|
);
|
|
expect(user).toMatchObject({ id: "bearer-user" });
|
|
expect(verifyOAuthTokenMock).toHaveBeenCalledWith("valid-token");
|
|
});
|
|
it("returns the user resolved from a valid x-api-key", async () => {
|
|
reset();
|
|
authMock.api.verifyApiKey.mockResolvedValueOnce({ valid: true, key: { referenceId: "user-1" } });
|
|
setupDbResolves({ id: "user-1", name: "Alice" });
|
|
|
|
const headers = new Headers({ "x-api-key": "abc123" });
|
|
const user = await resolveUserFromRequestHeaders(headers);
|
|
|
|
expect(authMock.api.verifyApiKey).toHaveBeenCalledWith({ body: { key: "abc123" } });
|
|
expect(user).toMatchObject({ id: "user-1", name: "Alice" });
|
|
});
|
|
|
|
it("uses Bearer token when present and no api key", async () => {
|
|
reset();
|
|
verifyOAuthTokenMock.mockResolvedValueOnce({ sub: "user-bearer" });
|
|
setupDbResolves({ id: "user-bearer", name: "Bob" });
|
|
|
|
const headers = new Headers({ authorization: "Bearer xxx.yyy.zzz" });
|
|
const user = await resolveUserFromRequestHeaders(headers);
|
|
|
|
expect(verifyOAuthTokenMock).toHaveBeenCalledWith("xxx.yyy.zzz");
|
|
expect(user).toMatchObject({ id: "user-bearer", name: "Bob" });
|
|
});
|
|
});
|
|
|
|
describe("cookie request origins", () => {
|
|
it.each([{ origin: "https://attacker.example" }, { origin: "null" }, { "sec-fetch-site": "cross-site" }])(
|
|
"rejects ambient credentials from another origin: %j",
|
|
async (originHeaders) => {
|
|
reset();
|
|
authMock.api.getSession.mockResolvedValue({ user: { id: "owner" } });
|
|
const headers = new Headers({ cookie: "session=valid", ...originHeaders });
|
|
await expect(resolveUserFromRequestHeaders(headers)).rejects.toMatchObject({ code: "FORBIDDEN" });
|
|
expect(authMock.api.getSession).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
it("accepts same-origin cookies and explicit bearer credentials from other origins", async () => {
|
|
reset();
|
|
authMock.api.getSession.mockResolvedValue({ user: { id: "session-user" } });
|
|
setupDbResolves({ id: "session-user" });
|
|
await expect(
|
|
resolveUserFromRequestHeaders(new Headers({ cookie: "session=valid", origin: new URL(env.APP_URL).origin })),
|
|
).resolves.toMatchObject({ id: "session-user" });
|
|
verifyOAuthTokenMock.mockResolvedValueOnce({ sub: "token-user" });
|
|
setupDbResolves({ id: "token-user" });
|
|
await expect(
|
|
resolveUserFromRequestHeaders(
|
|
new Headers({ authorization: "Bearer token", cookie: "session=valid", origin: "https://client.example" }),
|
|
),
|
|
).resolves.toMatchObject({ id: "token-user" });
|
|
});
|
|
});
|
|
|
|
describe("credential authorization", () => {
|
|
it.each([
|
|
{ banned: true, banExpires: null, allowed: false },
|
|
{ banned: true, banExpires: new Date("2999-01-01"), allowed: false },
|
|
{ banned: true, banExpires: new Date("2000-01-01"), allowed: true },
|
|
])("enforces active bans for explicit credentials: %j", async ({ banned, banExpires, allowed }) => {
|
|
reset();
|
|
authMock.api.verifyApiKey.mockResolvedValue({ valid: true, key: { referenceId: "owner" } });
|
|
setupDbResolves({ id: "owner", banned, banExpires });
|
|
const result = await resolveUserFromRequestHeaders(new Headers({ "x-api-key": "key" }));
|
|
expect(result?.id ?? null).toBe(allowed ? "owner" : null);
|
|
});
|
|
|
|
it.each([
|
|
{ permissions: { api: ["read"] }, method: "GET" as const, allowed: true },
|
|
{ permissions: { api: ["read"] }, method: "POST" as const, allowed: false },
|
|
{ permissions: { api: ["write"] }, method: "DELETE" as const, allowed: false },
|
|
{ permissions: null, method: "POST" as const, allowed: true },
|
|
{ permissions: {}, method: "GET" as const, allowed: false },
|
|
])("enforces API-key permissions at procedure execution: %j", async ({ permissions, method, allowed }) => {
|
|
reset();
|
|
authMock.api.verifyApiKey.mockResolvedValue({ valid: true, key: { referenceId: "owner", permissions } });
|
|
setupDbResolves({ id: "owner" });
|
|
const operation = createProcedureClient(
|
|
protectedProcedure.route({ method }).handler(() => "executed"),
|
|
{
|
|
context: { locale: "en-US", reqHeaders: new Headers({ "x-api-key": "key" }) },
|
|
},
|
|
);
|
|
if (allowed) await expect(operation(undefined)).resolves.toBe("executed");
|
|
else await expect(operation(undefined)).rejects.toMatchObject({ code: "FORBIDDEN" });
|
|
});
|
|
|
|
it("preserves OAuth read-only scope when another identity cookie is present", async () => {
|
|
reset();
|
|
verifyOAuthTokenMock.mockResolvedValue({ sub: "owner", scope: "api:read" });
|
|
setupDbResolves({ id: "owner" });
|
|
const authentication = await resolveAuthenticationFromRequestHeaders(
|
|
new Headers({ authorization: "Bearer token", cookie: "session=other" }),
|
|
);
|
|
const operation = createProcedureClient(
|
|
protectedProcedure.route({ method: "POST" }).handler(() => "executed"),
|
|
{
|
|
context: { locale: "en-US", reqHeaders: new Headers(), authentication },
|
|
},
|
|
);
|
|
await expect(operation(undefined)).rejects.toMatchObject({ code: "FORBIDDEN" });
|
|
expect(authMock.api.getSession).not.toHaveBeenCalled();
|
|
});
|
|
});
|