mirror of
https://github.com/C4illin/ConvertX.git
synced 2026-10-28 07:44:21 +01:00
2135c6b9f1
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
368 lines
12 KiB
TypeScript
368 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");
|
|
});
|
|
});
|