2025-08-25 07:31:31 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
test('IP allowlist with single IPs', function () {
|
|
|
|
|
$testCases = [
|
|
|
|
|
['ip' => '192.168.1.100', 'allowlist' => ['192.168.1.100'], 'expected' => true],
|
|
|
|
|
['ip' => '192.168.1.101', 'allowlist' => ['192.168.1.100'], 'expected' => false],
|
|
|
|
|
['ip' => '10.0.0.1', 'allowlist' => ['10.0.0.1', '192.168.1.100'], 'expected' => true],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
foreach ($testCases as $case) {
|
2025-09-09 07:00:35 +00:00
|
|
|
$result = checkIPAgainstAllowlist($case['ip'], $case['allowlist']);
|
2025-08-25 07:31:31 +00:00
|
|
|
expect($result)->toBe($case['expected']);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('IP allowlist with CIDR notation', function () {
|
|
|
|
|
$testCases = [
|
|
|
|
|
['ip' => '192.168.1.50', 'allowlist' => ['192.168.1.0/24'], 'expected' => true],
|
|
|
|
|
['ip' => '192.168.2.50', 'allowlist' => ['192.168.1.0/24'], 'expected' => false],
|
|
|
|
|
['ip' => '10.0.0.5', 'allowlist' => ['10.0.0.0/8'], 'expected' => true],
|
|
|
|
|
['ip' => '11.0.0.5', 'allowlist' => ['10.0.0.0/8'], 'expected' => false],
|
|
|
|
|
['ip' => '172.16.5.10', 'allowlist' => ['172.16.0.0/12'], 'expected' => true],
|
|
|
|
|
['ip' => '172.32.0.1', 'allowlist' => ['172.16.0.0/12'], 'expected' => false],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
foreach ($testCases as $case) {
|
2025-09-09 07:00:35 +00:00
|
|
|
$result = checkIPAgainstAllowlist($case['ip'], $case['allowlist']);
|
2025-08-25 07:31:31 +00:00
|
|
|
expect($result)->toBe($case['expected']);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('IP allowlist with 0.0.0.0 allows all', function () {
|
|
|
|
|
$testIps = [
|
|
|
|
|
'1.2.3.4',
|
|
|
|
|
'192.168.1.1',
|
|
|
|
|
'10.0.0.1',
|
|
|
|
|
'255.255.255.255',
|
|
|
|
|
'127.0.0.1',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Test 0.0.0.0 without subnet
|
|
|
|
|
foreach ($testIps as $ip) {
|
2025-09-09 07:00:35 +00:00
|
|
|
$result = checkIPAgainstAllowlist($ip, ['0.0.0.0']);
|
2025-08-25 07:31:31 +00:00
|
|
|
expect($result)->toBeTrue();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test 0.0.0.0 with any subnet notation - should still allow all
|
|
|
|
|
foreach ($testIps as $ip) {
|
2025-09-09 07:00:35 +00:00
|
|
|
expect(checkIPAgainstAllowlist($ip, ['0.0.0.0/0']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist($ip, ['0.0.0.0/8']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist($ip, ['0.0.0.0/24']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist($ip, ['0.0.0.0/32']))->toBeTrue();
|
2025-08-25 07:31:31 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('IP allowlist with mixed entries', function () {
|
|
|
|
|
$allowlist = ['192.168.1.100', '10.0.0.0/8', '172.16.0.0/16'];
|
|
|
|
|
|
|
|
|
|
$testCases = [
|
|
|
|
|
['ip' => '192.168.1.100', 'expected' => true], // Exact match
|
|
|
|
|
['ip' => '192.168.1.101', 'expected' => false], // No match
|
|
|
|
|
['ip' => '10.5.5.5', 'expected' => true], // Matches 10.0.0.0/8
|
|
|
|
|
['ip' => '172.16.255.255', 'expected' => true], // Matches 172.16.0.0/16
|
|
|
|
|
['ip' => '172.17.0.1', 'expected' => false], // Outside 172.16.0.0/16
|
|
|
|
|
['ip' => '8.8.8.8', 'expected' => false], // No match
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
foreach ($testCases as $case) {
|
2025-09-09 07:00:35 +00:00
|
|
|
$result = checkIPAgainstAllowlist($case['ip'], $allowlist);
|
2025-08-25 07:31:31 +00:00
|
|
|
expect($result)->toBe($case['expected']);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('IP allowlist handles empty and invalid entries', function () {
|
|
|
|
|
// Empty allowlist blocks all
|
2025-09-09 07:00:35 +00:00
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.1', []))->toBeFalse();
|
|
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.1', ['']))->toBeFalse();
|
2025-08-25 07:31:31 +00:00
|
|
|
|
|
|
|
|
// Handles spaces
|
2025-09-09 07:00:35 +00:00
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.100', [' 192.168.1.100 ']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('10.0.0.5', [' 10.0.0.0/8 ']))->toBeTrue();
|
2025-08-25 07:31:31 +00:00
|
|
|
|
|
|
|
|
// Invalid entries are skipped
|
2025-09-09 07:00:35 +00:00
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.1', ['invalid.ip']))->toBeFalse();
|
|
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/33']))->toBeFalse(); // Invalid mask
|
|
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/-1']))->toBeFalse(); // Invalid mask
|
2025-08-25 07:31:31 +00:00
|
|
|
});
|
|
|
|
|
|
2026-03-03 15:43:29 +00:00
|
|
|
test('IP allowlist with various IPv4 subnet sizes', function () {
|
2025-08-25 07:31:31 +00:00
|
|
|
// /32 - single host
|
2025-09-09 07:00:35 +00:00
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.1/32']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.2', ['192.168.1.1/32']))->toBeFalse();
|
2025-08-25 07:31:31 +00:00
|
|
|
|
|
|
|
|
// /31 - point-to-point link
|
2025-09-09 07:00:35 +00:00
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.0', ['192.168.1.0/31']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/31']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.2', ['192.168.1.0/31']))->toBeFalse();
|
2025-08-25 07:31:31 +00:00
|
|
|
|
2026-03-03 15:43:29 +00:00
|
|
|
// /25 - half a /24
|
|
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/25']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.127', ['192.168.1.0/25']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.128', ['192.168.1.0/25']))->toBeFalse();
|
|
|
|
|
|
|
|
|
|
// /16
|
2025-09-09 07:00:35 +00:00
|
|
|
expect(checkIPAgainstAllowlist('172.16.0.1', ['172.16.0.0/16']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('172.16.255.255', ['172.16.0.0/16']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('172.17.0.1', ['172.16.0.0/16']))->toBeFalse();
|
2025-08-25 07:31:31 +00:00
|
|
|
|
2026-03-03 15:43:29 +00:00
|
|
|
// /12
|
|
|
|
|
expect(checkIPAgainstAllowlist('172.16.0.1', ['172.16.0.0/12']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('172.31.255.255', ['172.16.0.0/12']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('172.32.0.1', ['172.16.0.0/12']))->toBeFalse();
|
|
|
|
|
|
|
|
|
|
// /8
|
|
|
|
|
expect(checkIPAgainstAllowlist('10.255.255.255', ['10.0.0.0/8']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('11.0.0.1', ['10.0.0.0/8']))->toBeFalse();
|
|
|
|
|
|
2025-08-25 07:31:31 +00:00
|
|
|
// /0 - all addresses
|
2025-09-09 07:00:35 +00:00
|
|
|
expect(checkIPAgainstAllowlist('1.1.1.1', ['0.0.0.0/0']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('255.255.255.255', ['0.0.0.0/0']))->toBeTrue();
|
2025-08-25 07:31:31 +00:00
|
|
|
});
|
|
|
|
|
|
2026-03-03 15:43:29 +00:00
|
|
|
test('IP allowlist with various IPv6 subnet sizes', function () {
|
|
|
|
|
// /128 - single host
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::1/128']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8::2', ['2001:db8::1/128']))->toBeFalse();
|
|
|
|
|
|
|
|
|
|
// /127 - point-to-point link
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8::0', ['2001:db8::/127']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::/127']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8::2', ['2001:db8::/127']))->toBeFalse();
|
|
|
|
|
|
|
|
|
|
// /64 - standard subnet
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8:abcd:1234::1', ['2001:db8:abcd:1234::/64']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8:abcd:1234:ffff:ffff:ffff:ffff', ['2001:db8:abcd:1234::/64']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8:abcd:1235::1', ['2001:db8:abcd:1234::/64']))->toBeFalse();
|
|
|
|
|
|
|
|
|
|
// /48 - site prefix
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8:1234::1', ['2001:db8:1234::/48']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8:1234:ffff::1', ['2001:db8:1234::/48']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8:1235::1', ['2001:db8:1234::/48']))->toBeFalse();
|
|
|
|
|
|
|
|
|
|
// /32 - ISP allocation
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::/32']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8:ffff:ffff::1', ['2001:db8::/32']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db9::1', ['2001:db8::/32']))->toBeFalse();
|
|
|
|
|
|
|
|
|
|
// /16
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:0000::1', ['2001::/16']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:ffff:ffff::1', ['2001::/16']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2002::1', ['2001::/16']))->toBeFalse();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('IP allowlist with bare IPv6 addresses', function () {
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::1']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8::2', ['2001:db8::1']))->toBeFalse();
|
|
|
|
|
expect(checkIPAgainstAllowlist('::1', ['::1']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('::1', ['::2']))->toBeFalse();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('IP allowlist with IPv6 CIDR notation', function () {
|
|
|
|
|
// /64 prefix — issue #8729 exact case
|
|
|
|
|
expect(checkIPAgainstAllowlist('2a01:e0a:21d:8230::1', ['2a01:e0a:21d:8230::/64']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2a01:e0a:21d:8230:abcd:ef01:2345:6789', ['2a01:e0a:21d:8230::/64']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2a01:e0a:21d:8231::1', ['2a01:e0a:21d:8230::/64']))->toBeFalse();
|
|
|
|
|
|
|
|
|
|
// /128 — single host
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::1/128']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8::2', ['2001:db8::1/128']))->toBeFalse();
|
|
|
|
|
|
|
|
|
|
// /48 prefix
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8:1234::1', ['2001:db8:1234::/48']))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8:1235::1', ['2001:db8:1234::/48']))->toBeFalse();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('IP allowlist with mixed IPv4 and IPv6', function () {
|
|
|
|
|
$allowlist = ['192.168.1.100', '10.0.0.0/8', '2a01:e0a:21d:8230::/64'];
|
|
|
|
|
|
|
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.100', $allowlist))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('10.5.5.5', $allowlist))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2a01:e0a:21d:8230::cafe', $allowlist))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('2a01:e0a:21d:8231::1', $allowlist))->toBeFalse();
|
|
|
|
|
expect(checkIPAgainstAllowlist('8.8.8.8', $allowlist))->toBeFalse();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('IP allowlist handles invalid IPv6 masks', function () {
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::/129']))->toBeFalse(); // mask > 128
|
|
|
|
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::/-1']))->toBeFalse(); // negative mask
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-25 07:31:31 +00:00
|
|
|
test('IP allowlist comma-separated string input', function () {
|
|
|
|
|
// Test with comma-separated string (as it would come from the settings)
|
|
|
|
|
$allowlistString = '192.168.1.100,10.0.0.0/8,172.16.0.0/16';
|
|
|
|
|
$allowlist = explode(',', $allowlistString);
|
|
|
|
|
|
2025-09-09 07:00:35 +00:00
|
|
|
expect(checkIPAgainstAllowlist('192.168.1.100', $allowlist))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('10.5.5.5', $allowlist))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('172.16.10.10', $allowlist))->toBeTrue();
|
|
|
|
|
expect(checkIPAgainstAllowlist('8.8.8.8', $allowlist))->toBeFalse();
|
2025-08-25 07:31:31 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('ValidIpOrCidr validation rule', function () {
|
|
|
|
|
$rule = new \App\Rules\ValidIpOrCidr;
|
|
|
|
|
|
|
|
|
|
// Helper function to test validation
|
|
|
|
|
$validate = function ($value) use ($rule) {
|
|
|
|
|
$errors = [];
|
|
|
|
|
$fail = function ($message) use (&$errors) {
|
|
|
|
|
$errors[] = $message;
|
|
|
|
|
};
|
|
|
|
|
$rule->validate('allowed_ips', $value, $fail);
|
|
|
|
|
|
|
|
|
|
return empty($errors);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Valid cases - should pass
|
|
|
|
|
expect($validate(''))->toBeTrue(); // Empty is allowed
|
|
|
|
|
expect($validate('0.0.0.0'))->toBeTrue(); // 0.0.0.0 is allowed
|
2026-03-03 15:43:29 +00:00
|
|
|
expect($validate('192.168.1.1'))->toBeTrue(); // Valid IPv4
|
|
|
|
|
expect($validate('192.168.1.0/24'))->toBeTrue(); // Valid IPv4 CIDR
|
|
|
|
|
expect($validate('10.0.0.0/8'))->toBeTrue(); // Valid IPv4 CIDR
|
2025-08-25 07:31:31 +00:00
|
|
|
expect($validate('192.168.1.1,10.0.0.1'))->toBeTrue(); // Multiple valid IPs
|
|
|
|
|
expect($validate('192.168.1.0/24,10.0.0.0/8'))->toBeTrue(); // Multiple CIDRs
|
|
|
|
|
expect($validate('0.0.0.0/0'))->toBeTrue(); // 0.0.0.0 with subnet
|
|
|
|
|
expect($validate('0.0.0.0/24'))->toBeTrue(); // 0.0.0.0 with any subnet
|
|
|
|
|
expect($validate(' 192.168.1.1 '))->toBeTrue(); // With spaces
|
2026-03-03 15:43:29 +00:00
|
|
|
// IPv6 valid cases — issue #8729
|
|
|
|
|
expect($validate('2001:db8::1'))->toBeTrue(); // Valid bare IPv6
|
|
|
|
|
expect($validate('::1'))->toBeTrue(); // Loopback IPv6
|
|
|
|
|
expect($validate('2a01:e0a:21d:8230::/64'))->toBeTrue(); // IPv6 /64 CIDR
|
|
|
|
|
expect($validate('2001:db8::/48'))->toBeTrue(); // IPv6 /48 CIDR
|
|
|
|
|
expect($validate('2001:db8::1/128'))->toBeTrue(); // IPv6 /128 CIDR
|
|
|
|
|
expect($validate('192.168.1.1,2a01:e0a:21d:8230::/64'))->toBeTrue(); // Mixed IPv4 + IPv6 CIDR
|
2025-08-25 07:31:31 +00:00
|
|
|
|
|
|
|
|
// Invalid cases - should fail
|
|
|
|
|
expect($validate('1'))->toBeFalse(); // Single digit
|
|
|
|
|
expect($validate('abc'))->toBeFalse(); // Invalid text
|
|
|
|
|
expect($validate('192.168.1.256'))->toBeFalse(); // Invalid IP (256)
|
|
|
|
|
expect($validate('192.168.1.0/33'))->toBeFalse(); // Invalid CIDR mask (>32)
|
|
|
|
|
expect($validate('192.168.1.0/-1'))->toBeFalse(); // Invalid CIDR mask (<0)
|
|
|
|
|
expect($validate('192.168.1.1,abc'))->toBeFalse(); // Mix of valid and invalid
|
|
|
|
|
expect($validate('192.168.1.1,192.168.1.256'))->toBeFalse(); // Mix with invalid IP
|
|
|
|
|
expect($validate('192.168.1.0/24/32'))->toBeFalse(); // Invalid CIDR format
|
|
|
|
|
expect($validate('not.an.ip.address'))->toBeFalse(); // Invalid format
|
|
|
|
|
expect($validate('192.168'))->toBeFalse(); // Incomplete IP
|
|
|
|
|
expect($validate('192.168.1.1.1'))->toBeFalse(); // Too many octets
|
2026-03-03 15:43:29 +00:00
|
|
|
expect($validate('2001:db8::/129'))->toBeFalse(); // IPv6 mask > 128
|
2025-08-25 07:31:31 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('ValidIpOrCidr validation rule error messages', function () {
|
|
|
|
|
$rule = new \App\Rules\ValidIpOrCidr;
|
|
|
|
|
|
|
|
|
|
// Helper function to get error message
|
|
|
|
|
$getError = function ($value) use ($rule) {
|
|
|
|
|
$errors = [];
|
|
|
|
|
$fail = function ($message) use (&$errors) {
|
|
|
|
|
$errors[] = $message;
|
|
|
|
|
};
|
|
|
|
|
$rule->validate('allowed_ips', $value, $fail);
|
|
|
|
|
|
|
|
|
|
return $errors[0] ?? null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Test error messages
|
|
|
|
|
$error = $getError('1');
|
|
|
|
|
expect($error)->toContain('not valid IP addresses or CIDR notations');
|
|
|
|
|
expect($error)->toContain('1');
|
|
|
|
|
|
|
|
|
|
$error = $getError('192.168.1.1,abc,10.0.0.256');
|
|
|
|
|
expect($error)->toContain('abc');
|
|
|
|
|
expect($error)->toContain('10.0.0.256');
|
|
|
|
|
expect($error)->not->toContain('192.168.1.1'); // Valid IP should not be in error
|
|
|
|
|
});
|
2026-03-03 15:43:29 +00:00
|
|
|
|
|
|
|
|
test('deduplicateAllowlist removes bare IPv4 covered by various subnets', function () {
|
|
|
|
|
// /24
|
|
|
|
|
expect(deduplicateAllowlist(['192.168.1.5', '192.168.1.0/24']))->toBe(['192.168.1.0/24']);
|
|
|
|
|
// /16
|
|
|
|
|
expect(deduplicateAllowlist(['172.16.5.10', '172.16.0.0/16']))->toBe(['172.16.0.0/16']);
|
|
|
|
|
// /8
|
|
|
|
|
expect(deduplicateAllowlist(['10.50.100.200', '10.0.0.0/8']))->toBe(['10.0.0.0/8']);
|
|
|
|
|
// /32 — same host, first entry wins (both equivalent)
|
|
|
|
|
expect(deduplicateAllowlist(['192.168.1.1', '192.168.1.1/32']))->toBe(['192.168.1.1']);
|
|
|
|
|
// /31 — point-to-point
|
|
|
|
|
expect(deduplicateAllowlist(['192.168.1.0', '192.168.1.0/31']))->toBe(['192.168.1.0/31']);
|
|
|
|
|
// IP outside subnet — both preserved
|
|
|
|
|
expect(deduplicateAllowlist(['172.17.0.1', '172.16.0.0/16']))->toBe(['172.17.0.1', '172.16.0.0/16']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('deduplicateAllowlist removes narrow IPv4 CIDR covered by broader CIDR', function () {
|
|
|
|
|
// /32 inside /24
|
|
|
|
|
expect(deduplicateAllowlist(['192.168.1.1/32', '192.168.1.0/24']))->toBe(['192.168.1.0/24']);
|
|
|
|
|
// /25 inside /24
|
|
|
|
|
expect(deduplicateAllowlist(['192.168.1.0/25', '192.168.1.0/24']))->toBe(['192.168.1.0/24']);
|
|
|
|
|
// /24 inside /16
|
|
|
|
|
expect(deduplicateAllowlist(['192.168.1.0/24', '192.168.0.0/16']))->toBe(['192.168.0.0/16']);
|
|
|
|
|
// /16 inside /12
|
|
|
|
|
expect(deduplicateAllowlist(['172.16.0.0/16', '172.16.0.0/12']))->toBe(['172.16.0.0/12']);
|
|
|
|
|
// /16 inside /8
|
|
|
|
|
expect(deduplicateAllowlist(['10.1.0.0/16', '10.0.0.0/8']))->toBe(['10.0.0.0/8']);
|
|
|
|
|
// /24 inside /8
|
|
|
|
|
expect(deduplicateAllowlist(['10.1.2.0/24', '10.0.0.0/8']))->toBe(['10.0.0.0/8']);
|
|
|
|
|
// /12 inside /8
|
|
|
|
|
expect(deduplicateAllowlist(['172.16.0.0/12', '172.0.0.0/8']))->toBe(['172.0.0.0/8']);
|
|
|
|
|
// /31 inside /24
|
|
|
|
|
expect(deduplicateAllowlist(['192.168.1.0/31', '192.168.1.0/24']))->toBe(['192.168.1.0/24']);
|
|
|
|
|
// Non-overlapping CIDRs — both preserved
|
|
|
|
|
expect(deduplicateAllowlist(['192.168.1.0/24', '10.0.0.0/8']))->toBe(['192.168.1.0/24', '10.0.0.0/8']);
|
|
|
|
|
expect(deduplicateAllowlist(['172.16.0.0/16', '192.168.0.0/16']))->toBe(['172.16.0.0/16', '192.168.0.0/16']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('deduplicateAllowlist removes bare IPv6 covered by various prefixes', function () {
|
|
|
|
|
// /64 — issue #8729 exact scenario
|
|
|
|
|
expect(deduplicateAllowlist(['2a01:e0a:21d:8230::', '127.0.0.1', '2a01:e0a:21d:8230::/64']))
|
|
|
|
|
->toBe(['127.0.0.1', '2a01:e0a:21d:8230::/64']);
|
|
|
|
|
// /48
|
|
|
|
|
expect(deduplicateAllowlist(['2001:db8:1234::1', '2001:db8:1234::/48']))->toBe(['2001:db8:1234::/48']);
|
|
|
|
|
// /128 — same host, first entry wins (both equivalent)
|
|
|
|
|
expect(deduplicateAllowlist(['2001:db8::1', '2001:db8::1/128']))->toBe(['2001:db8::1']);
|
|
|
|
|
// IP outside prefix — both preserved
|
|
|
|
|
expect(deduplicateAllowlist(['2001:db8:1235::1', '2001:db8:1234::/48']))
|
|
|
|
|
->toBe(['2001:db8:1235::1', '2001:db8:1234::/48']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('deduplicateAllowlist removes narrow IPv6 CIDR covered by broader prefix', function () {
|
|
|
|
|
// /128 inside /64
|
|
|
|
|
expect(deduplicateAllowlist(['2a01:e0a:21d:8230::5/128', '2a01:e0a:21d:8230::/64']))->toBe(['2a01:e0a:21d:8230::/64']);
|
|
|
|
|
// /127 inside /64
|
|
|
|
|
expect(deduplicateAllowlist(['2001:db8:1234:5678::/127', '2001:db8:1234:5678::/64']))->toBe(['2001:db8:1234:5678::/64']);
|
|
|
|
|
// /64 inside /48
|
|
|
|
|
expect(deduplicateAllowlist(['2001:db8:1234:5678::/64', '2001:db8:1234::/48']))->toBe(['2001:db8:1234::/48']);
|
|
|
|
|
// /48 inside /32
|
|
|
|
|
expect(deduplicateAllowlist(['2001:db8:abcd::/48', '2001:db8::/32']))->toBe(['2001:db8::/32']);
|
|
|
|
|
// /32 inside /16
|
|
|
|
|
expect(deduplicateAllowlist(['2001:db8::/32', '2001::/16']))->toBe(['2001::/16']);
|
|
|
|
|
// /64 inside /32
|
|
|
|
|
expect(deduplicateAllowlist(['2001:db8:1234:5678::/64', '2001:db8::/32']))->toBe(['2001:db8::/32']);
|
|
|
|
|
// Non-overlapping IPv6 — both preserved
|
|
|
|
|
expect(deduplicateAllowlist(['2001:db8::/32', 'fd00::/8']))->toBe(['2001:db8::/32', 'fd00::/8']);
|
|
|
|
|
expect(deduplicateAllowlist(['2001:db8:1234::/48', '2001:db8:5678::/48']))->toBe(['2001:db8:1234::/48', '2001:db8:5678::/48']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('deduplicateAllowlist mixed IPv4 and IPv6 subnets', function () {
|
|
|
|
|
$result = deduplicateAllowlist([
|
|
|
|
|
'192.168.1.5', // covered by 192.168.0.0/16
|
|
|
|
|
'192.168.0.0/16',
|
|
|
|
|
'2a01:e0a:21d:8230::1', // covered by ::/64
|
|
|
|
|
'2a01:e0a:21d:8230::/64',
|
|
|
|
|
'10.0.0.1', // not covered by anything
|
|
|
|
|
'::1', // not covered by anything
|
|
|
|
|
]);
|
|
|
|
|
expect($result)->toBe(['192.168.0.0/16', '2a01:e0a:21d:8230::/64', '10.0.0.1', '::1']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('deduplicateAllowlist preserves non-overlapping entries', function () {
|
|
|
|
|
$result = deduplicateAllowlist(['192.168.1.1', '10.0.0.1', '172.16.0.0/16']);
|
|
|
|
|
expect($result)->toBe(['192.168.1.1', '10.0.0.1', '172.16.0.0/16']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('deduplicateAllowlist handles exact duplicates', function () {
|
|
|
|
|
expect(deduplicateAllowlist(['192.168.1.1', '192.168.1.1']))->toBe(['192.168.1.1']);
|
|
|
|
|
expect(deduplicateAllowlist(['10.0.0.0/8', '10.0.0.0/8']))->toBe(['10.0.0.0/8']);
|
|
|
|
|
expect(deduplicateAllowlist(['2001:db8::1', '2001:db8::1']))->toBe(['2001:db8::1']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('deduplicateAllowlist handles single entry and empty array', function () {
|
|
|
|
|
expect(deduplicateAllowlist(['10.0.0.1']))->toBe(['10.0.0.1']);
|
|
|
|
|
expect(deduplicateAllowlist([]))->toBe([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('deduplicateAllowlist with 0.0.0.0 removes everything else', function () {
|
|
|
|
|
$result = deduplicateAllowlist(['192.168.1.1', '0.0.0.0', '10.0.0.0/8']);
|
|
|
|
|
expect($result)->toBe(['0.0.0.0']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('deduplicateAllowlist multiple nested CIDRs keeps only broadest', function () {
|
|
|
|
|
// IPv4: three levels of nesting
|
|
|
|
|
expect(deduplicateAllowlist(['10.1.2.0/24', '10.1.0.0/16', '10.0.0.0/8']))->toBe(['10.0.0.0/8']);
|
|
|
|
|
// IPv6: three levels of nesting
|
|
|
|
|
expect(deduplicateAllowlist(['2001:db8:1234:5678::/64', '2001:db8:1234::/48', '2001:db8::/32']))->toBe(['2001:db8::/32']);
|
|
|
|
|
});
|