Rich-Model Mahlzeit (MahlzeitStatus, Wochentag) mit Invarianten und Zustandsübergängen; DTOs + MahlzeitFactory (reine Mapping-Funktionen); Manager (create hängt an Ende an: maxSortOrder+1, find → 404) und Create/Update/Delete-UseCases mit Transaktion + Events nach Commit. Data-Layer: MahlzeitEntity (Tabelle essens_mahlzeiten, wochentage als CSV), Mapper (CSV/Enum/null), Provider (find/list/maxSortOrder), Processor (create/save/delete). DI-Bindings in services.yaml. ClockInterface (Logic) + SystemClock (Data). Tests: Model/Factory/UseCase-Unit + Provider/Processor/Mapper-Integration (In-Mem-SQLite). composer test grün (53 Tests, 161 Assertions).
158 lines
5.0 KiB
PHP
158 lines
5.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Logic\Essen\Model;
|
|
|
|
use App\Logic\Common\Exception\BusinessRuleViolationException;
|
|
use App\Logic\Essen\Model\Mahlzeit;
|
|
use App\Logic\Essen\Model\MahlzeitStatus;
|
|
use App\Logic\Essen\Model\Wochentag;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class MahlzeitTest extends TestCase
|
|
{
|
|
private function jetzt(): \DateTimeImmutable
|
|
{
|
|
return new \DateTimeImmutable('2026-09-18 12:00:00');
|
|
}
|
|
|
|
private function geplant(): Mahlzeit
|
|
{
|
|
return Mahlzeit::anlegen(
|
|
name: 'Spaghetti Bolognese',
|
|
notiz: 'mit mehr Soße',
|
|
wochentage: [Wochentag::Mo, Wochentag::Di],
|
|
sortOrder: 1,
|
|
jetzt: $this->jetzt(),
|
|
);
|
|
}
|
|
|
|
public function testAnlegenErzeugtGeplanteMahlzeit(): void
|
|
{
|
|
$m = $this->geplant();
|
|
|
|
self::assertSame(0, $m->id);
|
|
self::assertSame('Spaghetti Bolognese', $m->name);
|
|
self::assertSame('mit mehr Soße', $m->notiz);
|
|
self::assertSame([Wochentag::Mo, Wochentag::Di], $m->wochentage());
|
|
self::assertSame(1, $m->sortOrder);
|
|
self::assertSame(MahlzeitStatus::Geplant, $m->status);
|
|
self::assertNull($m->gegessenAm);
|
|
self::assertTrue($m->istGeplant());
|
|
}
|
|
|
|
public function testAnlegenTrimmtNameUndNotiz(): void
|
|
{
|
|
$m = Mahlzeit::anlegen(' Bolognese ', ' Notiz ', [], 0, $this->jetzt());
|
|
|
|
self::assertSame('Bolognese', $m->name);
|
|
self::assertSame('Notiz', $m->notiz);
|
|
}
|
|
|
|
public function testMarkAsGegessenSetztStatusZeitpunktUndAktualisiertAm(): void
|
|
{
|
|
$m = $this->geplant();
|
|
$später = $this->jetzt()->modify('+2 days');
|
|
|
|
$gegessen = $m->markAsGegessen($später);
|
|
|
|
self::assertSame(MahlzeitStatus::Gegessen, $gegessen->status);
|
|
self::assertSame($später, $gegessen->gegessenAm);
|
|
self::assertSame($später, $gegessen->aktualisiertAm);
|
|
self::assertTrue($gegessen->istGegessen());
|
|
// Ursprung bleibt unverändert (Unveränderlichkeit)
|
|
self::assertSame(MahlzeitStatus::Geplant, $m->status);
|
|
}
|
|
|
|
public function testMarkAsGeplantSetztGegessenAmZurück(): void
|
|
{
|
|
$gegessen = $this->geplant()->markAsGegessen($this->jetzt());
|
|
|
|
$geplant = $gegessen->markAsGeplant($this->jetzt());
|
|
|
|
self::assertSame(MahlzeitStatus::Geplant, $geplant->status);
|
|
self::assertNull($geplant->gegessenAm);
|
|
self::assertTrue($geplant->istGeplant());
|
|
}
|
|
|
|
public function testMarkAsGegessenIstIdempotent(): void
|
|
{
|
|
$m = $this->geplant();
|
|
$einmal = $m->markAsGegessen($this->jetzt());
|
|
$zweimal = $einmal->markAsGegessen($this->jetzt());
|
|
|
|
self::assertSame($einmal->gegessenAm, $zweimal->gegessenAm);
|
|
self::assertSame(MahlzeitStatus::Gegessen, $zweimal->status);
|
|
}
|
|
|
|
public function testAktualisierenÄndertInhaltUndBelässtStatus(): void
|
|
{
|
|
$gegessen = $this->geplant()->markAsGegessen($this->jetzt());
|
|
|
|
$aktualisiert = $gegessen->aktualisieren(
|
|
name: 'Linsen',
|
|
notiz: 'scharf',
|
|
wochentage: [Wochentag::Mi],
|
|
sortOrder: $gegessen->sortOrder,
|
|
jetzt: $this->jetzt(),
|
|
);
|
|
|
|
self::assertSame('Linsen', $aktualisiert->name);
|
|
self::assertSame('scharf', $aktualisiert->notiz);
|
|
self::assertSame([Wochentag::Mi], $aktualisiert->wochentage());
|
|
self::assertSame(MahlzeitStatus::Gegessen, $aktualisiert->status);
|
|
self::assertNotNull($aktualisiert->gegessenAm);
|
|
}
|
|
|
|
public function testAktualisierenMitLeerenNameWirft(): void
|
|
{
|
|
$this->expectException(BusinessRuleViolationException::class);
|
|
|
|
$this->geplant()->aktualisieren(' ', '', [], 1, $this->jetzt());
|
|
}
|
|
|
|
public function testMitSortOrderNegativWirft(): void
|
|
{
|
|
$this->expectException(BusinessRuleViolationException::class);
|
|
|
|
$this->geplant()->mitSortOrder(-1);
|
|
}
|
|
|
|
public function testMitSortOrderÄndertNurPosition(): void
|
|
{
|
|
$m = $this->geplant()->mitSortOrder(5);
|
|
|
|
self::assertSame(5, $m->sortOrder);
|
|
self::assertSame('Spaghetti Bolognese', $m->name);
|
|
}
|
|
|
|
public function testMitIdSetztPersistenzId(): void
|
|
{
|
|
$m = $this->geplant()->mitId(42);
|
|
|
|
self::assertSame(42, $m->id);
|
|
}
|
|
|
|
public function testGegessenOhneZeitpunktWirft(): void
|
|
{
|
|
$this->expectException(BusinessRuleViolationException::class);
|
|
|
|
new Mahlzeit(1, 'X', '', [], 0, MahlzeitStatus::Gegessen, $this->jetzt(), $this->jetzt(), null);
|
|
}
|
|
|
|
public function testGeplanteMitGegessenAmWirft(): void
|
|
{
|
|
$this->expectException(BusinessRuleViolationException::class);
|
|
|
|
new Mahlzeit(1, 'X', '', [], 0, MahlzeitStatus::Geplant, $this->jetzt(), $this->jetzt(), $this->jetzt());
|
|
}
|
|
|
|
public function testUngültigerWochentagWirft(): void
|
|
{
|
|
$this->expectException(\TypeError::class);
|
|
|
|
Mahlzeit::anlegen('X', '', ['kein-tag'], 0, $this->jetzt());
|
|
}
|
|
}
|