cidr = '192.168.1.0/30'; $hosts = $scanner->scan($range, 123); self::assertSame(['192.168.1.1', '192.168.1.2'], array_map( static fn (DiscoveredHost $host): string => $host->ipAddress, $hosts )); self::assertSame([123, 123], array_map( static fn (DiscoveredHost $host): ?int => $host->scanJobId, $repository->saved )); } public function testScanRejectsRangesLargerThanTwentyFourForMvp(): void { $scanner = new NetworkScanner( new AlwaysAlivePingScanner(), new EmptyTcpPortScanner(), new EmptyArpTableReader(), new StableFingerprintService(), new InMemoryDiscoveredHostRepository() ); $range = new NetworkRange(); $range->cidr = '192.168.0.0/23'; $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('larger than /24'); $scanner->scan($range, 321); } public function testResumableScanStartsAtSavedIndexAndReportsProgress(): void { $repository = new InMemoryDiscoveredHostRepository(); $scanner = new NetworkScanner( new AlwaysAlivePingScanner(), new EmptyTcpPortScanner(), new EmptyArpTableReader(), new StableFingerprintService(), $repository ); $range = new NetworkRange(); $range->cidr = '192.168.1.0/30'; $progress = []; $result = $scanner->scanResumable( $range, 456, 1, static fn (): bool => false, static function (array $state) use (&$progress): void { $progress[] = $state; } ); self::assertSame(['192.168.1.2'], array_map( static fn (DiscoveredHost $host): string => $host->ipAddress, $result['hosts'] )); self::assertTrue($result['completed']); self::assertSame(2, $result['next_ip_index']); self::assertSame(2, $result['total_ips']); self::assertSame(1, $result['scanned_count']); self::assertSame(1, $progress[0]['next_ip_index']); self::assertSame(2, $progress[1]['next_ip_index']); } } final class AlwaysAlivePingScanner extends PingScanner { public function ping(string $ip, int $timeoutMs = 500): bool { return true; } } final class EmptyTcpPortScanner extends TcpPortScanner { public function scan(string $ip, array $ports, int $timeoutMs = 200, int $maxConcurrency = 50): array { return []; } } final class EmptyArpTableReader extends ArpTableReader { public function read(): array { return []; } } final class StableFingerprintService extends HostFingerprintService { public function guessVendor(array $openPorts, ?string $hostname = null): ?string { return null; } public function calculateConfidence(int $openPortCount, bool $hasHostname): int { return 50; } } final class InMemoryDiscoveredHostRepository extends DiscoveredHostRepository { /** @var DiscoveredHost[] */ public array $saved = []; public function __construct() { } public function save(DiscoveredHost $host): void { $this->saved[] = $host; } }