P2 Items Completed (P2-062 to P2-068): - Switch AgentApiKey from SHA-256 to Argon2id hashing - Add 200+ tests for models, services, and AI providers - Create agent_plans migration with phases and workspace states Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
590 lines
20 KiB
PHP
590 lines
20 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Core\Mod\Agentic\Tests\Feature;
|
|
|
|
use Core\Mod\Agentic\Models\AgentApiKey;
|
|
use Core\Mod\Agentic\Services\IpRestrictionService;
|
|
use Core\Tenant\Models\Workspace;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Tests for the IpRestrictionService.
|
|
*
|
|
* Covers IPv4/IPv6 validation, CIDR matching, and edge cases.
|
|
*/
|
|
class IpRestrictionServiceTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private Workspace $workspace;
|
|
|
|
private IpRestrictionService $service;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->workspace = Workspace::factory()->create();
|
|
$this->service = app(IpRestrictionService::class);
|
|
}
|
|
|
|
// =========================================================================
|
|
// IPv4 Basic Tests
|
|
// =========================================================================
|
|
|
|
public function test_validates_exact_ipv4_match(): void
|
|
{
|
|
$result = $this->service->isIpInWhitelist('192.168.1.100', ['192.168.1.100']);
|
|
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
public function test_rejects_non_matching_ipv4(): void
|
|
{
|
|
$result = $this->service->isIpInWhitelist('192.168.1.100', ['192.168.1.200']);
|
|
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
public function test_validates_ipv4_in_multiple_entries(): void
|
|
{
|
|
$whitelist = ['10.0.0.1', '192.168.1.100', '172.16.0.1'];
|
|
|
|
$result = $this->service->isIpInWhitelist('192.168.1.100', $whitelist);
|
|
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
public function test_rejects_invalid_ipv4(): void
|
|
{
|
|
$result = $this->service->isIpInWhitelist('invalid', ['192.168.1.100']);
|
|
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
public function test_rejects_ipv4_out_of_range(): void
|
|
{
|
|
$result = $this->service->isIpInWhitelist('256.256.256.256', ['192.168.1.100']);
|
|
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// IPv4 CIDR Tests
|
|
// =========================================================================
|
|
|
|
public function test_validates_ipv4_in_cidr_24(): void
|
|
{
|
|
$this->assertTrue($this->service->isIpInWhitelist('192.168.1.0', ['192.168.1.0/24']));
|
|
$this->assertTrue($this->service->isIpInWhitelist('192.168.1.1', ['192.168.1.0/24']));
|
|
$this->assertTrue($this->service->isIpInWhitelist('192.168.1.128', ['192.168.1.0/24']));
|
|
$this->assertTrue($this->service->isIpInWhitelist('192.168.1.255', ['192.168.1.0/24']));
|
|
}
|
|
|
|
public function test_rejects_ipv4_outside_cidr_24(): void
|
|
{
|
|
$this->assertFalse($this->service->isIpInWhitelist('192.168.2.0', ['192.168.1.0/24']));
|
|
$this->assertFalse($this->service->isIpInWhitelist('192.168.0.255', ['192.168.1.0/24']));
|
|
}
|
|
|
|
public function test_validates_ipv4_in_cidr_16(): void
|
|
{
|
|
$this->assertTrue($this->service->isIpInWhitelist('192.168.0.1', ['192.168.0.0/16']));
|
|
$this->assertTrue($this->service->isIpInWhitelist('192.168.255.255', ['192.168.0.0/16']));
|
|
}
|
|
|
|
public function test_rejects_ipv4_outside_cidr_16(): void
|
|
{
|
|
$this->assertFalse($this->service->isIpInWhitelist('192.169.0.1', ['192.168.0.0/16']));
|
|
}
|
|
|
|
public function test_validates_ipv4_in_cidr_8(): void
|
|
{
|
|
$this->assertTrue($this->service->isIpInWhitelist('10.0.0.1', ['10.0.0.0/8']));
|
|
$this->assertTrue($this->service->isIpInWhitelist('10.255.255.255', ['10.0.0.0/8']));
|
|
}
|
|
|
|
public function test_rejects_ipv4_outside_cidr_8(): void
|
|
{
|
|
$this->assertFalse($this->service->isIpInWhitelist('11.0.0.1', ['10.0.0.0/8']));
|
|
}
|
|
|
|
public function test_validates_ipv4_in_cidr_32(): void
|
|
{
|
|
$this->assertTrue($this->service->isIpInWhitelist('192.168.1.100', ['192.168.1.100/32']));
|
|
$this->assertFalse($this->service->isIpInWhitelist('192.168.1.101', ['192.168.1.100/32']));
|
|
}
|
|
|
|
public function test_validates_ipv4_in_cidr_0(): void
|
|
{
|
|
// /0 means all IPv4 addresses
|
|
$this->assertTrue($this->service->isIpInWhitelist('1.2.3.4', ['0.0.0.0/0']));
|
|
$this->assertTrue($this->service->isIpInWhitelist('255.255.255.255', ['0.0.0.0/0']));
|
|
}
|
|
|
|
public function test_validates_ipv4_in_non_standard_cidr(): void
|
|
{
|
|
// /28 gives 16 addresses
|
|
$this->assertTrue($this->service->isIpInWhitelist('192.168.1.0', ['192.168.1.0/28']));
|
|
$this->assertTrue($this->service->isIpInWhitelist('192.168.1.15', ['192.168.1.0/28']));
|
|
$this->assertFalse($this->service->isIpInWhitelist('192.168.1.16', ['192.168.1.0/28']));
|
|
}
|
|
|
|
// =========================================================================
|
|
// IPv6 Basic Tests
|
|
// =========================================================================
|
|
|
|
public function test_validates_exact_ipv6_match(): void
|
|
{
|
|
$result = $this->service->isIpInWhitelist('2001:db8::1', ['2001:db8::1']);
|
|
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
public function test_validates_localhost_ipv6(): void
|
|
{
|
|
$result = $this->service->isIpInWhitelist('::1', ['::1']);
|
|
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
public function test_rejects_non_matching_ipv6(): void
|
|
{
|
|
$result = $this->service->isIpInWhitelist('2001:db8::1', ['2001:db8::2']);
|
|
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
public function test_normalises_ipv6_for_comparison(): void
|
|
{
|
|
// These are the same address in different formats
|
|
$this->assertTrue($this->service->isIpInWhitelist(
|
|
'2001:0db8:0000:0000:0000:0000:0000:0001',
|
|
['2001:db8::1']
|
|
));
|
|
}
|
|
|
|
// =========================================================================
|
|
// IPv6 CIDR Tests
|
|
// =========================================================================
|
|
|
|
public function test_validates_ipv6_in_cidr_64(): void
|
|
{
|
|
$this->assertTrue($this->service->isIpInWhitelist(
|
|
'2001:db8:abcd:0012::1',
|
|
['2001:db8:abcd:0012::/64']
|
|
));
|
|
$this->assertTrue($this->service->isIpInWhitelist(
|
|
'2001:db8:abcd:0012:ffff:ffff:ffff:ffff',
|
|
['2001:db8:abcd:0012::/64']
|
|
));
|
|
}
|
|
|
|
public function test_rejects_ipv6_outside_cidr_64(): void
|
|
{
|
|
$this->assertFalse($this->service->isIpInWhitelist(
|
|
'2001:db8:abcd:0013::1',
|
|
['2001:db8:abcd:0012::/64']
|
|
));
|
|
}
|
|
|
|
public function test_validates_ipv6_in_cidr_32(): void
|
|
{
|
|
$this->assertTrue($this->service->isIpInWhitelist(
|
|
'2001:db8:0:0:0:0:0:1',
|
|
['2001:db8::/32']
|
|
));
|
|
$this->assertTrue($this->service->isIpInWhitelist(
|
|
'2001:db8:ffff:ffff:ffff:ffff:ffff:ffff',
|
|
['2001:db8::/32']
|
|
));
|
|
}
|
|
|
|
public function test_rejects_ipv6_outside_cidr_32(): void
|
|
{
|
|
$this->assertFalse($this->service->isIpInWhitelist(
|
|
'2001:db9::1',
|
|
['2001:db8::/32']
|
|
));
|
|
}
|
|
|
|
public function test_validates_ipv6_in_cidr_128(): void
|
|
{
|
|
$this->assertTrue($this->service->isIpInWhitelist(
|
|
'2001:db8::1',
|
|
['2001:db8::1/128']
|
|
));
|
|
$this->assertFalse($this->service->isIpInWhitelist(
|
|
'2001:db8::2',
|
|
['2001:db8::1/128']
|
|
));
|
|
}
|
|
|
|
// =========================================================================
|
|
// IPv4/IPv6 Mixed Tests
|
|
// =========================================================================
|
|
|
|
public function test_ipv4_does_not_match_ipv6_cidr(): void
|
|
{
|
|
$this->assertFalse($this->service->isIpInWhitelist(
|
|
'192.168.1.1',
|
|
['2001:db8::/32']
|
|
));
|
|
}
|
|
|
|
public function test_ipv6_does_not_match_ipv4_cidr(): void
|
|
{
|
|
$this->assertFalse($this->service->isIpInWhitelist(
|
|
'2001:db8::1',
|
|
['192.168.1.0/24']
|
|
));
|
|
}
|
|
|
|
public function test_whitelist_can_contain_both_ipv4_and_ipv6(): void
|
|
{
|
|
$whitelist = ['192.168.1.0/24', '2001:db8::/32'];
|
|
|
|
$this->assertTrue($this->service->isIpInWhitelist('192.168.1.100', $whitelist));
|
|
$this->assertTrue($this->service->isIpInWhitelist('2001:db8::1', $whitelist));
|
|
$this->assertFalse($this->service->isIpInWhitelist('10.0.0.1', $whitelist));
|
|
}
|
|
|
|
// =========================================================================
|
|
// API Key Integration Tests
|
|
// =========================================================================
|
|
|
|
public function test_validate_ip_returns_true_when_restrictions_disabled(): void
|
|
{
|
|
$key = AgentApiKey::generate($this->workspace, 'Test Key');
|
|
|
|
$result = $this->service->validateIp($key, '192.168.1.100');
|
|
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
public function test_validate_ip_returns_false_when_enabled_with_empty_whitelist(): void
|
|
{
|
|
$key = AgentApiKey::generate($this->workspace, 'Test Key');
|
|
$key->enableIpRestriction();
|
|
|
|
$result = $this->service->validateIp($key->fresh(), '192.168.1.100');
|
|
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
public function test_validate_ip_checks_whitelist(): void
|
|
{
|
|
$key = AgentApiKey::generate($this->workspace, 'Test Key');
|
|
$key->enableIpRestriction();
|
|
$key->updateIpWhitelist(['192.168.1.100', '10.0.0.0/8']);
|
|
|
|
$fresh = $key->fresh();
|
|
|
|
$this->assertTrue($this->service->validateIp($fresh, '192.168.1.100'));
|
|
$this->assertTrue($this->service->validateIp($fresh, '10.0.0.50'));
|
|
$this->assertFalse($this->service->validateIp($fresh, '172.16.0.1'));
|
|
}
|
|
|
|
// =========================================================================
|
|
// Entry Validation Tests
|
|
// =========================================================================
|
|
|
|
public function test_validate_entry_accepts_valid_ipv4(): void
|
|
{
|
|
$result = $this->service->validateEntry('192.168.1.1');
|
|
|
|
$this->assertTrue($result['valid']);
|
|
$this->assertNull($result['error']);
|
|
}
|
|
|
|
public function test_validate_entry_accepts_valid_ipv6(): void
|
|
{
|
|
$result = $this->service->validateEntry('2001:db8::1');
|
|
|
|
$this->assertTrue($result['valid']);
|
|
$this->assertNull($result['error']);
|
|
}
|
|
|
|
public function test_validate_entry_accepts_valid_ipv4_cidr(): void
|
|
{
|
|
$result = $this->service->validateEntry('192.168.1.0/24');
|
|
|
|
$this->assertTrue($result['valid']);
|
|
$this->assertNull($result['error']);
|
|
}
|
|
|
|
public function test_validate_entry_accepts_valid_ipv6_cidr(): void
|
|
{
|
|
$result = $this->service->validateEntry('2001:db8::/32');
|
|
|
|
$this->assertTrue($result['valid']);
|
|
$this->assertNull($result['error']);
|
|
}
|
|
|
|
public function test_validate_entry_rejects_empty(): void
|
|
{
|
|
$result = $this->service->validateEntry('');
|
|
|
|
$this->assertFalse($result['valid']);
|
|
$this->assertEquals('Empty entry', $result['error']);
|
|
}
|
|
|
|
public function test_validate_entry_rejects_invalid_ip(): void
|
|
{
|
|
$result = $this->service->validateEntry('not-an-ip');
|
|
|
|
$this->assertFalse($result['valid']);
|
|
$this->assertEquals('Invalid IP address', $result['error']);
|
|
}
|
|
|
|
public function test_validate_entry_rejects_invalid_cidr(): void
|
|
{
|
|
$result = $this->service->validateEntry('192.168.1.0/');
|
|
|
|
$this->assertFalse($result['valid']);
|
|
}
|
|
|
|
// =========================================================================
|
|
// CIDR Validation Tests
|
|
// =========================================================================
|
|
|
|
public function test_validate_cidr_accepts_valid_ipv4_prefixes(): void
|
|
{
|
|
$this->assertTrue($this->service->validateCidr('192.168.1.0/0')['valid']);
|
|
$this->assertTrue($this->service->validateCidr('192.168.1.0/16')['valid']);
|
|
$this->assertTrue($this->service->validateCidr('192.168.1.0/32')['valid']);
|
|
}
|
|
|
|
public function test_validate_cidr_rejects_invalid_ipv4_prefixes(): void
|
|
{
|
|
$result = $this->service->validateCidr('192.168.1.0/33');
|
|
|
|
$this->assertFalse($result['valid']);
|
|
$this->assertStringContainsString('IPv4 prefix must be', $result['error']);
|
|
}
|
|
|
|
public function test_validate_cidr_accepts_valid_ipv6_prefixes(): void
|
|
{
|
|
$this->assertTrue($this->service->validateCidr('2001:db8::/0')['valid']);
|
|
$this->assertTrue($this->service->validateCidr('2001:db8::/64')['valid']);
|
|
$this->assertTrue($this->service->validateCidr('2001:db8::/128')['valid']);
|
|
}
|
|
|
|
public function test_validate_cidr_rejects_invalid_ipv6_prefixes(): void
|
|
{
|
|
$result = $this->service->validateCidr('2001:db8::/129');
|
|
|
|
$this->assertFalse($result['valid']);
|
|
$this->assertStringContainsString('IPv6 prefix must be', $result['error']);
|
|
}
|
|
|
|
public function test_validate_cidr_rejects_negative_prefix(): void
|
|
{
|
|
$result = $this->service->validateCidr('192.168.1.0/-1');
|
|
|
|
$this->assertFalse($result['valid']);
|
|
}
|
|
|
|
public function test_validate_cidr_rejects_non_numeric_prefix(): void
|
|
{
|
|
$result = $this->service->validateCidr('192.168.1.0/abc');
|
|
|
|
$this->assertFalse($result['valid']);
|
|
$this->assertEquals('Invalid prefix length', $result['error']);
|
|
}
|
|
|
|
public function test_validate_cidr_rejects_invalid_ip_in_cidr(): void
|
|
{
|
|
$result = $this->service->validateCidr('invalid/24');
|
|
|
|
$this->assertFalse($result['valid']);
|
|
$this->assertEquals('Invalid IP address in CIDR', $result['error']);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Parse Whitelist Input Tests
|
|
// =========================================================================
|
|
|
|
public function test_parse_whitelist_input_handles_newlines(): void
|
|
{
|
|
$input = "192.168.1.1\n192.168.1.2\n192.168.1.3";
|
|
|
|
$result = $this->service->parseWhitelistInput($input);
|
|
|
|
$this->assertCount(3, $result['entries']);
|
|
$this->assertEmpty($result['errors']);
|
|
}
|
|
|
|
public function test_parse_whitelist_input_handles_commas(): void
|
|
{
|
|
$input = '192.168.1.1,192.168.1.2,192.168.1.3';
|
|
|
|
$result = $this->service->parseWhitelistInput($input);
|
|
|
|
$this->assertCount(3, $result['entries']);
|
|
}
|
|
|
|
public function test_parse_whitelist_input_handles_carriage_returns(): void
|
|
{
|
|
$input = "192.168.1.1\r\n192.168.1.2\r\n192.168.1.3";
|
|
|
|
$result = $this->service->parseWhitelistInput($input);
|
|
|
|
$this->assertCount(3, $result['entries']);
|
|
}
|
|
|
|
public function test_parse_whitelist_input_trims_whitespace(): void
|
|
{
|
|
$input = " 192.168.1.1 \n 192.168.1.2 ";
|
|
|
|
$result = $this->service->parseWhitelistInput($input);
|
|
|
|
$this->assertContains('192.168.1.1', $result['entries']);
|
|
$this->assertContains('192.168.1.2', $result['entries']);
|
|
}
|
|
|
|
public function test_parse_whitelist_input_skips_empty_lines(): void
|
|
{
|
|
$input = "192.168.1.1\n\n\n192.168.1.2";
|
|
|
|
$result = $this->service->parseWhitelistInput($input);
|
|
|
|
$this->assertCount(2, $result['entries']);
|
|
}
|
|
|
|
public function test_parse_whitelist_input_skips_comments(): void
|
|
{
|
|
$input = "# This is a comment\n192.168.1.1\n# Another comment\n192.168.1.2";
|
|
|
|
$result = $this->service->parseWhitelistInput($input);
|
|
|
|
$this->assertCount(2, $result['entries']);
|
|
$this->assertNotContains('# This is a comment', $result['entries']);
|
|
}
|
|
|
|
public function test_parse_whitelist_input_collects_errors(): void
|
|
{
|
|
$input = "192.168.1.1\ninvalid\n192.168.1.2\nalso-invalid";
|
|
|
|
$result = $this->service->parseWhitelistInput($input);
|
|
|
|
$this->assertCount(2, $result['entries']);
|
|
$this->assertCount(2, $result['errors']);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Format Whitelist Tests
|
|
// =========================================================================
|
|
|
|
public function test_format_whitelist_for_display_joins_with_newlines(): void
|
|
{
|
|
$whitelist = ['192.168.1.1', '10.0.0.0/8', '2001:db8::/32'];
|
|
|
|
$result = $this->service->formatWhitelistForDisplay($whitelist);
|
|
|
|
$this->assertEquals("192.168.1.1\n10.0.0.0/8\n2001:db8::/32", $result);
|
|
}
|
|
|
|
public function test_format_whitelist_for_display_handles_empty(): void
|
|
{
|
|
$result = $this->service->formatWhitelistForDisplay([]);
|
|
|
|
$this->assertEquals('', $result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Describe CIDR Tests
|
|
// =========================================================================
|
|
|
|
public function test_describe_cidr_for_ipv4(): void
|
|
{
|
|
$this->assertStringContainsString('256 addresses', $this->service->describeCidr('192.168.1.0/24'));
|
|
$this->assertStringContainsString('1 addresses', $this->service->describeCidr('192.168.1.0/32'));
|
|
}
|
|
|
|
public function test_describe_cidr_for_ipv6(): void
|
|
{
|
|
$result = $this->service->describeCidr('2001:db8::/32');
|
|
|
|
$this->assertStringContainsString('2001:db8::/32', $result);
|
|
$this->assertStringContainsString('addresses', $result);
|
|
}
|
|
|
|
public function test_describe_cidr_returns_original_for_invalid(): void
|
|
{
|
|
$result = $this->service->describeCidr('invalid');
|
|
|
|
$this->assertEquals('invalid', $result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Normalise IP Tests
|
|
// =========================================================================
|
|
|
|
public function test_normalise_ip_returns_same_for_ipv4(): void
|
|
{
|
|
$result = $this->service->normaliseIp('192.168.1.1');
|
|
|
|
$this->assertEquals('192.168.1.1', $result);
|
|
}
|
|
|
|
public function test_normalise_ip_compresses_ipv6(): void
|
|
{
|
|
$result = $this->service->normaliseIp('2001:0db8:0000:0000:0000:0000:0000:0001');
|
|
|
|
$this->assertEquals('2001:db8::1', $result);
|
|
}
|
|
|
|
public function test_normalise_ip_returns_original_for_invalid(): void
|
|
{
|
|
$result = $this->service->normaliseIp('invalid');
|
|
|
|
$this->assertEquals('invalid', $result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Edge Cases
|
|
// =========================================================================
|
|
|
|
public function test_handles_trimmed_whitelist_entries(): void
|
|
{
|
|
$result = $this->service->isIpInWhitelist('192.168.1.1', [' 192.168.1.1 ']);
|
|
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
public function test_skips_empty_whitelist_entries(): void
|
|
{
|
|
$result = $this->service->isIpInWhitelist('192.168.1.1', ['', '192.168.1.1', '']);
|
|
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
public function test_returns_false_for_empty_whitelist(): void
|
|
{
|
|
$result = $this->service->isIpInWhitelist('192.168.1.1', []);
|
|
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
public function test_handles_loopback_addresses(): void
|
|
{
|
|
$this->assertTrue($this->service->isIpInWhitelist('127.0.0.1', ['127.0.0.0/8']));
|
|
$this->assertTrue($this->service->isIpInWhitelist('::1', ['::1']));
|
|
}
|
|
|
|
public function test_handles_private_ranges(): void
|
|
{
|
|
// RFC 1918 private ranges
|
|
$this->assertTrue($this->service->isIpInWhitelist('10.0.0.1', ['10.0.0.0/8']));
|
|
$this->assertTrue($this->service->isIpInWhitelist('172.16.0.1', ['172.16.0.0/12']));
|
|
$this->assertTrue($this->service->isIpInWhitelist('192.168.0.1', ['192.168.0.0/16']));
|
|
}
|
|
|
|
public function test_handles_link_local_ipv6(): void
|
|
{
|
|
$this->assertTrue($this->service->isIpInWhitelist('fe80::1', ['fe80::/10']));
|
|
}
|
|
}
|