buckets/tests/Unit/Actions/CreateBucketActionTest.php

427 lines
14 KiB
PHP

<?php
namespace Tests\Unit\Actions;
use App\Actions\CreateBucketAction;
use App\Enums\BucketAllocationTypeEnum;
use App\Enums\BucketTypeEnum;
use App\Models\Bucket;
use App\Models\Scenario;
use Illuminate\Foundation\Testing\RefreshDatabase;
use InvalidArgumentException;
use Tests\TestCase;
class CreateBucketActionTest extends TestCase
{
use RefreshDatabase;
private CreateBucketAction $action;
private Scenario $scenario;
protected function setUp(): void
{
parent::setUp();
$this->action = new CreateBucketAction;
$this->scenario = Scenario::factory()->create();
}
public function test_can_create_fixed_limit_bucket(): void
{
$bucket = $this->action->execute(
$this->scenario,
'Test Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT,
allocationValue: 100000
);
$this->assertInstanceOf(Bucket::class, $bucket);
$this->assertEquals('Test Bucket', $bucket->name);
$this->assertEquals(BucketTypeEnum::NEED, $bucket->type);
$this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $bucket->allocation_type);
$this->assertEquals(100000, $bucket->allocation_value);
$this->assertEquals(1, $bucket->priority);
$this->assertEquals(1, $bucket->sort_order);
$this->assertEquals($this->scenario->id, $bucket->scenario_id);
}
public function test_can_create_percentage_bucket(): void
{
$bucket = $this->action->execute(
$this->scenario,
'Percentage Bucket',
BucketAllocationTypeEnum::PERCENTAGE,
allocationValue: 2550
);
$this->assertEquals(BucketAllocationTypeEnum::PERCENTAGE, $bucket->allocation_type);
$this->assertEquals(2550, $bucket->allocation_value);
}
public function test_can_create_unlimited_overflow_bucket(): void
{
$bucket = $this->action->execute(
$this->scenario,
'Overflow Bucket',
BucketAllocationTypeEnum::UNLIMITED,
BucketTypeEnum::OVERFLOW
);
$this->assertEquals(BucketTypeEnum::OVERFLOW, $bucket->type);
$this->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $bucket->allocation_type);
$this->assertNull($bucket->allocation_value);
}
public function test_unlimited_overflow_bucket_ignores_allocation_value(): void
{
$bucket = $this->action->execute(
$this->scenario,
'Overflow Bucket',
BucketAllocationTypeEnum::UNLIMITED,
BucketTypeEnum::OVERFLOW,
99999
);
$this->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $bucket->allocation_type);
$this->assertNull($bucket->allocation_value);
}
public function test_can_create_need_bucket(): void
{
$bucket = $this->action->execute(
$this->scenario,
'Need Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT,
BucketTypeEnum::NEED,
500
);
$this->assertEquals(BucketTypeEnum::NEED, $bucket->type);
}
public function test_can_create_want_bucket(): void
{
$bucket = $this->action->execute(
$this->scenario,
'Want Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT,
BucketTypeEnum::WANT,
500
);
$this->assertEquals(BucketTypeEnum::WANT, $bucket->type);
}
public function test_default_type_is_need(): void
{
$bucket = $this->action->execute(
$this->scenario,
'Default Type Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT,
allocationValue: 100
);
$this->assertEquals(BucketTypeEnum::NEED, $bucket->type);
}
public function test_overflow_bucket_must_use_unlimited_allocation(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid allocation type for overflow bucket. Allowed: unlimited');
$this->action->execute(
$this->scenario,
'Bad Overflow',
BucketAllocationTypeEnum::FIXED_LIMIT,
BucketTypeEnum::OVERFLOW,
100
);
}
public function test_non_overflow_bucket_cannot_use_unlimited_allocation(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid allocation type for need bucket. Allowed: fixed_limit, percentage');
$this->action->execute(
$this->scenario,
'Bad Need Bucket',
BucketAllocationTypeEnum::UNLIMITED,
BucketTypeEnum::NEED
);
}
public function test_cannot_create_second_overflow_bucket(): void
{
$this->action->execute(
$this->scenario,
'First Overflow',
BucketAllocationTypeEnum::UNLIMITED,
BucketTypeEnum::OVERFLOW
);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('A scenario can only have one overflow bucket');
$this->action->execute(
$this->scenario,
'Second Overflow',
BucketAllocationTypeEnum::UNLIMITED,
BucketTypeEnum::OVERFLOW
);
}
public function test_want_bucket_cannot_use_unlimited_allocation(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid allocation type for want bucket. Allowed: fixed_limit, percentage');
$this->action->execute(
$this->scenario,
'Bad Want Bucket',
BucketAllocationTypeEnum::UNLIMITED,
BucketTypeEnum::WANT
);
}
public function test_priority_auto_increments_when_not_specified(): void
{
$bucket1 = $this->action->execute(
$this->scenario,
'First Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT,
allocationValue: 100
);
$bucket2 = $this->action->execute(
$this->scenario,
'Second Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT,
allocationValue: 200
);
$this->assertEquals(1, $bucket1->priority);
$this->assertEquals(2, $bucket2->priority);
}
public function test_can_specify_custom_priority(): void
{
$bucket = $this->action->execute(
$this->scenario,
'Priority Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT,
allocationValue: 100,
priority: 5
);
$this->assertEquals(5, $bucket->priority);
}
public function test_existing_priorities_are_shifted_when_inserting(): void
{
// Create initial buckets
$bucket1 = $this->action->execute($this->scenario, 'Bucket 1', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 100, priority: 1);
$bucket2 = $this->action->execute($this->scenario, 'Bucket 2', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 200, priority: 2);
$bucket3 = $this->action->execute($this->scenario, 'Bucket 3', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 300, priority: 3);
// Insert a bucket at priority 2
$newBucket = $this->action->execute($this->scenario, 'New Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 150, priority: 2);
// Refresh models from database
$bucket1->refresh();
$bucket2->refresh();
$bucket3->refresh();
// Check that priorities were shifted correctly
$this->assertEquals(1, $bucket1->priority);
$this->assertEquals(2, $newBucket->priority);
$this->assertEquals(3, $bucket2->priority); // Shifted from 2 to 3
$this->assertEquals(4, $bucket3->priority); // Shifted from 3 to 4
}
public function test_throws_exception_for_fixed_limit_without_allocation_value(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Fixed limit buckets require an allocation value');
$this->action->execute(
$this->scenario,
'Test Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT
);
}
public function test_throws_exception_for_negative_fixed_limit_value(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Fixed limit allocation value must be non-negative');
$this->action->execute(
$this->scenario,
'Test Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT,
allocationValue: -100
);
}
public function test_throws_exception_for_percentage_without_allocation_value(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Percentage buckets require an allocation value');
$this->action->execute(
$this->scenario,
'Test Bucket',
BucketAllocationTypeEnum::PERCENTAGE
);
}
public function test_throws_exception_for_percentage_below_minimum(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Percentage allocation value must be between 1 and 10000');
$this->action->execute(
$this->scenario,
'Test Bucket',
BucketAllocationTypeEnum::PERCENTAGE,
allocationValue: 0
);
}
public function test_throws_exception_for_percentage_above_maximum(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Percentage allocation value must be between 1 and 10000');
$this->action->execute(
$this->scenario,
'Test Bucket',
BucketAllocationTypeEnum::PERCENTAGE,
allocationValue: 10001
);
}
public function test_throws_exception_for_negative_priority(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Priority must be at least 1');
$this->action->execute(
$this->scenario,
'Test Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT,
allocationValue: 100,
priority: 0
);
}
public function test_create_default_buckets(): void
{
$this->action->createDefaultBuckets($this->scenario);
$buckets = $this->scenario->buckets()->orderBy('priority')->get();
$this->assertCount(3, $buckets);
// Monthly Expenses - Need
$this->assertEquals('Monthly Expenses', $buckets[0]->name);
$this->assertEquals(BucketTypeEnum::NEED, $buckets[0]->type);
$this->assertEquals(1, $buckets[0]->priority);
$this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $buckets[0]->allocation_type);
$this->assertEquals(0, $buckets[0]->allocation_value);
// Emergency Fund - Need
$this->assertEquals('Emergency Fund', $buckets[1]->name);
$this->assertEquals(BucketTypeEnum::NEED, $buckets[1]->type);
$this->assertEquals(2, $buckets[1]->priority);
$this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $buckets[1]->allocation_type);
$this->assertEquals(0, $buckets[1]->allocation_value);
// Overflow
$this->assertEquals('Overflow', $buckets[2]->name);
$this->assertEquals(BucketTypeEnum::OVERFLOW, $buckets[2]->type);
$this->assertEquals(3, $buckets[2]->priority);
$this->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $buckets[2]->allocation_type);
$this->assertNull($buckets[2]->allocation_value);
}
public function test_creates_buckets_in_database_transaction(): void
{
// This test ensures database consistency by creating multiple buckets
// and verifying they all exist with correct priorities
$this->action->execute($this->scenario, 'Bucket 1', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 100, priority: 1);
$this->action->execute($this->scenario, 'Bucket 2', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 200, priority: 1); // Insert at priority 1
// Both buckets should exist with correct priorities
$buckets = $this->scenario->buckets()->orderBy('priority')->get();
$this->assertCount(2, $buckets);
$this->assertEquals('Bucket 2', $buckets[0]->name); // New bucket at priority 1
$this->assertEquals('Bucket 1', $buckets[1]->name); // Original bucket shifted to priority 2
}
public function test_can_create_fixed_limit_bucket_with_buffer_multiplier(): void
{
$bucket = $this->action->execute(
$this->scenario,
'Buffered Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT,
allocationValue: 50000,
bufferMultiplier: 1.5,
);
$this->assertEquals('1.50', $bucket->buffer_multiplier);
}
public function test_buffer_multiplier_defaults_to_zero(): void
{
$bucket = $this->action->execute(
$this->scenario,
'No Buffer Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT,
allocationValue: 50000,
);
$this->assertEquals('0.00', $bucket->buffer_multiplier);
}
public function test_buffer_multiplier_forced_to_zero_for_percentage_bucket(): void
{
$bucket = $this->action->execute(
$this->scenario,
'Percentage Bucket',
BucketAllocationTypeEnum::PERCENTAGE,
allocationValue: 2500,
bufferMultiplier: 1.0,
);
$this->assertEquals('0.00', $bucket->buffer_multiplier);
}
public function test_buffer_multiplier_forced_to_zero_for_unlimited_bucket(): void
{
$bucket = $this->action->execute(
$this->scenario,
'Unlimited Bucket',
BucketAllocationTypeEnum::UNLIMITED,
BucketTypeEnum::OVERFLOW,
bufferMultiplier: 1.0,
);
$this->assertEquals('0.00', $bucket->buffer_multiplier);
}
public function test_throws_exception_for_negative_buffer_multiplier(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Buffer multiplier must be non-negative');
$this->action->execute(
$this->scenario,
'Bad Buffer',
BucketAllocationTypeEnum::FIXED_LIMIT,
allocationValue: 50000,
bufferMultiplier: -0.5,
);
}
}