85ba992432
- server.js: Node HTTP server with all API routes, static file serving, SPA fallback - Path traversal protection on static file serving - Async handler wrapped with .catch() to prevent unhandled rejections - readBody: size limit (1MB) + error handler - letter validation: single [A-Z] char check on progress/mnemonics routes - phrase length limit (500 chars) on POST /api/mnemonics - requireAdmin reads ADMIN_PASSWORD at request time for testability - logout uses session.token from requireAuth (not re-read from header) - 51 server tests passing (auth, progress, admin routes + edge cases) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
145 lines
4.2 KiB
JavaScript
145 lines
4.2 KiB
JavaScript
'use strict'
|
|
const { test, describe, before, after } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const http = require('node:http')
|
|
const { initDb } = require('../db.js')
|
|
const { createServer } = require('../server.js')
|
|
|
|
function request(server, method, path, body, headers = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const addr = server.address()
|
|
const opts = {
|
|
hostname: '127.0.0.1',
|
|
port: addr.port,
|
|
path,
|
|
method,
|
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
}
|
|
const req = http.request(opts, res => {
|
|
let data = ''
|
|
res.on('data', c => { data += c })
|
|
res.on('end', () => {
|
|
let json = null
|
|
try { json = JSON.parse(data) } catch {}
|
|
resolve({ status: res.statusCode, headers: res.headers, body: json, raw: data })
|
|
})
|
|
})
|
|
req.on('error', reject)
|
|
if (body) req.write(JSON.stringify(body))
|
|
req.end()
|
|
})
|
|
}
|
|
|
|
let db, server
|
|
|
|
before(() => {
|
|
db = initDb(':memory:')
|
|
server = createServer(db)
|
|
return new Promise(resolve => server.listen(0, resolve))
|
|
})
|
|
|
|
after(() => {
|
|
db._db.close()
|
|
return new Promise(resolve => server.close(resolve))
|
|
})
|
|
|
|
describe('POST /api/auth/request', () => {
|
|
test('returns 200 ok for valid email', async () => {
|
|
const r = await request(server, 'POST', '/api/auth/request', { email: 'test@example.com' })
|
|
assert.equal(r.status, 200)
|
|
assert.equal(r.body.ok, true)
|
|
})
|
|
|
|
test('creates profile if new email', async () => {
|
|
await request(server, 'POST', '/api/auth/request', { email: 'new@example.com' })
|
|
const p = db.findProfileByEmail('new@example.com')
|
|
assert.ok(p)
|
|
assert.equal(p.email, 'new@example.com')
|
|
})
|
|
|
|
test('returns 400 if email missing', async () => {
|
|
const r = await request(server, 'POST', '/api/auth/request', {})
|
|
assert.equal(r.status, 400)
|
|
})
|
|
})
|
|
|
|
describe('GET /api/auth/verify', () => {
|
|
let profileId
|
|
|
|
before(() => {
|
|
const p = db.createProfile('verify@example.com', 'Verify')
|
|
profileId = p.id
|
|
db.createMagicToken('testtoken123', profileId, Date.now() + 86400000)
|
|
})
|
|
|
|
test('redirects to /?session=... on valid token', async () => {
|
|
const r = await request(server, 'GET', '/api/auth/verify?token=testtoken123', null, {})
|
|
assert.equal(r.status, 302)
|
|
assert.ok(r.headers.location?.startsWith('/?session='))
|
|
})
|
|
|
|
test('returns 400 for invalid token', async () => {
|
|
const r = await request(server, 'GET', '/api/auth/verify?token=badtoken', null)
|
|
assert.equal(r.status, 400)
|
|
})
|
|
|
|
test('returns 400 for already-used token', async () => {
|
|
// testtoken123 was already used in the first verify test
|
|
const r = await request(server, 'GET', '/api/auth/verify?token=testtoken123', null)
|
|
assert.equal(r.status, 400)
|
|
})
|
|
})
|
|
|
|
describe('GET /api/auth/me', () => {
|
|
let sessionToken
|
|
|
|
before(() => {
|
|
const p = db.createProfile('me@example.com', 'Me')
|
|
sessionToken = 'sessmetoken'
|
|
db.createSession(sessionToken, p.id)
|
|
})
|
|
|
|
test('returns profile for valid session', async () => {
|
|
const r = await request(server, 'GET', '/api/auth/me', null, {
|
|
Authorization: `Bearer ${sessionToken}`,
|
|
})
|
|
assert.equal(r.status, 200)
|
|
assert.equal(r.body.email, 'me@example.com')
|
|
})
|
|
|
|
test('returns 401 without auth header', async () => {
|
|
const r = await request(server, 'GET', '/api/auth/me', null)
|
|
assert.equal(r.status, 401)
|
|
})
|
|
})
|
|
|
|
describe('DELETE /api/auth/session', () => {
|
|
let sessionToken
|
|
|
|
before(() => {
|
|
const p = db.createProfile('logout@example.com', 'Logout')
|
|
sessionToken = 'sesslogouttoken'
|
|
db.createSession(sessionToken, p.id)
|
|
})
|
|
|
|
test('returns 200 and session is gone', async () => {
|
|
const r = await request(server, 'DELETE', '/api/auth/session', null, {
|
|
Authorization: `Bearer ${sessionToken}`,
|
|
})
|
|
assert.equal(r.status, 200)
|
|
assert.equal(db.getSession(sessionToken), null)
|
|
})
|
|
|
|
test('returns 401 without auth header', async () => {
|
|
const r = await request(server, 'DELETE', '/api/auth/session', null)
|
|
assert.equal(r.status, 401)
|
|
})
|
|
|
|
test('returns 401 when using deleted session token', async () => {
|
|
const r = await request(server, 'GET', '/api/auth/me', null, {
|
|
Authorization: `Bearer ${sessionToken}`,
|
|
})
|
|
assert.equal(r.status, 401)
|
|
})
|
|
})
|