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. } } }