chore(mam-api): wire node:test runner + test app + DB helper
This commit is contained in:
parent
99fae69960
commit
5011d45391
4 changed files with 107 additions and 1 deletions
|
|
@ -6,7 +6,8 @@
|
|||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "node --watch src/index.js"
|
||||
"dev": "node --watch src/index.js",
|
||||
"test": "node --test test/**/*.test.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
|
|
|
|||
44
services/mam-api/test/helpers/setup-db.js
Normal file
44
services/mam-api/test/helpers/setup-db.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Pure helper for integration tests. Requires TEST_DATABASE_URL pointing at
|
||||
// a throwaway Postgres. Returns a pg Pool with the full schema applied.
|
||||
// Tests that need this should `skip` (not fail) when TEST_DATABASE_URL is unset.
|
||||
import { Pool } from 'pg';
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const migrationsDir = join(__dirname, '..', '..', 'src', 'db', 'migrations');
|
||||
const schemaFile = join(__dirname, '..', '..', 'src', 'db', 'schema.sql');
|
||||
const patches = [
|
||||
'schema_patch_ampp.sql',
|
||||
'schema_patch_editor.sql',
|
||||
'schema_patch_groups_tokens.sql',
|
||||
];
|
||||
|
||||
export function isTestDbConfigured() {
|
||||
return !!process.env.TEST_DATABASE_URL;
|
||||
}
|
||||
|
||||
export async function setupTestDb() {
|
||||
if (!process.env.TEST_DATABASE_URL) {
|
||||
throw new Error('TEST_DATABASE_URL not set');
|
||||
}
|
||||
const pool = new Pool({ connectionString: process.env.TEST_DATABASE_URL });
|
||||
|
||||
// Wipe and recreate the public schema so each test run starts clean.
|
||||
await pool.query('DROP SCHEMA IF EXISTS public CASCADE');
|
||||
await pool.query('CREATE SCHEMA public');
|
||||
await pool.query('GRANT ALL ON SCHEMA public TO PUBLIC');
|
||||
|
||||
// Base schema, then patches, then migrations in order.
|
||||
await pool.query(readFileSync(schemaFile, 'utf8'));
|
||||
for (const p of patches) {
|
||||
await pool.query(readFileSync(join(__dirname, '..', '..', 'src', 'db', p), 'utf8'));
|
||||
}
|
||||
const migrations = readdirSync(migrationsDir).filter(f => f.endsWith('.sql')).sort();
|
||||
for (const f of migrations) {
|
||||
await pool.query(readFileSync(join(migrationsDir, f), 'utf8'));
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
45
services/mam-api/test/helpers/test-app.js
Normal file
45
services/mam-api/test/helpers/test-app.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Builds an Express app with the production middleware stack pointed at a
|
||||
// test pool, listens on an ephemeral port, returns { baseUrl, server, pool, cleanup }.
|
||||
// Tests use fetch() against baseUrl — no supertest dep needed.
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import session from 'express-session';
|
||||
import connectPgSimple from 'connect-pg-simple';
|
||||
import { setupTestDb } from './setup-db.js';
|
||||
|
||||
const PgStore = connectPgSimple(session);
|
||||
|
||||
export async function createTestApp({ authEnabled = true } = {}) {
|
||||
const pool = await setupTestDb();
|
||||
process.env.AUTH_ENABLED = authEnabled ? 'true' : 'false';
|
||||
process.env.SESSION_SECRET = process.env.SESSION_SECRET || 'test-secret-' + Math.random();
|
||||
|
||||
const app = express();
|
||||
app.use(cors({ origin: true, credentials: true }));
|
||||
app.use(express.json({ limit: '5mb' }));
|
||||
app.use(session({
|
||||
store: new PgStore({ pool, tableName: 'sessions' }),
|
||||
secret: process.env.SESSION_SECRET,
|
||||
name: 'dragonflight.sid',
|
||||
cookie: { httpOnly: true, sameSite: 'lax', secure: false, path: '/', maxAge: 8 * 3600 * 1000 },
|
||||
rolling: false,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
}));
|
||||
|
||||
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
|
||||
|
||||
// Tests that need additional routes mount them on the returned `app`
|
||||
// before calling listen.
|
||||
return new Promise(resolve => {
|
||||
const server = app.listen(0, '127.0.0.1', () => {
|
||||
const port = server.address().port;
|
||||
const baseUrl = `http://127.0.0.1:${port}`;
|
||||
const cleanup = async () => {
|
||||
await new Promise(r => server.close(r));
|
||||
await pool.end();
|
||||
};
|
||||
resolve({ app, server, pool, baseUrl, cleanup });
|
||||
});
|
||||
});
|
||||
}
|
||||
16
services/mam-api/test/smoke.test.js
Normal file
16
services/mam-api/test/smoke.test.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { isTestDbConfigured } from './helpers/setup-db.js';
|
||||
import { createTestApp } from './helpers/test-app.js';
|
||||
|
||||
test('test infra: /health responds on an ephemeral port', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
|
||||
const { baseUrl, cleanup } = await createTestApp();
|
||||
try {
|
||||
const res = await fetch(baseUrl + '/health');
|
||||
assert.equal(res.status, 200);
|
||||
const body = await res.json();
|
||||
assert.equal(body.status, 'ok');
|
||||
} finally {
|
||||
await cleanup();
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue