// Point the DB at a closed port BEFORE requiring anything that builds the pool — // these cases reject at requireAuth (no session token) before any query runs. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, after } = require('node:test') const assert = require('node:assert/strict') const { startApp } = require('./_helper') const authRouter = require('../src/router/v1/auth') const db = require('../src/utils/db') after(() => db.close()) // The push-notification self surface (/auth/me/devices*, /auth/me/notifications/*) // must be mounted AND gated: an unauthenticated caller gets 401 on every route — // never 404 (route missing) and never 200 (gate bypassed). test('/auth/me push routes reject unauthenticated callers with 401', async () => { const app = await startApp((a) => a.use('/api/v1/auth', authRouter)) try { const calls = [ ['GET', '/api/v1/auth/me/devices'], ['POST', '/api/v1/auth/me/devices', { endpoint: 'https://ntfy.example.com/UPabc' }], ['DELETE', '/api/v1/auth/me/devices/1'], ['GET', '/api/v1/auth/me/notifications/streams'], ['GET', '/api/v1/auth/me/notifications/subscriptions'], ['PUT', '/api/v1/auth/me/notifications/subscriptions', { streams: ['news.post'] }], // Phase 6's two. Per-Team preferences are as much a private fact as the // subscriptions beside them — the LIST of Teams a user may be notified // about answers "which guilds is this person in". ['GET', '/api/v1/auth/me/notifications/teams'], ['PUT', '/api/v1/auth/me/notifications/teams', { teams: [] }], ] for (const [method, path, body] of calls) { const res = await fetch(app.url + path, { method, headers: body ? { 'Content-Type': 'application/json' } : {}, body: body ? JSON.stringify(body) : undefined, }) assert.equal(res.status, 401, `${method} ${path} should be 401, got ${res.status}`) } } finally { await app.close() } }) // ── The unsubscribe endpoint (TEAMS.md §6.4) ─────────────────────────────── // // The mirror image of every test above: this one is reached WITHOUT a session, on // purpose, because the person following it is reading their mail. So the things // worth asserting are that it is mounted unauthenticated, that it says the same // thing whatever the token was, and that the GET twin does not write. const publicRouter = require('../src/router/v1/public') test('unsubscribe answers 200 unauthenticated, and says nothing about the token', async () => { const app = await startApp((a) => a.use('/api/v1/public', publicRouter)) try { // A forged token and a well-formed one must be indistinguishable from the // outside — otherwise this is an oracle for which (user, Team) pairs exist. for (const token of ['1.1.1.AAAAAAAAAAAAAAAAAAAAAA', 'total-nonsense']) { const res = await fetch(`${app.url}/api/v1/public/teams/unsubscribe/${token}`, { method: 'POST' }) assert.equal(res.status, 200) assert.deepEqual(await res.json(), { ok: true }) } } finally { await app.close() } }) test('a GET on the same path redirects and does not act', async () => { const app = await startApp((a) => a.use('/api/v1/public', publicRouter)) try { const res = await fetch(`${app.url}/api/v1/public/teams/unsubscribe/1.1.1.AAAAAAAAAAAAAAAAAAAAAA`, { redirect: 'manual', }) assert.equal(res.status, 302) assert.match(res.headers.get('location') || '', /\/unsubscribe\//) } finally { await app.close() } })