withCapacity(10)->create(); $second = Slot::factory()->withCapacity(5)->create(); $this->getJson('/slots/availability') ->assertOk() ->assertExactJson([ ['slot_id' => $first->id, 'capacity' => 10, 'remaining' => 10], ['slot_id' => $second->id, 'capacity' => 5, 'remaining' => 5], ]); } public function test_active_holds_are_subtracted_from_remaining(): void { $slot = Slot::factory()->withCapacity(5)->create(); Hold::factory()->count(2)->for($slot)->create(); $this->getJson('/slots/availability') ->assertOk() ->assertJsonPath('0.remaining', 3); } public function test_expired_holds_stop_reserving_capacity(): void { $slot = Slot::factory()->withCapacity(2)->create(); Hold::factory()->expired()->for($slot)->create(); $this->getJson('/slots/availability') ->assertOk() ->assertJsonPath('0.remaining', 2); } public function test_cancelled_holds_stop_reserving_capacity(): void { $slot = Slot::factory()->withCapacity(2)->create(); Hold::factory()->cancelled()->for($slot)->create(); $this->getJson('/slots/availability') ->assertOk() ->assertJsonPath('0.remaining', 2); } public function test_the_response_is_served_from_cache_within_the_ttl(): void { $slot = Slot::factory()->withCapacity(3)->create(); $this->getJson('/slots/availability')->assertJsonPath('0.remaining', 3); // Меняем источник истины за спиной кеша. $slot->update(['remaining' => 1]); $this->getJson('/slots/availability')->assertJsonPath('0.remaining', 3); } public function test_the_cache_expires_after_the_configured_ttl(): void { $slot = Slot::factory()->withCapacity(3)->create(); $this->getJson('/slots/availability')->assertJsonPath('0.remaining', 3); $slot->update(['remaining' => 1]); $this->travel(config('slots.availability_cache_ttl') + 1)->seconds(); $this->getJson('/slots/availability')->assertJsonPath('0.remaining', 1); } public function test_a_caller_that_loses_the_recompute_race_is_served_stale_data(): void { $slot = Slot::factory()->withCapacity(3)->create(); // Прогревает и основную запись, и stale-копию. $this->getJson('/slots/availability')->assertJsonPath('0.remaining', 3); $this->travel(config('slots.availability_cache_ttl') + 1)->seconds(); $slot->update(['remaining' => 0]); // Имитируем воркер, который пересчитывает прямо сейчас. $lock = Cache::lock('slots:availability:lock', 10); $this->assertTrue($lock->get()); DB::enableQueryLog(); $response = $this->getJson('/slots/availability'); $queries = DB::getQueryLog(); DB::disableQueryLog(); $lock->release(); $response->assertOk()->assertJsonPath('0.remaining', 3); $this->assertSame([], $queries, 'Проигравший гонку за пересчёт не должен обращаться к базе.'); } }