opnsense-nordvpn-plugin/tests/unit/StoreAndInspectorTest.php

121 lines
4.4 KiB
PHP

<?php
use OPNsense\NordVPN\EndpointStore;
use OPNsense\NordVPN\EnrollmentInspector;
use OPNsense\NordVPN\NordApi;
use OPNsense\NordVPN\ReconciliationLock;
use OPNsense\NordVPN\StatusStore;
use PHPUnit\Framework\TestCase;
final class StoreAndInspectorTest extends TestCase
{
public function testEndpointRefreshWritesAtomicReusableTable(): 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);
$this->assertTrue($store->refreshIfStale($api));
$this->assertTrue($store->available());
$this->assertSame("203.0.113.10\n", file_get_contents($path));
$this->assertFalse($store->refreshIfStale($api));
$this->assertSame(1, $requests);
unlink($path);
unlink($path . '.refreshed');
rmdir($directory);
}
public function testEndpointRefreshDefersAfterFailure(): void
{
$directory = sys_get_temp_dir() . '/nordvpn-endpoints-' . bin2hex(random_bytes(8));
$path = $directory . '/endpoints.txt';
$now = time();
$requests = 0;
$api = new NordApi(static function () use (&$requests): array {
$requests++;
throw new RuntimeException('Synthetic endpoint failure.');
});
$store = new EndpointStore($path, static function () use (&$now): int {
return $now;
});
try {
$store->refreshIfStale($api);
$this->fail('Expected endpoint refresh to fail.');
} catch (RuntimeException $exception) {
$this->assertSame('Synthetic endpoint failure.', $exception->getMessage());
}
$this->assertTrue($store->retryPending());
$this->assertFalse($store->refreshIfStale($api));
$this->assertSame(1, $requests);
$now += 301;
try {
$store->refreshIfStale($api);
$this->fail('Expected endpoint refresh retry to fail.');
} catch (RuntimeException $exception) {
$this->assertSame('Synthetic endpoint failure.', $exception->getMessage());
}
$this->assertSame(2, $requests);
unlink($path . '.retry');
rmdir($directory);
}
public function testRfc1918WarningFollowsDestinationSelectorSemantics(): 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'],
]);
$this->assertTrue($inspector->includesPrivateDestinations('any', false));
$this->assertTrue($inspector->includesPrivateDestinations('nested', false));
$this->assertFalse($inspector->includesPrivateDestinations('public', false));
$this->assertFalse($inspector->includesPrivateDestinations('2001:db8::/32', false));
$this->assertFalse($inspector->includesPrivateDestinations('private', true));
$this->assertTrue($inspector->includesPrivateDestinations('10.0.0.0/8', true));
}
public function testStatusWritesAtomicallyAndRetainsDefaults(): void
{
$path = sys_get_temp_dir() . '/nordvpn-status-' . bin2hex(random_bytes(8)) . '.json';
$store = new StatusStore($path);
$this->assertFalse($store->read()['available']);
$store->write(['available' => true, 'state' => 'available']);
$status = $store->read();
$this->assertTrue($status['available']);
$this->assertGreaterThan(0, $status['updated_at']);
unlink($path);
}
public function testReconciliationLockRejectsConcurrentOperations(): void
{
$path = sys_get_temp_dir() . '/nordvpn-lock-' . bin2hex(random_bytes(8));
$first = new ReconciliationLock($path);
$second = new ReconciliationLock($path);
$first->acquire();
try {
$second->acquire();
$this->fail('A concurrent operation acquired the lock.');
} catch (RuntimeException $exception) {
$this->assertSame('A NordVPN operation is already running.', $exception->getMessage());
} finally {
$first->release();
}
$second->acquire();
$second->release();
unlink($path);
}
}