get(route('admin.datacenters'))->assertRedirect(route('admin.login')); $this->actingAs(User::factory()->create(['is_admin' => false]))->get(route('admin.datacenters'))->assertForbidden(); $this->actingAs(admin(), 'operator')->get(route('admin.datacenters'))->assertOk()->assertSee(__('datacenters.title')); }); it('creates a datacenter', function () { Livewire::actingAs(admin(), 'operator') ->test(Datacenters::class) ->set('code', 'NBG') ->set('name', 'Nürnberg') ->set('location', 'DE') ->call('save') ->assertHasNoErrors(); expect(Datacenter::query()->where('code', 'nbg')->where('name', 'Nürnberg')->exists())->toBeTrue(); }); it('rejects a duplicate datacenter code', function () { Datacenter::query()->firstOrCreate(['code' => 'fsn'], ['name' => 'Falkenstein']); Livewire::actingAs(admin(), 'operator') ->test(Datacenters::class) ->set('code', 'fsn')->set('name', 'Falkenstein') ->call('save') ->assertHasErrors(['code']); }); it('edits a datacenter name and country via the modal', function () { $dc = Datacenter::factory()->create(['code' => 'nbg', 'name' => 'Old', 'location' => 'AT']); Livewire::actingAs(admin(), 'operator') ->test(\App\Livewire\Admin\EditDatacenter::class, ['uuid' => $dc->uuid]) ->set('name', 'Nürnberg') ->set('location', 'DE') ->call('save') ->assertHasNoErrors(); $dc->refresh(); expect($dc->name)->toBe('Nürnberg')->and($dc->location)->toBe('DE')->and($dc->code)->toBe('nbg'); }); /** * Renaming a code, and why it stops being allowed. * * The code was immutable, which made a typo permanent. It cannot be freely * mutable either: hosts.datacenter references it, every past order stores it as * plain text, and — the part no cascade reaches — each host's DNS name was * minted from it and registered at the provider, while the machine's own * hostname was set from it as it was built. Rename a code with hosts in it and * you get a datacenter called hel full of machines called fsn-01. * * Free while nothing references it, which is the case that matters: a typo * noticed shortly after creating one. */ it('renames a code while nothing depends on it', function () { $dc = Datacenter::factory()->create(['code' => 'nbg', 'name' => 'Nürnberg']); Livewire::actingAs(admin(), 'operator') ->test(\App\Livewire\Admin\EditDatacenter::class, ['uuid' => $dc->uuid]) ->set('code', 'NBG2') ->call('save') ->assertHasNoErrors(); // Lowercased on the way in, like the create form does it. expect($dc->refresh()->code)->toBe('nbg2'); }); it('leaves the code alone once a host is placed there, whatever the browser sends back', function () { // fsn and hel exist from the migration that created the table. $dc = Datacenter::query()->firstOrCreate(['code' => 'fsn'], ['name' => 'Falkenstein']); Host::factory()->create(['datacenter' => 'fsn']); // set() on a locked field is exactly the forged request this guards // against: the input is not rendered, so nothing honest can send it. Livewire::actingAs(admin(), 'operator') ->test(\App\Livewire\Admin\EditDatacenter::class, ['uuid' => $dc->uuid]) ->set('code', 'hel') ->set('name', 'Falkenstein Süd') ->call('save') ->assertHasNoErrors(); expect($dc->refresh()->code)->toBe('fsn') ->and($dc->name)->toBe('Falkenstein Süd'); // the rest of the form still saves }); it('leaves the code alone once an order carries it, even with no hosts left', function () { // Orders are the reference no foreign key protects: a plain string column, // and the record of what a customer actually bought. $dc = Datacenter::query()->firstOrCreate(['code' => 'fsn'], ['name' => 'Falkenstein']); \App\Models\Order::factory()->create(['datacenter' => 'fsn']); Livewire::actingAs(admin(), 'operator') ->test(\App\Livewire\Admin\EditDatacenter::class, ['uuid' => $dc->uuid]) ->set('code', 'hel') ->call('save') ->assertHasNoErrors(); expect($dc->refresh()->code)->toBe('fsn'); }); it('holds a renamed code to the same rules as a new one', function () { // hel comes from the migration that created the table. $dc = Datacenter::factory()->create(['code' => 'nbg']); $component = Livewire::actingAs(admin(), 'operator') ->test(\App\Livewire\Admin\EditDatacenter::class, ['uuid' => $dc->uuid]); // Not a DNS label — the code becomes one (fsn-01.node.clupilot.com). $component->set('code', 'nbg_1')->call('save')->assertHasErrors(['code']); // Already taken. $component->set('code', 'hel')->call('save')->assertHasErrors(['code']); expect($dc->refresh()->code)->toBe('nbg'); }); it('lets a datacenter keep its own code when only the name is edited', function () { // The unique rule has to ignore this row, or saving anything at all would // fail on the code the record already holds. $dc = Datacenter::factory()->create(['code' => 'nbg', 'name' => 'Old']); Livewire::actingAs(admin(), 'operator') ->test(\App\Livewire\Admin\EditDatacenter::class, ['uuid' => $dc->uuid]) ->set('name', 'Nürnberg') ->call('save') ->assertHasNoErrors(); expect($dc->refresh()->name)->toBe('Nürnberg')->and($dc->code)->toBe('nbg'); }); /** * Which building, not which city. * * A provider runs several datacenters within one location, on separate power * and separate uplinks. "Falkenstein (fsn)" twice over is not a choice anybody * can make — and placing a second machine for redundancy is exactly when it * matters which hall it lands in. */ it('stores the specific datacenter and shows it where hosts are placed', function () { Livewire::actingAs(admin(), 'operator') ->test(Datacenters::class) ->set('code', 'nbg') ->set('name', 'Nürnberg') ->set('facility', 'nbg-dc-15') ->call('save') ->assertHasNoErrors(); expect(Datacenter::query()->where('code', 'nbg')->value('facility'))->toBe('nbg-dc-15'); Livewire::actingAs(admin(), 'operator') ->test(HostCreate::class) ->assertSee('nbg-dc-15'); }); it('keeps the specific datacenter optional, and empty rather than blank', function () { // '' in the column would read as "there is one and it has no name" to // every @if that decides whether to print it. $dc = Datacenter::factory()->create(['code' => 'nbg', 'facility' => 'nbg-dc-3']); Livewire::actingAs(admin(), 'operator') ->test(\App\Livewire\Admin\EditDatacenter::class, ['uuid' => $dc->uuid]) ->set('facility', '') ->call('save') ->assertHasNoErrors(); expect($dc->refresh()->facility)->toBeNull(); }); it('rejects an invalid country code on edit', function () { $dc = Datacenter::factory()->create(['code' => 'nbg', 'location' => 'DE']); Livewire::actingAs(admin(), 'operator') ->test(\App\Livewire\Admin\EditDatacenter::class, ['uuid' => $dc->uuid]) ->set('location', 'ZZ')->call('save')->assertHasErrors(['location']); }); it('deletes an unused datacenter', function () { $dc = Datacenter::factory()->create(['code' => 'nbg']); Livewire::actingAs(admin(), 'operator')->test(ConfirmDeleteDatacenter::class, ['uuid' => $dc->uuid])->call('delete'); expect(Datacenter::query()->where('uuid', $dc->uuid)->exists())->toBeFalse(); }); it('refuses to delete a datacenter that still has hosts', function () { $dc = Datacenter::factory()->create(['code' => 'nbg']); Host::factory()->create(['datacenter' => 'nbg']); Livewire::actingAs(admin(), 'operator')->test(ConfirmDeleteDatacenter::class, ['uuid' => $dc->uuid]) ->assertSet('hostCount', 1) ->call('delete'); expect(Datacenter::query()->where('uuid', $dc->uuid)->exists())->toBeTrue(); }); it('suspends and reactivates a customer', function () { $customer = \App\Models\Customer::factory()->create(['status' => 'active']); Livewire::actingAs(admin(), 'operator')->test(\App\Livewire\Admin\Customers::class) ->call('toggleSuspend', $customer->uuid); expect($customer->fresh()->status)->toBe('suspended'); Livewire::actingAs(admin(), 'operator')->test(\App\Livewire\Admin\Customers::class) ->call('toggleSuspend', $customer->uuid); expect($customer->fresh()->status)->toBe('active'); }); it('does not let the suspend toggle resurrect a closed customer', function () { $customer = \App\Models\Customer::factory()->create(['status' => 'closed', 'closed_at' => now()]); Livewire::actingAs(admin(), 'operator')->test(\App\Livewire\Admin\Customers::class) ->call('toggleSuspend', $customer->uuid); $customer->refresh(); expect($customer->status)->toBe('closed')->and($customer->closed_at)->not->toBeNull(); }); it('toggles a datacenter active flag', function () { $dc = Datacenter::factory()->create(['active' => true]); Livewire::actingAs(admin(), 'operator')->test(Datacenters::class)->call('toggle', $dc->uuid); expect($dc->fresh()->active)->toBeFalse(); }); it('offers only known datacenter codes when adding a host', function () { Datacenter::query()->firstOrCreate(['code' => 'fsn'], ['name' => 'Falkenstein']); // unknown code rejected Livewire::actingAs(admin(), 'operator') ->test(HostCreate::class) ->set('name', 'pve-x')->set('datacenter', 'ghost')->set('public_ip', '203.0.113.5')->set('root_password', 'supersecret') ->call('save') ->assertHasErrors(['datacenter']); // known code accepted (defaults to the first active datacenter on mount) Livewire::actingAs(admin(), 'operator') ->test(HostCreate::class) ->assertSet('datacenter', 'fsn'); }); it('rejects creating a host in an inactive datacenter', function () { Datacenter::query()->firstOrCreate(['code' => 'fsn'], ['name' => 'Falkenstein']); Datacenter::factory()->create(['code' => 'old', 'active' => false]); Livewire::actingAs(admin(), 'operator') ->test(HostCreate::class) ->set('name', 'pve-old')->set('datacenter', 'old')->set('public_ip', '203.0.113.6')->set('root_password', 'supersecret') ->call('save') ->assertHasErrors(['datacenter']); }); it('can switch a location off, and does not evict what already runs there', function () { // The column and the scope existed; the form did not offer them, so a // datacenter could be created and never switched off again. $dc = App\Models\Datacenter::factory()->create(['active' => true]); App\Models\Host::factory()->create(['datacenter' => $dc->code]); Livewire::actingAs(operator('Owner'), 'operator') ->test(App\Livewire\Admin\EditDatacenter::class, ['uuid' => $dc->uuid]) ->assertSet('active', true) ->set('active', false) ->call('save') ->assertHasNoErrors(); expect($dc->refresh()->active)->toBeFalse() // Switching off stops NEW placements; it does not evict what runs there. ->and(App\Models\Host::query()->where('datacenter', $dc->code)->exists())->toBeTrue(); }); it('warns about running hosts only when it actually switches a location off', function () { // An already-inactive location announced that it had just been switched off // every time its name was edited. $dc = App\Models\Datacenter::factory()->create(['active' => false]); App\Models\Host::factory()->create(['datacenter' => $dc->code]); Livewire::actingAs(operator('Owner'), 'operator') ->test(App\Livewire\Admin\EditDatacenter::class, ['uuid' => $dc->uuid]) ->set('name', 'Neuer Name') ->call('save') ->assertDispatched('notify', message: __('datacenters.updated')); });