clock = $this->createMock(ClockInterface::class); $this->connection = $this->createMock(Connection::class); $this->messageBus = $this->createMock(MessageBusInterface::class); $this->logger = $this->createMock(LoggerInterface::class); $this->firstNow = new \DateTimeImmutable('2024-01-02T07:15:00+00:00'); $this->cronjob = new DailyNotificationDigestCronjob( $this->clock, $this->connection, $this->messageBus, $this->logger ); } public function testGetKey(): void { $this->assertEquals('daily-notification-digest', $this->cronjob->getKey()); } /** * @dataProvider canRunTimeDataProvider */ public function testCanRunWithNullCronJobExecution(int $hour, bool $expected): void { $now = new \DateTimeImmutable("2024-01-01 {$hour}:00:00"); $this->clock->expects($this->once()) ->method('now') ->willReturn($now); $result = $this->cronjob->canRun(null); $this->assertEquals($expected, $result); } public static function canRunTimeDataProvider(): array { return [ 'hour 5 - should not run' => [5, false], 'hour 6 - should run' => [6, true], 'hour 7 - should run' => [7, true], 'hour 8 - should run' => [8, true], 'hour 9 - should not run' => [9, false], 'hour 10 - should not run' => [10, false], 'hour 23 - should not run' => [23, false], ]; } public function testRunFirstExecutionReturnsStateAndDispatches(): array { // Use MockClock for deterministic time $firstNow = $this->firstNow; $clock = new MockClock($firstNow); // Mock DBAL statement/result $statement = $this->createMock(Statement::class); $result = $this->createMock(Result::class); $this->connection->method('prepare')->willReturn($statement); $statement->method('bindValue')->willReturnSelf(); $statement->method('executeQuery')->willReturn($result); $rows = [ ['user_id' => 10], ['user_id' => 42], ]; $result->method('fetchAllAssociative')->willReturn($rows); $dispatched = []; $this->messageBus->method('dispatch')->willReturnCallback(function ($message) use (&$dispatched) { $dispatched[] = $message; return new Envelope($message); }); $cron = new DailyNotificationDigestCronjob($clock, $this->connection, $this->messageBus, $this->logger); $state = $cron->run([]); // Assert dispatch count and message contents self::assertCount(2, $dispatched); $expectedLast = $firstNow->sub(new \DateInterval('P1D')); foreach ($dispatched as $i => $msg) { self::assertInstanceOf(ScheduleDailyNotificationDigestMessage::class, $msg); self::assertTrue(in_array($msg->getUserId(), [10, 42], true)); self::assertEquals($firstNow, $msg->getCurrentDateTime(), 'compare the current date'); self::assertEquals($expectedLast, $msg->getLastExecutionDateTime(), 'compare the last execution date'); } // Assert returned state self::assertIsArray($state); self::assertArrayHasKey('last_execution', $state); self::assertSame($firstNow->format(\DateTimeInterface::ATOM), $state['last_execution']); return $state; } /** * @depends testRunFirstExecutionReturnsStateAndDispatches */ public function testRunSecondExecutionUsesPreviousState(array $previousState): void { $firstNow = $this->firstNow; $secondNow = $firstNow->add(new \DateInterval('P1D')); $clock = new MockClock($secondNow); // Mock DBAL for a single user this time $statement = $this->createMock(Statement::class); $result = $this->createMock(Result::class); $this->connection->method('prepare')->willReturn($statement); $statement->method('bindValue')->willReturnSelf(); $statement->method('executeQuery')->willReturn($result); $rows = [ ['user_id' => 7], ]; $result->method('fetchAllAssociative')->willReturn($rows); $captured = []; $this->messageBus->method('dispatch')->willReturnCallback(function ($message) use (&$captured) { $captured[] = $message; return new Envelope($message); }); $cron = new DailyNotificationDigestCronjob($clock, $this->connection, $this->messageBus, $this->logger); $cron->run($previousState); self::assertCount(1, $captured); $msg = $captured[0]; self::assertInstanceOf(ScheduleDailyNotificationDigestMessage::class, $msg); self::assertEquals(7, $msg->getUserId()); self::assertEquals($secondNow, $msg->getCurrentDateTime(), 'compare the current date'); self::assertEquals($firstNow, $msg->getLastExecutionDateTime(), 'compare the last execution date'); } public function testRunWithInvalidExecutionState(): void { $firstNow = new \DateTimeImmutable('2025-10-14T10:30:00 Europe/Brussels'); $previousExpected = $firstNow->sub(new \DateInterval('P1D')); $clock = new MockClock($firstNow); // Mock DBAL for a single user this time $statement = $this->createMock(Statement::class); $result = $this->createMock(Result::class); $this->connection->method('prepare')->willReturn($statement); $statement->method('bindValue')->willReturnSelf(); $statement->method('executeQuery')->willReturn($result); $rows = [ ['user_id' => 7], ]; $result->method('fetchAllAssociative')->willReturn($rows); $captured = []; $this->messageBus->method('dispatch')->willReturnCallback(function ($message) use (&$captured) { $captured[] = $message; return new Envelope($message); }); $cron = new DailyNotificationDigestCronjob($clock, $this->connection, $this->messageBus, $this->logger); $cron->run(['last_execution' => 'invalid data']); self::assertCount(1, $captured); $msg = $captured[0]; self::assertInstanceOf(ScheduleDailyNotificationDigestMessage::class, $msg); self::assertEquals(7, $msg->getUserId()); self::assertEquals($firstNow, $msg->getCurrentDateTime(), 'compare the current date'); self::assertEquals($previousExpected, $msg->getLastExecutionDateTime(), 'compare the last execution date'); } }