Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions tests/Unit/Filters/ScheduleStatusFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

use Cachet\Enums\ScheduleStatusEnum;
use Cachet\Filters\ScheduleStatusFilter;
use Cachet\Models\Schedule;

it('filters by a numeric status value', function () {
$upcoming = Schedule::factory()->inTheFuture()->create();
Schedule::factory()->inProgress()->create();
Schedule::factory()->inThePast()->create();

$query = Schedule::query();

(new ScheduleStatusFilter)($query, ScheduleStatusEnum::upcoming->value, 'status');

expect($query->get())
->toHaveCount(1)
->first()->id->toBe($upcoming->id);
});

it('filters by an enum instance', function () {
Schedule::factory()->inTheFuture()->create();
$inProgress = Schedule::factory()->inProgress()->create();
Schedule::factory()->inThePast()->create();

$query = Schedule::query();

(new ScheduleStatusFilter)($query, ScheduleStatusEnum::in_progress, 'status');

expect($query->get())
->toHaveCount(1)
->first()->id->toBe($inProgress->id);
});

it('leaves the query untouched when given an invalid value', function () {
Schedule::factory()->inTheFuture()->create();
Schedule::factory()->inProgress()->create();
Schedule::factory()->inThePast()->create();

$query = Schedule::query();

(new ScheduleStatusFilter)($query, 999, 'status');

expect($query->get())->toHaveCount(3);
});

it('leaves the query untouched when given a non-numeric string', function () {
Schedule::factory()->inTheFuture()->create();
Schedule::factory()->inProgress()->create();

$query = Schedule::query();

(new ScheduleStatusFilter)($query, 'not-a-status', 'status');

expect($query->get())->toHaveCount(2);
});

it('filters by multiple statuses', function () {
Schedule::factory()->inTheFuture()->create();
$inProgress = Schedule::factory()->inProgress()->create();
$completed = Schedule::factory()->completed()->create();

$query = Schedule::query();

(new ScheduleStatusFilter)($query, [
ScheduleStatusEnum::in_progress->value,
ScheduleStatusEnum::complete->value,
], 'status');

expect($query->pluck('id')->all())
->toHaveCount(2)
->toContain($inProgress->id, $completed->id);
});

it('ignores invalid entries in a multi-value filter', function () {
Schedule::factory()->inTheFuture()->create();
$inProgress = Schedule::factory()->inProgress()->create();
Schedule::factory()->inThePast()->create();

$query = Schedule::query();

(new ScheduleStatusFilter)($query, [
ScheduleStatusEnum::in_progress->value,
999,
'bogus',
], 'status');

expect($query->get())
->toHaveCount(1)
->first()->id->toBe($inProgress->id);
});

it('applies no status constraint when every multi-value entry is invalid', function () {
Schedule::factory()->inTheFuture()->create();
Schedule::factory()->inProgress()->create();
Schedule::factory()->inThePast()->create();

$query = Schedule::query();

(new ScheduleStatusFilter)($query, [999, 'bogus'], 'status');

expect($query->get())->toHaveCount(3);
});
57 changes: 57 additions & 0 deletions tests/Unit/Http/Middleware/SetAppLocaleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

use Cachet\Http\Middleware\SetAppLocale;
use Illuminate\Http\Request;
use Workbench\App\User;

beforeEach(function () {
config(['cachet.supported_locales' => [
'en' => 'English',
'fr' => 'Français',
'de' => 'Deutsch',
]]);

app()->setLocale('en');
});

it('leaves the app locale untouched when no user is authenticated', function () {
$request = Request::create('/');

(new SetAppLocale)->handle($request, fn () => null);

expect(app()->getLocale())->toBe('en');
});

it('uses the authenticated user\'s preferred locale when available', function () {
$user = User::factory()->create(['preferred_locale' => 'fr']);
$request = Request::create('/');
$request->setUserResolver(fn () => $user);

(new SetAppLocale)->handle($request, fn () => null);

expect(app()->getLocale())->toBe('fr');
});

it('falls back to the request preferred language when the user has no preferred locale', function () {
$user = User::factory()->create(['preferred_locale' => null]);
$request = Request::create('/', 'GET', server: ['HTTP_ACCEPT_LANGUAGE' => 'de-DE,de;q=0.9,en;q=0.8']);
$request->setUserResolver(fn () => $user);

(new SetAppLocale)->handle($request, fn () => null);

expect(app()->getLocale())->toBe('de');
});

it('passes the request through to the next middleware', function () {
$request = Request::create('/');
$called = false;

(new SetAppLocale)->handle($request, function ($passed) use (&$called, $request) {
expect($passed)->toBe($request);
$called = true;

return 'ok';
});

expect($called)->toBeTrue();
});
94 changes: 94 additions & 0 deletions tests/Unit/Listeners/WebhookCallEventListenerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

