'2026-04-01', 'duration' => 7200, 'count' => 3], ['day' => '2026-04-02', 'duration' => 3600, 'count' => 1], ]; $service = $this->createServiceWithResults($mockResults); $result = $service->getDailyAggregation( $this->createMock(User::class), new \DateTimeImmutable('2026-04-01'), new \DateTimeImmutable('2026-04-30') ); $this->assertCount(2, $result); $this->assertEquals('2026-04-01', $result[0]['date']); $this->assertEquals(2.0, $result[0]['hours']); $this->assertEquals(3, $result[0]['count']); $this->assertEquals('2026-04-02', $result[1]['date']); $this->assertEquals(1.0, $result[1]['hours']); $this->assertEquals(1, $result[1]['count']); } public function testGetDailyAggregationReturnsEmptyForNoData(): void { $service = $this->createServiceWithResults([]); $result = $service->getDailyAggregation( $this->createMock(User::class), new \DateTimeImmutable('2026-04-01'), new \DateTimeImmutable('2026-04-30') ); $this->assertCount(0, $result); } public function testHoursRoundedToTwoDecimals(): void { $mockResults = [ ['day' => '2026-04-01', 'duration' => 5431, 'count' => 2], ]; $service = $this->createServiceWithResults($mockResults); $result = $service->getDailyAggregation( $this->createMock(User::class), new \DateTimeImmutable('2026-04-01'), new \DateTimeImmutable('2026-04-30') ); $this->assertEquals(1.51, $result[0]['hours']); } public function testGetHourlyAggregationReturnsFormattedResults(): void { $service = $this->createServiceWithNativeResults([ ['hour_slot' => 9, 'duration' => 7200, 'count' => 5], ]); $user = $this->createMock(User::class); $user->method('getTimezone')->willReturn('Europe/Berlin'); $user->method('getId')->willReturn(1); $result = $service->getHourlyAggregation( $user, new \DateTimeImmutable('2026-04-01'), new \DateTimeImmutable('2026-04-30') ); $this->assertCount(1, $result); $this->assertSame(9, $result[0]['hour']); $this->assertSame(2.0, $result[0]['hours']); $this->assertSame(5, $result[0]['count']); } public function testGetHourlyAggregationReturnsEmptyForNoData(): void { $service = $this->createServiceWithNativeResults([]); $user = $this->createMock(User::class); $user->method('getTimezone')->willReturn('UTC'); $user->method('getId')->willReturn(1); $result = $service->getHourlyAggregation( $user, new \DateTimeImmutable('2026-04-01'), new \DateTimeImmutable('2026-04-30') ); $this->assertCount(0, $result); } public function testGetDayHourAggregationReturnsFormattedResults(): void { // MySQL DAYOFWEEK: 2 = Monday // weekStart=monday -> Monday = day 0 $service = $this->createServiceWithNativeResults([ ['dow' => 2, 'hour_slot' => 14, 'duration' => 3600, 'count' => 1], ]); $user = $this->createMock(User::class); $user->method('getTimezone')->willReturn('Europe/Berlin'); $user->method('getId')->willReturn(1); $result = $service->getDayHourAggregation( $user, new \DateTimeImmutable('2026-04-01'), new \DateTimeImmutable('2026-04-30'), null, null, null, 'monday' ); $this->assertCount(1, $result); $this->assertSame(0, $result[0]['day']); $this->assertSame(14, $result[0]['hour']); $this->assertSame(1.0, $result[0]['hours']); $this->assertSame(1, $result[0]['count']); } public function testGetDayHourAggregationSundayStart(): void { // MySQL DAYOFWEEK: 2 = Monday // weekStart=sunday -> Monday = day 1 $service = $this->createServiceWithNativeResults([ ['dow' => 2, 'hour_slot' => 14, 'duration' => 3600, 'count' => 1], ]); $user = $this->createMock(User::class); $user->method('getTimezone')->willReturn('Europe/Berlin'); $user->method('getId')->willReturn(1); $result = $service->getDayHourAggregation( $user, new \DateTimeImmutable('2026-04-01'), new \DateTimeImmutable('2026-04-30'), null, null, null, 'sunday' ); $this->assertCount(1, $result); $this->assertSame(1, $result[0]['day']); $this->assertSame(14, $result[0]['hour']); $this->assertSame(1.0, $result[0]['hours']); $this->assertSame(1, $result[0]['count']); } public function testGetDailyAggregationWithActivityFilter(): void { $service = $this->createServiceWithResults([ ['day' => '2026-04-01', 'duration' => 3600, 'count' => 1], ]); $result = $service->getDailyAggregation( $this->createMock(User::class), new \DateTimeImmutable('2026-04-01'), new \DateTimeImmutable('2026-04-30'), null, null, 42 ); $this->assertCount(1, $result); } public function testGetDailyAggregationWithCustomerFilter(): void { $service = $this->createServiceWithResults([ ['day' => '2026-04-01', 'duration' => 3600, 'count' => 1], ]); $result = $service->getDailyAggregation( $this->createMock(User::class), new \DateTimeImmutable('2026-04-01'), new \DateTimeImmutable('2026-04-30'), null, 99 ); $this->assertCount(1, $result); } public function testGetUserCustomersReturnsFormattedResults(): void { $service = $this->createServiceWithResults([ ['customerId' => 1, 'name' => 'Acme'], ]); $result = $service->getUserCustomers($this->createMock(User::class)); $this->assertCount(1, $result); $this->assertSame(1, $result[0]['id']); $this->assertSame('Acme', $result[0]['name']); } public function testGetUserActivitiesReturnsFormattedResults(): void { $service = $this->createServiceWithResults([ ['activityId' => 5, 'name' => 'Dev'], ]); $result = $service->getUserActivities($this->createMock(User::class)); $this->assertCount(1, $result); $this->assertSame(5, $result[0]['id']); $this->assertSame('Dev', $result[0]['name']); } public function testGetUserActivitiesWithProjectScope(): void { $service = $this->createServiceWithResults([ ['activityId' => 5, 'name' => 'Dev'], ]); $result = $service->getUserActivities($this->createMock(User::class), 10); $this->assertCount(1, $result); $this->assertSame(5, $result[0]['id']); } private function createServiceWithResults(array $results): HeatmapService { $query = $this->createMock(AbstractQuery::class); $query->method('getResult')->willReturn($results); $qb = $this->createMock(QueryBuilder::class); $qb->method('select')->willReturnSelf(); $qb->method('addSelect')->willReturnSelf(); $qb->method('andWhere')->willReturnSelf(); $qb->method('setParameter')->willReturnSelf(); $qb->method('groupBy')->willReturnSelf(); $qb->method('orderBy')->willReturnSelf(); $qb->method('join')->willReturnSelf(); $qb->method('expr')->willReturn(new Expr()); $qb->method('getQuery')->willReturn($query); $repo = $this->createMock(TimesheetRepository::class); $repo->method('createQueryBuilder')->willReturn($qb); $em = $this->createMock(\Doctrine\ORM\EntityManagerInterface::class); return new HeatmapService($repo, $em); } private function createServiceWithNativeResults(array $results): HeatmapService { $statement = $this->createMock(\Doctrine\DBAL\Result::class); $statement->method('fetchAllAssociative')->willReturn($results); $connection = $this->createMock(\Doctrine\DBAL\Connection::class); $connection->method('executeQuery')->willReturn($statement); $em = $this->createMock(\Doctrine\ORM\EntityManagerInterface::class); $em->method('getConnection')->willReturn($connection); $repo = $this->createMock(TimesheetRepository::class); return new HeatmapService($repo, $em); } }