From 28ecfabe37b8a7360f0bda7be1533561e1005e20 Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:51:43 +0700 Subject: [PATCH] fix: preserve dev attendance policy wall-clock times --- .../attendance-policy-dev-profile-timezone.md | 83 ++++++++++++++++++ src/main/resources/application-dev.yaml | 4 - ...rDevelopmentProfileWebIntegrationTest.java | 87 +++++++++++++++++++ 3 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 docs/tests/integration/attendance-policy-dev-profile-timezone.md create mode 100644 src/test/java/com/lab/labtimesheet/feature/attendance/controller/CalendarDevelopmentProfileWebIntegrationTest.java diff --git a/docs/tests/integration/attendance-policy-dev-profile-timezone.md b/docs/tests/integration/attendance-policy-dev-profile-timezone.md new file mode 100644 index 0000000..872b313 --- /dev/null +++ b/docs/tests/integration/attendance-policy-dev-profile-timezone.md @@ -0,0 +1,83 @@ +# Test Evidence: development-profile attendance policy time hydration + +- **Test type:** Integration +- **Requirement IDs:** `ATT-002`, `ATT-003`, `I1-ATT-01` +- **Scenario IDs:** `AC-ATT-001` +- **Test class/method:** `com.lab.labtimesheet.feature.attendance.controller.CalendarDevelopmentProfileWebIntegrationTest#v1SeededPolicyLetsFormAuthenticatedAdminOpenCalendarInAsiaHoChiMinhDevelopmentProfile` +- **Implementation commit:** pending + +## Protected behavior + +The unmodified V1 attendance policy must hydrate its `time` schedule as the configured local wall-clock values when the development profile runs in `Asia/Ho_Chi_Minh`. A form-authenticated Admin can therefore open the calendar without weakening the policy rule that requires the checkout cutoff to be before local midnight. + +## Test method + +The test starts the application with the real `dev` profile plus isolated test configuration, forces the JVM default zone to `Asia/Ho_Chi_Minh` before JPA starts, and uses PostgreSQL 18.4 Testcontainers with Flyway V1. It bootstraps an Admin through the form, logs in through the form, and requests `/attendance/calendar`, which resolves the current policy through `AttendanceApplicationService.currentBusinessDate`. + +## Hand-derived expected result + +V1 explicitly stores `scheduled_start = 08:30`, `scheduled_end = 15:30`, and `checkout_grace_minutes = 30`. The checkout cutoff is therefore `16:00`, which is strictly before local midnight, so the calendar request returns HTTP 200. + +## RED + +**Command** + +```text +env JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:/opt/homebrew/bin:/usr/bin:/bin DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock ./mvnw '-Dtest=CalendarDevelopmentProfileWebIntegrationTest' test +``` + +**Observed result** + +```text +PostgreSQL 18.4 Testcontainers applied Flyway V1, then the form-authenticated GET /attendance/calendar failed. +BUILD FAILURE: CalendarDevelopmentProfileWebIntegrationTest ... ServletException caused by +IllegalArgumentException: checkout cutoff must be before local midnight +at AttendancePolicy.java:58 via AttendancePolicyEntity.toDomain and AttendanceApplicationService.currentBusinessDate. +``` + +## GREEN + +**Command** + +```text +env JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:/opt/homebrew/bin:/usr/bin:/bin DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock ./mvnw '-Dtest=CalendarDevelopmentProfileWebIntegrationTest' test +``` + +**Observed result** + +```text +PostgreSQL 18.4 Testcontainers applied Flyway V1. +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +``` + +## Affected suite + +**Command and result** + +```text +env JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:/opt/homebrew/bin:/usr/bin:/bin DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock ./mvnw '-Dtest=CalendarAuthorizationWebIntegrationTest,CalendarDevelopmentProfileWebIntegrationTest,RoleDashboardWebIntegrationTest,AttendancePersistenceIntegrationTest,AttendancePolicyTest' test + +Tests run: 13, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS + +env JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:/opt/homebrew/bin:/usr/bin:/bin DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock ./mvnw '-Dtest=*Attendance*Test,*Calendar*Test,Dashboard*Test,RoleDashboardWebIntegrationTest,AdminDashboardWebTest' test + +Tests run: 60, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +``` + +## Java 26 smoke + +**Command and result** + +```text +env JAVA_HOME=/Users/sechmachine/Library/Java/JavaVirtualMachines/corretto-26.0.2/Contents/Home PATH=/Users/sechmachine/Library/Java/JavaVirtualMachines/corretto-26.0.2/Contents/Home/bin:/opt/homebrew/bin:/usr/bin:/bin ./mvnw clean compile -DskipTests + +Amazon Corretto 26.0.2 compiled 129 source files with release 25. +BUILD SUCCESS +``` + +## External-test boundaries + +This integration test proves the fresh Flyway/JPA/real-login calendar path under the development profile and Vietnam JVM zone. It does not operate the already-running browser-gate application or exercise the Intern dashboard UI itself; both paths resolve the same policy timeline. diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index 6f88464..4f449ad 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -10,10 +10,6 @@ spring: hibernate: ddl-auto: validate open-in-view: false - properties: - hibernate: - jdbc: - time_zone: UTC flyway: enabled: true locations: classpath:db/migration diff --git a/src/test/java/com/lab/labtimesheet/feature/attendance/controller/CalendarDevelopmentProfileWebIntegrationTest.java b/src/test/java/com/lab/labtimesheet/feature/attendance/controller/CalendarDevelopmentProfileWebIntegrationTest.java new file mode 100644 index 0000000..7a006b5 --- /dev/null +++ b/src/test/java/com/lab/labtimesheet/feature/attendance/controller/CalendarDevelopmentProfileWebIntegrationTest.java @@ -0,0 +1,87 @@ +package com.lab.labtimesheet.feature.attendance.controller; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; +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.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.lab.labtimesheet.config.TestcontainersConfiguration; +import java.util.TimeZone; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Import; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; + +@Import(TestcontainersConfiguration.class) +@SpringBootTest(properties = { + "LAB_SMTP_HOST=localhost", + "LAB_SMTP_PORT=1025", + "LAB_SERVER_PORT=0", + "LAB_FORWARD_HEADERS_STRATEGY=none", + "LAB_PUBLIC_ORIGIN=http://localhost:8080", + "LAB_SECURITY_MASTER_KEY=AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" +}) +@AutoConfigureMockMvc +@ActiveProfiles({"dev", "test"}) +@ContextConfiguration(initializers = CalendarDevelopmentProfileWebIntegrationTest.AsiaHoChiMinhTimeZoneInitializer.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class CalendarDevelopmentProfileWebIntegrationTest { + private static final String ADMIN_EMAIL = "admin@example.test"; + private static final String PASSWORD = "correct horse battery staple"; + + @Autowired + private MockMvc mockMvc; + + @Test + void v1SeededPolicyLetsFormAuthenticatedAdminOpenCalendarInAsiaHoChiMinhDevelopmentProfile() throws Exception { + mockMvc.perform(post("/bootstrap") + .with(csrf()) + .param("email", ADMIN_EMAIL) + .param("displayName", "Admin") + .param("password", PASSWORD)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/admin/smtp?onboarding")); + + var login = mockMvc.perform(post("/login") + .with(csrf()) + .param("username", ADMIN_EMAIL) + .param("password", PASSWORD)) + .andExpect(status().is3xxRedirection()) + .andExpect(authenticated().withUsername(ADMIN_EMAIL)) + .andReturn(); + + mockMvc.perform(get("/attendance/calendar") + .session((MockHttpSession) login.getRequest().getSession(false))) + .andExpect(status().isOk()); + } + + @AfterAll + static void restoreSystemDefaultTimeZone() { + TimeZone.setDefault(AsiaHoChiMinhTimeZoneInitializer.originalDefaultTimeZone()); + } + + static final class AsiaHoChiMinhTimeZoneInitializer + implements ApplicationContextInitializer { + private static final TimeZone ORIGINAL_DEFAULT_TIME_ZONE = TimeZone.getDefault(); + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Ho_Chi_Minh")); + } + + static TimeZone originalDefaultTimeZone() { + return ORIGINAL_DEFAULT_TIME_ZONE; + } + } +}