buckets/tests/Entity/BucketPersistenceTest.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();
}
}