GitNexus/gitnexus/test/unit/server-cors-stack.test.ts
Gergő Magyar 8db51184ab
fix(server): restore gitnexus serve startup under Express 5 (#1749)
* fix(server): restore gitnexus serve startup under Express 5

Express 5 rejects app.options('*'), which broke CI e2e when the backend
failed to start. Move PNA middleware before cors so preflight responses
include Access-Control-Allow-Private-Network, and add regression tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(server): address PR review — prettier, ephemeral port, cleanup

- Format integration and rate-limit test files for CI quality/format
- Use OS-assigned port instead of random 47xxx range
- Remove per-test GITNEXUS_HOME temp dir in afterEach
- Use regex for PNA-before-cors structural guard (indent-agnostic)

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 10:18:09 +01:00

101 lines
3.1 KiB
TypeScript

/**
* Regression tests for createServer() CORS + PNA middleware (Express 5).
*
* Express 5 / path-to-regexp v8 rejects bare `app.options('*')`, which broke
* `gitnexus serve` in CI after #872. These tests mirror the registration order
* in createServer() without booting LadybugDB or MCP.
*/
import express from 'express';
import cors from 'cors';
import http from 'node:http';
import { afterEach, describe, expect, it } from 'vitest';
import { isAllowedOrigin } from '../../src/server/api.js';
/** Mirrors createServer() trust proxy + PNA + cors + json stack. */
const buildCreateServerCorsStack = (): express.Express => {
const app = express();
app.disable('x-powered-by');
app.set('trust proxy', 'loopback, linklocal, uniquelocal');
app.use((_req, res, next) => {
res.setHeader('Access-Control-Allow-Private-Network', 'true');
next();
});
app.use(
cors({
origin: (origin, callback) => {
callback(null, isAllowedOrigin(origin));
},
}),
);
app.use(express.json({ limit: '10mb' }));
return app;
};
describe('createServer CORS/PNA stack — Express 5 registration', () => {
it('registers without path-to-regexp wildcard errors', () => {
expect(() => buildCreateServerCorsStack()).not.toThrow();
});
});
describe('createServer CORS/PNA stack — OPTIONS preflight', () => {
let server: http.Server | undefined;
let baseUrl = '';
afterEach(
() =>
new Promise<void>((resolve, reject) => {
if (!server) {
resolve();
return;
}
server.close((err) => (err ? reject(err) : resolve()));
}),
);
const start = (app: express.Express): Promise<void> =>
new Promise((resolve) => {
server = app.listen(0, '127.0.0.1', () => {
const addr = server!.address();
if (typeof addr === 'object' && addr) {
baseUrl = `http://127.0.0.1:${addr.port}`;
}
resolve();
});
});
it('OPTIONS /api/repos includes PNA and ACAO for allowed origin', async () => {
const app = buildCreateServerCorsStack();
await start(app);
const res = await fetch(`${baseUrl}/api/repos`, {
method: 'OPTIONS',
headers: {
Origin: 'https://gitnexus.vercel.app',
'Access-Control-Request-Method': 'GET',
'Access-Control-Request-Private-Network': 'true',
},
});
expect(res.status).toBe(204);
expect(res.headers.get('access-control-allow-origin')).toBe('https://gitnexus.vercel.app');
expect(res.headers.get('access-control-allow-private-network')).toBe('true');
});
it('OPTIONS / includes PNA header for localhost bridge', async () => {
const app = buildCreateServerCorsStack();
await start(app);
const res = await fetch(`${baseUrl}/`, {
method: 'OPTIONS',
headers: {
Origin: 'http://localhost:5173',
'Access-Control-Request-Method': 'GET',
'Access-Control-Request-Private-Network': 'true',
},
});
expect(res.status).toBe(204);
expect(res.headers.get('access-control-allow-origin')).toBe('http://localhost:5173');
expect(res.headers.get('access-control-allow-private-network')).toBe('true');
});
});