diff --git a/src/Status.php b/src/Status.php index 32c0f795..d2af932e 100644 --- a/src/Status.php +++ b/src/Status.php @@ -7,6 +7,7 @@ use Cachet\Enums\SystemStatusEnum; use Cachet\Models\Component; use Cachet\Models\Incident; +use Cachet\Models\Update; use Cachet\Settings\AppSettings; use Carbon\CarbonInterface; use Illuminate\Database\Eloquent\Builder; @@ -104,14 +105,30 @@ public function incidents(): object } /** - * Get the most recent update timestamp across enabled components. + * Get the most recent update timestamp across enabled components and public incidents. + * + * Linking a component to an incident does not touch the component itself, so + * incidents and their updates are considered too. Otherwise the status page + * keeps showing the last time a component was edited, even after incidents + * have been opened and resolved since. */ public function lastUpdated(): ?CarbonInterface { - return Component::query() - ->enabled() - ->latest('updated_at') - ->first(['updated_at'])?->updated_at; + return collect([ + Component::query() + ->enabled() + ->latest('updated_at') + ->value('updated_at'), + Incident::query() + ->viewableBy(false) + ->latest('updated_at') + ->value('updated_at'), + Update::query() + ->where('updateable_type', (new Incident)->getMorphClass()) + ->whereIn('updateable_id', Incident::query()->viewableBy(false)->select('id')) + ->latest('updated_at') + ->value('updated_at'), + ])->filter()->max(); } /** diff --git a/tests/Unit/StatusTest.php b/tests/Unit/StatusTest.php index fbcd06ce..ebc97960 100644 --- a/tests/Unit/StatusTest.php +++ b/tests/Unit/StatusTest.php @@ -121,6 +121,70 @@ ->toEqual($component->updated_at); }); +it('includes public incidents in the last updated timestamp', function () { + Component::factory()->create([ + 'enabled' => true, + 'updated_at' => now()->subYear(), + ]); + + $this->travel(-1)->hours(); + $incident = Incident::factory()->create([ + 'visible' => ResourceVisibilityEnum::guest, + ]); + $this->travelBack(); + + expect((new Status)->lastUpdated())->toEqual($incident->updated_at); +}); + +it('includes updates to public incidents in the last updated timestamp', function () { + Component::factory()->create([ + 'enabled' => true, + 'updated_at' => now()->subYear(), + ]); + + $this->travel(-1)->days(); + $incident = Incident::factory()->create([ + 'status' => IncidentStatusEnum::identified->value, + 'visible' => ResourceVisibilityEnum::guest, + ]); + $this->travelBack(); + + $this->travel(-1)->hours(); + $update = Update::factory()->forIncident($incident)->create([ + 'status' => IncidentStatusEnum::identified->value, + ]); + $this->travelBack(); + + expect((new Status)->lastUpdated())->toEqual($update->updated_at); +}); + +it('excludes non-public incidents and their updates from the last updated timestamp', function (ResourceVisibilityEnum $visibility) { + $component = Component::factory()->create([ + 'enabled' => true, + 'updated_at' => now()->subYear(), + ]); + + $incident = Incident::factory()->create([ + 'visible' => $visibility, + ]); + Update::factory()->forIncident($incident)->create(); + + expect((new Status)->lastUpdated())->toEqual($component->updated_at); +})->with([ResourceVisibilityEnum::authenticated, ResourceVisibilityEnum::hidden]); + +it('excludes unpublished incidents from the last updated timestamp', function () { + $component = Component::factory()->create([ + 'enabled' => true, + 'updated_at' => now()->subYear(), + ]); + + Incident::factory()->scheduled()->create([ + 'visible' => ResourceVisibilityEnum::guest, + ]); + + expect((new Status)->lastUpdated())->toEqual($component->updated_at); +}); + it('excludes disabled components from component overview', function () { Component::factory()->create([ 'status' => ComponentStatusEnum::operational->value,