Files
kitadmin fe5cbaec2b feat(auth): guest login with random telegraph operator name
No email required — players pick male/female/mystery, get assigned
a humorous period-appropriate operator name (e.g. "Dusty Clicksworth",
"Ada Dottsworth", "The Phantom Sender"), can reroll, then hit
"Begin Transmitting!" for instant play. Email/magic link moved to
a collapsible section for cross-device access only.

- server: POST /api/auth/guest creates guest profile + session
- server/db: profiles.email now nullable, is_guest column added,
  migration recreates table for existing installs
- client: names.ts with 15 names per gender category
- client: LoginPage redesigned — gender picker → name display → play
- client: useAuth gains setAuth() for direct session injection
- 55 server + 44 client tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 04:22:05 +00:00

176 lines
5.5 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/guest', () => {
test('creates guest profile and returns token + profile', async () => {
const r = await request(server, 'POST', '/api/auth/guest', { display_name: 'Dusty Clicksworth' })
assert.equal(r.status, 200)
assert.ok(r.body.token)
assert.equal(r.body.profile.display_name, 'Dusty Clicksworth')
assert.equal(r.body.profile.is_guest, true)
assert.equal(r.body.profile.email, null)
})
test('session token from guest login works with /api/auth/me', async () => {
const g = await request(server, 'POST', '/api/auth/guest', { display_name: 'Flash Henderson' })
const me = await request(server, 'GET', '/api/auth/me', null, {
Authorization: `Bearer ${g.body.token}`,
})
assert.equal(me.status, 200)
assert.equal(me.body.display_name, 'Flash Henderson')
assert.equal(me.body.is_guest, true)
})
test('returns 400 if display_name missing', async () => {
const r = await request(server, 'POST', '/api/auth/guest', {})
assert.equal(r.status, 400)
})
test('returns 400 if display_name too short', async () => {
const r = await request(server, 'POST', '/api/auth/guest', { display_name: 'X' })
assert.equal(r.status, 400)
})
})
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)
})
})