mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-22 00:31:17 +00:00
* feat: configure prettier with pre-commit hook integration Add prettier, lint-staged, and prettier-plugin-tailwindcss at the repo root with husky pre-commit hook integration. Moves husky from gitnexus/ to root package.json for reliable hook installation. - Root package.json with prepare/format/format:check scripts - .prettierrc with endOfLine:lf and tailwindStylesheet for TW v4 - .prettierignore excluding fixtures, vendor, generated, *.d.ts, *.md - .gitattributes enforcing LF line endings for Windows consistency - Pre-commit hook uses direct node_modules/.bin/ paths (no npx) * style: apply prettier formatting to entire codebase One-time bulk format. No logic changes. Use .git-blame-ignore-revs to skip this commit in git blame. * chore: add .git-blame-ignore-revs for prettier format commit * perf: pre-commit hook runs only tests related to staged files Use vitest --related to scope test execution to tests that import the changed files, instead of running the full suite on every commit. * perf: remove vitest from pre-commit hook, keep in CI only Pre-commit now runs lint-staged + tsc only. Tests run in CI (ci-tests.yml) where they belong — keeps commits fast. * ci: add prettier format check to quality workflow PRs will now fail if code isn't formatted with prettier.
183 lines
6.5 KiB
TypeScript
183 lines
6.5 KiB
TypeScript
/**
|
|
* Unit Tests: CORS origin allowlist
|
|
*
|
|
* Tests isAllowedOrigin() from server/api.ts, which controls which HTTP
|
|
* Origins are permitted by the Express CORS middleware.
|
|
*
|
|
* Policy:
|
|
* - No origin (non-browser) → allowed
|
|
* - http://localhost:<port> → allowed
|
|
* - http://127.0.0.1:<port> → allowed
|
|
* - RFC 1918 private network ranges → allowed
|
|
* 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
|
|
* - https://gitnexus.vercel.app → allowed
|
|
* - Everything else → rejected
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { isAllowedOrigin } from '../../src/server/api.js';
|
|
|
|
// ─── No origin (non-browser / curl) ──────────────────────────────────
|
|
|
|
describe('isAllowedOrigin: no origin', () => {
|
|
it('allows undefined origin (curl, server-to-server)', () => {
|
|
expect(isAllowedOrigin(undefined)).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Localhost variants ───────────────────────────────────────────────
|
|
|
|
describe('isAllowedOrigin: localhost', () => {
|
|
it('allows http://localhost:3000', () => {
|
|
expect(isAllowedOrigin('http://localhost:3000')).toBe(true);
|
|
});
|
|
|
|
it('allows http://localhost:5173 (Vite default)', () => {
|
|
expect(isAllowedOrigin('http://localhost:5173')).toBe(true);
|
|
});
|
|
|
|
it('allows http://localhost:8080', () => {
|
|
expect(isAllowedOrigin('http://localhost:8080')).toBe(true);
|
|
});
|
|
|
|
it('allows http://127.0.0.1:3000', () => {
|
|
expect(isAllowedOrigin('http://127.0.0.1:3000')).toBe(true);
|
|
});
|
|
|
|
it('allows http://127.0.0.1:5173', () => {
|
|
expect(isAllowedOrigin('http://127.0.0.1:5173')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Deployed site ────────────────────────────────────────────────────
|
|
|
|
describe('isAllowedOrigin: vercel.app', () => {
|
|
it('allows https://gitnexus.vercel.app', () => {
|
|
expect(isAllowedOrigin('https://gitnexus.vercel.app')).toBe(true);
|
|
});
|
|
|
|
it('rejects other vercel.app subdomains', () => {
|
|
expect(isAllowedOrigin('https://evil.vercel.app')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── RFC 1918: 10.0.0.0/8 ────────────────────────────────────────────
|
|
|
|
describe('isAllowedOrigin: 10.x.x.x (RFC 1918, /8)', () => {
|
|
it('allows http://10.0.0.1:3000', () => {
|
|
expect(isAllowedOrigin('http://10.0.0.1:3000')).toBe(true);
|
|
});
|
|
|
|
it('allows http://10.1.2.3:5173', () => {
|
|
expect(isAllowedOrigin('http://10.1.2.3:5173')).toBe(true);
|
|
});
|
|
|
|
it('allows http://10.255.255.255:8080', () => {
|
|
expect(isAllowedOrigin('http://10.255.255.255:8080')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── RFC 1918: 172.16.0.0/12 ─────────────────────────────────────────
|
|
|
|
describe('isAllowedOrigin: 172.16-31.x.x (RFC 1918, /12)', () => {
|
|
it('allows http://172.16.0.1:3000 (lower bound)', () => {
|
|
expect(isAllowedOrigin('http://172.16.0.1:3000')).toBe(true);
|
|
});
|
|
|
|
it('allows http://172.20.1.2:3000 (middle of range)', () => {
|
|
expect(isAllowedOrigin('http://172.20.1.2:3000')).toBe(true);
|
|
});
|
|
|
|
it('allows http://172.31.255.255:3000 (upper bound)', () => {
|
|
expect(isAllowedOrigin('http://172.31.255.255:3000')).toBe(true);
|
|
});
|
|
|
|
it('rejects http://172.15.0.1:3000 (below range)', () => {
|
|
expect(isAllowedOrigin('http://172.15.0.1:3000')).toBe(false);
|
|
});
|
|
|
|
it('rejects http://172.32.0.1:3000 (above range)', () => {
|
|
expect(isAllowedOrigin('http://172.32.0.1:3000')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── RFC 1918: 192.168.0.0/16 ────────────────────────────────────────
|
|
|
|
describe('isAllowedOrigin: 192.168.x.x (RFC 1918, /16)', () => {
|
|
it('allows http://192.168.0.1:3000 (typical home router gateway)', () => {
|
|
expect(isAllowedOrigin('http://192.168.0.1:3000')).toBe(true);
|
|
});
|
|
|
|
it('allows http://192.168.1.100:5173', () => {
|
|
expect(isAllowedOrigin('http://192.168.1.100:5173')).toBe(true);
|
|
});
|
|
|
|
it('allows http://192.168.255.254:8080', () => {
|
|
expect(isAllowedOrigin('http://192.168.255.254:8080')).toBe(true);
|
|
});
|
|
|
|
it('rejects http://192.167.1.1:3000 (adjacent, not private)', () => {
|
|
expect(isAllowedOrigin('http://192.167.1.1:3000')).toBe(false);
|
|
});
|
|
|
|
it('rejects http://192.169.1.1:3000 (adjacent, not private)', () => {
|
|
expect(isAllowedOrigin('http://192.169.1.1:3000')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── Public / untrusted origins ───────────────────────────────────────
|
|
|
|
describe('isAllowedOrigin: rejected origins', () => {
|
|
it('rejects https://evil.com', () => {
|
|
expect(isAllowedOrigin('https://evil.com')).toBe(false);
|
|
});
|
|
|
|
it('rejects https://example.com', () => {
|
|
expect(isAllowedOrigin('https://example.com')).toBe(false);
|
|
});
|
|
|
|
it('rejects http://8.8.8.8:3000 (Google DNS, public IP)', () => {
|
|
expect(isAllowedOrigin('http://8.8.8.8:3000')).toBe(false);
|
|
});
|
|
|
|
it('rejects https://gitnexus.example.com (not the official domain)', () => {
|
|
expect(isAllowedOrigin('https://gitnexus.example.com')).toBe(false);
|
|
});
|
|
|
|
it('rejects malformed origin string', () => {
|
|
expect(isAllowedOrigin('not-a-url')).toBe(false);
|
|
});
|
|
|
|
it('rejects empty string', () => {
|
|
expect(isAllowedOrigin('')).toBe(false);
|
|
});
|
|
|
|
// Localhost without explicit port (port 80 implied)
|
|
it('allows http://localhost without port', () => {
|
|
expect(isAllowedOrigin('http://localhost')).toBe(true);
|
|
});
|
|
|
|
it('allows http://127.0.0.1 without port', () => {
|
|
expect(isAllowedOrigin('http://127.0.0.1')).toBe(true);
|
|
});
|
|
|
|
// IPv6 loopback
|
|
it('allows IPv6 loopback http://[::1]:3000', () => {
|
|
expect(isAllowedOrigin('http://[::1]:3000')).toBe(true);
|
|
});
|
|
|
|
it('allows IPv6 loopback http://[::1] without port', () => {
|
|
expect(isAllowedOrigin('http://[::1]')).toBe(true);
|
|
});
|
|
|
|
// Protocol validation
|
|
it('rejects non-HTTP(S) origins from private IPs', () => {
|
|
expect(isAllowedOrigin('ftp://10.0.0.1')).toBe(false);
|
|
expect(isAllowedOrigin('ftp://192.168.1.1')).toBe(false);
|
|
});
|
|
|
|
it('allows HTTP and HTTPS from private IPs', () => {
|
|
expect(isAllowedOrigin('http://192.168.1.100')).toBe(true);
|
|
expect(isAllowedOrigin('https://10.0.0.50')).toBe(true);
|
|
expect(isAllowedOrigin('http://172.16.5.1:3000')).toBe(true);
|
|
});
|
|
});
|