auth.test.js
6.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// Integration test for T19 (register/login/logout/me), dev-plan-3.md.
// Spawns server.js on a throwaway port against an isolated tmpdir copy
// (own data.sqlite too — db.js defaults to __dirname/data.sqlite), same
// isolation approach as admin-auth.test.js / uploads-route.test.js.
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const os = require("node:os");
const { spawn } = require("node:child_process");
const PORT = 24934 + Math.floor(Math.random() * 1000);
const BASE = `http://localhost:${PORT}`;
const ADMIN_PASSWORD = "test-password-only-for-this-run";
function waitForServer(url, timeoutMs = 10000) {
const deadline = Date.now() + timeoutMs;
return (async function poll() {
while (Date.now() < deadline) {
try {
await fetch(url);
return;
} catch {
await new Promise((r) => setTimeout(r, 100));
}
}
throw new Error("server did not come up in time");
})();
}
function sessionCookie(res) {
const setCookie = res.headers.get("set-cookie") || "";
return setCookie.split(";")[0];
}
let child;
let cwd;
test.before(async () => {
cwd = fs.mkdtempSync(path.join(os.tmpdir(), "auth-test-"));
for (const f of ["server.js", "ingest.js", "uploads.js", "store.js", "dressup-store.js", "prompt-template.js", "db.js"]) {
fs.copyFileSync(path.join(__dirname, f), path.join(cwd, f));
}
child = spawn(process.execPath, ["server.js"], {
cwd,
env: { ...process.env, PORT: String(PORT), H3_CONSOLE_BASE: "", H3_CONSOLE_TOKEN: "", ADMIN_PASSWORD },
stdio: "pipe",
});
await waitForServer(BASE);
});
test.after(() => {
child.kill();
fs.rmSync(cwd, { recursive: true, force: true });
});
test("register: rejects a malformed email", async () => {
const res = await fetch(`${BASE}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "not-an-email", password: "longenoughpassword" }),
});
assert.equal(res.status, 400);
});
test("register: rejects a too-short password", async () => {
const res = await fetch(`${BASE}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "short-pw@example.com", password: "short1" }),
});
assert.equal(res.status, 400);
});
test("register -> auto-logged-in session -> GET /api/auth/me -> logout closes the loop", async () => {
const registerRes = await fetch(`${BASE}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "alice@example.com", password: "correct-horse-battery" }),
});
assert.equal(registerRes.status, 200);
const registered = await registerRes.json();
assert.equal(registered.email, "alice@example.com");
assert.equal(registered.emailVerified, false);
const cookie = sessionCookie(registerRes);
assert.ok(cookie.startsWith("user_session="), "register should auto-issue a user_session cookie, distinct from admin_session");
const meRes = await fetch(`${BASE}/api/auth/me`, { headers: { Cookie: cookie } });
assert.equal(meRes.status, 200);
const me = await meRes.json();
assert.equal(me.id, registered.id);
assert.equal(me.email, "alice@example.com");
const logoutRes = await fetch(`${BASE}/api/auth/logout`, { method: "POST", headers: { Cookie: cookie } });
assert.equal(logoutRes.status, 200);
const meAfterLogoutRes = await fetch(`${BASE}/api/auth/me`, { headers: { Cookie: cookie } });
assert.equal(meAfterLogoutRes.status, 401, "the old session token must be dead after logout");
});
test("GET /api/auth/me with no cookie at all is unauthorized", async () => {
const res = await fetch(`${BASE}/api/auth/me`);
assert.equal(res.status, 401);
});
test("register: duplicate email is a 409, not a silently-created second account", async () => {
await fetch(`${BASE}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "dup@example.com", password: "correct-horse-battery" }),
});
const secondRes = await fetch(`${BASE}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "dup@example.com", password: "a-totally-different-password" }),
});
assert.equal(secondRes.status, 409);
});
test("login: correct credentials issue a fresh session", async () => {
await fetch(`${BASE}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "bob@example.com", password: "correct-horse-battery" }),
});
const loginRes = await fetch(`${BASE}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "bob@example.com", password: "correct-horse-battery" }),
});
assert.equal(loginRes.status, 200);
const cookie = sessionCookie(loginRes);
const meRes = await fetch(`${BASE}/api/auth/me`, { headers: { Cookie: cookie } });
assert.equal(meRes.status, 200);
const me = await meRes.json();
assert.equal(me.email, "bob@example.com");
});
test("login: wrong password and unknown email return the exact same error (no email enumeration)", async () => {
await fetch(`${BASE}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "carol@example.com", password: "correct-horse-battery" }),
});
const wrongPasswordRes = await fetch(`${BASE}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "carol@example.com", password: "totally-wrong-password" }),
});
const unknownEmailRes = await fetch(`${BASE}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "nobody-registered-this@example.com", password: "whatever" }),
});
assert.equal(wrongPasswordRes.status, 401);
assert.equal(unknownEmailRes.status, 401);
const [wrongBody, unknownBody] = await Promise.all([wrongPasswordRes.json(), unknownEmailRes.json()]);
assert.deepEqual(wrongBody, unknownBody, "the two failure modes must be indistinguishable to the caller");
});
test("a user_session cookie doesn't unlock /api/admin/* (the two session systems stay separate)", async () => {
const registerRes = await fetch(`${BASE}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "dana@example.com", password: "correct-horse-battery" }),
});
const cookie = sessionCookie(registerRes);
const res = await fetch(`${BASE}/api/admin/characters`, { headers: { Cookie: cookie } });
assert.equal(res.status, 401);
});