use Cachet\Enums\WebhookEventEnum;
use Cachet\Listeners\WebhookCallEventListener;
use Cachet\Models\WebhookAttempt;
use Cachet\Models\WebhookSubscription;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\TransferStats;
use Spatie\WebhookServer\Events\WebhookCallFailedEvent;
use Spatie\WebhookServer\Events\WebhookCallSucceededEvent;

function makeWebhookEvent(string $eventClass, WebhookSubscription $subscription, ?Response $response = null, ?TransferStats $stats = null, int $attempt = 1): object
{
return new $eventClass(
httpVerb: 'POST',
webhookUrl: $subscription->url,
payload: ['event' => WebhookEventEnum::component_created->value, 'body' => []],
headers: [],
meta: [
'subscription_id' => $subscription->id,
'event' => WebhookEventEnum::component_created->value,
],
tags: [],
attempt: $attempt,
response: $response,
errorType: null,
errorMessage: null,
uuid: 'test-uuid',
transferStats: $stats,
);
}

it('records a successful webhook attempt', function () {
$subscription = WebhookSubscription::factory()->create();

$event = makeWebhookEvent(
WebhookCallSucceededEvent::class,
$subscription,
response: new Response(200),
stats: new TransferStats(
new GuzzleHttp\Psr7\Request('POST', $subscription->url),
new Response(200),
0.42,
),
);

app(WebhookCallEventListener::class)->handle($event);

$attempt = WebhookAttempt::query()->firstOrFail();

expect($attempt)
->subscription_id->toBe($subscription->id)
->event->toBe(WebhookEventEnum::component_created)
->attempt->toBe(1)
->response_code->toBe(200)
->transfer_time->toEqual(0.42);

expect(json_decode($attempt->payload, true))
->toBe(['event' => WebhookEventEnum::component_created->value, 'body' => []]);
});

it('records a failed webhook attempt without response or transfer stats', function () {
$subscription = WebhookSubscription::factory()->create();

$event = makeWebhookEvent(
WebhookCallFailedEvent::class,
$subscription,
attempt: 3,
);

app(WebhookCallEventListener::class)->handle($event);

$attempt = WebhookAttempt::query()->firstOrFail();

expect($attempt)
->subscription_id->toBe($subscription->id)
->attempt->toBe(3)
->response_code->toBeNull()
->transfer_time->toBeNull();
});

it('recalculates the subscription success rate after recording an attempt', function () {
$subscription = WebhookSubscription::factory()->create(['success_rate_24h' => 0]);

$event = makeWebhookEvent(
WebhookCallSucceededEvent::class,
$subscription,
response: new Response(204),
);

app(WebhookCallEventListener::class)->handle($event);

expect($subscription->fresh()->success_rate_24h)->toBe('100.00%');
});
45 changes: 45 additions & 0 deletions tests/Unit/Models/WebhookAttemptTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

use Cachet\Models\WebhookAttempt;
use Cachet\Models\WebhookSubscription;
use Illuminate\Support\Carbon;

it('is successful when the response code is 2xx', function (int $code) {
$attempt = WebhookAttempt::factory()->make(['response_code' => $code]);

expect($attempt->isSuccess())->toBeTrue();
})->with([200, 201, 204, 299]);

it('is not successful when the response code is outside 2xx', function (?int $code) {
$attempt = WebhookAttempt::factory()->make(['response_code' => $code]);

expect($attempt->isSuccess())->toBeFalse();
})->with([100, 199, 300, 400, 500, null]);

it('scopes to successful attempts', function () {
$subscription = WebhookSubscription::factory()->create();
WebhookAttempt::factory()->count(2)->create(['subscription_id' => $subscription->id, 'response_code' => 200]);
WebhookAttempt::factory()->create(['subscription_id' => $subscription->id, 'response_code' => 500]);
WebhookAttempt::factory()->create(['subscription_id' => $subscription->id, 'response_code' => 404]);

expect(WebhookAttempt::query()->whereSuccessful()->count())->toBe(2);
});

it('prunes attempts older than the configured retention window', function () {
config(['cachet.webhooks.logs.prune_logs_after_days' => 7]);
$subscription = WebhookSubscription::factory()->create();

$fresh = WebhookAttempt::factory()->create([
'subscription_id' => $subscription->id,
'created_at' => Carbon::now()->subDays(1),
]);
$stale = WebhookAttempt::factory()->create([
'subscription_id' => $subscription->id,
'created_at' => Carbon::now()->subDays(30),
]);

$prunable = (new WebhookAttempt)->prunable()->pluck('id');

expect($prunable->all())->toBe([$stale->id])
->and($prunable)->not->toContain($fresh->id);
});
Loading