diff --git a/tests/Architecture/CommandsTest.php b/tests/Architecture/CommandsTest.php index 3adf50e0..c5179ebd 100644 --- a/tests/Architecture/CommandsTest.php +++ b/tests/Architecture/CommandsTest.php @@ -3,7 +3,7 @@ use Illuminate\Console\Command; test('commands test') - ->expect('Cachet\Console\Commands') + ->expect('Cachet\Commands') ->toBeClasses() ->toExtend(Command::class) ->toHaveSuffix('Command'); diff --git a/tests/Architecture/FiltersTest.php b/tests/Architecture/FiltersTest.php new file mode 100644 index 00000000..33f6a2d5 --- /dev/null +++ b/tests/Architecture/FiltersTest.php @@ -0,0 +1,9 @@ +expect('Cachet\Filters') + ->toBeClasses() + ->toImplement(Filter::class) + ->toHaveSuffix('Filter'); diff --git a/tests/Architecture/McpTest.php b/tests/Architecture/McpTest.php new file mode 100644 index 00000000..8a061bff --- /dev/null +++ b/tests/Architecture/McpTest.php @@ -0,0 +1,8 @@ +expect('Cachet\Mcp\Tools') + ->toBeClasses() + ->toExtend(Tool::class); diff --git a/tests/Architecture/NotificationsTest.php b/tests/Architecture/NotificationsTest.php new file mode 100644 index 00000000..9c2f0cd8 --- /dev/null +++ b/tests/Architecture/NotificationsTest.php @@ -0,0 +1,13 @@ +expect('Cachet\Notifications') + ->toBeClasses() + ->toExtend(Notification::class) + ->toImplement(ShouldQueue::class) + ->toHaveSuffix('Notification') + ->ignoring(VerifySubscriberEmail::class); diff --git a/tests/Architecture/QueryBuildersTest.php b/tests/Architecture/QueryBuildersTest.php new file mode 100644 index 00000000..e8543563 --- /dev/null +++ b/tests/Architecture/QueryBuildersTest.php @@ -0,0 +1,9 @@ +expect('Cachet\QueryBuilders') + ->toBeClasses() + ->toExtend(Builder::class) + ->toHaveSuffix('Builder'); diff --git a/tests/Architecture/RenderersTest.php b/tests/Architecture/RenderersTest.php new file mode 100644 index 00000000..a459939c --- /dev/null +++ b/tests/Architecture/RenderersTest.php @@ -0,0 +1,14 @@ +expect([ + BladeRenderer::class, + TwigRenderer::class, + ]) + ->toBeClasses() + ->toImplement(Renderer::class) + ->toHaveSuffix('Renderer'); diff --git a/tests/Architecture/SettingsTest.php b/tests/Architecture/SettingsTest.php new file mode 100644 index 00000000..c86dc78d --- /dev/null +++ b/tests/Architecture/SettingsTest.php @@ -0,0 +1,9 @@ +expect('Cachet\Settings') + ->toBeClasses() + ->toExtend(Settings::class) + ->toHaveSuffix('Settings'); diff --git a/tests/Unit/Actions/Webhook/DispatchWebhooksTest.php b/tests/Unit/Actions/Webhook/DispatchWebhooksTest.php new file mode 100644 index 00000000..30ccac80 --- /dev/null +++ b/tests/Unit/Actions/Webhook/DispatchWebhooksTest.php @@ -0,0 +1,59 @@ +create(); + + $matching = WebhookSubscription::factory() + ->selectedEvents([WebhookEventEnum::component_created]) + ->create(); + + WebhookSubscription::factory() + ->selectedEvents([WebhookEventEnum::component_updated]) + ->create(); + + app(DispatchWebhooks::class)->handle(new ComponentCreated($component)); + + Queue::assertPushed(CallWebhookJob::class, function (CallWebhookJob $job) use ($component, $matching): bool { + return $job->webhookUrl === $matching->url + && $job->payload === [ + 'event' => WebhookEventEnum::component_created->value, + 'body' => $component->toArray(), + ] + && $job->meta === [ + 'subscription_id' => $matching->id, + 'event' => WebhookEventEnum::component_created->value, + ]; + }); + + Queue::assertPushed(CallWebhookJob::class, 1); +}); + +it('dispatches subscriptions configured for all events', function () { + WebhookSubscription::factory()->allEvents()->create(); + + Component::factory()->create(); + + Queue::assertPushed(CallWebhookJob::class, 1); +}); + +it('does not dispatch when no subscriptions match the event', function () { + WebhookSubscription::factory() + ->selectedEvents([WebhookEventEnum::component_updated]) + ->create(); + + app(DispatchWebhooks::class)->handle(new ComponentCreated(Component::factory()->create())); + + Queue::assertNothingPushed(); +}); diff --git a/tests/Unit/Data/FlexibleDateTimeCastTest.php b/tests/Unit/Data/FlexibleDateTimeCastTest.php new file mode 100644 index 00000000..126ae317 --- /dev/null +++ b/tests/Unit/Data/FlexibleDateTimeCastTest.php @@ -0,0 +1,41 @@ +cast( + mock(DataProperty::class), + $value, + [], + mock(CreationContext::class), + ); + + expect($result) + ->toBeInstanceOf(Carbon::class) + ->and($result->toIso8601String())->toBe($expected); +})->with([ + 'date time' => [new DateTimeImmutable('2026-08-10T12:30:00+00:00'), '2026-08-10T12:30:00+00:00'], + 'ISO 8601 string' => ['2026-08-10T13:30:00+01:00', '2026-08-10T12:30:00+00:00'], + 'timestamp' => [1786365000, '2026-08-10T12:30:00+00:00'], +]); + +it('returns uncastable for unsupported or invalid values', function (mixed $value) { + $result = (new FlexibleDateTimeCast)->cast( + mock(DataProperty::class), + $value, + [], + mock(CreationContext::class), + ); + + expect($result)->toBeInstanceOf(Uncastable::class); +})->with([ + 'null' => null, + 'boolean' => true, + 'array' => [[]], + 'object' => [new stdClass], + 'invalid string' => 'not a date', +]); diff --git a/tests/Unit/Models/HasMetaTest.php b/tests/Unit/Models/HasMetaTest.php new file mode 100644 index 00000000..db451e97 --- /dev/null +++ b/tests/Unit/Models/HasMetaTest.php @@ -0,0 +1,52 @@ +create(); + + $component->syncMeta([ + 'region' => 'eu-west', + 'priority' => 3, + 'enabled' => true, + 'options' => ['replicas' => 2], + ]); + + expect($component->metaValues())->toBe([ + 'region' => 'eu-west', + 'priority' => 3, + 'enabled' => true, + 'options' => ['replicas' => 2], + ]); +}); + +it('updates supplied metadata and removes omitted keys', function () { + $component = Component::factory()->create(); + $component->syncMeta(['region' => 'eu-west', 'stale' => true]); + + $component->load('meta'); + $component->syncMeta(['region' => 'us-east']); + + expect($component->relationLoaded('meta'))->toBeFalse() + ->and($component->metaValues())->toBe(['region' => 'us-east']); +}); + +it('retains metadata when its owner is soft deleted', function () { + $component = Component::factory()->create(); + $component->syncMeta(['region' => 'eu-west']); + $metaId = $component->meta()->sole()->id; + + $component->delete(); + + $this->assertDatabaseHas('meta', ['id' => $metaId]); +}); + +it('deletes metadata when its owner is force deleted', function () { + $component = Component::factory()->create(); + $component->syncMeta(['region' => 'eu-west']); + $metaId = $component->meta()->sole()->id; + + $component->forceDelete(); + + $this->assertDatabaseMissing('meta', ['id' => $metaId]); +});