From c94919f1b942f80aa4123b256878b9582b763197 Mon Sep 17 00:00:00 2001 From: James Brooks Date: Thu, 6 Oct 2016 17:21:18 +0100 Subject: [PATCH] Backport Incident Updates from v3.0.0 --- .../RemoveIncidentUpdateCommand.php | 41 +++++ .../ReportIncidentUpdateCommand.php | 81 ++++++++ .../UpdateIncidentUpdateCommand.php | 81 ++++++++ .../IncidentUpdateEventInterface.php | 24 +++ .../IncidentUpdateWasRemovedEvent.php | 41 +++++ .../IncidentUpdateWasReportedEvent.php | 41 +++++ .../IncidentUpdateWasUpdatedEvent.php | 41 +++++ .../RemoveIncidentUpdateCommandHandler.php | 39 ++++ .../ReportIncidentUpdateCommandHandler.php | 65 +++++++ .../UpdateIncidentUpdateCommandHandler.php | 59 ++++++ app/Console/Commands/DemoSeederCommand.php | 105 ++++++----- .../Providers/EventServiceProvider.php | 9 + .../Api/IncidentUpdateController.php | 132 +++++++++++++ .../Dashboard/IncidentController.php | 59 +++++- app/Http/Controllers/StatusPageController.php | 2 +- app/Http/Routes/ApiRoutes.php | 6 + app/Http/Routes/Dashboard/IncidentRoutes.php | 5 + app/Integrations/Core/System.php | 5 +- app/Models/Incident.php | 68 +++++++ app/Models/IncidentUpdate.php | 95 ++++++++++ app/Presenters/IncidentPresenter.php | 134 ++++++++++++-- app/Presenters/IncidentUpdatePresenter.php | 173 ++++++++++++++++++ database/factories/ModelFactory.php | 10 + ...3_08_125729_CreateIncidentUpdatesTable.php | 46 +++++ .../assets/sass/status-page/_status-page.scss | 8 + resources/lang/en/cachet.php | 1 + resources/lang/en/dashboard.php | 5 + .../views/dashboard/incidents/index.blade.php | 3 +- .../dashboard/incidents/update.blade.php | 64 +++++++ resources/views/partials/incident.blade.php | 11 ++ resources/views/partials/incidents.blade.php | 4 +- resources/views/single-incident.blade.php | 27 ++- tests/Api/IncidentUpdateTest.php | 102 +++++++++++ .../AbstractIncidentUpdateCommandTest.php | 31 ++++ .../RemoveIncidentUpdateCommandTest.php | 41 +++++ .../ReportIncidentUpdateCommandTest.php | 52 ++++++ .../UpdateIncidentUpdateCommandTest.php | 52 ++++++ .../AbstractIncidentUpdateEventTestCase.php | 26 +++ .../IncidentUpdateWasRemovedEventTest.php | 31 ++++ .../IncidentUpdateWasReportedEventTest.php | 31 ++++ .../IncidentUpdateWasUpdatedEventTest.php | 31 ++++ .../Controllers/StatusPageControllerTest.php | 6 +- tests/Models/IncidentUpdateTest.php | 31 ++++ 43 files changed, 1834 insertions(+), 85 deletions(-) create mode 100644 app/Bus/Commands/IncidentUpdate/RemoveIncidentUpdateCommand.php create mode 100644 app/Bus/Commands/IncidentUpdate/ReportIncidentUpdateCommand.php create mode 100644 app/Bus/Commands/IncidentUpdate/UpdateIncidentUpdateCommand.php create mode 100644 app/Bus/Events/IncidentUpdate/IncidentUpdateEventInterface.php create mode 100644 app/Bus/Events/IncidentUpdate/IncidentUpdateWasRemovedEvent.php create mode 100644 app/Bus/Events/IncidentUpdate/IncidentUpdateWasReportedEvent.php create mode 100644 app/Bus/Events/IncidentUpdate/IncidentUpdateWasUpdatedEvent.php create mode 100644 app/Bus/Handlers/Commands/IncidentUpdate/RemoveIncidentUpdateCommandHandler.php create mode 100644 app/Bus/Handlers/Commands/IncidentUpdate/ReportIncidentUpdateCommandHandler.php create mode 100644 app/Bus/Handlers/Commands/IncidentUpdate/UpdateIncidentUpdateCommandHandler.php create mode 100644 app/Http/Controllers/Api/IncidentUpdateController.php create mode 100644 app/Models/IncidentUpdate.php create mode 100644 app/Presenters/IncidentUpdatePresenter.php create mode 100644 database/migrations/2016_03_08_125729_CreateIncidentUpdatesTable.php create mode 100644 resources/views/dashboard/incidents/update.blade.php create mode 100644 tests/Api/IncidentUpdateTest.php create mode 100644 tests/Bus/Commands/IncidentUpdate/AbstractIncidentUpdateCommandTest.php create mode 100644 tests/Bus/Commands/IncidentUpdate/RemoveIncidentUpdateCommandTest.php create mode 100644 tests/Bus/Commands/IncidentUpdate/ReportIncidentUpdateCommandTest.php create mode 100644 tests/Bus/Commands/IncidentUpdate/UpdateIncidentUpdateCommandTest.php create mode 100644 tests/Bus/Events/IncidentUpdate/AbstractIncidentUpdateEventTestCase.php create mode 100644 tests/Bus/Events/IncidentUpdate/IncidentUpdateWasRemovedEventTest.php create mode 100644 tests/Bus/Events/IncidentUpdate/IncidentUpdateWasReportedEventTest.php create mode 100644 tests/Bus/Events/IncidentUpdate/IncidentUpdateWasUpdatedEventTest.php create mode 100644 tests/Models/IncidentUpdateTest.php diff --git a/app/Bus/Commands/IncidentUpdate/RemoveIncidentUpdateCommand.php b/app/Bus/Commands/IncidentUpdate/RemoveIncidentUpdateCommand.php new file mode 100644 index 000000000000..7440e3529dd1 --- /dev/null +++ b/app/Bus/Commands/IncidentUpdate/RemoveIncidentUpdateCommand.php @@ -0,0 +1,41 @@ + + */ +final class RemoveIncidentUpdateCommand +{ + /** + * The incident update to remove. + * + * @var \CachetHQ\Cachet\Models\IncidentUpdate + */ + public $incidentUpdate; + + /** + * Create a new remove incident update command instance. + * + * @param \CachetHQ\Cachet\Models\IncidentUpdate $incidentUpdate + * + * @return void + */ + public function __construct(IncidentUpdate $incidentUpdate) + { + $this->incidentUpdate = $incidentUpdate; + } +} diff --git a/app/Bus/Commands/IncidentUpdate/ReportIncidentUpdateCommand.php b/app/Bus/Commands/IncidentUpdate/ReportIncidentUpdateCommand.php new file mode 100644 index 000000000000..d88e51401127 --- /dev/null +++ b/app/Bus/Commands/IncidentUpdate/ReportIncidentUpdateCommand.php @@ -0,0 +1,81 @@ + + */ +final class ReportIncidentUpdateCommand +{ + /** + * The incident. + * + * @var \CachetHQ\Cachet\Models\Incident + */ + public $incident; + + /** + * The incident status. + * + * @var int + */ + public $status; + + /** + * The incident message. + * + * @var string + */ + public $message; + + /** + * The user. + * + * @var \CachetHQ\Cachet\Models\User + */ + public $user; + + /** + * The validation rules. + * + * @var string[] + */ + public $rules = [ + 'incident' => 'required', + 'status' => 'required|int|min:1|max:4', + 'message' => 'required|string', + 'user' => 'required', + ]; + + /** + * Create a new report incident update command instance. + * + * @param \CachetHQ\Cachet\Models\Incident $incident + * @param string $status + * @param string $message + * @param \CachetHQ\Cachet\Models\User $user + * + * @return void + */ + public function __construct(Incident $incident, $status, $message, User $user) + { + $this->incident = $incident; + $this->status = $status; + $this->message = $message; + $this->user = $user; + } +} diff --git a/app/Bus/Commands/IncidentUpdate/UpdateIncidentUpdateCommand.php b/app/Bus/Commands/IncidentUpdate/UpdateIncidentUpdateCommand.php new file mode 100644 index 000000000000..55370e2a6db8 --- /dev/null +++ b/app/Bus/Commands/IncidentUpdate/UpdateIncidentUpdateCommand.php @@ -0,0 +1,81 @@ + + */ +final class UpdateIncidentUpdateCommand +{ + /** + * The incident update. + * + * @var \CachetHQ\Cachet\Models\IncidentUpdate + */ + public $update; + + /** + * The incident status. + * + * @var int + */ + public $status; + + /** + * The incident message. + * + * @var string + */ + public $message; + + /** + * The user. + * + * @var \CachetHQ\Cachet\Models\User + */ + public $user; + + /** + * The validation rules. + * + * @var string[] + */ + public $rules = [ + 'update' => 'required', + 'status' => 'int|min:1|max:4', + 'message' => 'string', + 'user' => 'required', + ]; + + /** + * Create a new update incident update command instance. + * + * @param \CachetHQ\Cachet\Models\IncidentUpdate $update + * @param string $status + * @param string $message + * @param \CachetHQ\Cachet\Models\User $user + * + * @return void + */ + public function __construct(IncidentUpdate $update, $status, $message, User $user) + { + $this->update = $update; + $this->status = $status; + $this->message = $message; + $this->user = $user; + } +} diff --git a/app/Bus/Events/IncidentUpdate/IncidentUpdateEventInterface.php b/app/Bus/Events/IncidentUpdate/IncidentUpdateEventInterface.php new file mode 100644 index 000000000000..9f1687afccbc --- /dev/null +++ b/app/Bus/Events/IncidentUpdate/IncidentUpdateEventInterface.php @@ -0,0 +1,24 @@ + + */ +interface IncidentUpdateEventInterface extends EventInterface +{ + // +} diff --git a/app/Bus/Events/IncidentUpdate/IncidentUpdateWasRemovedEvent.php b/app/Bus/Events/IncidentUpdate/IncidentUpdateWasRemovedEvent.php new file mode 100644 index 000000000000..1fba40aeca55 --- /dev/null +++ b/app/Bus/Events/IncidentUpdate/IncidentUpdateWasRemovedEvent.php @@ -0,0 +1,41 @@ + + */ +final class IncidentUpdateWasRemovedEvent implements IncidentUpdateEventInterface +{ + /** + * The incident update that has been removed. + * + * @var \CachetHQ\Cachet\Models\IncidentUpdate + */ + public $update; + + /** + * Create a new incident update was removed event instance. + * + * @param \CachetHQ\Cachet\Models\IncidentUpdate $update + * + * @return void + */ + public function __construct(IncidentUpdate $update) + { + $this->update = $update; + } +} diff --git a/app/Bus/Events/IncidentUpdate/IncidentUpdateWasReportedEvent.php b/app/Bus/Events/IncidentUpdate/IncidentUpdateWasReportedEvent.php new file mode 100644 index 000000000000..e2cf892887c3 --- /dev/null +++ b/app/Bus/Events/IncidentUpdate/IncidentUpdateWasReportedEvent.php @@ -0,0 +1,41 @@ + + */ +final class IncidentUpdateWasReportedEvent implements IncidentUpdateEventInterface +{ + /** + * The incident update that has been reported. + * + * @var \CachetHQ\Cachet\Models\IncidentUpdate + */ + public $update; + + /** + * Create a new incident update was reported event instance. + * + * @param \CachetHQ\Cachet\Models\IncidentUpdate $update + * + * @return void + */ + public function __construct(IncidentUpdate $update) + { + $this->update = $update; + } +} diff --git a/app/Bus/Events/IncidentUpdate/IncidentUpdateWasUpdatedEvent.php b/app/Bus/Events/IncidentUpdate/IncidentUpdateWasUpdatedEvent.php new file mode 100644 index 000000000000..042b3af71225 --- /dev/null +++ b/app/Bus/Events/IncidentUpdate/IncidentUpdateWasUpdatedEvent.php @@ -0,0 +1,41 @@ + + */ +final class IncidentUpdateWasUpdatedEvent implements IncidentUpdateEventInterface +{ + /** + * The incident update that has been updated. + * + * @var \CachetHQ\Cachet\Models\IncidentUpdate + */ + public $update; + + /** + * Create a new incident update was updated event instance. + * + * @param \CachetHQ\Cachet\Models\IncidentUpdate $update + * + * @return void + */ + public function __construct(IncidentUpdate $update) + { + $this->update = $update; + } +} diff --git a/app/Bus/Handlers/Commands/IncidentUpdate/RemoveIncidentUpdateCommandHandler.php b/app/Bus/Handlers/Commands/IncidentUpdate/RemoveIncidentUpdateCommandHandler.php new file mode 100644 index 000000000000..40bc6def6298 --- /dev/null +++ b/app/Bus/Handlers/Commands/IncidentUpdate/RemoveIncidentUpdateCommandHandler.php @@ -0,0 +1,39 @@ + + */ +class RemoveIncidentUpdateCommandHandler +{ + /** + * Handle the remove incident update command. + * + * @param \CachetHQ\Cachet\Bus\Commands\IncidentUpdate\RemoveIncidentUpdateCommand $command + * + * @return void + */ + public function handle(RemoveIncidentUpdateCommand $command) + { + $update = $command->incidentUpdate; + + event(new IncidentUpdateWasRemovedEvent($update)); + + $update->delete(); + } +} diff --git a/app/Bus/Handlers/Commands/IncidentUpdate/ReportIncidentUpdateCommandHandler.php b/app/Bus/Handlers/Commands/IncidentUpdate/ReportIncidentUpdateCommandHandler.php new file mode 100644 index 000000000000..dd33c73e96ab --- /dev/null +++ b/app/Bus/Handlers/Commands/IncidentUpdate/ReportIncidentUpdateCommandHandler.php @@ -0,0 +1,65 @@ + + */ +class ReportIncidentUpdateCommandHandler +{ + /** + * Handle the report incident command. + * + * @param \CachetHQ\Cachet\Bus\Commands\IncidentUpdate\ReportIncidentUpdateCommand $command + * + * @return \CachetHQ\Cachet\Models\IncidentUpdate + */ + public function handle(ReportIncidentUpdateCommand $command) + { + $data = [ + 'incident_id' => $command->incident->id, + 'status' => $command->status, + 'message' => $command->message, + 'user_id' => $command->user->id, + ]; + + // Create the incident update. + $update = IncidentUpdate::create($data); + + // Update the original incident with the new status. + dispatch(new UpdateIncidentCommand( + $incident, + null, + $command->status, + null, + null, + null, + null, + null, + null, + null, + null, + null + )); + + event(new IncidentUpdateWasReportedEvent($update)); + + return $update; + } +} diff --git a/app/Bus/Handlers/Commands/IncidentUpdate/UpdateIncidentUpdateCommandHandler.php b/app/Bus/Handlers/Commands/IncidentUpdate/UpdateIncidentUpdateCommandHandler.php new file mode 100644 index 000000000000..e65939803327 --- /dev/null +++ b/app/Bus/Handlers/Commands/IncidentUpdate/UpdateIncidentUpdateCommandHandler.php @@ -0,0 +1,59 @@ + + */ +class UpdateIncidentUpdateCommandHandler +{ + /** + * Handle the update incident update command. + * + * @param \CachetHQ\Cachet\Bus\Commands\IncidentUpdate\UpdateIncidentUpdateCommand $command + * + * @return \CachetHQ\Cachet\Models\IncidentUpdate + */ + public function handle(UpdateIncidentUpdateCommand $command) + { + $command->update->update($this->filter($command)); + + event(new IncidentUpdateWasUpdatedEvent($command->update)); + + return $command->update; + } + + /** + * Filter the command data. + * + * @param \CachetHQ\Cachet\Bus\Commands\IncidentUpdate\UpdateIncidentUpdateCommand $command + * + * @return array + */ + protected function filter(UpdateIncidentUpdateCommand $command) + { + $params = [ + 'status' => $command->status, + 'message' => $command->message, + 'user_id' => $command->user->id, + ]; + + return array_filter($params, function ($val) { + return $val !== null; + }); + } +} diff --git a/app/Console/Commands/DemoSeederCommand.php b/app/Console/Commands/DemoSeederCommand.php index 283f6d6182c1..c20251857dc1 100644 --- a/app/Console/Commands/DemoSeederCommand.php +++ b/app/Console/Commands/DemoSeederCommand.php @@ -15,6 +15,7 @@ use CachetHQ\Cachet\Models\ComponentGroup; use CachetHQ\Cachet\Models\Incident; use CachetHQ\Cachet\Models\IncidentTemplate; +use CachetHQ\Cachet\Models\IncidentUpdate; use CachetHQ\Cachet\Models\Metric; use CachetHQ\Cachet\Models\MetricPoint; use CachetHQ\Cachet\Models\Subscriber; @@ -201,74 +202,31 @@ protected function seedIncidents() $defaultIncidents = [ [ - 'name' => 'Cachet supports Markdown!', - 'message' => $incidentMessage, - 'status' => 4, + 'name' => 'Our monkeys aren\'t performing', + 'message' => 'We\'re investigating an issue with our monkeys not performing as they should be.', + 'status' => Incident::INVESTIGATING, 'component_id' => 0, 'scheduled_at' => null, 'visible' => 1, 'stickied' => false, ], [ - 'name' => 'Awesome', - 'message' => ':+1: We totally nailed the fix.', - 'status' => 4, + 'name' => 'This is an unresolved incident', + 'message' => 'Unresolved incidents are left without a **Fixed** update.', + 'status' => Incident::INVESTIGATING, 'component_id' => 0, 'scheduled_at' => null, 'visible' => 1, 'stickied' => false, ], - [ - 'name' => 'Monitoring the fix', - 'message' => ":ship: We've deployed a fix.", - 'status' => 3, - 'component_id' => 0, - 'scheduled_at' => null, - 'visible' => 1, - 'stickied' => false, - ], - [ - 'name' => 'Update', - 'message' => "We've identified the problem. Our engineers are currently looking at it.", - 'status' => 2, - 'component_id' => 0, - 'scheduled_at' => null, - 'visible' => 1, - 'stickied' => false, - ], - [ - 'name' => 'Test Incident', - 'message' => 'Something went wrong, with something or another.', - 'status' => 1, - 'component_id' => 0, - 'scheduled_at' => null, - 'visible' => 1, - 'stickied' => false, - ], - [ - 'name' => 'Investigating the API', - 'message' => ':zap: We\'ve seen high response times from our API. It looks to be fixing itself as time goes on.', - 'status' => 1, - 'component_id' => 1, - 'scheduled_at' => null, - 'visible' => 1, - 'stickied' => false, - ], - [ - 'name' => 'Sticked incidents!', - 'message' => 'Need to continually notify your customers of an incident? You can stick incidents to the top!', - 'status' => 1, - 'component_id' => 1, - 'scheduled_at' => null, - 'visible' => 1, - 'stickied' => true, - ], ]; Incident::truncate(); - foreach ($defaultIncidents as $incident) { - Incident::create($incident); + foreach ($defaultIncidents as $defaultIncident) { + $incident = Incident::create($defaultIncident); + + $this->seedIncidentUpdates($incident); } } @@ -282,6 +240,47 @@ protected function seedIncidentTemplates() IncidentTemplate::truncate(); } + /** + * Seed the incident updates table for a given incident. + * + * @return void + */ + protected function seedIncidentUpdates($incident) + { + $defaultUpdates = [ + 1 => [ + [ + 'status' => Incident::FIXED, + 'message' => 'The monkeys are back and rested!', + 'user_id' => 1, + ], [ + 'status' => Incident::WATCHED, + 'message' => 'Our monkeys need a break from performing. They\'ll be back after a good rest.', + 'user_id' => 1, + ], [ + 'status' => Incident::IDENTIFIED, + 'message' => 'We have identified the issue with our lovely performing monkeys.', + 'user_id' => 1, + ], + ], + 2 => [ + [ + 'status' => Incident::WATCHED, + 'message' => 'We\'re actively watching this issue, so it remains unresolved.', + 'user_id' => 1, + ], + ], + ]; + + $updates = $defaultUpdates[$incident->id]; + + foreach ($updates as $updateId => $update) { + $update['incident_id'] = $incident->id; + + IncidentUpdate::create($update); + } + } + /** * Seed the metric points table. * diff --git a/app/Foundation/Providers/EventServiceProvider.php b/app/Foundation/Providers/EventServiceProvider.php index b0f970da4b3b..bf8084049ac2 100644 --- a/app/Foundation/Providers/EventServiceProvider.php +++ b/app/Foundation/Providers/EventServiceProvider.php @@ -48,6 +48,15 @@ class EventServiceProvider extends ServiceProvider 'CachetHQ\Cachet\Bus\Events\Component\ComponentWasUpdatedEvent' => [ // ], + 'CachetHQ\Cachet\Bus\Events\IncidentUpdate\IncidentUpdateWasRemovedEvent' => [ + // + ], + 'CachetHQ\Cachet\Bus\Events\IncidentUpdate\IncidentUpdateWasReportedEvent' => [ + // + ], + 'CachetHQ\Cachet\Bus\Events\IncidentUpdate\IncidentUpdateWasUpdatedEvent' => [ + // + ], 'CachetHQ\Cachet\Bus\Events\Incident\IncidentWasRemovedEvent' => [ // ], diff --git a/app/Http/Controllers/Api/IncidentUpdateController.php b/app/Http/Controllers/Api/IncidentUpdateController.php new file mode 100644 index 000000000000..84a01d7ab5d4 --- /dev/null +++ b/app/Http/Controllers/Api/IncidentUpdateController.php @@ -0,0 +1,132 @@ + + */ +class IncidentUpdateController extends AbstractApiController +{ + /** + * Return all updates on the incident. + * + * @param \CachetHQ\Cachet\Models\Incident $incident + * + * @return \Illuminate\Http\JsonResponse + */ + public function getIncidentUpdates(Incident $incident) + { + $updates = IncidentUpdate::orderBy('created_at', 'desc'); + + if ($sortBy = Binput::get('sort')) { + $direction = Binput::has('order') && Binput::get('order') == 'desc'; + + $updates->sort($sortBy, $direction); + } + + $updates = $updates->paginate(Binput::get('per_page', 20)); + + return $this->paginator($updates, Request::instance()); + } + + /** + * Return a single incident update. + * + * @param \CachetHQ\Cachet\Models\Incident $incident + * @param \CachetHQ\Cachet\Models\IncidentUpdate $update + * + * @return \Illuminate\Http\JsonResponse + */ + public function getIncidentUpdate(Incident $incident, IncidentUpdate $update) + { + return $this->item($update); + } + + /** + * Create a new incident update. + * + * @param \CachetHQ\Cachet\Models\Incident $incident + * + * @return \Illuminate\Http\JsonResponse + */ + public function postIncidentUpdate(Incident $incident) + { + try { + $update = dispatch(new ReportIncidentUpdateCommand( + $incident, + Binput::get('status'), + Binput::get('message'), + Auth::user() + )); + } catch (QueryException $e) { + throw new BadRequestHttpException(); + } + + return $this->item($update); + } + + /** + * Update an incident update. + * + * @param \CachetHQ\Cachet\Models\Incident $incident + * @param \CachetHQ\Cachet\Models\IncidentUpdate $update + * + * @return \Illuminate\Http\JsonResponse + */ + public function putIncidentUpdate(Incident $incident, IncidentUpdate $update) + { + try { + $update = dispatch(new UpdateIncidentUpdateCommand( + $update, + Binput::get('status'), + Binput::get('message'), + Auth::user() + )); + } catch (QueryException $e) { + throw new BadRequestHttpException(); + } + + return $this->item($update); + } + + /** + * Create a new incident update. + * + * @param \CachetHQ\Cachet\Models\Incident $incident + * @param \CachetHQ\Cachet\Models\IncidentUpdate $update + * + * @return \Illuminate\Http\JsonResponse + */ + public function deleteIncidentUpdate(Incident $incident, IncidentUpdate $update) + { + try { + dispatch(new RemoveIncidentUpdateCommand($update)); + } catch (QueryException $e) { + throw new BadRequestHttpException(); + } + + return $this->noContent(); + } +} diff --git a/app/Http/Controllers/Dashboard/IncidentController.php b/app/Http/Controllers/Dashboard/IncidentController.php index 5a1284c86296..9d70716463d7 100644 --- a/app/Http/Controllers/Dashboard/IncidentController.php +++ b/app/Http/Controllers/Dashboard/IncidentController.php @@ -15,15 +15,22 @@ use CachetHQ\Cachet\Bus\Commands\Incident\RemoveIncidentCommand; use CachetHQ\Cachet\Bus\Commands\Incident\ReportIncidentCommand; use CachetHQ\Cachet\Bus\Commands\Incident\UpdateIncidentCommand; +use CachetHQ\Cachet\Bus\Commands\IncidentUpdate\ReportIncidentUpdateCommand; use CachetHQ\Cachet\Models\Component; use CachetHQ\Cachet\Models\ComponentGroup; use CachetHQ\Cachet\Models\Incident; use CachetHQ\Cachet\Models\IncidentTemplate; use GrahamCampbell\Binput\Facades\Binput; +use Illuminate\Contracts\Auth\Guard; use Illuminate\Routing\Controller; use Illuminate\Support\Facades\Redirect; use Illuminate\Support\Facades\View; +/** + * This is the incident controller. + * + * @author James Brooks + */ class IncidentController extends Controller { /** @@ -33,13 +40,24 @@ class IncidentController extends Controller */ protected $subMenu = []; + /** + * The guard instance. + * + * @var \Illuminate\Contracts\Auth\Guard + */ + protected $auth; + /** * Creates a new incident controller instance. * + * @param \Illuminate\Contracts\Auth\Guard $auth + * * @return void */ - public function __construct() + public function __construct(Guard $auth) { + $this->auth = $auth; + $this->subMenu = [ 'incidents' => [ 'title' => trans('dashboard.incidents.incidents'), @@ -281,4 +299,43 @@ public function editTemplateAction(IncidentTemplate $template) return Redirect::route('dashboard.templates.edit', ['id' => $template->id]) ->withUpdatedTemplate($template); } + + /** + * Shows the incident update form. + * + * @param \CachetHQ\Cachet\Models\Incident $incident + * + * @return \Illuminate\View\View + */ + public function showIncidentUpdateAction(Incident $incident) + { + return View::make('dashboard.incidents.update')->withIncident($incident); + } + + /** + * Creates a new incident update. + * + * @param \CachetHQ\Cachet\Models\Incident $incident + * + * @return \Illuminate\Http\RedirectResponse + */ + public function createIncidentUpdateAction(Incident $incident) + { + try { + $incident = dispatch(new ReportIncidentUpdateCommand( + $incident, + Binput::get('status'), + Binput::get('message'), + $this->auth->user() + )); + } catch (ValidationException $e) { + return Redirect::route('dashboard.incidents.update', ['id' => $incident->id]) + ->withInput(Binput::all()) + ->withTitle(sprintf('%s %s', trans('dashboard.notifications.whoops'), trans('dashboard.incidents.templates.edit.failure'))) + ->withErrors($e->getMessageBag()); + } + + return Redirect::route('dashboard.incidents.index') + ->withSuccess(sprintf('%s %s', trans('dashboard.notifications.awesome'), trans('dashboard.incidents.delete.success'))); + } } diff --git a/app/Http/Controllers/StatusPageController.php b/app/Http/Controllers/StatusPageController.php index 9d29febb8108..3c8feffe69df 100644 --- a/app/Http/Controllers/StatusPageController.php +++ b/app/Http/Controllers/StatusPageController.php @@ -85,7 +85,7 @@ public function showIndex() $allIncidents = Incident::notScheduled()->where('visible', '>=', $incidentVisibility)->whereBetween('created_at', [ $startDate->copy()->subDays($daysToShow)->format('Y-m-d').' 00:00:00', $startDate->format('Y-m-d').' 23:59:59', - ])->orderBy('scheduled_at', 'desc')->orderBy('created_at', 'desc')->get()->groupBy(function (Incident $incident) { + ])->orderBy('scheduled_at', 'desc')->orderBy('created_at', 'desc')->get()->load('updates')->groupBy(function (Incident $incident) { return app(DateFactory::class)->make($incident->is_scheduled ? $incident->scheduled_at : $incident->created_at)->toDateString(); }); diff --git a/app/Http/Routes/ApiRoutes.php b/app/Http/Routes/ApiRoutes.php index c288b96bc410..bb5303db398e 100644 --- a/app/Http/Routes/ApiRoutes.php +++ b/app/Http/Routes/ApiRoutes.php @@ -43,6 +43,9 @@ public function map(Registrar $router) $router->get('incidents', 'IncidentController@getIncidents'); $router->get('incidents/{incident}', 'IncidentController@getIncident'); + $router->get('incidents/{incident}/updates', 'IncidentUpdateController@getIncidentUpdates'); + $router->get('incidents/{incident}/updates/{update}', 'IncidentUpdateController@getIncidentUpdate'); + $router->get('metrics', 'MetricController@getMetrics'); $router->get('metrics/{metric}', 'MetricController@getMetric'); $router->get('metrics/{metric}/points', 'MetricController@getMetricPoints'); @@ -54,6 +57,7 @@ public function map(Registrar $router) $router->post('components', 'ComponentController@postComponents'); $router->post('components/groups', 'ComponentGroupController@postGroups'); $router->post('incidents', 'IncidentController@postIncidents'); + $router->post('incidents/{incident}/updates', 'IncidentUpdateController@postIncidentUpdate'); $router->post('metrics', 'MetricController@postMetrics'); $router->post('metrics/{metric}/points', 'MetricPointController@postMetricPoints'); $router->post('subscribers', 'SubscriberController@postSubscribers'); @@ -61,12 +65,14 @@ public function map(Registrar $router) $router->put('components/groups/{component_group}', 'ComponentGroupController@putGroup'); $router->put('components/{component}', 'ComponentController@putComponent'); $router->put('incidents/{incident}', 'IncidentController@putIncident'); + $router->put('incidents/{incident}/updates/{update}', 'IncidentUpdateController@putIncidentUpdate'); $router->put('metrics/{metric}', 'MetricController@putMetric'); $router->put('metrics/{metric}/points/{metric_point}', 'MetricPointController@putMetricPoint'); $router->delete('components/groups/{component_group}', 'ComponentGroupController@deleteGroup'); $router->delete('components/{component}', 'ComponentController@deleteComponent'); $router->delete('incidents/{incident}', 'IncidentController@deleteIncident'); + $router->delete('incidents/{incident}/updates/{update}', 'IncidentUpdateController@deleteIncidentUpdate'); $router->delete('metrics/{metric}', 'MetricController@deleteMetric'); $router->delete('metrics/{metric}/points/{metric_point}', 'MetricPointController@deleteMetricPoint'); $router->delete('subscribers/{subscriber}', 'SubscriberController@deleteSubscriber'); diff --git a/app/Http/Routes/Dashboard/IncidentRoutes.php b/app/Http/Routes/Dashboard/IncidentRoutes.php index d63ab4e6bbcc..68fa5ecc10e0 100644 --- a/app/Http/Routes/Dashboard/IncidentRoutes.php +++ b/app/Http/Routes/Dashboard/IncidentRoutes.php @@ -53,7 +53,12 @@ public function map(Registrar $router) 'as' => 'edit', 'uses' => 'IncidentController@showEditIncidentAction', ]); + $router->get('{incident}/update', [ + 'as' => 'update', + 'uses' => 'IncidentController@showIncidentUpdateAction', + ]); $router->post('{incident}/edit', 'IncidentController@editIncidentAction'); + $router->post('{incident}/update', 'IncidentController@createIncidentUpdateAction'); }); } } diff --git a/app/Integrations/Core/System.php b/app/Integrations/Core/System.php index 77c55e5776c4..77395af6b2da 100644 --- a/app/Integrations/Core/System.php +++ b/app/Integrations/Core/System.php @@ -53,8 +53,11 @@ public function getStatus() return $incident->status > 0; }); $incidentCount = $incidents->count(); + $unresolvedCount = $incidents->filter(function ($incident) { + return !$incident->is_resolved; + })->count(); - if ($incidentCount === 0 || ($incidentCount >= 1 && (int) $incidents->first()->status === 4)) { + if ($incidentCount === 0 || ($incidentCount >= 1 && $unresolvedCount === 0)) { $status = [ 'system_status' => 'success', 'system_message' => trans_choice('cachet.service.good', $totalComponents), diff --git a/app/Models/Incident.php b/app/Models/Incident.php index e89364c29f5f..b294a5f710bb 100644 --- a/app/Models/Incident.php +++ b/app/Models/Incident.php @@ -25,6 +25,43 @@ class Incident extends Model implements HasPresenter { use SearchableTrait, SoftDeletes, SortableTrait, ValidatingTrait; + /** + * Status for incident being investigated. + * + * @var int + */ + const INVESTIGATING = 1; + + /** + * Status for incident having been identified. + * + * @var int + */ + const IDENTIFIED = 2; + + /** + * Status for incident being watched. + * + * @var int + */ + const WATCHED = 3; + + /** + * Status for incident now being fixed. + * + * @var int + */ + const FIXED = 4; + + /** + * The accessors to append to the model's array form. + * + * @var string[] + */ + protected $appends = [ + 'is_resolved', + ]; + /** * The attributes that should be casted to native types. * @@ -96,6 +133,13 @@ class Incident extends Model implements HasPresenter 'message', ]; + /** + * The relations to eager load on every query. + * + * @var string[] + */ + protected $with = ['updates']; + /** * Get the component relation. * @@ -106,6 +150,16 @@ public function component() return $this->belongsTo(Component::class, 'component_id', 'id'); } + /** + * Get the updates relation. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function updates() + { + return $this->hasMany(IncidentUpdate::class)->orderBy('created_at', 'desc'); + } + /** * Finds all visible incidents. * @@ -168,6 +222,20 @@ public function getIsScheduledAttribute() return $this->getOriginal('scheduled_at') !== null; } + /** + * Is the incident resolved? + * + * @return bool + */ + public function getIsResolvedAttribute() + { + if ($updates = $this->updates->first()) { + return $updates->status === self::FIXED; + } + + return $this->status === self::FIXED; + } + /** * Get the presenter class. * diff --git a/app/Models/IncidentUpdate.php b/app/Models/IncidentUpdate.php new file mode 100644 index 000000000000..d905fb9ca8c5 --- /dev/null +++ b/app/Models/IncidentUpdate.php @@ -0,0 +1,95 @@ + + */ +class IncidentUpdate extends Model implements HasPresenter +{ + use SortableTrait, ValidatingTrait; + + /** + * The attributes that should be casted to native types. + * + * @var string[] + */ + protected $casts = [ + 'incident_id' => 'int', + 'status' => 'int', + 'message' => 'string', + 'user_id' => 'int', + ]; + + /** + * The fillable properties. + * + * @var string[] + */ + protected $fillable = [ + 'incident_id', + 'status', + 'message', + 'user_id', + ]; + + /** + * The validation rules. + * + * @var string[] + */ + public $rules = [ + 'incident_id' => 'int', + 'status' => 'required|int', + 'message' => 'required|string', + 'user_id' => 'required|int', + ]; + + /** + * The sortable fields. + * + * @var string[] + */ + protected $sortable = [ + 'id', + 'status', + 'user_id', + ]; + + /** + * Get the incident relation. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function incident() + { + return $this->belongsTo(Incident::class); + } + + /** + * Get the presenter class. + * + * @return string + */ + public function getPresenterClass() + { + return IncidentUpdatePresenter::class; + } +} diff --git a/app/Presenters/IncidentPresenter.php b/app/Presenters/IncidentPresenter.php index 655be27ccd7d..be2f6d88d846 100644 --- a/app/Presenters/IncidentPresenter.php +++ b/app/Presenters/IncidentPresenter.php @@ -22,6 +22,19 @@ class IncidentPresenter extends BasePresenter implements Arrayable { use TimestampsTrait; + /** + * Inciden icon lookup. + * + * @var array + */ + protected $icons = [ + 0 => 'icon ion-android-calendar', // Scheduled + 1 => 'icon ion-flag oranges', // Investigating + 2 => 'icon ion-alert yellows', // Identified + 3 => 'icon ion-eye blues', // Watching + 4 => 'icon ion-checkmark greens', // Fixed + ]; + /** * Renders the message from Markdown into HTML. * @@ -32,6 +45,16 @@ public function formattedMessage() return Markdown::convertToHtml($this->wrappedObject->message); } + /** + * Return the raw text of the message, even without Markdown. + * + * @return string + */ + public function raw_message() + { + return strip_tags($this->formattedMessage()); + } + /** * Present diff for humans date time. * @@ -157,19 +180,8 @@ public function timestamp_iso() */ public function icon() { - switch ($this->wrappedObject->status) { - case 0: // Scheduled - return 'icon ion-android-calendar'; - case 1: // Investigating - return 'icon ion-flag oranges'; - case 2: // Identified - return 'icon ion-alert yellows'; - case 3: // Watching - return 'icon ion-eye blues'; - case 4: // Fixed - return 'icon ion-checkmark greens'; - default: // Something actually broke, this shouldn't happen. - return ''; + if (isset($this->icons[$this->wrappedObject->status])) { + return $this->icons[$this->wrappedObject->status]; } } @@ -183,6 +195,88 @@ public function human_status() return trans('cachet.incidents.status.'.$this->wrappedObject->status); } + /** + * Returns the latest update. + * + * @return int|null + */ + public function latest_status() + { + if ($update = $this->latest()) { + return $update->status; + } + + return $this->wrappedObject->status; + } + + /** + * Returns the latest update. + * + * @return string|null + */ + public function latest_human_status() + { + if ($update = $this->latest()) { + return trans('cachet.incidents.status.'.$update->status); + } + + return $this->human_status(); + } + + /** + * Present the latest icon. + * + * @return string + */ + public function latest_icon() + { + if ($update = $this->latest()) { + if (isset($this->icons[$update->status])) { + return $this->icons[$update->status]; + } + } + + return $this->icon(); + } + + /** + * Fetch the latest incident update. + * + * @return \CachetHQ\Cachet\Models\IncidentUpdate|void + */ + public function latest() + { + if ($update = $this->wrappedObject->updates()->orderBy('created_at', 'desc')->first()) { + return $update; + } + } + + /** + * Get the incident permalink. + * + * @return string + */ + public function permalink() + { + return route('incident', $this->wrappedObject->id); + } + + /** + * The duration since the last update (in seconds). + * + * @return int + */ + public function duration() + { + if ($update = $this->latest()) { + dd($update->created_at->diffInSeconds($this->wrappedObject->created_at)); + + return $this->wrappedObject->created_at->diffInSeconds($update->created_at); + } + + return 0; + } + /** * Convert the presenter instance to an array. * @@ -191,10 +285,16 @@ public function human_status() public function toArray() { return array_merge($this->wrappedObject->toArray(), [ - 'human_status' => $this->human_status(), - 'scheduled_at' => $this->scheduled_at(), - 'created_at' => $this->created_at(), - 'updated_at' => $this->updated_at(), + 'human_status' => $this->human_status(), + 'latest_update_id' => $this->latest() ? $this->latest()->id : null, + 'latest_status' => $this->latest_status(), + 'latest_human_status' => $this->latest_human_status(), + 'latest_icon' => $this->latest_icon(), + 'permalink' => $this->permalink(), + 'duration' => $this->duration(), + 'scheduled_at' => $this->scheduled_at(), + 'created_at' => $this->created_at(), + 'updated_at' => $this->updated_at(), ]); } } diff --git a/app/Presenters/IncidentUpdatePresenter.php b/app/Presenters/IncidentUpdatePresenter.php new file mode 100644 index 000000000000..84ae8127417b --- /dev/null +++ b/app/Presenters/IncidentUpdatePresenter.php @@ -0,0 +1,173 @@ + + */ +class IncidentUpdatePresenter extends BasePresenter implements Arrayable +{ + use TimestampsTrait; + + /** + * Renders the message from Markdown into HTML. + * + * @return string + */ + public function formattedMessage() + { + return Markdown::convertToHtml($this->wrappedObject->message); + } + + /** + * Return the raw text of the message, even without Markdown. + * + * @return string + */ + public function raw_message() + { + return strip_tags($this->formattedMessage()); + } + + /** + * Present diff for humans date time. + * + * @return string + */ + public function created_at_diff() + { + return app(DateFactory::class)->make($this->wrappedObject->created_at)->diffForHumans(); + } + + /** + * Present formatted date time. + * + * @return string + */ + public function created_at_formatted() + { + return ucfirst(app(DateFactory::class)->make($this->wrappedObject->created_at)->format(Config::get('setting.incident_date_format', 'l jS F Y H:i:s'))); + } + + /** + * Formats the created_at time ready to be used by bootstrap-datetimepicker. + * + * @return string + */ + public function created_at_datetimepicker() + { + return app(DateFactory::class)->make($this->wrappedObject->created_at)->format('d/m/Y H:i'); + } + + /** + * Present formatted date time. + * + * @return string + */ + public function created_at_iso() + { + return app(DateFactory::class)->make($this->wrappedObject->created_at)->toISO8601String(); + } + + /** + * Returns a formatted timestamp for use within the timeline. + * + * @return string + */ + public function timestamp_formatted() + { + if ($this->wrappedObject->is_scheduled) { + return $this->scheduled_at_formatted; + } + + return $this->created_at_formatted; + } + + /** + * Return the iso timestamp for use within the timeline. + * + * @return string + */ + public function timestamp_iso() + { + if ($this->wrappedObject->is_scheduled) { + return $this->scheduled_at_iso; + } + + return $this->created_at_iso; + } + + /** + * Present the status with an icon. + * + * @return string + */ + public function icon() + { + switch ($this->wrappedObject->status) { + case 1: // Investigating + return 'icon ion-flag oranges'; + case 2: // Identified + return 'icon ion-alert yellows'; + case 3: // Watching + return 'icon ion-eye blues'; + case 4: // Fixed + return 'icon ion-checkmark greens'; + default: // Something actually broke, this shouldn't happen. + return ''; + } + } + + /** + * Returns a human readable version of the status. + * + * @return string + */ + public function human_status() + { + return trans('cachet.incidents.status.'.$this->wrappedObject->status); + } + + /** + * Generate a permalink to the incident update. + * + * @return string + */ + public function permalink() + { + return route('incident', ['incident' => $this->wrappedObject->incident]).'#update-'.$this->wrappedObject->id; + } + + /** + * Convert the presenter instance to an array. + * + * @return string[] + */ + public function toArray() + { + return array_merge($this->wrappedObject->toArray(), [ + 'human_status' => $this->human_status(), + 'permalink' => $this->permalink(), + 'created_at' => $this->created_at(), + 'updated_at' => $this->updated_at(), + ]); + } +} diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index de96e720d384..1df478453e53 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -13,6 +13,7 @@ use CachetHQ\Cachet\Models\ComponentGroup; use CachetHQ\Cachet\Models\Incident; use CachetHQ\Cachet\Models\IncidentTemplate; +use CachetHQ\Cachet\Models\IncidentUpdate; use CachetHQ\Cachet\Models\Metric; use CachetHQ\Cachet\Models\MetricPoint; use CachetHQ\Cachet\Models\Setting; @@ -58,6 +59,15 @@ ]; }); +$factory->define(IncidentUpdate::class, function ($faker) { + return [ + 'incident_id' => factory(Incident::class)->create()->id, + 'message' => $faker->paragraph(), + 'status' => random_int(1, 4), + 'user_id' => factory(User::class)->create()->id, + ]; +}); + $factory->define(Metric::class, function ($faker) { return [ 'name' => $faker->sentence(), diff --git a/database/migrations/2016_03_08_125729_CreateIncidentUpdatesTable.php b/database/migrations/2016_03_08_125729_CreateIncidentUpdatesTable.php new file mode 100644 index 000000000000..1f8f23bed2cd --- /dev/null +++ b/database/migrations/2016_03_08_125729_CreateIncidentUpdatesTable.php @@ -0,0 +1,46 @@ +increments('id'); + $table->integer('incident_id')->unsigned(); + $table->integer('status'); + $table->longText('message'); + $table->integer('user_id')->unsigned(); + $table->timestamps(); + + $table->index('incident_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('incident_updates'); + } +} diff --git a/resources/assets/sass/status-page/_status-page.scss b/resources/assets/sass/status-page/_status-page.scss index 1e8360d34968..764313d1cdd0 100644 --- a/resources/assets/sass/status-page/_status-page.scss +++ b/resources/assets/sass/status-page/_status-page.scss @@ -276,6 +276,14 @@ body.status-page { line-height: 1.3em; } + i.icon { + font-size: 21px; + line-height: 24px; + text-align: center; + display: inline-block; + min-width: 20px; + } + &.group-name { background-color: $cachet_gray_light; padding: { diff --git a/resources/lang/en/cachet.php b/resources/lang/en/cachet.php index 945afaff8aec..d883a4b6cc68 100644 --- a/resources/lang/en/cachet.php +++ b/resources/lang/en/cachet.php @@ -33,6 +33,7 @@ 'stickied' => 'Stickied Incidents', 'scheduled' => 'Scheduled Maintenance', 'scheduled_at' => ', scheduled :timestamp', + 'posted' => 'Posted :timestamp', 'status' => [ 0 => 'Scheduled', // TODO: Hopefully remove this. 1 => 'Investigating', diff --git a/resources/lang/en/dashboard.php b/resources/lang/en/dashboard.php index 4b47968fbcc4..d019e51d3293 100644 --- a/resources/lang/en/dashboard.php +++ b/resources/lang/en/dashboard.php @@ -20,6 +20,7 @@ 'logged' => '{0} There are no incidents, good work.|You have logged one incident.|You have reported :count incidents.', 'incident-create-template' => 'Create Template', 'incident-templates' => 'Incident Templates', + 'updates' => '{0} Zero Updates|One Update|:count Updates', 'add' => [ 'title' => 'Report an incident', 'success' => 'Incident added.', @@ -34,6 +35,10 @@ 'success' => 'The incident has been deleted and will not show on your status page.', 'failure' => 'The incident could not be deleted, please try again.', ], + 'update' => [ + 'title' => 'Create new incident update', + 'subtitle' => 'Add an update to :incident', + ], // Incident templates 'templates' => [ diff --git a/resources/views/dashboard/incidents/index.blade.php b/resources/views/dashboard/incidents/index.blade.php index d10ccfc6252a..0e20f418dfff 100644 --- a/resources/views/dashboard/incidents/index.blade.php +++ b/resources/views/dashboard/incidents/index.blade.php @@ -24,13 +24,14 @@ @foreach($incidents as $incident)
- {{ $incident->name }} + {{ $incident->name }} {{ trans_choice('dashboard.incidents.updates', $incident->updates->count()) }} @if($incident->message)

{{ Str::words($incident->message, 5) }}

@endif
diff --git a/resources/views/dashboard/incidents/update.blade.php b/resources/views/dashboard/incidents/update.blade.php new file mode 100644 index 000000000000..b1e29028a1b2 --- /dev/null +++ b/resources/views/dashboard/incidents/update.blade.php @@ -0,0 +1,64 @@ +@extends('layout.dashboard') + +@section('content') +
+ + + {{ trans('dashboard.incidents.incidents') }} + + > {{ trans('dashboard.incidents.update.title') }} +
+
+
+
+ @include('dashboard.partials.errors') +

{!! trans('dashboard.incidents.update.subtitle', ['incident' => $incident->name]) !!}

+
+ +
+
+
+ + + + +
+
+ +
+ +
+
+
+ + id }}> + +
+
+ + {{ trans('forms.cancel') }} +
+
+
+
+
+
+@stop diff --git a/resources/views/partials/incident.blade.php b/resources/views/partials/incident.blade.php index 0d4ea113ecd2..d7ee4b6458f5 100644 --- a/resources/views/partials/incident.blade.php +++ b/resources/views/partials/incident.blade.php @@ -22,4 +22,15 @@
{!! $incident->formattedMessage !!}
+ @if($incident->updates->count()) +
+ @foreach($incident->updates as $update) + + {{ Str::limit($update->raw_message, 20) }} + {{ $update->created_at_diff }} + + + @endforeach +
+ @endif diff --git a/resources/views/partials/incidents.blade.php b/resources/views/partials/incidents.blade.php index 85088084ad1b..a668e8865195 100644 --- a/resources/views/partials/incidents.blade.php +++ b/resources/views/partials/incidents.blade.php @@ -5,8 +5,8 @@
-
- +
+
diff --git a/resources/views/single-incident.blade.php b/resources/views/single-incident.blade.php index 8ab2522bbc33..ff97c9338abe 100644 --- a/resources/views/single-incident.blade.php +++ b/resources/views/single-incident.blade.php @@ -7,22 +7,39 @@ @stop @section('content') -

