Files
jens 1589088379 M1: CRUD-Slice 'Mahlzeit' Schritt 1 – Logik + Daten + Tests
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).
2026-09-19 08:55:54 +02:00

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());
}
}