File size: 6,442 Bytes
4e23b01 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | import Fastify, { type FastifyInstance } from 'fastify';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
createOriginHook,
isOriginAllowed,
originHost,
parseCorsOrigins,
} from '../src/middleware/origin';
describe('originHost', () => {
it('returns the host for a valid origin', () => {
expect(originHost('https://foo.example.com')).toBe('foo.example.com');
});
it('drops the default port', () => {
expect(originHost('http://localhost:80')).toBe('localhost');
});
it('keeps a non-default port', () => {
expect(originHost('http://127.0.0.1:58627')).toBe('127.0.0.1:58627');
});
it('returns undefined for a missing origin', () => {
expect(originHost(undefined)).toBeUndefined();
});
it('returns undefined for a malformed origin', () => {
expect(originHost('not a url')).toBeUndefined();
});
});
describe('isOriginAllowed', () => {
it('allows same-origin', () => {
expect(isOriginAllowed('http://localhost:80', 'localhost:80', [])).toBe(true);
});
it('denies cross-origin that is not whitelisted', () => {
expect(isOriginAllowed('http://evil.example.test', 'localhost:80', [])).toBe(false);
});
it('allows cross-origin that is whitelisted', () => {
expect(isOriginAllowed('https://foo.example.com', 'localhost:80', ['https://foo.example.com'])).toBe(true);
});
it('allows an absent origin', () => {
expect(isOriginAllowed(undefined, 'localhost:80', [])).toBe(true);
});
it('treats a malformed origin as absent (allowed)', () => {
expect(isOriginAllowed('not a url', 'h', [])).toBe(true);
});
it('treats localhost origin vs 127.0.0.1 host as same-origin (dev proxy)', () => {
expect(isOriginAllowed('http://localhost:5175', '127.0.0.1:58627', [])).toBe(true);
});
it('treats 127.0.0.1 origin vs localhost host as same-origin', () => {
expect(isOriginAllowed('http://127.0.0.1:5175', 'localhost:58627', [])).toBe(true);
});
it('treats [::1] origin vs localhost host as same-origin (IPv6 loopback)', () => {
expect(isOriginAllowed('http://[::1]:5175', 'localhost:58627', [])).toBe(true);
});
it('still denies a non-loopback cross-origin that is not whitelisted', () => {
expect(isOriginAllowed('http://evil.example.test', 'localhost:80', [])).toBe(false);
});
it('does not widen to a public host even when the origin is loopback', () => {
expect(isOriginAllowed('http://localhost:5175', 'example.com:80', [])).toBe(false);
});
});
describe('parseCorsOrigins', () => {
it('splits, trims, and drops empties', () => {
expect(
parseCorsOrigins({
KIMI_CODE_CORS_ORIGINS: ' https://a.example.com, https://b.example.com, ',
}),
).toEqual(['https://a.example.com', 'https://b.example.com']);
});
it('returns [] when unset', () => {
expect(parseCorsOrigins({})).toEqual([]);
});
});
describe('createOriginHook (onRequest hook)', () => {
let app: FastifyInstance;
beforeEach(async () => {
app = Fastify();
app.addHook('onRequest', createOriginHook({ allowedOrigins: ['https://foo.example.com'] }));
app.get('/api/v1/probe', async () => ({ ok: true }));
app.options('/api/v1/probe', async () => ({ ok: true }));
await app.ready();
});
afterEach(async () => {
await app.close();
});
it('echoes CORS headers for a same-origin request', async () => {
const res = await app.inject({
method: 'GET',
url: '/api/v1/probe',
headers: { origin: 'http://localhost:80', host: 'localhost:80' },
});
expect(res.statusCode).toBe(200);
expect(res.headers['access-control-allow-origin']).toBe('http://localhost:80');
});
it('echoes the whitelisted cross-origin and short-circuits OPTIONS to 204', async () => {
const res = await app.inject({
method: 'OPTIONS',
url: '/api/v1/probe',
headers: { origin: 'https://foo.example.com', host: 'localhost:80' },
});
expect(res.statusCode).toBe(204);
expect(res.headers['access-control-allow-origin']).toBe('https://foo.example.com');
expect(res.headers['access-control-allow-methods']).toBe(
'GET, POST, PUT, PATCH, DELETE, OPTIONS',
);
});
it('withholds CORS headers for a non-whitelisted cross-origin', async () => {
const res = await app.inject({
method: 'GET',
url: '/api/v1/probe',
headers: { origin: 'http://evil.example.test', host: 'localhost:80' },
});
expect(res.statusCode).toBe(200);
expect(res.headers['access-control-allow-origin']).toBeUndefined();
});
it('returns 204 without CORS headers for a non-whitelisted OPTIONS', async () => {
const res = await app.inject({
method: 'OPTIONS',
url: '/api/v1/probe',
headers: { origin: 'http://evil.example.test', host: 'localhost:80' },
});
expect(res.statusCode).toBe(204);
expect(res.headers['access-control-allow-origin']).toBeUndefined();
});
it('emits no CORS headers when Origin is absent', async () => {
const res = await app.inject({
method: 'GET',
url: '/api/v1/probe',
headers: { host: 'localhost:80' },
});
expect(res.statusCode).toBe(200);
expect(res.headers['access-control-allow-origin']).toBeUndefined();
});
it('reflects Access-Control-Request-Headers in Allow-Headers for an allowed origin', async () => {
const res = await app.inject({
method: 'OPTIONS',
url: '/api/v1/probe',
headers: {
origin: 'https://foo.example.com',
host: 'localhost:80',
'access-control-request-method': 'GET',
'access-control-request-headers': 'x-request-id, x-kimi-client-version, authorization',
},
});
expect(res.statusCode).toBe(204);
expect(res.headers['access-control-allow-origin']).toBe('https://foo.example.com');
expect(res.headers['access-control-allow-headers']).toBe(
'x-request-id, x-kimi-client-version, authorization',
);
});
it('falls back to CORS_ALLOW_HEADERS for non-preflight responses', async () => {
const res = await app.inject({
method: 'GET',
url: '/api/v1/probe',
headers: { origin: 'https://foo.example.com', host: 'localhost:80' },
});
expect(res.statusCode).toBe(200);
expect(res.headers['access-control-allow-headers']).toBe(
'Content-Type, Authorization, X-Kimi-Client-Id, X-Kimi-Client-Name, X-Kimi-Client-Version, X-Kimi-Client-Ui-Mode',
);
});
});
|