269 lines
8.8 KiB
PHP
269 lines
8.8 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace Tests\Unit\Actions;
|
||
|
|
|
||
|
|
use App\Actions\CreateBucketAction;
|
||
|
|
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',
|
||
|
|
Bucket::TYPE_FIXED_LIMIT,
|
||
|
|
1000.00
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->assertInstanceOf(Bucket::class, $bucket);
|
||
|
|
$this->assertEquals('Test Bucket', $bucket->name);
|
||
|
|
$this->assertEquals(Bucket::TYPE_FIXED_LIMIT, $bucket->allocation_type);
|
||
|
|
$this->assertEquals(1000.00, $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',
|
||
|
|
Bucket::TYPE_PERCENTAGE,
|
||
|
|
25.5
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->assertEquals(Bucket::TYPE_PERCENTAGE, $bucket->allocation_type);
|
||
|
|
$this->assertEquals(25.5, $bucket->allocation_value);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_can_create_unlimited_bucket(): void
|
||
|
|
{
|
||
|
|
$bucket = $this->action->execute(
|
||
|
|
$this->scenario,
|
||
|
|
'Unlimited Bucket',
|
||
|
|
Bucket::TYPE_UNLIMITED
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->assertEquals(Bucket::TYPE_UNLIMITED, $bucket->allocation_type);
|
||
|
|
$this->assertNull($bucket->allocation_value);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_unlimited_bucket_ignores_allocation_value(): void
|
||
|
|
{
|
||
|
|
$bucket = $this->action->execute(
|
||
|
|
$this->scenario,
|
||
|
|
'Unlimited Bucket',
|
||
|
|
Bucket::TYPE_UNLIMITED,
|
||
|
|
999.99 // This should be ignored and set to null
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->assertEquals(Bucket::TYPE_UNLIMITED, $bucket->allocation_type);
|
||
|
|
$this->assertNull($bucket->allocation_value);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_priority_auto_increments_when_not_specified(): void
|
||
|
|
{
|
||
|
|
$bucket1 = $this->action->execute(
|
||
|
|
$this->scenario,
|
||
|
|
'First Bucket',
|
||
|
|
Bucket::TYPE_FIXED_LIMIT,
|
||
|
|
100
|
||
|
|
);
|
||
|
|
|
||
|
|
$bucket2 = $this->action->execute(
|
||
|
|
$this->scenario,
|
||
|
|
'Second Bucket',
|
||
|
|
Bucket::TYPE_FIXED_LIMIT,
|
||
|
|
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',
|
||
|
|
Bucket::TYPE_FIXED_LIMIT,
|
||
|
|
100,
|
||
|
|
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', Bucket::TYPE_FIXED_LIMIT, 100, 1);
|
||
|
|
$bucket2 = $this->action->execute($this->scenario, 'Bucket 2', Bucket::TYPE_FIXED_LIMIT, 200, 2);
|
||
|
|
$bucket3 = $this->action->execute($this->scenario, 'Bucket 3', Bucket::TYPE_FIXED_LIMIT, 300, 3);
|
||
|
|
|
||
|
|
// Insert a bucket at priority 2
|
||
|
|
$newBucket = $this->action->execute($this->scenario, 'New Bucket', Bucket::TYPE_FIXED_LIMIT, 150, 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_invalid_allocation_type(): void
|
||
|
|
{
|
||
|
|
$this->expectException(InvalidArgumentException::class);
|
||
|
|
$this->expectExceptionMessage('Invalid allocation type: invalid_type');
|
||
|
|
|
||
|
|
$this->action->execute(
|
||
|
|
$this->scenario,
|
||
|
|
'Test Bucket',
|
||
|
|
'invalid_type',
|
||
|
|
100
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
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',
|
||
|
|
Bucket::TYPE_FIXED_LIMIT,
|
||
|
|
null
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
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',
|
||
|
|
Bucket::TYPE_FIXED_LIMIT,
|
||
|
|
-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',
|
||
|
|
Bucket::TYPE_PERCENTAGE,
|
||
|
|
null
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_throws_exception_for_percentage_below_minimum(): void
|
||
|
|
{
|
||
|
|
$this->expectException(InvalidArgumentException::class);
|
||
|
|
$this->expectExceptionMessage('Percentage allocation value must be between 0.01 and 100');
|
||
|
|
|
||
|
|
$this->action->execute(
|
||
|
|
$this->scenario,
|
||
|
|
'Test Bucket',
|
||
|
|
Bucket::TYPE_PERCENTAGE,
|
||
|
|
0.005
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_throws_exception_for_percentage_above_maximum(): void
|
||
|
|
{
|
||
|
|
$this->expectException(InvalidArgumentException::class);
|
||
|
|
$this->expectExceptionMessage('Percentage allocation value must be between 0.01 and 100');
|
||
|
|
|
||
|
|
$this->action->execute(
|
||
|
|
$this->scenario,
|
||
|
|
'Test Bucket',
|
||
|
|
Bucket::TYPE_PERCENTAGE,
|
||
|
|
101
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
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',
|
||
|
|
Bucket::TYPE_FIXED_LIMIT,
|
||
|
|
100,
|
||
|
|
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
|
||
|
|
$this->assertEquals('Monthly Expenses', $buckets[0]->name);
|
||
|
|
$this->assertEquals(1, $buckets[0]->priority);
|
||
|
|
$this->assertEquals(Bucket::TYPE_FIXED_LIMIT, $buckets[0]->allocation_type);
|
||
|
|
$this->assertEquals(0, $buckets[0]->allocation_value);
|
||
|
|
|
||
|
|
// Emergency Fund
|
||
|
|
$this->assertEquals('Emergency Fund', $buckets[1]->name);
|
||
|
|
$this->assertEquals(2, $buckets[1]->priority);
|
||
|
|
$this->assertEquals(Bucket::TYPE_FIXED_LIMIT, $buckets[1]->allocation_type);
|
||
|
|
$this->assertEquals(0, $buckets[1]->allocation_value);
|
||
|
|
|
||
|
|
// Investments
|
||
|
|
$this->assertEquals('Investments', $buckets[2]->name);
|
||
|
|
$this->assertEquals(3, $buckets[2]->priority);
|
||
|
|
$this->assertEquals(Bucket::TYPE_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', Bucket::TYPE_FIXED_LIMIT, 100, 1);
|
||
|
|
$this->action->execute($this->scenario, 'Bucket 2', Bucket::TYPE_FIXED_LIMIT, 200, 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
|
||
|
|
}
|
||
|
|
}
|