instance(WireguardHub::class, $hub); return $hub; } it('keeps the page and the nav entry away from operators without the capability', function () { vpnHub(); // Support may run the console, but a VPN access reaches the management network. $this->actingAs(operator('Support'))->get(route('admin.vpn'))->assertForbidden(); $this->actingAs(operator('Support'))->get(route('admin.overview')) ->assertOk() ->assertDontSee(route('admin.vpn')); $this->actingAs(operator('Admin'))->get(route('admin.vpn'))->assertOk(); $this->actingAs(operator('Owner'))->get(route('admin.overview')) ->assertOk() ->assertSee(route('admin.vpn')); }); it('creates an access, hands out the config once and pushes it to the hub', function () { vpnHub(); Queue::fake(); $component = Livewire::actingAs(operator('Owner')) ->test(Vpn::class) ->set('name', 'Notebook Boban') ->call('create') ->assertHasNoErrors(); $peer = VpnPeer::query()->firstOrFail(); expect($peer->name)->toBe('Notebook Boban') ->and($peer->enabled)->toBeTrue() ->and($peer->present)->toBeFalse(); // not on the hub until the job ran $config = $component->get('newConfig'); expect($config)->toContain('[Interface]') ->toContain('Address = '.$peer->allowed_ip.'/32') ->toContain('PublicKey = HUBPUBLICKEY0000000000000000000000000000000=') ->toContain('Endpoint = vpn.clupilot.test:51820'); // The private key is shown, never stored. preg_match('/PrivateKey = (\S+)/', $config, $m); expect(Keypair::isValidKey($m[1]))->toBeTrue() ->and(VpnPeer::query()->where('public_key', $m[1])->exists())->toBeFalse(); Queue::assertPushed(ApplyVpnPeer::class, fn ($job) => $job->publicKey === $peer->public_key && $job->enabled); }); it('accepts a peer-supplied public key and then has no private key to show', function () { vpnHub(); Queue::fake(); $own = Keypair::generate()->publicKey; $component = Livewire::actingAs(operator('Owner')) ->test(Vpn::class) ->set('name', 'Proxmox fsn1') ->set('publicKey', $own) ->call('create') ->assertHasNoErrors(); expect(VpnPeer::query()->where('public_key', $own)->exists())->toBeTrue() ->and($component->get('newConfig'))->toBeNull(); }); it('rejects a malformed key and a key that already has an access', function () { vpnHub(); $existing = VpnPeer::factory()->create(); Livewire::actingAs(operator('Owner'))->test(Vpn::class) ->set('name', 'kaputt')->set('publicKey', 'nope') ->call('create') ->assertHasErrors('publicKey'); Livewire::actingAs(operator('Owner'))->test(Vpn::class) ->set('name', 'doppelt')->set('publicKey', $existing->public_key) ->call('create') ->assertHasErrors('publicKey'); expect(VpnPeer::query()->count())->toBe(1); }); it('blocks and unblocks an access without losing the record', function () { vpnHub(); Queue::fake(); $peer = VpnPeer::factory()->create(); Livewire::actingAs(operator('Owner'))->test(Vpn::class)->call('toggle', $peer->uuid); expect($peer->fresh()->enabled)->toBeFalse(); Queue::assertPushed(ApplyVpnPeer::class, fn ($job) => ! $job->enabled); Livewire::actingAs(operator('Owner'))->test(Vpn::class)->call('toggle', $peer->uuid); expect($peer->fresh()->enabled)->toBeTrue() ->and(VpnPeer::query()->count())->toBe(1); }); it('removes the peer from the hub before deleting the row', function () { vpnHub(); Queue::fake(); $peer = VpnPeer::factory()->create(); Livewire::actingAs(operator('Owner')) ->test(ConfirmDeleteVpnPeer::class, ['uuid' => $peer->uuid]) ->call('delete'); expect(VpnPeer::query()->count())->toBe(0); Queue::assertPushed(ApplyVpnPeer::class, fn ($job) => $job->publicKey === $peer->public_key && ! $job->enabled); }); it('warns that deleting a host access cuts CluPilot off from that host', function () { vpnHub(); $host = Host::factory()->active()->create(['name' => 'pve-fsn1']); $peer = VpnPeer::factory()->create(['host_id' => $host->id]); Livewire::actingAs(operator('Owner')) ->test(ConfirmDeleteVpnPeer::class, ['uuid' => $peer->uuid]) ->assertSee('pve-fsn1'); }); it('stops mutations from a session whose capability was revoked mid-flight', function () { vpnHub(); Queue::fake(); $peer = VpnPeer::factory()->create(); $user = operator('Owner'); // Page is open and legitimate… $component = Livewire::actingAs($user)->test(Vpn::class); // …then the account is demoted. The already-rendered page must not keep its // powers: every action re-checks, it does not trust the mount. $user->syncRoles(['Support']); app(Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); $component->call('toggle', $peer->uuid)->assertForbidden(); expect($peer->fresh()->enabled)->toBeTrue(); Queue::assertNotPushed(ApplyVpnPeer::class); }); it('adopts host peers from the hub and marks vanished ones absent', function () { $hub = vpnHub(); $key = Keypair::generate()->publicKey; $host = Host::factory()->active()->create(['name' => 'pve-fsn1', 'wg_pubkey' => $key]); $hub->addPeer($key, '10.66.0.7'); $hub->snapshots[$key] = new PeerSnapshot( publicKey: $key, endpoint: '203.0.113.9:51820', allowedIps: '10.66.0.7/32', latestHandshake: now()->subSeconds(30)->toImmutable(), rxBytes: 2048, txBytes: 4096, ); // A peer that exists in the console but is no longer on the interface. $stale = VpnPeer::factory()->create(['present' => true]); (new SyncVpnPeers)->handle($hub); $adopted = VpnPeer::query()->where('public_key', $key)->firstOrFail(); expect($adopted->name)->toBe('pve-fsn1') // named after its host, not "unknown" ->and($adopted->host_id)->toBe($host->id) ->and($adopted->present)->toBeTrue() ->and($adopted->rx_bytes)->toBe(2048) ->and($adopted->endpoint)->toBe('203.0.113.9:51820') ->and($adopted->status())->toBe('online') ->and($stale->fresh()->present)->toBeFalse(); }); it('never lets a sync overwrite the operator intent to block', function () { $hub = vpnHub(); $peer = VpnPeer::factory()->blocked()->create(); // The hub still reports it (removal has not been applied yet). $hub->addPeer($peer->public_key, $peer->allowed_ip); (new SyncVpnPeers)->handle($hub); $peer->refresh(); expect($peer->enabled)->toBeFalse() // intent survives ->and($peer->present)->toBeTrue() // reality is recorded ->and($peer->status())->toBe('pending'); }); it('does not hand the same tunnel address to a host and a VPN access', function () { $host = Host::factory()->active()->create(['wg_ip' => '10.66.0.2']); VpnPeer::factory()->create(['allowed_ip' => '10.66.0.3']); // The real allocator, not the fake — this is the collision the fake cannot show. expect((new LocalWireguardHub)->allocateIp())->toBe('10.66.0.4'); }); it('does not let a failed removal resurrect a revoked access', function () { $hub = vpnHub(); Queue::fake(); $peer = VpnPeer::factory()->create(); $hub->addPeer($peer->public_key, $peer->allowed_ip); Livewire::actingAs(operator('Owner')) ->test(ConfirmDeleteVpnPeer::class, ['uuid' => $peer->uuid]) ->call('delete'); // Removal is queued but has not run — the peer is still on the hub. expect(VpnPeer::query()->count())->toBe(0) ->and(VpnPeer::withTrashed()->count())->toBe(1); (new SyncVpnPeers)->handle($hub); // The sync must NOT adopt it back as a live access… expect(VpnPeer::query()->count())->toBe(0); // …it must retry the removal instead. Queue::assertPushed(ApplyVpnPeer::class, fn ($job) => $job->publicKey === $peer->public_key && ! $job->enabled); }); it('clears the tombstone once the peer is really gone', function () { $hub = vpnHub(); $peer = VpnPeer::factory()->create(); $hub->addPeer($peer->public_key, $peer->allowed_ip); $peer->delete(); (new ApplyVpnPeer($peer->public_key, null, false))->handle($hub); expect($hub->peerIps())->toBe([]) ->and(VpnPeer::withTrashed()->count())->toBe(0); }); it('ignores a stale job that would undo a newer decision', function () { $hub = vpnHub(); $peer = VpnPeer::factory()->create(); $hub->addPeer($peer->public_key, $peer->allowed_ip); // Operator blocks, then immediately unblocks: two jobs are now in flight. $blockJob = new ApplyVpnPeer($peer->public_key, $peer->allowed_ip, false); $peer->update(['enabled' => false]); $peer->update(['enabled' => true]); // newer intent wins // The older block job runs late (a retry, say). It must not cut access. $blockJob->handle($hub); expect($hub->peerIps())->toHaveKey($peer->public_key) ->and($peer->fresh()->present)->toBeTrue(); }); it('keeps a revoked address and key reserved until the hub confirms', function () { vpnHub(); $peer = VpnPeer::factory()->create(['allowed_ip' => '10.66.0.2']); $peer->delete(); // The address is still configured on the hub — handing it out again would // give two machines the same tunnel IP. expect((new LocalWireguardHub)->allocateIp())->not->toBe('10.66.0.2'); Livewire::actingAs(operator('Owner'))->test(Vpn::class) ->set('name', 'wieder da')->set('publicKey', $peer->public_key) ->call('create') ->assertHasErrors('publicKey'); });