Skip to content

Commit 32b3637

Browse files
jbrooksukclaude
andauthored
tests: fill coverage gaps in filters, listeners, middleware, model (#350)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fbf9b16 commit 32b3637

4 files changed

Lines changed: 299 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
use Cachet\Enums\ScheduleStatusEnum;
4+
use Cachet\Filters\ScheduleStatusFilter;
5+
use Cachet\Models\Schedule;
6+
7+
it('filters by a numeric status value', function () {
8+
$upcoming = Schedule::factory()->inTheFuture()->create();
9+
Schedule::factory()->inProgress()->create();
10+
Schedule::factory()->inThePast()->create();
11+
12+
$query = Schedule::query();
13+
14+
(new ScheduleStatusFilter)($query, ScheduleStatusEnum::upcoming->value, 'status');
15+
16+
expect($query->get())
17+
->toHaveCount(1)
18+
->first()->id->toBe($upcoming->id);
19+
});
20+
21+
it('filters by an enum instance', function () {
22+
Schedule::factory()->inTheFuture()->create();
23+
$inProgress = Schedule::factory()->inProgress()->create();
24+
Schedule::factory()->inThePast()->create();
25+
26+
$query = Schedule::query();
27+
28+
(new ScheduleStatusFilter)($query, ScheduleStatusEnum::in_progress, 'status');
29+
30+
expect($query->get())
31+
->toHaveCount(1)
32+
->first()->id->toBe($inProgress->id);
33+
});
34+
35+
it('leaves the query untouched when given an invalid value', function () {
36+
Schedule::factory()->inTheFuture()->create();
37+
Schedule::factory()->inProgress()->create();
38+
Schedule::factory()->inThePast()->create();
39+
40+
$query = Schedule::query();
41+
42+
(new ScheduleStatusFilter)($query, 999, 'status');
43+
44+
expect($query->get())->toHaveCount(3);
45+
});
46+
47+
it('leaves the query untouched when given a non-numeric string', function () {
48+
Schedule::factory()->inTheFuture()->create();
49+
Schedule::factory()->inProgress()->create();
50+
51+
$query = Schedule::query();
52+
53+
(new ScheduleStatusFilter)($query, 'not-a-status', 'status');
54+
55+
expect($query->get())->toHaveCount(2);
56+
});
57+
58+
it('filters by multiple statuses', function () {
59+
Schedule::factory()->inTheFuture()->create();
60+
$inProgress = Schedule::factory()->inProgress()->create();
61+
$completed = Schedule::factory()->completed()->create();
62+
63+
$query = Schedule::query();
64+
65+
(new ScheduleStatusFilter)($query, [
66+
ScheduleStatusEnum::in_progress->value,
67+
ScheduleStatusEnum::complete->value,
68+
], 'status');
69+
70+
expect($query->pluck('id')->all())
71+
->toHaveCount(2)
72+
->toContain($inProgress->id, $completed->id);
73+
});
74+
75+
it('ignores invalid entries in a multi-value filter', function () {
76+
Schedule::factory()->inTheFuture()->create();
77+
$inProgress = Schedule::factory()->inProgress()->create();
78+
Schedule::factory()->inThePast()->create();
79+
80+
$query = Schedule::query();
81+
82+
(new ScheduleStatusFilter)($query, [
83+
ScheduleStatusEnum::in_progress->value,
84+
999,
85+
'bogus',
86+
], 'status');
87+
88+
expect($query->get())
89+
->toHaveCount(1)
90+
->first()->id->toBe($inProgress->id);
91+
});
92+
93+
it('applies no status constraint when every multi-value entry is invalid', function () {
94+
Schedule::factory()->inTheFuture()->create();
95+
Schedule::factory()->inProgress()->create();
96+
Schedule::factory()->inThePast()->create();
97+
98+
$query = Schedule::query();
99+
100+
(new ScheduleStatusFilter)($query, [999, 'bogus'], 'status');
101+
102+
expect($query->get())->toHaveCount(3);
103+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
use Cachet\Http\Middleware\SetAppLocale;
4+
use Illuminate\Http\Request;
5+
use Workbench\App\User;
6+
7+
beforeEach(function () {
8+
config(['cachet.supported_locales' => [
9+
'en' => 'English',
10+
'fr' => 'Français',
11+
'de' => 'Deutsch',
12+
]]);
13+
14+
app()->setLocale('en');
15+
});
16+
17+
it('leaves the app locale untouched when no user is authenticated', function () {
18+
$request = Request::create('/');
19+
20+
(new SetAppLocale)->handle($request, fn () => null);
21+
22+
expect(app()->getLocale())->toBe('en');
23+
});
24+
25+
it('uses the authenticated user\'s preferred locale when available', function () {
26+
$user = User::factory()->create(['preferred_locale' => 'fr']);
27+
$request = Request::create('/');
28+
$request->setUserResolver(fn () => $user);
29+
30+
(new SetAppLocale)->handle($request, fn () => null);
31+
32+
expect(app()->getLocale())->toBe('fr');
33+
});
34+
35+
it('falls back to the request preferred language when the user has no preferred locale', function () {
36+
$user = User::factory()->create(['preferred_locale' => null]);
37+
$request = Request::create('/', 'GET', server: ['HTTP_ACCEPT_LANGUAGE' => 'de-DE,de;q=0.9,en;q=0.8']);
38+
$request->setUserResolver(fn () => $user);
39+
40+
(new SetAppLocale)->handle($request, fn () => null);
41+
42+
expect(app()->getLocale())->toBe('de');
43+
});
44+
45+
it('passes the request through to the next middleware', function () {
46+
$request = Request::create('/');
47+
$called = false;
48+
49+
(new SetAppLocale)->handle($request, function ($passed) use (&$called, $request) {
50+
expect($passed)->toBe($request);
51+
$called = true;
52+
53+
return 'ok';
54+
});
55+
56+
expect($called)->toBeTrue();
57+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
use Cachet\Enums\WebhookEventEnum;
4+
use Cachet\Listeners\WebhookCallEventListener;
5+
use Cachet\Models\WebhookAttempt;
6+
use Cachet\Models\WebhookSubscription;
7+
use GuzzleHttp\Psr7\Response;
8+
use GuzzleHttp\TransferStats;
9+
use Spatie\WebhookServer\Events\WebhookCallFailedEvent;
10+
use Spatie\WebhookServer\Events\WebhookCallSucceededEvent;
11+
12+
function makeWebhookEvent(string $eventClass, WebhookSubscription $subscription, ?Response $response = null, ?TransferStats $stats = null, int $attempt = 1): object
13+
{
14+
return new $eventClass(
15+
httpVerb: 'POST',
16+
webhookUrl: $subscription->url,
17+
payload: ['event' => WebhookEventEnum::component_created->value, 'body' => []],
18+
headers: [],
19+
meta: [
20+
'subscription_id' => $subscription->id,
21+
'event' => WebhookEventEnum::component_created->value,
22+
],
23+
tags: [],
24+
attempt: $attempt,
25+
response: $response,
26+
errorType: null,
27+
errorMessage: null,
28+
uuid: 'test-uuid',
29+
transferStats: $stats,
30+
);
31+
}
32+
33+
it('records a successful webhook attempt', function () {
34+
$subscription = WebhookSubscription::factory()->create();
35+
36+
$event = makeWebhookEvent(
37+
WebhookCallSucceededEvent::class,
38+
$subscription,
39+
response: new Response(200),
40+
stats: new TransferStats(
41+
new GuzzleHttp\Psr7\Request('POST', $subscription->url),
42+
new Response(200),
43+
0.42,
44+
),
45+
);
46+
47+
app(WebhookCallEventListener::class)->handle($event);
48+
49+
$attempt = WebhookAttempt::query()->firstOrFail();
50+
51+
expect($attempt)
52+
->subscription_id->toBe($subscription->id)
53+
->event->toBe(WebhookEventEnum::component_created)
54+
->attempt->toBe(1)
55+
->response_code->toBe(200)
56+
->transfer_time->toEqual(0.42);
57+
58+
expect(json_decode($attempt->payload, true))
59+
->toBe(['event' => WebhookEventEnum::component_created->value, 'body' => []]);
60+
});
61+
62+
it('records a failed webhook attempt without response or transfer stats', function () {
63+
$subscription = WebhookSubscription::factory()->create();
64+
65+
$event = makeWebhookEvent(
66+
WebhookCallFailedEvent::class,
67+
$subscription,
68+
attempt: 3,
69+
);
70+
71+
app(WebhookCallEventListener::class)->handle($event);
72+
73+
$attempt = WebhookAttempt::query()->firstOrFail();
74+
75+
expect($attempt)
76+
->subscription_id->toBe($subscription->id)
77+
->attempt->toBe(3)
78+
->response_code->toBeNull()
79+
->transfer_time->toBeNull();
80+
});
81+
82+
it('recalculates the subscription success rate after recording an attempt', function () {
83+
$subscription = WebhookSubscription::factory()->create(['success_rate_24h' => 0]);
84+
85+
$event = makeWebhookEvent(
86+
WebhookCallSucceededEvent::class,
87+
$subscription,
88+
response: new Response(204),
89+
);
90+
91+
app(WebhookCallEventListener::class)->handle($event);
92+
93+
expect($subscription->fresh()->success_rate_24h)->toBe('100.00%');
94+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
use Cachet\Models\WebhookAttempt;
4+
use Cachet\Models\WebhookSubscription;
5+
use Illuminate\Support\Carbon;
6+
7+
it('is successful when the response code is 2xx', function (int $code) {
8+
$attempt = WebhookAttempt::factory()->make(['response_code' => $code]);
9+
10+
expect($attempt->isSuccess())->toBeTrue();
11+
})->with([200, 201, 204, 299]);
12+
13+
it('is not successful when the response code is outside 2xx', function (?int $code) {
14+
$attempt = WebhookAttempt::factory()->make(['response_code' => $code]);
15+
16+
expect($attempt->isSuccess())->toBeFalse();
17+
})->with([100, 199, 300, 400, 500, null]);
18+
19+
it('scopes to successful attempts', function () {
20+
$subscription = WebhookSubscription::factory()->create();
21+
WebhookAttempt::factory()->count(2)->create(['subscription_id' => $subscription->id, 'response_code' => 200]);
22+
WebhookAttempt::factory()->create(['subscription_id' => $subscription->id, 'response_code' => 500]);
23+
WebhookAttempt::factory()->create(['subscription_id' => $subscription->id, 'response_code' => 404]);
24+
25+
expect(WebhookAttempt::query()->whereSuccessful()->count())->toBe(2);
26+
});
27+
28+
it('prunes attempts older than the configured retention window', function () {
29+
config(['cachet.webhooks.logs.prune_logs_after_days' => 7]);
30+
$subscription = WebhookSubscription::factory()->create();
31+
32+
$fresh = WebhookAttempt::factory()->create([
33+
'subscription_id' => $subscription->id,
34+
'created_at' => Carbon::now()->subDays(1),
35+
]);
36+
$stale = WebhookAttempt::factory()->create([
37+
'subscription_id' => $subscription->id,
38+
'created_at' => Carbon::now()->subDays(30),
39+
]);
40+
41+
$prunable = (new WebhookAttempt)->prunable()->pluck('id');
42+
43+
expect($prunable->all())->toBe([$stale->id])
44+
->and($prunable)->not->toContain($fresh->id);
45+
});

0 commit comments

Comments
 (0)