Files
ConvertX/tests/pages/user.test.ts
T
2026-10-13 08:41:14 +02:00

367 lines
12 KiB
TypeScript

import { afterEach, describe, expect, test } from "bun:test";
import type { User } from "../../src/db/types";
import {
app,
createUser,
db,
request,
resetEnv,
setCookie,
setCookieValue,
setEnv,
signToken,
} from "./helpers/app";
afterEach(() => {
resetEnv();
});
function findUser(email: string) {
return db.query("SELECT * FROM users WHERE email = ?").get(email) as User | null;
}
function findUserById(id: number) {
return db.query("SELECT * FROM users WHERE id = ?").get(id) as User | null;
}
const uniqueEmail = () => `${crypto.randomUUID()}@example.com`;
describe("GET /register", () => {
test("redirects to the login page while registration is closed", async () => {
setEnv({ ACCOUNT_REGISTRATION: false });
const response = await request("/register");
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/login");
});
test("shows the registration form when registration is open", async () => {
setEnv({ ACCOUNT_REGISTRATION: true });
const response = await request("/register");
expect(response.status).toBe(200);
expect(await response.text()).toContain('value="Register"');
});
});
describe("POST /register", () => {
test("does not create an account while registration is closed", async () => {
setEnv({ ACCOUNT_REGISTRATION: false });
const email = uniqueEmail();
const response = await request("/register", { form: { email, password: "secret" } });
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/login");
expect(findUser(email)).toBeNull();
});
test("creates the account, logs it in and redirects home when registration is open", async () => {
setEnv({ ACCOUNT_REGISTRATION: true });
const email = uniqueEmail();
const response = await request("/register", { form: { email, password: "secret" } });
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/");
const user = findUser(email);
expect(user).not.toBeNull();
expect(await Bun.password.verify("secret", user?.password ?? "")).toBe(true);
const token = setCookieValue(response, "auth") ?? "";
expect(await app.decorator.jwt.verify(token)).toMatchObject({ id: String(user?.id) });
expect(setCookie(response, "auth")).toContain("HttpOnly");
expect(setCookie(response, "auth")).toContain("Secure");
expect(setCookie(response, "auth")).toContain("SameSite=Strict");
expect(setCookie(response, "auth")).toContain(`Max-Age=${7 * 24 * 60 * 60}`);
});
test("omits the Secure cookie flag when plain HTTP is allowed", async () => {
setEnv({ ACCOUNT_REGISTRATION: true, HTTP_ALLOWED: true });
const response = await request("/register", {
form: { email: uniqueEmail(), password: "secret" },
});
expect(setCookie(response, "auth")).not.toContain("Secure");
});
test("rejects an email that is already registered", async () => {
setEnv({ ACCOUNT_REGISTRATION: true });
const existing = await createUser();
const response = await request("/register", {
form: { email: existing.email, password: "another password" },
});
expect(response.status).toBe(400);
expect(await response.json()).toEqual({ message: "Email already in use." });
// the existing account keeps its password
expect(
await Bun.password.verify(existing.password, findUser(existing.email)?.password ?? ""),
).toBe(true);
});
test("rejects a request without a password", async () => {
setEnv({ ACCOUNT_REGISTRATION: true });
const response = await request("/register", { form: { email: uniqueEmail() } });
expect(response.status).toBe(422);
});
});
const expectErrorPage = async (response: Response, message: string) => {
expect(response.headers.get("content-type")).toContain("text/html");
const body = await response.text();
expect(body).toContain('role="alert"');
expect(body).toContain(message);
};
describe("GET /login", () => {
test("shows the login form without a register link while registration is closed", async () => {
setEnv({ ACCOUNT_REGISTRATION: false });
const body = await (await request("/login")).text();
expect(body).toContain('value="Login"');
expect(body).not.toContain('href="/register"');
});
test("links to the registration when it is open", async () => {
setEnv({ ACCOUNT_REGISTRATION: true });
const body = await (await request("/login")).text();
expect(body).toContain('href="/register"');
});
test("redirects users that are already logged in to the home page", async () => {
const user = await createUser();
const response = await request("/login", { cookies: { auth: user.token } });
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/");
});
test("clears an invalid session and shows the login form", async () => {
const response = await request("/login", { cookies: { auth: "not-a-jwt" } });
expect(response.status).toBe(200);
expect(setCookie(response, "auth")).toContain("Max-Age=0");
});
});
describe("POST /login", () => {
test("logs in with valid credentials and redirects home", async () => {
const user = await createUser();
const response = await request("/login", {
form: { email: user.email, password: user.password },
});
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/");
const token = setCookieValue(response, "auth") ?? "";
expect(await app.decorator.jwt.verify(token)).toMatchObject({ id: String(user.id) });
expect(setCookie(response, "auth")).toContain("HttpOnly");
});
test.each([
["the password is wrong", "wrong password"],
["the password is empty", ""],
])("rejects the login when %s", async (_, password) => {
const user = await createUser();
const response = await request("/login", { form: { email: user.email, password } });
expect(response.status).toBe(403);
await expectErrorPage(response, "Invalid credentials.");
expect(setCookie(response, "auth")).toBeUndefined();
});
test("gives the same answer for unknown emails, so accounts cannot be enumerated", async () => {
const response = await request("/login", {
form: { email: uniqueEmail(), password: "whatever" },
});
expect(response.status).toBe(403);
await expectErrorPage(response, "Invalid credentials.");
});
test("keeps the entered email in the form after a failed login", async () => {
const user = await createUser();
const response = await request("/login", {
form: { email: user.email, password: "wrong password" },
});
expect(await response.text()).toContain(`value="${user.email}"`);
});
test("rejects a request without a password", async () => {
const user = await createUser();
const response = await request("/login", { form: { email: user.email } });
expect(response.status).toBe(422);
});
});
describe.each(["GET", "POST"])("%s /logoff", (method) => {
test("clears the session and redirects to the login page", async () => {
const user = await createUser();
const response = await request("/logoff", { method, cookies: { auth: user.token } });
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/login");
expect(setCookie(response, "auth")).toContain("Max-Age=0");
});
test("redirects to the login page without a session", async () => {
const response = await request("/logoff", { method });
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/login");
expect(setCookie(response, "auth")).toBeUndefined();
});
});
describe("GET /account", () => {
test("redirects to the login page without a session", async () => {
const response = await request("/account", { headers: { accept: "text/html" } });
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/login");
});
test("shows the account form with the current email", async () => {
const user = await createUser();
const response = await request("/account", { cookies: { auth: user.token } });
expect(response.status).toBe(200);
expect(await response.text()).toContain(`value="${user.email}"`);
});
test("redirects home when the account no longer exists", async () => {
const response = await request("/account", { cookies: { auth: await signToken(999_999) } });
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/");
});
});
describe("POST /account", () => {
const updateAccount = (
token: string,
form: { email?: string; newPassword?: string; password: string },
) =>
request("/account", {
cookies: { auth: token },
form: { email: "", newPassword: "", ...form },
});
test("rejects the update when the current password is wrong", async () => {
const user = await createUser();
const response = await updateAccount(user.token, {
email: uniqueEmail(),
password: "wrong password",
});
expect(response.status).toBe(403);
await expectErrorPage(response, "Invalid credentials.");
expect(findUserById(user.id)?.email).toBe(user.email);
});
test("changes the email", async () => {
const user = await createUser();
const newEmail = uniqueEmail();
const response = await updateAccount(user.token, { email: newEmail, password: user.password });
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/");
expect(findUserById(user.id)?.email).toBe(newEmail);
});
test("allows submitting the unchanged email", async () => {
const user = await createUser();
const response = await updateAccount(user.token, {
email: user.email,
password: user.password,
});
expect(response.status).toBe(302);
expect(findUserById(user.id)?.email).toBe(user.email);
});
test("refuses an email that belongs to another account", async () => {
const user = await createUser();
const other = await createUser();
const response = await updateAccount(user.token, {
email: other.email,
password: user.password,
});
expect(response.status).toBe(409);
await expectErrorPage(response, "Email already in use.");
expect(findUserById(user.id)?.email).toBe(user.email);
});
test("changes the password", async () => {
const user = await createUser();
await updateAccount(user.token, { newPassword: "new password", password: user.password });
const login = await request("/login", {
form: { email: user.email, password: "new password" },
});
expect(login.status).toBe(302);
const oldLogin = await request("/login", {
form: { email: user.email, password: user.password },
});
expect(oldLogin.status).toBe(403);
});
test("keeps everything unchanged when email and new password are left blank", async () => {
const user = await createUser();
const before = findUserById(user.id);
const response = await updateAccount(user.token, { password: user.password });
expect(response.status).toBe(302);
expect(findUserById(user.id)).toEqual(before);
});
test("requires a session cookie", async () => {
const response = await request("/account", {
form: { email: "", newPassword: "", password: "secret" },
});
expect(response.status).toBe(422);
});
test("redirects to the login page for an invalid session", async () => {
const response = await updateAccount("not-a-jwt", { password: "secret" });
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/login");
});
test("clears the session of an account that no longer exists", async () => {
const response = await updateAccount(await signToken(999_999), { password: "secret" });
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/login");
expect(setCookie(response, "auth")).toContain("Max-Age=0");
});
});