mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* 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>
101 lines
3.1 KiB
TypeScript
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');
|
|
});
|
|
});
|