2026-05-27 14:52:07 -04:00
import { test } from 'node:test' ;
import assert from 'node:assert/strict' ;
import express from 'express' ;
import session from 'express-session' ;
import { isTestDbConfigured , setupTestDb } from '../helpers/setup-db.js' ;
import tokensRouter from '../../src/routes/tokens.js' ;
import authRouter from '../../src/routes/auth.js' ;
2026-05-27 15:03:35 -04:00
import { requireAuth , requireUiHeader } from '../../src/middleware/auth.js' ;
2026-05-27 14:52:07 -04:00
import { hashPassword } from '../../src/auth/passwords.js' ;
async function app ( pool ) {
process . env . DATABASE _URL = process . env . TEST _DATABASE _URL ;
process . env . AUTH _ENABLED = 'true' ;
const ConnectPg = ( await import ( 'connect-pg-simple' ) ) . default ( session ) ;
const a = express ( ) ;
a . use ( express . json ( ) ) ;
a . use ( session ( {
store : new ConnectPg ( { pool , tableName : 'sessions' } ) ,
secret : 'test' , name : 'dragonflight.sid' ,
cookie : { httpOnly : true , sameSite : 'lax' , secure : false , maxAge : 8 * 3600 * 1000 } ,
rolling : false , resave : false , saveUninitialized : false ,
} ) ) ;
2026-05-27 15:03:35 -04:00
a . use ( '/api/v1' , requireUiHeader ) ;
2026-05-27 14:52:07 -04:00
a . use ( '/api/v1/auth' , authRouter ) ;
a . use ( '/api/v1/auth/tokens' , requireAuth , tokensRouter ) ;
a . get ( '/api/v1/protected/ping' , requireAuth , ( req , res ) => res . json ( { user : req . user . username } ) ) ;
return new Promise ( r => {
const srv = a . listen ( 0 , '127.0.0.1' , ( ) => {
r ( { baseUrl : 'http://127.0.0.1:' + srv . address ( ) . port , close : ( ) => new Promise ( rs => srv . close ( rs ) ) } ) ;
} ) ;
} ) ;
}
async function loginCookie ( baseUrl , u , p ) {
const r = await fetch ( baseUrl + '/api/v1/auth/login' , {
2026-05-27 14:58:02 -04:00
method : 'POST' , headers : { 'Content-Type' : 'application/json' , 'X-Requested-With' : 'dragonflight-ui' } ,
2026-05-27 14:52:07 -04:00
body : JSON . stringify ( { username : u , password : p } ) ,
} ) ;
return ( r . headers . get ( 'set-cookie' ) || '' ) . split ( ';' ) [ 0 ] ;
}
test ( 'tokens: create returns the raw token exactly once; bearer of that token works; revoke 401s subsequent calls' , { skip : ! isTestDbConfigured ( ) && 'TEST_DATABASE_URL not set' } , async ( ) => {
const pool = await setupTestDb ( ) ;
await pool . query ( ` INSERT INTO users (username, password_hash) VALUES ('alice', $ 1) ` , [ await hashPassword ( 'correct-horse-battery' ) ] ) ;
const { baseUrl , close } = await app ( pool ) ;
try {
const cookie = await loginCookie ( baseUrl , 'alice' , 'correct-horse-battery' ) ;
// Create
const create = await fetch ( baseUrl + '/api/v1/auth/tokens' , {
2026-05-27 14:58:02 -04:00
method : 'POST' , headers : { 'Content-Type' : 'application/json' , 'X-Requested-With' : 'dragonflight-ui' , cookie } ,
2026-05-27 14:52:07 -04:00
body : JSON . stringify ( { name : 'Premiere panel' } ) ,
} ) ;
assert . equal ( create . status , 201 ) ;
const created = await create . json ( ) ;
assert . match ( created . token , /^dfl_[0-9a-f]{64}$/ ) ;
assert . equal ( created . prefix , created . token . slice ( 0 , 8 ) ) ;
// List — should NOT include the raw token, only the prefix.
const list = await fetch ( baseUrl + '/api/v1/auth/tokens' , { headers : { cookie } } ) ;
const rows = await list . json ( ) ;
assert . equal ( rows . length , 1 ) ;
assert . equal ( rows [ 0 ] . prefix , created . prefix ) ;
assert . equal ( rows [ 0 ] . token , undefined ) ;
// The raw token authenticates as a bearer.
const ping = await fetch ( baseUrl + '/api/v1/protected/ping' , {
headers : { authorization : 'Bearer ' + created . token } ,
} ) ;
assert . equal ( ping . status , 200 ) ;
// Revoke.
const rev = await fetch ( baseUrl + '/api/v1/auth/tokens/' + created . id , {
method : 'DELETE' , headers : { cookie } ,
} ) ;
assert . equal ( rev . status , 204 ) ;
// Same bearer now 401s.
const ping2 = await fetch ( baseUrl + '/api/v1/protected/ping' , {
headers : { authorization : 'Bearer ' + created . token } ,
} ) ;
assert . equal ( ping2 . status , 401 ) ;
} finally { await close ( ) ; await pool . end ( ) ; }
} ) ;
test ( 'tokens: cannot revoke another users token' , { skip : ! isTestDbConfigured ( ) && 'TEST_DATABASE_URL not set' } , async ( ) => {
const pool = await setupTestDb ( ) ;
await pool . query ( ` INSERT INTO users (username, password_hash) VALUES ('alice', $ 1) ` , [ await hashPassword ( 'correct-horse-battery' ) ] ) ;
await pool . query ( ` INSERT INTO users (username, password_hash) VALUES ('bob', $ 1) ` , [ await hashPassword ( 'bob-passphrase-12' ) ] ) ;
const { baseUrl , close } = await app ( pool ) ;
try {
const aliceCookie = await loginCookie ( baseUrl , 'alice' , 'correct-horse-battery' ) ;
const bobCookie = await loginCookie ( baseUrl , 'bob' , 'bob-passphrase-12' ) ;
const aliceTok = await ( await fetch ( baseUrl + '/api/v1/auth/tokens' , {
2026-05-27 14:58:02 -04:00
method : 'POST' , headers : { 'Content-Type' : 'application/json' , 'X-Requested-With' : 'dragonflight-ui' , cookie : aliceCookie } ,
2026-05-27 14:52:07 -04:00
body : JSON . stringify ( { name : 'alice token' } ) ,
} ) ) . json ( ) ;
const r = await fetch ( baseUrl + '/api/v1/auth/tokens/' + aliceTok . id , {
method : 'DELETE' , headers : { cookie : bobCookie } ,
} ) ;
assert . equal ( r . status , 404 ) ; // not found from bob's perspective
} finally { await close ( ) ; await pool . end ( ) ; }
} ) ;