$key]; }); $check($api->credentials('synthetic-token') === $key); }); $test('recommendations apply the country allowlist', static function () use ($check, $key): void { $requests = []; $api = new NordApi(static function ($path, $query) use (&$requests, $key): array { $requests[] = [$path, $query]; if ($path === '/v1/servers/countries') { return [['id' => 99, 'code' => 'nl', 'name' => 'Netherlands']]; } return [[ 'hostname' => 'nl.example.invalid', 'station' => '198.51.100.10', 'locations' => [['country' => ['code' => 'nl']]], 'technologies' => [[ 'id' => 35, 'pivot' => ['status' => 'online'], 'metadata' => [['name' => 'public_key', 'value' => $key]], ]], ]]; }); $servers = $api->recommendations(['NL']); $check(count($servers) === 1); $check($servers[0]['country'] === 'NL'); $check($requests[1][1]['filters[country_id]'] === 99); $check($requests[1][1]['filters[servers_technologies]'] === 35); }); $test('an empty country allowlist queries recommendations directly', static function () use ($check): void { $api = new NordApi(static function ($path): array { if ($path === '/v1/servers/countries') { throw new RuntimeException('countries should not be queried'); } return []; }); $check($api->recommendations([]) === []); }); $test('endpoint discovery paginates the complete NordLynx address set', static function () use ($check): void { $requests = []; $api = new NordApi(static function ($path, $query) use (&$requests): array { $requests[] = [$path, $query]; if ($query['offset'] === 0) { return array_map( static fn($index) => ['station' => sprintf('198.51.%d.%d', intdiv($index, 250), $index % 250 + 1)], range(0, 499) ); } return [['station' => '203.0.113.10'], ['station' => 'invalid']]; }); $addresses = $api->endpointAddresses(); $check(count($addresses) === 501); $check($requests[0][0] === '/v1/servers'); $check($requests[0][1]['limit'] === 500); $check($requests[1][1]['offset'] === 500); $check($requests[0][1]['filters[servers_technologies]'] === 35); }); $test('endpoint refresh writes an atomic reusable table', static function () use ($check): void { $directory = sys_get_temp_dir() . '/nordvpn-endpoints-' . bin2hex(random_bytes(8)); $path = $directory . '/endpoints.txt'; $requests = 0; $api = new NordApi(static function () use (&$requests): array { $requests++; return [['station' => '203.0.113.10']]; }); $store = new EndpointStore($path); $check($store->refreshIfStale($api)); $check($store->available()); $check(file_get_contents($path) === "203.0.113.10\n"); $check(!$store->refreshIfStale($api)); $check($requests === 1); unlink($path); rmdir($directory); }); $test('RFC 1918 warning follows destination selector semantics', static function () use ($check): void { $inspector = new EnrollmentInspector([ 'private' => ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'], 'public' => ['203.0.113.0/24'], 'nested' => ['private'], ]); $check($inspector->includesPrivateDestinations('any', false)); $check($inspector->includesPrivateDestinations('nested', false)); $check(!$inspector->includesPrivateDestinations('public', false)); $check(!$inspector->includesPrivateDestinations('2001:db8::/32', false)); $check(!$inspector->includesPrivateDestinations('private', true)); $check($inspector->includesPrivateDestinations('10.0.0.0/8', true)); }); $test('status writes atomically and retains defaults', static function () use ($check): void { $path = sys_get_temp_dir() . '/nordvpn-status-' . bin2hex(random_bytes(8)) . '.json'; $store = new StatusStore($path); $check($store->read()['available'] === false); $store->write(['available' => true, 'state' => 'available']); $status = $store->read(); $check($status['available'] === true); $check($status['updated_at'] > 0); unlink($path); }); $test('tunnel health requires a recent handshake', static function () use ($check): void { $runner = new SystemRunner(static fn() => "public-key\t900\n"); $recent = new TunnelHealth($runner, 180, static fn() => 1000); $stale = new TunnelHealth($runner, 50, static fn() => 1000); $check($recent->check('wg0')['available']); $check(!$stale->check('wg0')['available']); $check(!$recent->check('invalid')['available']); }); $test('WireGuard apply follows the OPNsense registration sequence', static function () use ($check): void { $commands = []; $runner = new SystemRunner(static function ($command) use (&$commands): string { $commands[] = $command; return ''; }); $runner->applyWireGuard(); $check($commands === [ ['/usr/local/sbin/configctl', 'interface', 'invoke', 'registration'], ['/usr/local/sbin/configctl', 'template', 'reload', 'OPNsense/Wireguard'], ['/usr/local/opnsense/scripts/wireguard/wg-service-control.php', '-a', 'configure'], ]); }); $test('firewall apply reloads rules before evicting plugin states', static function () use ($check): void { $commands = []; $runner = new SystemRunner(static function ($command) use (&$commands): string { $commands[] = $command; return ''; }); $runner->applyFirewall(); $check($commands === [ ['/usr/local/etc/rc.filter_configure'], ['/sbin/pfctl', '-k', 'label', '-k', SystemRunner::FIREWALL_LABEL], ]); }); $failures = 0; foreach ($tests as $name => $body) { try { $body(); echo "ok - {$name}\n"; } catch (Throwable $exception) { $failures++; fwrite(STDERR, "not ok - {$name}: {$exception->getMessage()}\n"); } } $command = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(__DIR__ . '/reconciler.php'); passthru($command, $status); if ($status !== 0) { $failures++; } exit($failures === 0 ? 0 : 1);