← back to Ventura Claw Leads
tests/routes.test.js
132 lines
// Smoke test the public routes — boots the Express app in-process without
// pm2 and hits every route through supertest-equivalent (built-in fetch +
// http.createServer.listen on port 0).
require('dotenv').config({ path: require('path').resolve(__dirname, '..', '.env') });
process.env.NODE_ENV = 'test';
process.env.SESSION_SECRET = process.env.SESSION_SECRET || 'test-secret';
// Avoid colliding with the dev server on :9789.
process.env.PORT = '0';
const test = require('node:test');
const assert = require('node:assert/strict');
const http = require('node:http');
const path = require('node:path');
// Re-implement just enough of server.js to mount the public routes for testing.
// Mounting the actual server.js would call app.listen synchronously; we'd
// rather avoid the side-effect and stand up our own short-lived server.
const express = require('express');
const session = require('express-session');
const PgSession = require('connect-pg-simple')(session);
const helmet = require('helmet');
const auth = require('../lib/auth');
const { csrfMiddleware } = require('../lib/csrf');
const db = require('../lib/db');
const publicRoutes = require('../routes/public');
let server, baseUrl;
test.before(async () => {
const app = express();
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '..', 'views'));
app.use(helmet({ contentSecurityPolicy: false }));
app.use(session({
store: new PgSession({ pool: db.pool, tableName: 'session' }),
secret: 'test', resave: false, saveUninitialized: false,
cookie: { httpOnly: true, secure: false, sameSite: 'lax' }
}));
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use((req, res, next) => { res.locals.publicUrl = 'http://test.local'; res.locals.path = req.path; next(); });
app.use(auth.attachBusiness);
app.use(csrfMiddleware);
app.use('/', publicRoutes);
app.use((err, req, res, next) => {
console.error('[test-app]', err.stack);
res.status(500).send('boom');
});
await new Promise((resolve) => {
server = app.listen(0, () => {
const port = server.address().port;
baseUrl = `http://127.0.0.1:${port}`;
resolve();
});
});
});
test.after(async () => {
await new Promise((resolve) => server.close(resolve));
await db.pool.end();
});
const ROUTES = [
{ path: '/', expect: 200 },
{ path: '/find', expect: 200 },
{ path: '/find?vertical=food', expect: 200 },
{ path: '/find?vertical=invalid', expect: 200 }, // unknown vertical = silent skip, no 4xx
{ path: '/map', expect: 200 },
{ path: '/about', expect: 200 },
{ path: '/for-businesses', expect: 200 },
{ path: '/privacy', expect: 200 },
{ path: '/terms', expect: 200 },
{ path: '/healthz', expect: 200 },
{ path: '/api/businesses.geo', expect: 200 },
{ path: '/business/non-existent-slug', expect: 404 },
{ path: '/business/non-existent/contact', method: 'GET', expect: 404 }
];
for (const r of ROUTES) {
test(`${r.method || 'GET'} ${r.path} → ${r.expect}`, async () => {
const res = await fetch(baseUrl + r.path, { method: r.method || 'GET' });
assert.equal(res.status, r.expect);
});
}
test('homepage HTML contains brand mark', async () => {
const res = await fetch(baseUrl + '/');
const body = await res.text();
assert.ok(body.includes('Ventura Claw'), 'brand name in body');
assert.ok(body.includes('every business worth knowing') || body.includes('Every business worth knowing'),
'hero copy present');
});
test('/find renders ItemList JSON-LD', async () => {
const res = await fetch(baseUrl + '/find');
const body = await res.text();
assert.ok(body.includes('"@type":"ItemList"'), 'ItemList JSON-LD present');
});
test('/api/businesses.geo returns JSON with businesses array', async () => {
const res = await fetch(baseUrl + '/api/businesses.geo');
assert.equal(res.headers.get('content-type'), 'application/json; charset=utf-8');
const body = await res.json();
assert.ok(Array.isArray(body.businesses));
});
test('contact form rejects bad email', async () => {
// Pull a real business slug + CSRF token.
const slug = await db.one(`SELECT slug FROM businesses WHERE status='active' ORDER BY id LIMIT 1`);
if (!slug) { console.warn(' (no businesses seeded — skipping)'); return; }
const j = new Map();
const r1 = await fetch(baseUrl + '/business/' + slug.slug);
for (const c of r1.headers.getSetCookie()) j.set(c.split(';')[0].split('=')[0], c.split(';')[0]);
const cookie = Array.from(j.values()).join('; ');
const html = await r1.text();
const csrf = (html.match(/name="_csrf" value="([^"]+)"/) || [])[1];
assert.ok(csrf, 'csrf extracted');
const params = new URLSearchParams({ _csrf: csrf, name: 'Smoke', email: 'not-a-real-email', message: 'hi' });
const r2 = await fetch(baseUrl + '/business/' + slug.slug + '/contact', {
method: 'POST',
headers: { cookie, 'content-type': 'application/x-www-form-urlencoded' },
body: params.toString()
});
assert.equal(r2.status, 400);
const body2 = await r2.text();
assert.ok(body2.includes('valid email') || body2.includes('Email needed'));
});