{{ formatted_date($incident->created_at) }}

+

{{ $incident->name }} {{ formatted_date($incident->created_at) }}

+
+ +
+ {!! $incident->formattedMessage !!} +
+ +@if($incident->updates)
-
+ @foreach ($incident->updates as $index => $update) +
-
- +
+
- @include('partials.incident', ['incident' => $incident, 'with_link' => false]) +
+
+
+ {!! $update->formattedMessage !!} +
+
+ +
+ @endforeach
+@endif @stop diff --git a/tests/Api/IncidentUpdateTest.php b/tests/Api/IncidentUpdateTest.php new file mode 100644 index 000000000000..c23137a0e69b --- /dev/null +++ b/tests/Api/IncidentUpdateTest.php @@ -0,0 +1,102 @@ + + */ +class IncidentUpdateTest extends AbstractApiTestCase +{ + public function testGetIncidentUpdates() + { + $incident = factory('CachetHQ\Cachet\Models\Incident')->create(); + $updates = factory('CachetHQ\Cachet\Models\IncidentUpdate', 3)->create([ + 'incident_id' => $incident->id, + ]); + + $this->get("/api/v1/incidents/{$incident->id}/updates"); + + $this->assertResponseOk(); + + $this->seeJson(['id' => $updates[0]->id]); + $this->seeJson(['id' => $updates[1]->id]); + $this->seeJson(['id' => $updates[2]->id]); + } + + public function testGetInvalidIncidentUpdate() + { + $this->get('/api/v1/incidents/1/updates/1'); + + $this->assertResponseStatus(404); + } + + public function testPostIncidentUpdateUnauthorized() + { + $incident = factory('CachetHQ\Cachet\Models\Incident')->create(); + $this->post("/api/v1/incidents/{$incident->id}/updates"); + + $this->assertResponseStatus(401); + } + + public function testPostIncidentUpdateNoData() + { + $this->beUser(); + $incident = factory('CachetHQ\Cachet\Models\Incident')->create(); + + $this->post("/api/v1/incidents/{$incident->id}/updates"); + + $this->assertResponseStatus(400); + } + + public function testPostIncidentUpdate() + { + $this->beUser(); + $incident = factory('CachetHQ\Cachet\Models\Incident')->create(); + + $this->post("/api/v1/incidents/{$incident->id}/updates", [ + 'status' => 4, + 'message' => 'Incident fixed!', + ]); + + $this->assertResponseOk(); + + $this->seeJson(['incident_id' => $incident->id]); + } + + public function testPutIncidentUpdate() + { + $this->beUser(); + $incident = factory('CachetHQ\Cachet\Models\Incident')->create(); + $update = factory('CachetHQ\Cachet\Models\IncidentUpdate')->create(); + + $this->put("/api/v1/incidents/{$incident->id}/updates/{$update->id}", [ + 'message' => 'Message updated :smile:', + ]); + + $this->assertResponseOk(); + + $this->seeJson(['message' => 'Message updated :smile:']); + } + + public function testDeleteIncidentUpdate() + { + $this->beUser(); + $incident = factory('CachetHQ\Cachet\Models\Incident')->create(); + $update = factory('CachetHQ\Cachet\Models\IncidentUpdate')->create(); + + $this->delete("/api/v1/incidents/{$incident->id}/updates/{$update->id}"); + + $this->assertResponseStatus(204); + } +} diff --git a/tests/Bus/Commands/IncidentUpdate/AbstractIncidentUpdateCommandTest.php b/tests/Bus/Commands/IncidentUpdate/AbstractIncidentUpdateCommandTest.php new file mode 100644 index 000000000000..1bc30dcf2719 --- /dev/null +++ b/tests/Bus/Commands/IncidentUpdate/AbstractIncidentUpdateCommandTest.php @@ -0,0 +1,31 @@ + + */ +abstract class AbstractIncidentUpdateCommandTest extends AbstractTestCase +{ + use EventTrait; + + protected function getEventInterfaces() + { + return [IncidentUpdateEventInterface::class]; + } +} diff --git a/tests/Bus/Commands/IncidentUpdate/RemoveIncidentUpdateCommandTest.php b/tests/Bus/Commands/IncidentUpdate/RemoveIncidentUpdateCommandTest.php new file mode 100644 index 000000000000..b59503f13b8f --- /dev/null +++ b/tests/Bus/Commands/IncidentUpdate/RemoveIncidentUpdateCommandTest.php @@ -0,0 +1,41 @@ + + */ +class RemoveIncidentUpdateCommandTest extends AbstractTestCase +{ + use CommandTrait; + + protected function getObjectAndParams() + { + $params = ['incidentUpdate' => new IncidentUpdate()]; + $object = new RemoveIncidentUpdateCommand($params['incidentUpdate']); + + return compact('params', 'object'); + } + + protected function getHandlerClass() + { + return RemoveIncidentUpdateCommandHandler::class; + } +} diff --git a/tests/Bus/Commands/IncidentUpdate/ReportIncidentUpdateCommandTest.php b/tests/Bus/Commands/IncidentUpdate/ReportIncidentUpdateCommandTest.php new file mode 100644 index 000000000000..3a81fd188aa6 --- /dev/null +++ b/tests/Bus/Commands/IncidentUpdate/ReportIncidentUpdateCommandTest.php @@ -0,0 +1,52 @@ + + */ +class ReportIncidentUpdateCommandTest extends AbstractTestCase +{ + use CommandTrait; + + protected function getObjectAndParams() + { + $params = [ + 'incident' => new Incident(), + 'status' => 1, + 'message' => 'Foo', + 'user' => new User(), + ]; + $object = new ReportIncidentUpdateCommand($params['incident'], $params['status'], $params['message'], $params['user']); + + return compact('params', 'object'); + } + + protected function objectHasRules() + { + return true; + } + + protected function getHandlerClass() + { + return ReportIncidentUpdateCommandHandler::class; + } +} diff --git a/tests/Bus/Commands/IncidentUpdate/UpdateIncidentUpdateCommandTest.php b/tests/Bus/Commands/IncidentUpdate/UpdateIncidentUpdateCommandTest.php new file mode 100644 index 000000000000..a78d3845bb09 --- /dev/null +++ b/tests/Bus/Commands/IncidentUpdate/UpdateIncidentUpdateCommandTest.php @@ -0,0 +1,52 @@ + + */ +class UpdateIncidentUpdateCommandTest extends AbstractTestCase +{ + use CommandTrait; + + protected function getObjectAndParams() + { + $params = ['update' => new IncidentUpdate(), 'status' => 1, 'message' => 'Updating!', 'user' => new User()]; + $object = new UpdateIncidentUpdateCommand( + $params['update'], + $params['status'], + $params['message'], + $params['user'] + ); + + return compact('params', 'object'); + } + + protected function objectHasRules() + { + return true; + } + + protected function getHandlerClass() + { + return UpdateIncidentUpdateCommandHandler::class; + } +} diff --git a/tests/Bus/Events/IncidentUpdate/AbstractIncidentUpdateEventTestCase.php b/tests/Bus/Events/IncidentUpdate/AbstractIncidentUpdateEventTestCase.php new file mode 100644 index 000000000000..b0acab8a9776 --- /dev/null +++ b/tests/Bus/Events/IncidentUpdate/AbstractIncidentUpdateEventTestCase.php @@ -0,0 +1,26 @@ + new IncidentUpdate()]; + $object = new IncidentUpdateWasRemovedEvent($params['update']); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/IncidentUpdate/IncidentUpdateWasReportedEventTest.php b/tests/Bus/Events/IncidentUpdate/IncidentUpdateWasReportedEventTest.php new file mode 100644 index 000000000000..f7d014781823 --- /dev/null +++ b/tests/Bus/Events/IncidentUpdate/IncidentUpdateWasReportedEventTest.php @@ -0,0 +1,31 @@ + new IncidentUpdate()]; + $object = new IncidentUpdateWasReportedEvent($params['update']); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/IncidentUpdate/IncidentUpdateWasUpdatedEventTest.php b/tests/Bus/Events/IncidentUpdate/IncidentUpdateWasUpdatedEventTest.php new file mode 100644 index 000000000000..c576beeb9435 --- /dev/null +++ b/tests/Bus/Events/IncidentUpdate/IncidentUpdateWasUpdatedEventTest.php @@ -0,0 +1,31 @@ + new IncidentUpdate()]; + $object = new IncidentUpdateWasUpdatedEvent($params['update']); + + return compact('params', 'object'); + } +} diff --git a/tests/Http/Controllers/StatusPageControllerTest.php b/tests/Http/Controllers/StatusPageControllerTest.php index 6f9a3c68e1f7..9c36494a2ed3 100644 --- a/tests/Http/Controllers/StatusPageControllerTest.php +++ b/tests/Http/Controllers/StatusPageControllerTest.php @@ -38,16 +38,14 @@ protected function setUp() ->setupConfig(); } - /** @test */ - public function on_index_only_public_component_groups_are_shown_to_a_guest() + public function testIndexShowsOnlyPublicComponentGroupsToGues() { $this->visit('/') ->see(self::COMPONENT_GROUP_1_NAME) ->dontSee(self::COMPONENT_GROUP_2_NAME); } - /** @test */ - public function on_index_all_component_groups_are_displayed_to_logged_in_users() + public function testIndexShowsAllComponentGroupsToLoggedInUsers() { $this->signIn(); diff --git a/tests/Models/IncidentUpdateTest.php b/tests/Models/IncidentUpdateTest.php new file mode 100644 index 000000000000..e66e94d5be30 --- /dev/null +++ b/tests/Models/IncidentUpdateTest.php @@ -0,0 +1,31 @@ + + */ +class IncidentUpdateTest extends AbstractTestCase +{ + use ValidationTrait; + + public function testValidation() + { + $this->checkRules(new IncidentUpdate()); + } +}