mirror of
https://github.com/C4illin/ConvertX.git
synced 2026-10-27 23:30:55 +01:00
213 lines
7.1 KiB
TypeScript
213 lines
7.1 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { randomUUID } from "node:crypto";
|
|
import { jwt } from "@elysiajs/jwt";
|
|
import { Elysia } from "elysia";
|
|
import { WEBROOT } from "../../src/helpers/env";
|
|
import { userService } from "../../src/services/user";
|
|
|
|
let protectedHandlerCalls = 0;
|
|
|
|
const app = new Elysia()
|
|
.use(userService)
|
|
.get(
|
|
"/me",
|
|
({ user }) => {
|
|
protectedHandlerCalls++;
|
|
return user;
|
|
},
|
|
{ auth: true },
|
|
)
|
|
.post("/me", ({ user }) => user, { auth: true })
|
|
.get("/public", () => "public")
|
|
.post("/sign-in", ({ body }) => body, { body: "signIn" })
|
|
.get("/session", ({ cookie: { auth, jobId } }) => ({ auth: auth.value, jobId: jobId.value }), {
|
|
cookie: "session",
|
|
});
|
|
|
|
const signer = app.decorator.jwt;
|
|
|
|
// tokens the service must reject
|
|
const foreignToken = await new Elysia()
|
|
.use(jwt({ name: "jwt", secret: randomUUID() }))
|
|
.decorator.jwt.sign({ id: "42" });
|
|
const expiredToken = await signer.sign({ id: "42", exp: Math.floor(Date.now() / 1000) - 60 });
|
|
// payload violates the { id: string } schema
|
|
const wrongSchemaToken = await signer.sign({ id: 42 as unknown as string });
|
|
|
|
function request(path: string, init: RequestInit & { cookie?: string } = {}) {
|
|
const { cookie, headers, ...rest } = init;
|
|
return app.handle(
|
|
new Request(`http://localhost${path}`, {
|
|
...rest,
|
|
headers: { ...(cookie ? { cookie } : {}), ...headers },
|
|
}),
|
|
);
|
|
}
|
|
|
|
const htmlPage = { accept: "text/html" };
|
|
const apiCall = { accept: "application/json" };
|
|
|
|
function expectAuthCookieCleared(response: Response) {
|
|
const setCookie = response.headers.get("set-cookie") ?? "";
|
|
expect(setCookie).toMatch(/^auth=;/);
|
|
expect(setCookie).toContain("Max-Age=0");
|
|
}
|
|
|
|
describe("auth macro with a valid token", () => {
|
|
test("resolves the token payload as user", async () => {
|
|
const token = await signer.sign({ id: "42" });
|
|
|
|
const response = await request("/me", { cookie: `auth=${token}` });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(await response.json()).toMatchObject({ id: "42" });
|
|
});
|
|
|
|
test("issues tokens that expire after seven days", async () => {
|
|
const token = await signer.sign({ id: "42" });
|
|
|
|
const user = (await (await request("/me", { cookie: `auth=${token}` })).json()) as {
|
|
exp: number;
|
|
iat: number;
|
|
};
|
|
|
|
expect(user.exp - user.iat).toBe(7 * 24 * 60 * 60);
|
|
});
|
|
|
|
test("accepts an optional jobId cookie", async () => {
|
|
const token = await signer.sign({ id: "42" });
|
|
|
|
const response = await request("/me", { cookie: `auth=${token}; jobId=7` });
|
|
|
|
expect(response.status).toBe(200);
|
|
});
|
|
|
|
test("does not touch the auth cookie", async () => {
|
|
const token = await signer.sign({ id: "42" });
|
|
|
|
const response = await request("/me", { cookie: `auth=${token}` });
|
|
|
|
expect(response.headers.get("set-cookie")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("auth macro without a token", () => {
|
|
test("redirects HTML page requests to the login page", async () => {
|
|
const response = await request("/me", { headers: htmlPage, redirect: "manual" });
|
|
|
|
expect(response.status).toBe(302);
|
|
expect(response.headers.get("location")).toBe(`${WEBROOT}/login`);
|
|
expect(await response.json()).toEqual({ success: false, message: "Redirecting to login" });
|
|
});
|
|
|
|
test("redirects HEAD page requests to the login page", async () => {
|
|
const response = await request("/me", { method: "HEAD", headers: htmlPage });
|
|
|
|
expect(response.status).toBe(302);
|
|
expect(response.headers.get("location")).toBe(`${WEBROOT}/login`);
|
|
});
|
|
|
|
test("returns 401 JSON for API requests", async () => {
|
|
const response = await request("/me", { headers: apiCall });
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(response.headers.get("location")).toBeNull();
|
|
expect(await response.json()).toEqual({ success: false, message: "Unauthorized" });
|
|
});
|
|
|
|
test("returns 401 for non-GET requests even if they accept HTML", async () => {
|
|
const response = await request("/me", { method: "POST", headers: htmlPage });
|
|
|
|
expect(response.status).toBe(401);
|
|
});
|
|
|
|
test("treats an empty auth cookie as missing", async () => {
|
|
const response = await request("/me", { cookie: "auth=", headers: htmlPage });
|
|
|
|
expect(response.status).toBe(302);
|
|
// there is no session to clear
|
|
expect(response.headers.get("set-cookie")).toBeNull();
|
|
});
|
|
|
|
test("does not run the protected handler", async () => {
|
|
const callsBefore = protectedHandlerCalls;
|
|
|
|
await request("/me", { headers: apiCall });
|
|
await request("/me", { headers: htmlPage });
|
|
|
|
expect(protectedHandlerCalls).toBe(callsBefore);
|
|
});
|
|
});
|
|
|
|
describe("auth macro with an invalid token", () => {
|
|
test.each([
|
|
["a malformed token", "not-a-jwt"],
|
|
["a token signed with another secret", foreignToken],
|
|
["an expired token", expiredToken],
|
|
["a token whose payload does not match the schema", wrongSchemaToken],
|
|
])("rejects %s and clears the auth cookie", async (_, token) => {
|
|
const response = await request("/me", { cookie: `auth=${token}`, headers: apiCall });
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(await response.json()).toEqual({ success: false, message: "Unauthorized" });
|
|
expectAuthCookieCleared(response);
|
|
});
|
|
|
|
test("redirects HTML page requests to login and clears the auth cookie", async () => {
|
|
const response = await request("/me", { cookie: "auth=not-a-jwt", headers: htmlPage });
|
|
|
|
expect(response.status).toBe(302);
|
|
expect(response.headers.get("location")).toBe(`${WEBROOT}/login`);
|
|
expectAuthCookieCleared(response);
|
|
});
|
|
});
|
|
|
|
describe("routes without auth", () => {
|
|
test("are reachable without a token", async () => {
|
|
const response = await request("/public", { headers: htmlPage });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(await response.text()).toBe("public");
|
|
});
|
|
});
|
|
|
|
describe("models", () => {
|
|
const postJson = (body: unknown) =>
|
|
request("/sign-in", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
test("signIn accepts an email and a password", async () => {
|
|
const response = await postJson({ email: "user@example.com", password: "secret" });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(await response.json()).toEqual({ email: "user@example.com", password: "secret" });
|
|
});
|
|
|
|
test.each([
|
|
["the password is missing", { email: "user@example.com" }],
|
|
["the email is missing", { password: "secret" }],
|
|
["the email is not a string", { email: 42, password: "secret" }],
|
|
])("signIn rejects the body when %s", async (_, body) => {
|
|
const response = await postJson(body);
|
|
|
|
expect(response.status).toBe(422);
|
|
});
|
|
|
|
test("session requires an auth cookie", async () => {
|
|
const response = await request("/session", { cookie: "jobId=3" });
|
|
|
|
expect(response.status).toBe(422);
|
|
});
|
|
|
|
test("session accepts an auth cookie with an optional jobId", async () => {
|
|
const withJob = await request("/session", { cookie: "auth=token; jobId=3" });
|
|
const withoutJob = await request("/session", { cookie: "auth=token" });
|
|
|
|
expect(await withJob.json()).toEqual({ auth: "token", jobId: "3" });
|
|
expect(await withoutJob.json()).toEqual({ auth: "token" });
|
|
});
|
|
});
|