CluPilotCloud/tests/Feature/Admin/IncidentConsoleTest.php

122 lines
4.8 KiB
PHP

<?php
use App\Livewire\Admin\Incidents as IncidentConsole;
use App\Models\Incident;
use App\Support\LocalTime;
use Livewire\Livewire;
/**
* Writing the record.
*
* The public page can only be as honest as what is typed here, so these tests
* are about the two ways the console could make it dishonest: letting somebody
* revise what was already said, and letting the incident and its timeline drift
* apart.
*/
it('publishes an incident together with its first update, or not at all', function () {
Livewire::actingAs(operator('Owner'), 'operator')
->test(IncidentConsole::class)
->set('title', 'Instanzen nicht erreichbar')
->set('components', ['instances'])
->set('impact', 'down')
->set('startedAt', LocalTime::toField(now()->subMinutes(20)))
->set('firstUpdate', 'Mehrere Instanzen antworten nicht.')
->call('create')
->assertHasNoErrors();
$incident = Incident::query()->firstOrFail();
// A headline with nothing under it is what a reader would see if the
// second insert were allowed to fail on its own.
expect($incident->updates)->toHaveCount(1)
->and($incident->updates->first()->status)->toBe('investigating')
->and($incident->isPublished())->toBeTrue()
->and($incident->components)->toBe(['instances']);
});
it('refuses an incident that names no service', function () {
Livewire::actingAs(operator('Owner'), 'operator')
->test(IncidentConsole::class)
->set('title', 'Irgendwas')
->set('components', [])
->set('firstUpdate', 'Text')
->call('create')
->assertHasErrors('components');
expect(Incident::query()->count())->toBe(0);
});
it('closes the incident with the same action that says it is resolved', function () {
// Two separate actions is how a status page ends up with a "behoben" note
// under an incident that still reports as ongoing.
$incident = Incident::query()->create([
'title' => 'x', 'impact' => 'down', 'components' => ['portal'],
'started_at' => now()->subHour(), 'published_at' => now()->subHour(),
]);
Livewire::actingAs(operator('Owner'), 'operator')
->test(IncidentConsole::class)
->call('startUpdate', $incident->uuid)
->set('updateStage', 'resolved')
->set('updateBody', 'Seit 20:51 stabil.')
->call('postUpdate')
->assertHasNoErrors();
expect($incident->fresh()->isResolved())->toBeTrue()
->and($incident->fresh()->stage())->toBe('resolved');
});
it('takes a correction as a further entry, never as an edit', function () {
// An update is a statement made at a time. There is deliberately no way
// here to change one after the fact.
$incident = Incident::query()->create([
'title' => 'x', 'impact' => 'degraded', 'components' => ['backups'],
'started_at' => now()->subHour(), 'published_at' => now()->subHour(),
]);
$incident->updates()->create(['status' => 'investigating', 'body' => 'Erste Einschätzung.']);
Livewire::actingAs(operator('Owner'), 'operator')
->test(IncidentConsole::class)
->call('startUpdate', $incident->uuid)
->set('updateStage', 'identified')
->set('updateBody', 'Korrektur: es betrifft nur einen Host.')
->call('postUpdate');
$bodies = $incident->fresh()->updates->pluck('body');
expect($bodies)->toHaveCount(2)
->and($bodies)->toContain('Erste Einschätzung.');
expect(class_exists(App\Livewire\Admin\Incidents::class))->toBeTrue();
expect(method_exists(App\Livewire\Admin\Incidents::class, 'editUpdate'))->toBeFalse();
});
it('needs the capability, not merely an operator account', function () {
// Asserted on the PAGE rather than on the action: render() authorizes too,
// so an operator without the capability never gets far enough to press a
// button — and a Livewire::test() against a component that refuses to
// render fails with a snapshot error that says nothing about permissions.
$this->actingAs(operator('Support'), 'operator')
->get(route('admin.incidents'))
->assertForbidden();
});
it('records the operator who wrote each line', function () {
// "Nachvollziehbar" includes who said it. Not shown publicly — this is for
// the operator looking back at their own record months later.
$owner = operator('Owner');
Livewire::actingAs($owner, 'operator')
->test(IncidentConsole::class)
->set('title', 'x')
->set('components', ['portal'])
->set('startedAt', LocalTime::toField(now()))
->set('firstUpdate', 'Text')
->call('create');
$incident = Incident::query()->firstOrFail();
expect($incident->created_by)->toBe($owner->email)
->and($incident->updates->first()->created_by)->toBe($owner->email);
});