Files
ConvertX/tests/services/user.test.ts
Emrik Östling 2135c6b9f1 chore: use bun fmt and bun check (#685)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-10-23 18:23:49 +02:00

215 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" });
});
});