296 lines
12 KiB
PHP
296 lines
12 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Admin\ConfirmDeleteDatacenter;
|
|
use App\Livewire\Admin\Datacenters;
|
|
use App\Livewire\Admin\HostCreate;
|
|
use App\Models\Datacenter;
|
|
use App\Models\Host;
|
|
use App\Models\User;
|
|
use Livewire\Livewire;
|
|
|
|
// admin() helper is defined in HostManagementTest (global Pest function).
|
|
|
|
it('gates the datacenters page', function () {
|
|
$this->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'));
|
|
});
|