Compare commits

...
5 changed files with 194 additions and 4 deletions
@@ -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.
+4
View File
@@ -46,3 +46,7 @@ No browser loop or Impeccable detector is run on this branch; the root owner per
After merging exact reviewed `main` `32c8a2d315d2175760c5d4792988cd0aa5ab6dd0`, `npm ci`, the 1/1 UI test, frontend build, compile, Project-scoped Javadoc/doclint, and diff check all succeeded. The first affected Java command added the updated shared `UiContractWebTest` and passed 45/45 tests with no failures, errors, or skips.
The bounded post-review affected command reran `ProjectControllerTest,ProjectEntityTest,ProjectPersistenceStructureTest,ProjectServiceIntegrationTest,ProjectTaskMutationContextTest,LayerStructureTest,UiContractWebTest` and passed 47/47 with no failures, errors, or skips, including 9/9 Project service tests against PostgreSQL 18.4. The UI test remained 1/1; frontend build, compile, Project-scoped Javadoc/doclint, and `git diff --check` also succeeded.
The root-owned final full suite then exposed a branch-induced MVC-slice fixture RED: 213 tests ran with 0 failures and 3 errors, all `ProjectTaskFormAccessibilityWebTest` context errors because the slice did not provide the new ProjectController AccountService dependency. A focused reproduction ran the class at 3 tests, 0 failures, 3 errors and reported the same missing AccountService constructor dependency.
The smallest test-only correction supplies the controller's AccountService and Clock dependencies and the existing ProjectQueryService mock's authenticated Mentor response. The intermediate focused runs exposed each dependency in order; no production code changed. Final focused GREEN: `env JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ./mvnw '-Dtest=ProjectTaskFormAccessibilityWebTest' test` passed 3/3 with no failures, errors, or skips. The root owner retains the broader rerun.
-4
View File
@@ -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
@@ -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<ConfigurableApplicationContext> {
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;
}
}
}
@@ -8,14 +8,20 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.lab.labtimesheet.feature.account.service.AccountService;
import com.lab.labtimesheet.feature.integration.service.SmtpConfigurationService;
import com.lab.labtimesheet.feature.project.controller.ProjectController;
import com.lab.labtimesheet.feature.project.model.dto.ProjectActorView;
import com.lab.labtimesheet.feature.project.service.ProjectQueryService;
import com.lab.labtimesheet.feature.project.service.ProjectService;
import com.lab.labtimesheet.feature.task.controller.TaskController;
import com.lab.labtimesheet.feature.task.model.dto.TaskAssigneeChoice;
import com.lab.labtimesheet.feature.task.service.TaskService;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
@@ -34,12 +40,26 @@ class ProjectTaskFormAccessibilityWebTest {
@MockitoBean
private ProjectService projects;
@MockitoBean
private AccountService accounts;
@MockitoBean
private Clock clock;
@MockitoBean
private TaskService tasks;
@MockitoBean
private SmtpConfigurationService smtpConfiguration;
@BeforeEach
void mentorActor() {
given(projectQueries.authenticatedActor("mentor@example.test"))
.willReturn(new ProjectActorView(10L, "MENTOR"));
given(clock.instant()).willReturn(Instant.parse("2026-08-15T01:00:00Z"));
given(clock.getZone()).willReturn(ZoneId.of("Asia/Ho_Chi_Minh"));
}
@Test
void projectFieldErrorsHaveStableIdsAndInputAssociations() throws Exception {
mvc.perform(post("/projects")