diff --git a/backend/src/test/java/com/example/app/report/ReportServiceTest.java b/backend/src/test/java/com/example/app/report/ReportServiceTest.java new file mode 100644 index 0000000..2e35b66 --- /dev/null +++ b/backend/src/test/java/com/example/app/report/ReportServiceTest.java @@ -0,0 +1,63 @@ +package com.example.app.report; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +import com.example.app.report.ReportDtos.WeeklyReport; +import com.example.app.time.TimeEntryRepository; +import java.sql.Date; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ReportServiceTest { + + @Mock TimeEntryRepository timeEntryRepository; + @InjectMocks ReportService reportService; + + @Test + void weeklyReport_aggregatesDayTotalsCorrectly() { + LocalDate monday = ReportService.parseIsoWeekToMonday("2026-W20"); + Object[] row = {Date.valueOf(monday), 120L, 9600L}; // 2h, 96.00 EUR + + when(timeEntryRepository.aggregateByDay(anyLong(), any(), any())).thenReturn(List.of(row)); + + WeeklyReport report = reportService.weeklyReport(1L, "2026-W20"); + + assertThat(report.days()).hasSize(7); + assertThat(report.weekTotalMinutes()).isEqualTo(120); + assertThat(report.weekTotalCostCents()).isEqualTo(9600); + // first day has data, rest are zero + assertThat(report.days().get(0).totalMinutes()).isEqualTo(120); + assertThat(report.days().get(1).totalMinutes()).isEqualTo(0); + } + + @Test + void parseIsoWeekToMonday_returnsCorrectMonday() { + // 2026-W01 should start on Monday 2025-12-29 (ISO week based year) + LocalDate monday = ReportService.parseIsoWeekToMonday("2026-W20"); + assertThat(monday.getDayOfWeek().getValue()).isEqualTo(1); // Monday + } + + @Test + void monthlyReport_marksBreachedProjects() { + // budget 60 minutes, logged 90 — breached + Object[] row = {1L, "Acme", 60, 100_00, 90L, 120_00L}; + when(timeEntryRepository.aggregateByProjectForMonth(anyLong(), anyInt(), anyInt())) + .thenReturn(List.of(row)); + + var report = reportService.monthlyReport(1L, "2026-05"); + + assertThat(report.projects()).hasSize(1); + assertThat(report.projects().get(0).breached()).isTrue(); + assertThat(report.projects().get(0).timeBudgetPct()).isGreaterThan(100.0); + } +} diff --git a/backend/src/test/java/com/example/app/time/TimeEntryEndToEndTest.java b/backend/src/test/java/com/example/app/time/TimeEntryEndToEndTest.java new file mode 100644 index 0000000..8253a71 --- /dev/null +++ b/backend/src/test/java/com/example/app/time/TimeEntryEndToEndTest.java @@ -0,0 +1,69 @@ +package com.example.app.time; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.example.app.project.ProjectRepository; +import com.example.app.user.UserRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +class TimeEntryEndToEndTest { + + @Autowired MockMvc mockMvc; + @Autowired ObjectMapper objectMapper; + @Autowired TimeEntryRepository timeEntryRepository; + @Autowired UserRepository userRepository; + @Autowired ProjectRepository projectRepository; + + @AfterEach + void cleanup() { + timeEntryRepository.deleteAll(); + } + + @Test + void postTimeEntry_thenAppearInWeeklyReport() throws Exception { + long userId = userRepository.findAll().get(0).getId(); + long projectId = projectRepository.findAll().get(0).getId(); + LocalDate today = LocalDate.now(); + String isoWeek = today.format(DateTimeFormatter.ofPattern("YYYY-'W'ww")); + + String body = + objectMapper.writeValueAsString( + Map.of( + "userId", + userId, + "projectId", + projectId, + "entryDate", + today.toString(), + "durationMinutes", + 90)); + + mockMvc + .perform(post("/api/time-entries").contentType(MediaType.APPLICATION_JSON).content(body)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.durationMinutes").value(90)); + + mockMvc + .perform( + get("/api/reports/weekly") + .param("userId", String.valueOf(userId)) + .param("week", isoWeek)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.weekTotalMinutes").value(90)); + } +} diff --git a/backend/src/test/java/com/example/app/time/TimeEntryRepositoryTest.java b/backend/src/test/java/com/example/app/time/TimeEntryRepositoryTest.java new file mode 100644 index 0000000..8b72971 --- /dev/null +++ b/backend/src/test/java/com/example/app/time/TimeEntryRepositoryTest.java @@ -0,0 +1,56 @@ +package com.example.app.time; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.example.app.project.Project; +import com.example.app.project.ProjectRepository; +import com.example.app.user.User; +import com.example.app.user.UserRepository; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.TestPropertySource; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@TestPropertySource( + properties = {"spring.jpa.hibernate.ddl-auto=validate", "spring.flyway.enabled=true"}) +class TimeEntryRepositoryTest { + + @Autowired TimeEntryRepository timeEntryRepository; + @Autowired UserRepository userRepository; + @Autowired ProjectRepository projectRepository; + + @Test + void findByUserAndDateRange_returnsOnlyEntriesInRange() { + User user = userRepository.findAll().get(0); + Project project = projectRepository.findAll().get(0); + + LocalDate today = LocalDate.now(); + TimeEntry inRange = entry(user, project, today, 60); + TimeEntry outOfRange = entry(user, project, today.minusDays(10), 30); + timeEntryRepository.saveAll(List.of(inRange, outOfRange)); + + List result = + timeEntryRepository.findByUserAndDateRange( + user.getId(), today.minusDays(1), today.plusDays(1)); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getDurationMinutes()).isEqualTo(60); + + // cleanup + timeEntryRepository.deleteAll(List.of(inRange, outOfRange)); + } + + private TimeEntry entry(User user, Project project, LocalDate date, int minutes) { + TimeEntry e = new TimeEntry(); + e.setUser(user); + e.setProject(project); + e.setEntryDate(date); + e.setDurationMinutes(minutes); + return e; + } +}