test(timetable): Phase C — 57 new tests covering session 2 surface
Nine test files under tests/Feature/Artist/ exercising:
ArtistEngagementStateMachineTest 8 tests — terminal blocks, conditional
gates (Option/Contracted), full happy
path, cancel cascade
LaneCascadeServiceTest 5 tests — simple move, cascade-bump,
version mismatch, park, unpark
BumaVatCalculationTest 6 tests — D26 formula coverage:
Organisation/BookingAgency/NotApplicable,
VAT off, breakdown sum, zero fee
DemoteExpiredOptionsTest 4 tests — expired demote, future
untouched, non-Option untouched, run
twice → single option_expired entry
IdempotencyKey60sRedisTest 4 tests — missing header 400, first
cache, replay header, failed not cached
ArtistControllerTest 8 tests — index/create/destroy + cross-
tenant + duplicate detection + restore
StageControllerTest 7 tests — create + uniqueness, destroy
cascade-park, reorder permutation,
replaceDays orphan 409 + force_orphan
ArtistEngagementControllerTest 5 tests — index/create/update/destroy +
422 on invalid status transition
TimetableMoveControllerTest 3 tests — happy path with idempotency
header, missing header → 400, version
mismatch → 409
ArtistPolicyTest 6 tests — role checks, cross-tenant
denial, super_admin bypass, D27 active-
engagement gate
ActivityLogShapeTest 4 tests — performance.moved cascade
props, status_changed vs cancelled,
stage.day_added subject + props,
stage.reordered on Event subject
Bug fixes surfaced by Phase C:
Schema reality: events table uses `start_date`/`end_date` (date), not
`start_at`/`end_at`. Updated WithinEventBounds rule and the two stage_day
resolvers (LaneCascadeService + MoveTimetablePerformanceRequest) to
query the actual columns. ArtistResource.engagements_summary upcoming
filter likewise.
performances table has no organisation_id column (FK-chain via
engagement_id). Removed the org-id filter from the Rule::exists in
MoveTimetablePerformanceRequest; cross-tenant is caught by the policy
in TimetableMoveController.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
98
api/tests/Feature/Artist/IdempotencyKey60sRedisTest.php
Normal file
98
api/tests/Feature/Artist/IdempotencyKey60sRedisTest.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Artist;
|
||||
|
||||
use App\Http\Middleware\IdempotencyKey60sRedis;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class IdempotencyKey60sRedisTest extends TestCase
|
||||
{
|
||||
public function test_missing_header_returns_400(): void
|
||||
{
|
||||
$middleware = new IdempotencyKey60sRedis;
|
||||
$request = Request::create('/x', 'POST');
|
||||
|
||||
$response = $middleware->handle($request, fn () => response('ok'));
|
||||
|
||||
$this->assertSame(400, $response->getStatusCode());
|
||||
$this->assertStringContainsString('idempotency_key_required', (string) $response->getContent());
|
||||
}
|
||||
|
||||
public function test_first_request_caches_and_passes_through(): void
|
||||
{
|
||||
Cache::flush();
|
||||
|
||||
$middleware = new IdempotencyKey60sRedis;
|
||||
$request = Request::create('/x', 'POST');
|
||||
$request->headers->set('Idempotency-Key', 'abc-123');
|
||||
|
||||
$count = 0;
|
||||
$response = $middleware->handle($request, function () use (&$count): Response {
|
||||
$count++;
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
});
|
||||
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame(1, $count);
|
||||
}
|
||||
|
||||
public function test_replayed_request_returns_cached_body_with_replayed_header(): void
|
||||
{
|
||||
Cache::flush();
|
||||
|
||||
$middleware = new IdempotencyKey60sRedis;
|
||||
|
||||
$request1 = Request::create('/x', 'POST');
|
||||
$request1->headers->set('Idempotency-Key', 'replay-key');
|
||||
|
||||
$count = 0;
|
||||
$middleware->handle($request1, function () use (&$count) {
|
||||
$count++;
|
||||
|
||||
return response()->json(['result' => 'one']);
|
||||
});
|
||||
|
||||
$request2 = Request::create('/x', 'POST');
|
||||
$request2->headers->set('Idempotency-Key', 'replay-key');
|
||||
|
||||
$response2 = $middleware->handle($request2, function () use (&$count) {
|
||||
$count++;
|
||||
|
||||
return response()->json(['result' => 'two']);
|
||||
});
|
||||
|
||||
$this->assertSame(1, $count, 'inner handler should not run on replay');
|
||||
$this->assertSame('true', $response2->headers->get('Idempotency-Replayed'));
|
||||
$this->assertStringContainsString('one', (string) $response2->getContent());
|
||||
}
|
||||
|
||||
public function test_failed_response_not_cached(): void
|
||||
{
|
||||
Cache::flush();
|
||||
|
||||
$middleware = new IdempotencyKey60sRedis;
|
||||
|
||||
$request1 = Request::create('/x', 'POST');
|
||||
$request1->headers->set('Idempotency-Key', 'fail-key');
|
||||
|
||||
$middleware->handle($request1, fn () => response()->json(['x' => 1], 422));
|
||||
|
||||
$request2 = Request::create('/x', 'POST');
|
||||
$request2->headers->set('Idempotency-Key', 'fail-key');
|
||||
|
||||
$count = 0;
|
||||
$middleware->handle($request2, function () use (&$count) {
|
||||
$count++;
|
||||
|
||||
return response()->json(['x' => 2]);
|
||||
});
|
||||
|
||||
$this->assertSame(1, $count, 'failed responses should not be cached for replay');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user