From 3519ff08f25ab843a438dc8cfc5dd8988a1ffd70 Mon Sep 17 00:00:00 2001 From: Jens Date: Sat, 19 Sep 2026 10:54:52 +0200 Subject: [PATCH] =?UTF-8?q?M2:=20Status=20&=20Wochentage=20=E2=80=93=20Sch?= =?UTF-8?q?ritt=201=20(Logik):=20CheckOff/Uncheck=20UseCases=20+=20Status-?= =?UTF-8?q?Event=20+=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Event/MahlzeitStatusChangedEvent.php | 24 ++++ .../Essen/UseCase/MahlzeitCheckOffUseCase.php | 48 +++++++ .../Essen/UseCase/MahlzeitUncheckUseCase.php | 47 +++++++ .../Data/Essen/MahlzeitProcessorTest.php | 30 +++++ .../UseCase/MahlzeitCheckOffUseCaseTest.php | 120 ++++++++++++++++++ .../UseCase/MahlzeitUncheckUseCaseTest.php | 118 +++++++++++++++++ 6 files changed, 387 insertions(+) create mode 100644 src/Logic/Essen/Event/MahlzeitStatusChangedEvent.php create mode 100644 src/Logic/Essen/UseCase/MahlzeitCheckOffUseCase.php create mode 100644 src/Logic/Essen/UseCase/MahlzeitUncheckUseCase.php create mode 100644 tests/Unit/Logic/Essen/UseCase/MahlzeitCheckOffUseCaseTest.php create mode 100644 tests/Unit/Logic/Essen/UseCase/MahlzeitUncheckUseCaseTest.php diff --git a/src/Logic/Essen/Event/MahlzeitStatusChangedEvent.php b/src/Logic/Essen/Event/MahlzeitStatusChangedEvent.php new file mode 100644 index 0000000..a5b2c52 --- /dev/null +++ b/src/Logic/Essen/Event/MahlzeitStatusChangedEvent.php @@ -0,0 +1,24 @@ +clock->now(); + $bestehend = $this->mahlzeiten->find($id); + $markiert = $bestehend->markAsGegessen($jetzt); + + $gespeichert = $this->tx->execute(fn () => $this->mahlzeiten->update($markiert)); + + $this->events->dispatch(new MahlzeitStatusChangedEvent( + id: $id, + name: $gespeichert->name, + status: $gespeichert->status, + gegessenAm: $gespeichert->gegessenAm, + )); + + return MahlzeitResponse::fromModel($gespeichert); + } +} diff --git a/src/Logic/Essen/UseCase/MahlzeitUncheckUseCase.php b/src/Logic/Essen/UseCase/MahlzeitUncheckUseCase.php new file mode 100644 index 0000000..b885455 --- /dev/null +++ b/src/Logic/Essen/UseCase/MahlzeitUncheckUseCase.php @@ -0,0 +1,47 @@ +clock->now(); + $bestehend = $this->mahlzeiten->find($id); + $markiert = $bestehend->markAsGeplant($jetzt); + + $gespeichert = $this->tx->execute(fn () => $this->mahlzeiten->update($markiert)); + + $this->events->dispatch(new MahlzeitStatusChangedEvent( + id: $id, + name: $gespeichert->name, + status: $gespeichert->status, + gegessenAm: $gespeichert->gegessenAm, + )); + + return MahlzeitResponse::fromModel($gespeichert); + } +} diff --git a/tests/Integration/Data/Essen/MahlzeitProcessorTest.php b/tests/Integration/Data/Essen/MahlzeitProcessorTest.php index f225e04..fa99ed0 100644 --- a/tests/Integration/Data/Essen/MahlzeitProcessorTest.php +++ b/tests/Integration/Data/Essen/MahlzeitProcessorTest.php @@ -92,4 +92,34 @@ final class MahlzeitProcessorTest extends DatabaseTestCase $this->processor()->delete($m); } + + public function testSavePersistiertAbhakenAlsGegessen(): void + { + $jetzt = $this->jetzt(); + $created = $this->processor()->create(Mahlzeit::anlegen('Bolognese', '', [Wochentag::Mo], 1, $jetzt)); + + $gegessen = $created->markAsGegessen($jetzt); + $this->processor()->save($gegessen); + + $neu = (new MahlzeitProvider($this->em, new MahlzeitMapper()))->find($created->id); + self::assertNotNull($neu); + self::assertSame(MahlzeitStatus::Gegessen, $neu->status); + self::assertSame($jetzt, $neu->gegessenAm); + self::assertTrue($neu->istGegessen()); + } + + public function testSavePersistiertAufhebenAlsGeplant(): void + { + $jetzt = $this->jetzt(); + $created = $this->processor()->create(Mahlzeit::anlegen('Bolognese', '', [Wochentag::Mo], 1, $jetzt)->markAsGegessen($jetzt)); + + $geplant = $created->markAsGeplant($jetzt); + $this->processor()->save($geplant); + + $neu = (new MahlzeitProvider($this->em, new MahlzeitMapper()))->find($created->id); + self::assertNotNull($neu); + self::assertSame(MahlzeitStatus::Geplant, $neu->status); + self::assertNull($neu->gegessenAm); + self::assertTrue($neu->istGeplant()); + } } diff --git a/tests/Unit/Logic/Essen/UseCase/MahlzeitCheckOffUseCaseTest.php b/tests/Unit/Logic/Essen/UseCase/MahlzeitCheckOffUseCaseTest.php new file mode 100644 index 0000000..a29b02d --- /dev/null +++ b/tests/Unit/Logic/Essen/UseCase/MahlzeitCheckOffUseCaseTest.php @@ -0,0 +1,120 @@ +createStub(TransactionManagerInterface::class); + $tx->method('execute')->willReturnCallback(static fn (\Closure $work): mixed => $work($work)); + return $tx; + } + + private function clock(): ClockInterface + { + $clock = $this->createStub(ClockInterface::class); + $clock->method('now')->willReturn($this->jetzt()); + return $clock; + } + + private function geplant(): Mahlzeit + { + $jetzt = $this->jetzt(); + return new Mahlzeit(7, 'Bolognese', '', [Wochentag::Mo], 1, MahlzeitStatus::Geplant, $jetzt, $jetzt, null); + } + + public function testExecuteMarkiertAlsGegessenUndDispatchtEvent(): void + { + $erfasstesModell = null; + $manager = $this->createMock(MahlzeitManagerInterface::class); + $manager->expects(self::once())->method('find')->with(7)->willReturn($this->geplant()); + $manager->expects(self::once())->method('update')->willReturnCallback(function (Mahlzeit $m) use (&$erfasstesModell) { + $erfasstesModell = $m; + return $m; + }); + + $events = $this->createMock(EventDispatcherInterface::class); + $events->expects(self::once())->method('dispatch')->with( + self::equalTo(new MahlzeitStatusChangedEvent( + id: 7, + name: 'Bolognese', + status: MahlzeitStatus::Gegessen, + gegessenAm: $this->jetzt(), + )), + ); + + $useCase = new MahlzeitCheckOffUseCase($this->tx(), $manager, $events, $this->clock()); + + $response = $useCase->execute(7); + + self::assertSame(MahlzeitStatus::Gegessen, $erfasstesModell->status); + self::assertNotNull($erfasstesModell->gegessenAm); + self::assertEquals($this->jetzt(), $erfasstesModell->aktualisiertAm); + self::assertInstanceOf(MahlzeitResponse::class, $response); + self::assertSame(7, $response->id); + self::assertSame(MahlzeitStatus::Gegessen, $response->status); + self::assertNotNull($response->gegessenAm); + } + + public function testExecuteIstIdempotentFuerBereitsGegessene(): void + { + $jetzt = $this->jetzt(); + $bereitsGegessen = new Mahlzeit(7, 'Bolognese', '', [Wochentag::Mo], 1, MahlzeitStatus::Gegessen, $jetzt, $jetzt, $jetzt); + + $manager = $this->createMock(MahlzeitManagerInterface::class); + $manager->expects(self::once())->method('find')->with(7)->willReturn($bereitsGegessen); + $manager->expects(self::once())->method('update')->willReturnArgument(0); + + $events = $this->createMock(EventDispatcherInterface::class); + $events->expects(self::once())->method('dispatch'); + + $useCase = new MahlzeitCheckOffUseCase($this->tx(), $manager, $events, $this->clock()); + + $response = $useCase->execute(7); + + // Ursprünglicher gegessenAm-Zeitpunkt bleibt erhalten (markAsGegessen ist idempotent). + self::assertSame($jetzt, $response->gegessenAm); + self::assertSame(MahlzeitStatus::Gegessen, $response->status); + } + + public function testExecuteWirft404UndDispatchtKeinEvent(): void + { + $manager = $this->createStub(MahlzeitManagerInterface::class); + $manager->method('find')->willThrowException(new MahlzeitNotFoundException(99)); + + $events = $this->createMock(EventDispatcherInterface::class); + $events->expects(self::never())->method('dispatch'); + + $useCase = new MahlzeitCheckOffUseCase($this->tx(), $manager, $events, $this->clock()); + + $this->expectException(MahlzeitNotFoundException::class); + + try { + $useCase->execute(99); + } finally { + // Erwartete Exception oben. + } + } +} diff --git a/tests/Unit/Logic/Essen/UseCase/MahlzeitUncheckUseCaseTest.php b/tests/Unit/Logic/Essen/UseCase/MahlzeitUncheckUseCaseTest.php new file mode 100644 index 0000000..b25794b --- /dev/null +++ b/tests/Unit/Logic/Essen/UseCase/MahlzeitUncheckUseCaseTest.php @@ -0,0 +1,118 @@ +createStub(TransactionManagerInterface::class); + $tx->method('execute')->willReturnCallback(static fn (\Closure $work): mixed => $work($work)); + return $tx; + } + + private function clock(): ClockInterface + { + $clock = $this->createStub(ClockInterface::class); + $clock->method('now')->willReturn($this->jetzt()); + return $clock; + } + + private function gegessen(): Mahlzeit + { + $jetzt = $this->jetzt(); + return new Mahlzeit(7, 'Bolognese', '', [Wochentag::Mo], 1, MahlzeitStatus::Gegessen, $jetzt, $jetzt, $jetzt); + } + + public function testExecuteSetztZurueckAufGeplantUndDispatchtEvent(): void + { + $erfasstesModell = null; + $manager = $this->createMock(MahlzeitManagerInterface::class); + $manager->expects(self::once())->method('find')->with(7)->willReturn($this->gegessen()); + $manager->expects(self::once())->method('update')->willReturnCallback(function (Mahlzeit $m) use (&$erfasstesModell) { + $erfasstesModell = $m; + return $m; + }); + + $events = $this->createMock(EventDispatcherInterface::class); + $events->expects(self::once())->method('dispatch')->with( + self::equalTo(new MahlzeitStatusChangedEvent( + id: 7, + name: 'Bolognese', + status: MahlzeitStatus::Geplant, + gegessenAm: null, + )), + ); + + $useCase = new MahlzeitUncheckUseCase($this->tx(), $manager, $events, $this->clock()); + + $response = $useCase->execute(7); + + self::assertSame(MahlzeitStatus::Geplant, $erfasstesModell->status); + self::assertNull($erfasstesModell->gegessenAm, 'gegessenAm wird zurückgesetzt'); + self::assertInstanceOf(MahlzeitResponse::class, $response); + self::assertSame(7, $response->id); + self::assertSame(MahlzeitStatus::Geplant, $response->status); + self::assertNull($response->gegessenAm); + } + + public function testExecuteIstIdempotentFuerBereitsGeplante(): void + { + $jetzt = $this->jetzt(); + $bereitsGeplant = new Mahlzeit(7, 'Bolognese', '', [Wochentag::Mo], 1, MahlzeitStatus::Geplant, $jetzt, $jetzt, null); + + $manager = $this->createMock(MahlzeitManagerInterface::class); + $manager->expects(self::once())->method('find')->with(7)->willReturn($bereitsGeplant); + $manager->expects(self::once())->method('update')->willReturnArgument(0); + + $events = $this->createMock(EventDispatcherInterface::class); + $events->expects(self::once())->method('dispatch'); + + $useCase = new MahlzeitUncheckUseCase($this->tx(), $manager, $events, $this->clock()); + + $response = $useCase->execute(7); + + self::assertSame(MahlzeitStatus::Geplant, $response->status); + self::assertNull($response->gegessenAm); + } + + public function testExecuteWirft404UndDispatchtKeinEvent(): void + { + $manager = $this->createStub(MahlzeitManagerInterface::class); + $manager->method('find')->willThrowException(new MahlzeitNotFoundException(99)); + + $events = $this->createMock(EventDispatcherInterface::class); + $events->expects(self::never())->method('dispatch'); + + $useCase = new MahlzeitUncheckUseCase($this->tx(), $manager, $events, $this->clock()); + + $this->expectException(MahlzeitNotFoundException::class); + + try { + $useCase->execute(99); + } finally { + // Erwartete Exception oben. + } + } +}