Files
umami/src/lib/ip.test.ts
T

115 lines
3.5 KiB
TypeScript

import { afterEach, describe, expect, test, vi } from 'vitest';
import { getIpAddress, stripPort } from './ip';
function headers(init: Record<string, string>) {
return new Headers(init);
}
afterEach(() => {
vi.unstubAllEnvs();
});
describe('getIpAddress', () => {
test('returns undefined when no known headers are present', () => {
expect(getIpAddress(headers({}))).toBeUndefined();
});
test('extracts the first address from an x-forwarded-for chain', () => {
expect(getIpAddress(headers({ 'x-forwarded-for': '1.2.3.4, 5.6.7.8, 9.10.11.12' }))).toBe(
'1.2.3.4',
);
});
test('trims whitespace around the x-forwarded-for client address', () => {
expect(getIpAddress(headers({ 'x-forwarded-for': ' 1.2.3.4 , 5.6.7.8' }))).toBe('1.2.3.4');
});
test('reads the Cloudflare cf-connecting-ip header', () => {
expect(getIpAddress(headers({ 'cf-connecting-ip': '203.0.113.5' }))).toBe('203.0.113.5');
});
test('reads the Fastly fastly-client-ip header', () => {
expect(getIpAddress(headers({ 'fastly-client-ip': '198.51.100.7' }))).toBe('198.51.100.7');
});
test('reads the x-real-ip reverse proxy header', () => {
expect(getIpAddress(headers({ 'x-real-ip': '192.0.2.9' }))).toBe('192.0.2.9');
});
test('parses the RFC 7239 forwarded header', () => {
expect(getIpAddress(headers({ forwarded: 'for=192.0.2.60;proto=http;by=203.0.113.43' }))).toBe(
'192.0.2.60',
);
});
test('prefers higher-priority vendor headers over x-forwarded-for', () => {
expect(
getIpAddress(
headers({
'cf-connecting-ip': '203.0.113.5',
'x-forwarded-for': '1.2.3.4',
}),
),
).toBe('203.0.113.5');
});
test('normalizes a full IPv6 address to compressed form', () => {
expect(getIpAddress(headers({ 'x-real-ip': '2001:0db8:0000:0000:0000:0000:0000:0001' }))).toBe(
'2001:db8::1',
);
});
test('converts an IPv4-mapped IPv6 address to IPv4', () => {
expect(getIpAddress(headers({ 'x-real-ip': '::ffff:1.2.3.4' }))).toBe('1.2.3.4');
});
test('strips the port from an IPv4 address with a port', () => {
expect(getIpAddress(headers({ 'x-real-ip': '1.2.3.4:8080' }))).toBe('1.2.3.4');
});
test('returns garbage input unchanged as a fallback', () => {
expect(getIpAddress(headers({ 'x-real-ip': 'not-an-ip' }))).toBe('not-an-ip');
});
test('uses a custom CLIENT_IP_HEADER when configured', () => {
vi.stubEnv('CLIENT_IP_HEADER', 'x-custom-ip');
expect(
getIpAddress(
headers({
'x-custom-ip': '10.0.0.1',
'x-forwarded-for': '1.2.3.4',
}),
),
).toBe('10.0.0.1');
});
test('falls back to standard headers when CLIENT_IP_HEADER is absent from the request', () => {
vi.stubEnv('CLIENT_IP_HEADER', 'x-custom-ip');
expect(getIpAddress(headers({ 'x-real-ip': '192.0.2.9' }))).toBe('192.0.2.9');
});
});
describe('stripPort', () => {
test('returns nullish input unchanged', () => {
expect(stripPort(undefined)).toBeUndefined();
expect(stripPort(null)).toBeNull();
expect(stripPort('')).toBe('');
});
test('removes the port from an IPv4 address', () => {
expect(stripPort('1.2.3.4:8080')).toBe('1.2.3.4');
});
test('leaves a plain IPv4 address unchanged', () => {
expect(stripPort('1.2.3.4')).toBe('1.2.3.4');
});
test('keeps only the bracketed portion of a bracketed IPv6 address', () => {
expect(stripPort('[::1]:8080')).toBe('[::1]');
});
test('leaves an unbracketed IPv6 address unchanged', () => {
expect(stripPort('::1')).toBe('::1');
});
});