diff --git a/src/Actions/Incident/CreateIncident.php b/src/Actions/Incident/CreateIncident.php index 464b5911..0901cb72 100644 --- a/src/Actions/Incident/CreateIncident.php +++ b/src/Actions/Incident/CreateIncident.php @@ -3,6 +3,7 @@ namespace Cachet\Actions\Incident; use Cachet\Data\Requests\Incident\CreateIncidentRequestData; +use Cachet\Data\Requests\Incident\IncidentComponentRequestData; use Cachet\Models\Component; use Cachet\Models\Incident; use Cachet\Models\IncidentTemplate; @@ -26,10 +27,21 @@ public function handle(CreateIncidentRequestData $data): Incident // @todo Dispatch notification that incident was created. - return Incident::create(array_merge( + return tap(Incident::create(array_merge( ['guid' => Str::uuid()], - $data->toArray() - )); + $data->except('components')->toArray() + )), function (Incident $incident) use ($data) { + if (! $data->components) { + return; + } + + $components = collect($data->components)->map(fn (IncidentComponentRequestData $component) => [ + 'component_id' => $component->id, + 'component_status' => $component->status, + ])->all(); + + $incident->components()->sync($components); + }); } /** diff --git a/src/Data/Requests/Incident/CreateIncidentRequestData.php b/src/Data/Requests/Incident/CreateIncidentRequestData.php index 38df9993..3ea71fc1 100644 --- a/src/Data/Requests/Incident/CreateIncidentRequestData.php +++ b/src/Data/Requests/Incident/CreateIncidentRequestData.php @@ -7,6 +7,7 @@ use Cachet\Enums\IncidentStatusEnum; use Cachet\Models\Component; use Illuminate\Validation\Rule; +use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\Validation\Enum; use Spatie\LaravelData\Attributes\Validation\Exists; use Spatie\LaravelData\Attributes\Validation\Max; @@ -33,6 +34,8 @@ public function __construct( public readonly ?int $componentId = null, #[Enum(ComponentStatusEnum::class)] public readonly ?ComponentStatusEnum $componentStatus = null, + #[DataCollectionOf(IncidentComponentRequestData::class)] + public readonly ?array $components = null, ) {} public static function rules(ValidationContext $context): array @@ -49,6 +52,9 @@ public static function rules(ValidationContext $context): array 'template_vars' => ['array'], 'component_id' => [Rule::exists('components', 'id')], 'component_status' => ['nullable', Rule::enum(ComponentStatusEnum::class), 'required_with:component_id'], + 'components' => ['array'], + 'components.*.id' => ['required_with:components', 'int', 'exists:components,id'], + 'components.*.status' => ['required_with:components', 'int', Rule::enum(ComponentStatusEnum::class)], ]; } @@ -66,6 +72,7 @@ public function withMessage(string $message): self templateVars: $this->templateVars, componentId: $this->componentId, componentStatus: $this->componentStatus, + components: $this->components, ); } } diff --git a/src/Data/Requests/Incident/IncidentComponentRequestData.php b/src/Data/Requests/Incident/IncidentComponentRequestData.php new file mode 100644 index 00000000..20da6070 --- /dev/null +++ b/src/Data/Requests/Incident/IncidentComponentRequestData.php @@ -0,0 +1,17 @@ + $this->updated_at?->diffForHumans(), 'string' => $this->updated_at?->toDateTimeString(), ], + 'pivot' => $this->when(isset($this->pivot) && isset($this->pivot->component_status), function () { + return [ + 'component_status' => [ + 'human' => $this->pivot->component_status->getLabel(), + 'value' => $this->pivot->component_status->value, + ], + ]; + }), ]; } diff --git a/src/Models/Component.php b/src/Models/Component.php index 1887d49b..01fabab6 100644 --- a/src/Models/Component.php +++ b/src/Models/Component.php @@ -32,6 +32,7 @@ * @property bool $enabled * @property array $meta * @property ?ComponentGroup $componentGroup + * @property-read IncidentComponent|null $pivot * * @method static Builder|static disabled() * @method static Builder|static enabled() diff --git a/tests/Feature/Api/IncidentTest.php b/tests/Feature/Api/IncidentTest.php index 9ddf7926..15fdb0ec 100644 --- a/tests/Feature/Api/IncidentTest.php +++ b/tests/Feature/Api/IncidentTest.php @@ -1,8 +1,10 @@ create(), ['incidents.manage']); + + [$componentA, $componentB] = Component::factory(2)->create(); + + $response = postJson('/status/api/incidents?include=components', [ + 'name' => 'Incident With Components', + 'message' => 'Something went wrong.', + 'status' => IncidentStatusEnum::investigating->value, + 'components' => [ + ['id' => $componentA->id, 'status' => ComponentStatusEnum::partial_outage->value], + ['id' => $componentB->id, 'status' => ComponentStatusEnum::major_outage->value], + ], + ]); + + $response->assertCreated(); + + $incident = Incident::where('name', 'Incident With Components')->first(); + + expect($incident)->not->toBeNull(); + + $this->assertDatabaseHas('incident_components', [ + 'incident_id' => $incident->id, + 'component_id' => $componentA->id, + 'component_status' => ComponentStatusEnum::partial_outage->value, + ]); + + $this->assertDatabaseHas('incident_components', [ + 'incident_id' => $incident->id, + 'component_id' => $componentB->id, + 'component_status' => ComponentStatusEnum::major_outage->value, + ]); + + $included = collect($response->json('included')); + + expect($included)->toHaveCount(2); + + $componentAResource = $included->firstWhere('id', (string) $componentA->id); + $componentBResource = $included->firstWhere('id', (string) $componentB->id); + + expect($componentAResource['attributes']['pivot']['component_status']['value']) + ->toBe(ComponentStatusEnum::partial_outage->value); + + expect($componentBResource['attributes']['pivot']['component_status']['value']) + ->toBe(ComponentStatusEnum::major_outage->value); +}); + it('can create an incident with a template', function () { Sanctum::actingAs(User::factory()->create(), ['incidents.manage']); diff --git a/tests/Unit/Actions/Incident/CreateIncidentTest.php b/tests/Unit/Actions/Incident/CreateIncidentTest.php index 25309014..92b25db7 100644 --- a/tests/Unit/Actions/Incident/CreateIncidentTest.php +++ b/tests/Unit/Actions/Incident/CreateIncidentTest.php @@ -2,8 +2,10 @@ use Cachet\Actions\Incident\CreateIncident; use Cachet\Data\Requests\Incident\CreateIncidentRequestData; +use Cachet\Enums\ComponentStatusEnum; use Cachet\Enums\IncidentStatusEnum; use Cachet\Events\Incidents\IncidentCreated; +use Cachet\Models\Component; use Cachet\Models\IncidentTemplate; use Illuminate\Support\Facades\Event; @@ -90,3 +92,23 @@ Event::assertDispatched(IncidentCreated::class, fn ($event) => $event->incident->is($incident)); }); + +it('attaches provided components to the incident', function () { + [$componentA, $componentB] = Component::factory(2)->create(); + + $data = CreateIncidentRequestData::from([ + 'name' => 'My Incident', + 'message' => 'This is an incident message.', + 'components' => [ + ['id' => $componentA->id, 'status' => ComponentStatusEnum::performance_issues->value], + ['id' => $componentB->id, 'status' => ComponentStatusEnum::partial_outage->value], + ], + ]); + + $incident = app(CreateIncident::class)->handle($data); + + expect($incident->components)->toHaveCount(2) + ->and($incident->components->pluck('pivot.component_status')->all()) + ->toContain(ComponentStatusEnum::performance_issues) + ->toContain(ComponentStatusEnum::partial_outage); +});