-
Notifications
You must be signed in to change notification settings - Fork 0
Testing
Comprehensive testing strategies for availability rules and logic.
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\Resource;
use RomegaSoftware\Availability\Support\Effect;
use Illuminate\Foundation\Testing\RefreshDatabase;
class AvailabilityRuleTest extends TestCase
{
use RefreshDatabase;
public function test_can_create_availability_rule()
{
$resource = Resource::factory()->create();
$rule = $resource->availabilityRules()->create([
'type' => 'weekdays',
'config' => ['days' => [1, 2, 3, 4, 5]],
'effect' => Effect::Allow,
'priority' => 10,
'enabled' => true,
]);
$this->assertDatabaseHas('availability_rules', [
'subject_type' => Resource::class,
'subject_id' => $resource->id,
'type' => 'weekdays',
'effect' => 'allow',
]);
$this->assertEquals([1, 2, 3, 4, 5], $rule->config['days']);
}
}use Carbon\Carbon;
use RomegaSoftware\Availability\Support\AvailabilityEngine;
class AvailabilityCheckTest extends TestCase
{
private AvailabilityEngine $engine;
protected function setUp(): void
{
parent::setUp();
$this->engine = app(AvailabilityEngine::class);
}
public function test_resource_available_during_business_hours()
{
$resource = Resource::factory()->create([
'availability_default' => Effect::Deny,
]);
// Setup business hours
$resource->availabilityRules()->createMany([
[
'type' => 'weekdays',
'config' => ['days' => [1, 2, 3, 4, 5]],
'effect' => Effect::Allow,
'priority' => 10,
],
[
'type' => 'time_of_day',
'config' => ['from' => '09:00', 'to' => '17:00'],
'effect' => Effect::Allow,
'priority' => 20,
],
]);
// Monday at 10 AM - should be available
$monday10am = Carbon::parse('2025-01-13 10:00:00');
$this->assertTrue($this->engine->isAvailable($resource, $monday10am));
// Saturday at 10 AM - should not be available
$saturday10am = Carbon::parse('2025-01-18 10:00:00');
$this->assertFalse($this->engine->isAvailable($resource, $saturday10am));
// Monday at 8 PM - should not be available
$monday8pm = Carbon::parse('2025-01-13 20:00:00');
$this->assertFalse($this->engine->isAvailable($resource, $monday8pm));
}
}class PriorityTest extends TestCase
{
public function test_higher_priority_rules_override_lower()
{
$resource = Resource::factory()->create([
'availability_default' => Effect::Deny,
]);
// Base rule: Allow weekdays
$resource->availabilityRules()->create([
'type' => 'weekdays',
'config' => ['days' => [1, 2, 3, 4, 5]],
'effect' => Effect::Allow,
'priority' => 10,
]);
// Override: Deny on specific date
$resource->availabilityRules()->create([
'type' => 'blackout_date',
'config' => ['dates' => ['2025-01-15']],
'effect' => Effect::Deny,
'priority' => 50,
]);
$engine = app(AvailabilityEngine::class);
// Regular Wednesday - should be available
$wednesday = Carbon::parse('2025-01-08 10:00:00');
$this->assertTrue($engine->isAvailable($resource, $wednesday));
// Blackout Wednesday - should not be available
$blackoutWednesday = Carbon::parse('2025-01-15 10:00:00');
$this->assertFalse($engine->isAvailable($resource, $blackoutWednesday));
}
}namespace Tests\Unit\Evaluators;
use Tests\TestCase;
use App\Availability\Evaluators\MinimumNoticeEvaluator;
use App\Models\Resource;
use Carbon\Carbon;
class MinimumNoticeEvaluatorTest extends TestCase
{
private MinimumNoticeEvaluator $evaluator;
protected function setUp(): void
{
parent::setUp();
$this->evaluator = new MinimumNoticeEvaluator();
}
public function test_requires_minimum_notice_hours()
{
$resource = Resource::factory()->create();
$config = ['hours' => 48];
// 24 hours notice - not enough
Carbon::setTestNow('2025-01-01 10:00:00');
$tooSoon = Carbon::parse('2025-01-02 09:00:00');
$this->assertFalse(
$this->evaluator->matches($config, $tooSoon, $resource)
);
// 49 hours notice - enough
$farEnough = Carbon::parse('2025-01-03 11:00:00');
$this->assertTrue(
$this->evaluator->matches($config, $farEnough, $resource)
);
}
public function test_handles_business_hours_only()
{
$resource = Resource::factory()->create();
$config = [
'hours' => 24,
'business_hours_only' => true,
];
// Test that weekends don't count
Carbon::setTestNow('2025-01-03 17:00:00'); // Friday 5 PM
// Monday 5 PM would be 72 hours but only 8 business hours
$monday = Carbon::parse('2025-01-06 17:00:00');
$this->assertFalse(
$this->evaluator->matches($config, $monday, $resource)
);
}
}class InventoryGateTest extends TestCase
{
public function test_inventory_gate_blocks_when_insufficient_stock()
{
$product = Product::factory()->create([
'stock_quantity' => 5,
'availability_default' => Effect::Deny,
]);
// Configure resolver
config(['availability.inventory_gate.resolver' => function ($subject) {
return $subject->stock_quantity;
}]);
$product->availabilityRules()->create([
'type' => 'inventory_gate',
'config' => ['min' => 10],
'effect' => Effect::Allow,
'priority' => 10,
]);
$engine = app(AvailabilityEngine::class);
// Should not be available (5 < 10)
$this->assertFalse($engine->isAvailable($product, now()));
// Update stock
$product->update(['stock_quantity' => 15]);
// Should now be available (15 >= 10)
$this->assertTrue($engine->isAvailable($product, now()));
}
public function test_inventory_gate_with_boolean_resolver()
{
$service = Service::factory()->create();
$hasCapacity = true;
config(['availability.inventory_gate.resolver' => function () use (&$hasCapacity) {
return $hasCapacity;
}]);
$service->availabilityRules()->create([
'type' => 'inventory_gate',
'config' => ['min' => 1],
'effect' => Effect::Allow,
'priority' => 10,
]);
$engine = app(AvailabilityEngine::class);
// Should be available
$this->assertTrue($engine->isAvailable($service, now()));
// Change capacity
$hasCapacity = false;
// Should not be available
$this->assertFalse($engine->isAvailable($service, now()));
}
}class BookingIntegrationTest extends TestCase
{
public function test_complete_booking_flow()
{
$room = Room::factory()->create([
'availability_default' => Effect::Deny,
'availability_timezone' => 'America/New_York',
]);
// Setup availability
$this->setupRoomAvailability($room);
// Try to book during available time
$bookingTime = Carbon::parse('2025-01-15 14:00:00', 'America/New_York');
$response = $this->postJson('/api/bookings', [
'room_id' => $room->id,
'start_time' => $bookingTime->toIso8601String(),
'duration' => 60,
]);
$response->assertStatus(201);
$this->assertDatabaseHas('bookings', [
'room_id' => $room->id,
]);
// Try to double-book (should fail)
$response = $this->postJson('/api/bookings', [
'room_id' => $room->id,
'start_time' => $bookingTime->toIso8601String(),
'duration' => 30,
]);
$response->assertStatus(422);
$response->assertJsonPath('errors.room', ['Room is not available at this time']);
}
private function setupRoomAvailability($room)
{
$room->availabilityRules()->createMany([
[
'type' => 'weekdays',
'config' => ['days' => [1, 2, 3, 4, 5]],
'effect' => Effect::Allow,
'priority' => 10,
],
[
'type' => 'time_of_day',
'config' => ['from' => '09:00', 'to' => '17:00'],
'effect' => Effect::Allow,
'priority' => 20,
],
]);
}
}namespace Database\Factories;
use App\Models\Resource;
use RomegaSoftware\Availability\Support\Effect;
class ResourceFactory extends Factory
{
public function definition()
{
return [
'name' => $this->faker->words(3, true),
'availability_default' => Effect::Deny,
'availability_timezone' => 'UTC',
];
}
public function available()
{
return $this->state(function () {
return [
'availability_default' => Effect::Allow,
];
})->afterCreating(function (Resource $resource) {
$resource->availabilityRules()->create([
'type' => 'weekdays',
'config' => ['days' => [1, 2, 3, 4, 5]],
'effect' => Effect::Allow,
'priority' => 10,
]);
});
}
public function withBusinessHours()
{
return $this->afterCreating(function (Resource $resource) {
$resource->availabilityRules()->createMany([
[
'type' => 'weekdays',
'config' => ['days' => [1, 2, 3, 4, 5]],
'effect' => Effect::Allow,
'priority' => 10,
],
[
'type' => 'time_of_day',
'config' => ['from' => '09:00', 'to' => '17:00'],
'effect' => Effect::Allow,
'priority' => 20,
],
]);
});
}
}namespace Tests\Traits;
use RomegaSoftware\Availability\Support\Effect;
trait TestsAvailability
{
protected function assertAvailable($subject, $moment, $message = '')
{
$engine = app(AvailabilityEngine::class);
$this->assertTrue(
$engine->isAvailable($subject, $moment),
$message ?: "Expected {$subject->name} to be available at {$moment}"
);
}
protected function assertNotAvailable($subject, $moment, $message = '')
{
$engine = app(AvailabilityEngine::class);
$this->assertFalse(
$engine->isAvailable($subject, $moment),
$message ?: "Expected {$subject->name} to not be available at {$moment}"
);
}
protected function createAvailabilityRule($subject, $type, $config, $effect = Effect::Allow, $priority = 10)
{
return $subject->availabilityRules()->create([
'type' => $type,
'config' => $config,
'effect' => $effect,
'priority' => $priority,
'enabled' => true,
]);
}
}use Mockery;
class MockedAvailabilityTest extends TestCase
{
public function test_with_mocked_engine()
{
$mockEngine = Mockery::mock(AvailabilityEngine::class);
$mockEngine->shouldReceive('isAvailable')
->with(Mockery::type(Resource::class), Mockery::type(Carbon::class))
->andReturn(true);
$this->app->instance(AvailabilityEngine::class, $mockEngine);
$service = new BookingService();
$result = $service->checkAvailability(Resource::first(), now());
$this->assertTrue($result);
}
public function test_with_partial_mock()
{
$resource = Mockery::mock(Resource::class)->makePartial();
$resource->shouldReceive('availabilityRules->get')
->andReturn(collect([
new AvailabilityRule([
'type' => 'weekdays',
'config' => ['days' => [1]],
'effect' => Effect::Allow,
'priority' => 10,
]),
]));
$engine = app(AvailabilityEngine::class);
$monday = Carbon::parse('next monday');
$this->assertTrue($engine->isAvailable($resource, $monday));
}
}class PerformanceTest extends TestCase
{
public function test_availability_check_performance()
{
$resource = Resource::factory()->create();
// Create many rules
for ($i = 0; $i < 100; $i++) {
$resource->availabilityRules()->create([
'type' => 'time_of_day',
'config' => [
'from' => sprintf('%02d:00', $i % 24),
'to' => sprintf('%02d:59', $i % 24),
],
'effect' => Effect::Allow,
'priority' => $i,
]);
}
$engine = app(AvailabilityEngine::class);
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
$engine->isAvailable($resource, now()->addMinutes($i));
}
$duration = microtime(true) - $start;
// Should complete 1000 checks in under 1 second
$this->assertLessThan(1.0, $duration,
"Performance test failed: {$duration} seconds for 1000 checks");
}
}class EdgeCaseTest extends TestCase
{
public function test_daylight_saving_time_transition()
{
$resource = Resource::factory()->create([
'availability_timezone' => 'America/New_York',
]);
$resource->availabilityRules()->create([
'type' => 'time_of_day',
'config' => ['from' => '02:00', 'to' => '03:00'],
'effect' => Effect::Allow,
'priority' => 10,
]);
$engine = app(AvailabilityEngine::class);
// Spring forward (2 AM becomes 3 AM)
$springForward = Carbon::parse('2025-03-09 02:30:00', 'America/New_York');
// This time doesn't exist due to DST
$this->assertFalse($engine->isAvailable($resource, $springForward));
}
public function test_leap_year_february_29()
{
$resource = Resource::factory()->create();
$resource->availabilityRules()->create([
'type' => 'date_range',
'config' => [
'from' => '02-28',
'to' => '03-01',
'kind' => 'yearly',
],
'effect' => Effect::Allow,
'priority' => 10,
]);
$engine = app(AvailabilityEngine::class);
// Leap year
$leapYear = Carbon::parse('2024-02-29');
$this->assertTrue($engine->isAvailable($resource, $leapYear));
// Non-leap year
$normalYear = Carbon::parse('2025-02-28');
$this->assertTrue($engine->isAvailable($resource, $normalYear));
}
public function test_concurrent_rule_modifications()
{
$resource = Resource::factory()->create();
// Simulate concurrent requests
$promises = [];
for ($i = 0; $i < 10; $i++) {
$promises[] = function () use ($resource, $i) {
$resource->availabilityRules()->create([
'type' => 'weekdays',
'config' => ['days' => [$i % 7 + 1]],
'effect' => Effect::Allow,
'priority' => $i,
]);
};
}
// Execute concurrently
foreach ($promises as $promise) {
$promise();
}
// Verify all rules were created
$this->assertEquals(10, $resource->availabilityRules()->count());
}
}- Performance Tips - Optimize test performance
- Complex Scenarios - Testing complex setups
- Configuration - Test configuration
Romega Software is software development agency specializing in helping customers integrate AI and custom software into their business, helping companies in growth mode better acquire, visualize, and utilize their data, and helping entrepreneurs bring their ideas to life.
Installation
Set up the package in your Laravel app
Quick Start
Get running in 5 minutes
Basic Usage
Common patterns and examples
How It Works
Understanding the evaluation engine
Rule Types
Available rule types and configurations
Priority System
How rule priority affects evaluation
Inventory Gates
Dynamic availability based on stock
Custom Evaluators
Build your own rule types
Complex Scenarios
Real-world implementation patterns
Performance Tips
Optimization strategies
Configuration
Package configuration options
Models & Traits
Available models and traits
Testing
Testing your availability rules