- normalize.ts num(): parseFloat truncated at the first comma, so "1,200" silently became 1 (1000x error) and "5,5" became 5. Now strictly matches either US thousands-grouping or a plain number spanning the whole string; anything else (incl. ambiguous "5,5") returns null instead of a wrong value. - labs.ts confirm handler: the pending-status check ran before the request body was read, so two concurrent confirms could both pass it and double-insert. Added a guarded UPDATE ... WHERE status = 'pending' as the first statement inside the existing synchronous transaction; zero rows affected throws and the route returns 409, with the fast-path check kept for the common case. - Added missing `await` on two rejects.toThrow assertions (llm.test.ts, extract.test.ts) that were previously resolving before the assertion settled. - Bumped the 11th-failed-login rate-limit test to a 30s timeout — 10 sequential argon2id verifies can exceed bun:test's 5s default under load.
74 lines
3.1 KiB
TypeScript
74 lines
3.1 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { mkdtempSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { createApp } from "../src/app";
|
|
import { openDb } from "../src/db";
|
|
import { loadOrCreateKey } from "../src/lib/crypto";
|
|
|
|
function makeApp() {
|
|
const dir = mkdtempSync(join(tmpdir(), "helios-"));
|
|
return createApp({ db: openDb(dir), key: loadOrCreateKey(dir), dataDir: dir });
|
|
}
|
|
const json = (body: unknown) => ({
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
describe("auth", () => {
|
|
test("fresh instance needs setup; setup then login yields a session", async () => {
|
|
const app = makeApp();
|
|
let me = await (await app.request("/api/me")).json();
|
|
expect(me).toEqual({ needsSetup: true, authenticated: false });
|
|
|
|
expect((await app.request("/api/setup", json({ password: "hunter2hunter2" }))).status).toBe(204);
|
|
expect((await app.request("/api/setup", json({ password: "again-not-allowed" }))).status).toBe(400);
|
|
|
|
const login = await app.request("/api/login", json({ password: "hunter2hunter2" }));
|
|
expect(login.status).toBe(204);
|
|
const cookie = login.headers.get("set-cookie")!;
|
|
expect(cookie).toContain("helios_session=");
|
|
expect(cookie).toContain("HttpOnly");
|
|
|
|
me = await (await app.request("/api/me", { headers: { cookie } })).json();
|
|
expect(me.authenticated).toBe(true);
|
|
});
|
|
|
|
test("wrong password 401; protected route 401 without cookie", async () => {
|
|
const app = makeApp();
|
|
await app.request("/api/setup", json({ password: "hunter2hunter2" }));
|
|
expect((await app.request("/api/login", json({ password: "wrong-wrong-1" }))).status).toBe(401);
|
|
expect((await app.request("/api/settings")).status).toBe(401);
|
|
});
|
|
|
|
test("11th failed login is globally rate-limited, even for the correct password", async () => {
|
|
const app = makeApp();
|
|
await app.request("/api/setup", json({ password: "hunter2hunter2" }));
|
|
let last = 0;
|
|
for (let i = 0; i < 11; i++) {
|
|
last = (await app.request("/api/login", json({ password: "wrong-wrong-1" }))).status;
|
|
}
|
|
expect(last).toBe(429);
|
|
|
|
// The global window applies to everyone, including a request with the correct password.
|
|
const blocked = await app.request("/api/login", json({ password: "hunter2hunter2" }));
|
|
expect(blocked.status).toBe(429);
|
|
}, 30000); // 10 sequential argon2id verifies can exceed the 5s default timeout
|
|
|
|
test("unknown /api/* returns 404 when authed, 401 when unauthed", async () => {
|
|
const app = makeApp();
|
|
await app.request("/api/setup", json({ password: "hunter2hunter2" }));
|
|
|
|
// Unauthed request to unknown endpoint returns 401
|
|
expect((await app.request("/api/nope")).status).toBe(401);
|
|
|
|
// Authed request to unknown endpoint returns 404
|
|
const login = await app.request("/api/login", json({ password: "hunter2hunter2" }));
|
|
const cookie = login.headers.get("set-cookie")!;
|
|
const res = await app.request("/api/nope", { headers: { cookie } });
|
|
expect(res.status).toBe(404);
|
|
expect(await res.json()).toEqual({ error: "not found" });
|
|
});
|
|
});
|