208 lines
7.4 KiB
PHP
208 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Entity;
|
|
|
|
use App\Entity\Bucket;
|
|
use App\Entity\Scenario;
|
|
use App\Enum\BucketAllocationType;
|
|
use App\Enum\BucketType;
|
|
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
use Symfony\Component\Uid\UuidV7;
|
|
|
|
final class BucketPersistenceTest extends KernelTestCase
|
|
{
|
|
private EntityManagerInterface $em;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
self::bootKernel();
|
|
$this->em = self::getContainer()->get(EntityManagerInterface::class);
|
|
}
|
|
|
|
public function testItPersistsABucketLinkedToItsScenario(): void
|
|
{
|
|
$scenario = new Scenario();
|
|
$scenario->setName('Household Budget');
|
|
$this->em->persist($scenario);
|
|
$this->em->flush();
|
|
|
|
$scenarioId = $scenario->getId();
|
|
|
|
$bucket = new Bucket();
|
|
$bucket->setScenario($scenario);
|
|
$bucket->setType(BucketType::NEED);
|
|
$bucket->setName('Rent');
|
|
$bucket->setPriority(3);
|
|
$bucket->setSortOrder(2);
|
|
$bucket->setAllocationType(BucketAllocationType::FIXED_LIMIT);
|
|
$bucket->setAllocationValue(150000);
|
|
$bucket->setStartingAmount(5000);
|
|
$bucket->setBufferMultiplier('1.50');
|
|
|
|
$this->em->persist($bucket);
|
|
$this->em->flush();
|
|
|
|
$bucketId = $bucket->getId();
|
|
|
|
$this->em->clear();
|
|
|
|
$reloaded = $this->em->find(Bucket::class, $bucketId);
|
|
|
|
self::assertNotNull($reloaded, 'Bucket must be retrievable from the database after an identity-map clear.');
|
|
|
|
self::assertInstanceOf(Scenario::class, $reloaded->getScenario());
|
|
self::assertSame(
|
|
(string) $scenarioId,
|
|
(string) $reloaded->getScenario()->getId(),
|
|
'Reloaded bucket must be linked to the same scenario it was persisted with.',
|
|
);
|
|
|
|
self::assertInstanceOf(
|
|
BucketType::class,
|
|
$reloaded->getType(),
|
|
'type must hydrate as a BucketType enum instance, not a raw string.',
|
|
);
|
|
self::assertSame(BucketType::NEED, $reloaded->getType());
|
|
|
|
self::assertSame('Rent', $reloaded->getName());
|
|
self::assertSame(3, $reloaded->getPriority());
|
|
self::assertSame(2, $reloaded->getSortOrder());
|
|
|
|
self::assertInstanceOf(
|
|
BucketAllocationType::class,
|
|
$reloaded->getAllocationType(),
|
|
'allocation_type must hydrate as a BucketAllocationType enum instance, not a raw string.',
|
|
);
|
|
self::assertSame(BucketAllocationType::FIXED_LIMIT, $reloaded->getAllocationType());
|
|
|
|
self::assertSame(
|
|
150000,
|
|
$reloaded->getAllocationValue(),
|
|
'allocation_value must round-trip as an int (cents), not a string or float.',
|
|
);
|
|
self::assertSame(5000, $reloaded->getStartingAmount());
|
|
|
|
self::assertSame(
|
|
'1.50',
|
|
$reloaded->getBufferMultiplier(),
|
|
'buffer_multiplier is a Doctrine decimal column — it hydrates as a numeric string, not a float (precision-safe).',
|
|
);
|
|
}
|
|
|
|
public function testAllocationTypeDefaultsToFixedLimitWhenNotSet(): void
|
|
{
|
|
$scenario = new Scenario();
|
|
$scenario->setName('Household Budget');
|
|
$this->em->persist($scenario);
|
|
$this->em->flush();
|
|
|
|
$bucket = new Bucket();
|
|
$bucket->setScenario($scenario);
|
|
$bucket->setType(BucketType::NEED);
|
|
$bucket->setName('Rent');
|
|
$bucket->setPriority(1);
|
|
$bucket->setSortOrder(0);
|
|
// setAllocationType() deliberately NOT called — pins the default.
|
|
$bucket->setAllocationValue(150000);
|
|
$bucket->setStartingAmount(5000);
|
|
$bucket->setBufferMultiplier('0.00');
|
|
|
|
$this->em->persist($bucket);
|
|
$this->em->flush();
|
|
|
|
$bucketId = $bucket->getId();
|
|
|
|
$this->em->clear();
|
|
|
|
$reloaded = $this->em->find(Bucket::class, $bucketId);
|
|
|
|
self::assertNotNull($reloaded, 'Bucket must be retrievable from the database after an identity-map clear.');
|
|
|
|
self::assertSame(
|
|
BucketAllocationType::FIXED_LIMIT,
|
|
$reloaded->getAllocationType(),
|
|
'allocation_type must default to FIXED_LIMIT at the entity (PHP) level when not explicitly set. '
|
|
.'This does not exercise the DB column default, since Doctrine always includes allocation_type '
|
|
.'in its INSERT once the property holds a value — see testAllocationTypeDbColumnHasFixedLimitDefaultWhenOmittedFromInsert '
|
|
.'for the DB-level proof.',
|
|
);
|
|
}
|
|
|
|
public function testAllocationTypeDbColumnHasFixedLimitDefaultWhenOmittedFromInsert(): void
|
|
{
|
|
$scenario = new Scenario();
|
|
$scenario->setName('Household Budget');
|
|
$this->em->persist($scenario);
|
|
$this->em->flush();
|
|
|
|
$scenarioId = $scenario->getId();
|
|
|
|
$bucketId = new UuidV7();
|
|
$connection = $this->em->getConnection();
|
|
|
|
// Raw INSERT deliberately omits allocation_type — the DB column default must supply it.
|
|
// All other NOT NULL columns without a DB default (id, name, type, priority, scenario_id)
|
|
// are provided explicitly.
|
|
$connection->executeStatement(
|
|
'INSERT INTO bucket (id, name, type, priority, scenario_id) VALUES (:id, :name, :type, :priority, :scenarioId)',
|
|
[
|
|
'id' => $bucketId->toRfc4122(),
|
|
'name' => 'Rent',
|
|
'type' => BucketType::NEED->value,
|
|
'priority' => 1,
|
|
'scenarioId' => $scenarioId->toRfc4122(),
|
|
],
|
|
);
|
|
|
|
$allocationType = $connection->executeQuery(
|
|
'SELECT allocation_type FROM bucket WHERE id = :id',
|
|
['id' => $bucketId->toRfc4122()],
|
|
)->fetchOne();
|
|
|
|
self::assertSame(
|
|
'fixed_limit',
|
|
$allocationType,
|
|
'When allocation_type is omitted from a raw INSERT, the DB column default must supply '
|
|
.'\'fixed_limit\'. If this fails, the migration setting the column DEFAULT has regressed '
|
|
.'(the insert would fail outright with a not-null violation instead of defaulting).',
|
|
);
|
|
}
|
|
|
|
public function testScenarioPriorityUniquenessIsEnforced(): void
|
|
{
|
|
$scenario = new Scenario();
|
|
$scenario->setName('Household Budget');
|
|
$this->em->persist($scenario);
|
|
$this->em->flush();
|
|
|
|
$first = new Bucket();
|
|
$first->setScenario($scenario);
|
|
$first->setType(BucketType::NEED);
|
|
$first->setName('Rent');
|
|
$first->setPriority(3);
|
|
$first->setSortOrder(1);
|
|
$first->setAllocationType(BucketAllocationType::FIXED_LIMIT);
|
|
$first->setAllocationValue(150000);
|
|
$first->setStartingAmount(5000);
|
|
$first->setBufferMultiplier('0.00');
|
|
$this->em->persist($first);
|
|
$this->em->flush();
|
|
|
|
$second = new Bucket();
|
|
$second->setScenario($scenario);
|
|
$second->setType(BucketType::NEED);
|
|
$second->setName('Utilities');
|
|
$second->setPriority(3);
|
|
$second->setSortOrder(2);
|
|
$second->setAllocationType(BucketAllocationType::FIXED_LIMIT);
|
|
$second->setAllocationValue(50000);
|
|
$second->setStartingAmount(3000);
|
|
$second->setBufferMultiplier('0.00');
|
|
$this->em->persist($second);
|
|
|
|
$this->expectException(UniqueConstraintViolationException::class);
|
|
$this->em->flush();
|
|
}
|
|
}
|