feat: server HTTP API with auth, progress, and admin routes (Tasks 6+7)
- 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>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
'use strict'
|
||||
const http = require('node:http')
|
||||
const path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
const crypto = require('node:crypto')
|
||||
const { initDb } = require('./db.js')
|
||||
const { sendMagicLink } = require('./mailer.js')
|
||||
|
||||
const PORT = parseInt(process.env.PORT || '3001', 10)
|
||||
const DB_PATH = process.env.DB_PATH || undefined
|
||||
|
||||
const DIST_DIR = path.join(__dirname, '../client/dist')
|
||||
|
||||
function send(res, status, body) {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json' })
|
||||
res.end(JSON.stringify(body))
|
||||
}
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = ''
|
||||
req.on('error', reject)
|
||||
req.on('data', c => {
|
||||
data += c
|
||||
if (data.length > 1024 * 1024) {
|
||||
req.destroy(new Error('body too large'))
|
||||
}
|
||||
})
|
||||
req.on('end', () => {
|
||||
try { resolve(JSON.parse(data)) }
|
||||
catch { resolve({}) }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function createServer(db) {
|
||||
if (!db) {
|
||||
db = initDb(DB_PATH)
|
||||
}
|
||||
|
||||
function requireAuth(req, res) {
|
||||
const auth = req.headers['authorization'] || ''
|
||||
const token = auth.startsWith('Bearer ') ? auth.slice(7) : null
|
||||
if (!token) { send(res, 401, { error: 'unauthorized' }); return null }
|
||||
const session = db.getSession(token)
|
||||
if (!session) { send(res, 401, { error: 'unauthorized' }); return null }
|
||||
return session
|
||||
}
|
||||
|
||||
function requireAdmin(req, res) {
|
||||
const pwd = process.env.ADMIN_PASSWORD
|
||||
const auth = req.headers['authorization'] || ''
|
||||
const token = auth.startsWith('Bearer ') ? auth.slice(7) : null
|
||||
if (!pwd || token !== pwd) { send(res, 401, { error: 'unauthorized' }); return false }
|
||||
return true
|
||||
}
|
||||
|
||||
async function handleRequest(req, res) {
|
||||
const url = new URL(req.url, 'http://127.0.0.1')
|
||||
const pathname = url.pathname
|
||||
const method = req.method
|
||||
|
||||
// ── Auth routes ──────────────────────────────────────────────────────────
|
||||
|
||||
if (pathname === '/api/auth/request' && method === 'POST') {
|
||||
const body = await readBody(req)
|
||||
const email = body.email
|
||||
if (!email || typeof email !== 'string' || !email.includes('@') || !email.includes('.')) {
|
||||
return send(res, 400, { error: 'email required' })
|
||||
}
|
||||
let profile = db.findProfileByEmail(email)
|
||||
if (!profile) {
|
||||
profile = db.createProfile(email, null)
|
||||
}
|
||||
const token = crypto.randomBytes(32).toString('hex')
|
||||
const expiresAt = Date.now() + 24 * 60 * 60 * 1000
|
||||
db.createMagicToken(token, profile.id, expiresAt)
|
||||
await sendMagicLink(email, token)
|
||||
return send(res, 200, { ok: true })
|
||||
}
|
||||
|
||||
if (pathname === '/api/auth/verify' && method === 'GET') {
|
||||
const token = url.searchParams.get('token')
|
||||
if (!token) { return send(res, 400, { error: 'token required' }) }
|
||||
const profileId = db.useMagicToken(token)
|
||||
if (!profileId) { return send(res, 400, { error: 'invalid or expired token' }) }
|
||||
const sessionToken = crypto.randomBytes(32).toString('hex')
|
||||
db.createSession(sessionToken, profileId)
|
||||
res.writeHead(302, { Location: `/?session=${sessionToken}` })
|
||||
return res.end()
|
||||
}
|
||||
|
||||
if (pathname === '/api/auth/me' && method === 'GET') {
|
||||
const session = requireAuth(req, res)
|
||||
if (!session) return
|
||||
const profile = db.findProfileById(session.profile_id)
|
||||
if (!profile) { return send(res, 404, { error: 'not found' }) }
|
||||
return send(res, 200, {
|
||||
id: profile.id,
|
||||
email: profile.email,
|
||||
display_name: profile.display_name,
|
||||
})
|
||||
}
|
||||
|
||||
if (pathname === '/api/auth/session' && method === 'DELETE') {
|
||||
const session = requireAuth(req, res)
|
||||
if (!session) return
|
||||
db.deleteSession(session.token)
|
||||
return send(res, 200, { ok: true })
|
||||
}
|
||||
|
||||
// ── Progress routes ───────────────────────────────────────────────────────
|
||||
|
||||
if (pathname === '/api/progress' && method === 'GET') {
|
||||
const session = requireAuth(req, res)
|
||||
if (!session) return
|
||||
const progress = db.getOrCreateProgress(session.profile_id)
|
||||
return send(res, 200, progress)
|
||||
}
|
||||
|
||||
if (pathname === '/api/progress/answer' && method === 'POST') {
|
||||
const session = requireAuth(req, res)
|
||||
if (!session) return
|
||||
const body = await readBody(req)
|
||||
const letter = typeof body.letter === 'string' ? body.letter.toUpperCase() : ''
|
||||
if (!/^[A-Z]$/.test(letter)) {
|
||||
return send(res, 400, { error: 'letter must be a single letter A-Z' })
|
||||
}
|
||||
if (typeof body.correct !== 'boolean') {
|
||||
return send(res, 400, { error: 'correct required' })
|
||||
}
|
||||
const progress = db.recordAnswer(session.profile_id, letter, body.correct)
|
||||
return send(res, 200, progress)
|
||||
}
|
||||
|
||||
if (pathname === '/api/mnemonics' && method === 'GET') {
|
||||
const session = requireAuth(req, res)
|
||||
if (!session) return
|
||||
const mnemonics = db.getMnemonics(session.profile_id)
|
||||
return send(res, 200, mnemonics)
|
||||
}
|
||||
|
||||
if (pathname === '/api/mnemonics' && method === 'POST') {
|
||||
const session = requireAuth(req, res)
|
||||
if (!session) return
|
||||
const body = await readBody(req)
|
||||
const letter = typeof body.letter === 'string' ? body.letter.toUpperCase() : ''
|
||||
if (!/^[A-Z]$/.test(letter)) {
|
||||
return send(res, 400, { error: 'letter must be a single letter A-Z' })
|
||||
}
|
||||
if (!body.phrase || typeof body.phrase !== 'string' || body.phrase.length > 500) {
|
||||
return send(res, 400, { error: 'phrase required (max 500 chars)' })
|
||||
}
|
||||
db.saveMnemonic(session.profile_id, letter, body.phrase)
|
||||
return send(res, 200, { ok: true })
|
||||
}
|
||||
|
||||
// ── Admin routes ──────────────────────────────────────────────────────────
|
||||
|
||||
if (pathname === '/api/admin/users' && method === 'GET') {
|
||||
if (!requireAdmin(req, res)) return
|
||||
const users = db.getAdminUsers()
|
||||
return send(res, 200, users)
|
||||
}
|
||||
|
||||
if (pathname === '/api/admin/stats' && method === 'GET') {
|
||||
if (!requireAdmin(req, res)) return
|
||||
const stats = db.getAdminStats()
|
||||
return send(res, 200, stats)
|
||||
}
|
||||
|
||||
// ── Unknown /api/* ────────────────────────────────────────────────────────
|
||||
|
||||
if (pathname.startsWith('/api/')) {
|
||||
return send(res, 404, { error: 'not found' })
|
||||
}
|
||||
|
||||
// ── Static files (SPA) ───────────────────────────────────────────────────
|
||||
|
||||
// Prevent path traversal — ensure resolved path stays within DIST_DIR
|
||||
const relative = pathname.replace(/^\/+/, '')
|
||||
const filePath = path.join(DIST_DIR, relative)
|
||||
if (!filePath.startsWith(DIST_DIR + path.sep) && filePath !== DIST_DIR) {
|
||||
return send(res, 400, { error: 'bad request' })
|
||||
}
|
||||
|
||||
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
||||
const ext = path.extname(filePath)
|
||||
const mimeTypes = {
|
||||
'.html': 'text/html',
|
||||
'.js': 'application/javascript',
|
||||
'.css': 'text/css',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
}
|
||||
const contentType = mimeTypes[ext] || 'application/octet-stream'
|
||||
res.writeHead(200, { 'Content-Type': contentType })
|
||||
return fs.createReadStream(filePath).pipe(res)
|
||||
}
|
||||
|
||||
// SPA fallback — serve index.html
|
||||
const indexPath = path.join(DIST_DIR, 'index.html')
|
||||
if (fs.existsSync(indexPath)) {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||
return fs.createReadStream(indexPath).pipe(res)
|
||||
}
|
||||
|
||||
// No dist yet
|
||||
return send(res, 404, { error: 'not found' })
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
handleRequest(req, res).catch(err => {
|
||||
console.error('[server] unhandled:', err)
|
||||
if (!res.headersSent) { res.writeHead(500); res.end() }
|
||||
})
|
||||
})
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const db = initDb(DB_PATH)
|
||||
const server = createServer(db)
|
||||
server.listen(PORT, () => console.log(`[server] listening on :${PORT}`))
|
||||
}
|
||||
|
||||
module.exports = { createServer }
|
||||
@@ -0,0 +1,91 @@
|
||||
'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
|
||||
const ADMIN_PWD = 'testadminpwd'
|
||||
|
||||
before(() => {
|
||||
process.env.ADMIN_PASSWORD = ADMIN_PWD
|
||||
db = initDb(':memory:')
|
||||
server = createServer(db)
|
||||
const p1 = db.createProfile('adm1@example.com', 'Admin1')
|
||||
const p2 = db.createProfile('adm2@example.com', 'Admin2')
|
||||
db.recordAnswer(p1.id, 'A', true)
|
||||
db.recordAnswer(p2.id, 'B', false)
|
||||
return new Promise(resolve => server.listen(0, resolve))
|
||||
})
|
||||
|
||||
after(() => {
|
||||
delete process.env.ADMIN_PASSWORD
|
||||
db._db.close()
|
||||
return new Promise(resolve => server.close(resolve))
|
||||
})
|
||||
|
||||
describe('GET /api/admin/users', () => {
|
||||
test('returns user list with valid admin password', async () => {
|
||||
const r = await request(server, 'GET', '/api/admin/users', null, {
|
||||
Authorization: `Bearer ${ADMIN_PWD}`,
|
||||
})
|
||||
assert.equal(r.status, 200)
|
||||
assert.ok(Array.isArray(r.body))
|
||||
const emails = r.body.map(u => u.email)
|
||||
assert.ok(emails.includes('adm1@example.com'))
|
||||
})
|
||||
|
||||
test('returns 401 without admin password', async () => {
|
||||
const r = await request(server, 'GET', '/api/admin/users', null)
|
||||
assert.equal(r.status, 401)
|
||||
})
|
||||
|
||||
test('returns 401 with wrong admin password', async () => {
|
||||
const r = await request(server, 'GET', '/api/admin/users', null, {
|
||||
Authorization: 'Bearer wrongpwd',
|
||||
})
|
||||
assert.equal(r.status, 401)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/admin/stats', () => {
|
||||
test('returns stats with valid admin password', async () => {
|
||||
const r = await request(server, 'GET', '/api/admin/stats', null, {
|
||||
Authorization: `Bearer ${ADMIN_PWD}`,
|
||||
})
|
||||
assert.equal(r.status, 200)
|
||||
assert.ok(typeof r.body.totalUsers === 'number')
|
||||
assert.ok(Array.isArray(r.body.mostMissed))
|
||||
})
|
||||
|
||||
test('returns 401 without admin password', async () => {
|
||||
const r = await request(server, 'GET', '/api/admin/stats', null)
|
||||
assert.equal(r.status, 401)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,144 @@
|
||||
'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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,145 @@
|
||||
'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, sessionToken, profileId
|
||||
|
||||
before(() => {
|
||||
db = initDb(':memory:')
|
||||
server = createServer(db)
|
||||
const p = db.createProfile('prog@example.com', 'Progress')
|
||||
profileId = p.id
|
||||
sessionToken = 'progtoken'
|
||||
db.createSession(sessionToken, profileId)
|
||||
return new Promise(resolve => server.listen(0, resolve))
|
||||
})
|
||||
|
||||
after(() => {
|
||||
db._db.close()
|
||||
return new Promise(resolve => server.close(resolve))
|
||||
})
|
||||
|
||||
const authHeader = () => ({ Authorization: `Bearer ${sessionToken}` })
|
||||
|
||||
describe('GET /api/progress', () => {
|
||||
test('returns progress for authed user', async () => {
|
||||
const r = await request(server, 'GET', '/api/progress', null, authHeader())
|
||||
assert.equal(r.status, 200)
|
||||
assert.equal(r.body.level, 1)
|
||||
assert.equal(r.body.score, 0)
|
||||
})
|
||||
|
||||
test('returns 401 without auth', async () => {
|
||||
const r = await request(server, 'GET', '/api/progress', null)
|
||||
assert.equal(r.status, 401)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/progress/answer', () => {
|
||||
test('records correct answer and returns updated progress', async () => {
|
||||
const r = await request(server, 'POST', '/api/progress/answer',
|
||||
{ letter: 'A', correct: true }, authHeader())
|
||||
assert.equal(r.status, 200)
|
||||
assert.ok(r.body.score > 0)
|
||||
assert.equal(r.body.streak, 1)
|
||||
})
|
||||
|
||||
test('records incorrect answer (resets streak)', async () => {
|
||||
const r = await request(server, 'POST', '/api/progress/answer',
|
||||
{ letter: 'A', correct: false }, authHeader())
|
||||
assert.equal(r.status, 200)
|
||||
assert.equal(r.body.streak, 0)
|
||||
})
|
||||
|
||||
test('returns 401 without auth', async () => {
|
||||
const r = await request(server, 'POST', '/api/progress/answer',
|
||||
{ letter: 'A', correct: true })
|
||||
assert.equal(r.status, 401)
|
||||
})
|
||||
|
||||
test('returns 400 if letter missing', async () => {
|
||||
const r = await request(server, 'POST', '/api/progress/answer',
|
||||
{ correct: true }, authHeader())
|
||||
assert.equal(r.status, 400)
|
||||
})
|
||||
|
||||
test('returns 400 if correct is missing', async () => {
|
||||
const r = await request(server, 'POST', '/api/progress/answer',
|
||||
{ letter: 'A' }, authHeader())
|
||||
assert.equal(r.status, 400)
|
||||
})
|
||||
|
||||
test('returns 400 if correct is non-boolean', async () => {
|
||||
const r = await request(server, 'POST', '/api/progress/answer',
|
||||
{ letter: 'A', correct: 'true' }, authHeader())
|
||||
assert.equal(r.status, 400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/mnemonics', () => {
|
||||
before(() => {
|
||||
db.saveMnemonic(profileId, 'A', 'a phrase')
|
||||
})
|
||||
|
||||
test('returns mnemonic map', async () => {
|
||||
const r = await request(server, 'GET', '/api/mnemonics', null, authHeader())
|
||||
assert.equal(r.status, 200)
|
||||
assert.equal(r.body.A, 'a phrase')
|
||||
})
|
||||
|
||||
test('returns 401 without auth', async () => {
|
||||
const r = await request(server, 'GET', '/api/mnemonics', null)
|
||||
assert.equal(r.status, 401)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/mnemonics', () => {
|
||||
test('saves mnemonic and returns ok', async () => {
|
||||
const r = await request(server, 'POST', '/api/mnemonics',
|
||||
{ letter: 'E', phrase: 'yes' }, authHeader())
|
||||
assert.equal(r.status, 200)
|
||||
assert.equal(r.body.ok, true)
|
||||
const m = db.getMnemonics(profileId)
|
||||
assert.equal(m.E, 'yes')
|
||||
})
|
||||
|
||||
test('returns 400 if letter or phrase missing', async () => {
|
||||
const r = await request(server, 'POST', '/api/mnemonics',
|
||||
{ letter: 'T' }, authHeader())
|
||||
assert.equal(r.status, 400)
|
||||
})
|
||||
|
||||
test('returns 401 without auth', async () => {
|
||||
const r = await request(server, 'POST', '/api/mnemonics',
|
||||
{ letter: 'E', phrase: 'yes' })
|
||||
assert.equal(r.status, 401)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user