From a9fb9487692e84f5a7e7923570cbded58c362a2f Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:07:27 +0700 Subject: [PATCH] fix(platform): normalize legacy Vietnam timezone --- .../integration/windows-timezone-alias.md | 82 +++++++++++++++++++ .../labtimesheet/LabtimesheetApplication.java | 24 +++++- .../ApplicationTimeZoneIntegrationTest.java | 80 ++++++++++++++++++ 3 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 docs/tests/integration/windows-timezone-alias.md create mode 100644 src/test/java/com/lab/labtimesheet/ApplicationTimeZoneIntegrationTest.java diff --git a/docs/tests/integration/windows-timezone-alias.md b/docs/tests/integration/windows-timezone-alias.md new file mode 100644 index 0000000..1fbf9ae --- /dev/null +++ b/docs/tests/integration/windows-timezone-alias.md @@ -0,0 +1,82 @@ +# Test Evidence: Windows legacy Vietnam timezone startup + +- **Test type:** Integration +- **Requirement IDs:** `ARC-001`, `ARC-003`, `GOV-011`, `ATT-002`, `TST-001`, `TST-005` +- **Scenario IDs:** `N/A — user-reported cross-platform startup defect` +- **Test class/method:** `com.lab.labtimesheet.ApplicationTimeZoneIntegrationTest.mainCanonicalizesLegacyAliasBeforeStartingSpring`, `com.lab.labtimesheet.ApplicationTimeZoneIntegrationTest.canonicalizesLegacyVietnamAliasBeforePostgresConnects`, `com.lab.labtimesheet.ApplicationTimeZoneIntegrationTest.leavesSupportedSystemTimeZoneUnchanged` +- **Implementation commit:** `pending` + +## Protected behavior + +The executable entry point replaces the legacy Windows JVM timezone ID `Asia/Saigon` with the canonical business timezone ID `Asia/Ho_Chi_Minh` before pgJDBC opens a PostgreSQL connection. Other supported operating-system timezone IDs remain unchanged. + +## Test method + +The entry-point test replaces Spring startup with Mockito's existing static test seam, invokes the real `main` method with a legacy JVM default, and checks that normalization happens before Spring starts. The PostgreSQL test starts a real PostgreSQL 18.4 Testcontainer, proves pgJDBC 42.7.11 is rejected while the JVM default is `Asia/Saigon`, invokes the same startup normalization, and then opens a valid JDBC connection. A negative test verifies that an unrelated supported timezone is not overwritten. + +## Hand-derived expected result + +PostgreSQL does not accept `Asia/Saigon` as a startup `TimeZone`, while the approved business timezone is `Asia/Ho_Chi_Minh`. Therefore only the legacy alias is replaced, the following connection succeeds, and a supported non-Vietnam timezone remains unchanged. + +## 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=ApplicationTimeZoneIntegrationTest test +``` + +**Observed result** + +```text +BUILD FAILURE during test compilation. +ApplicationTimeZoneIntegrationTest.java: cannot find symbol normalizeDefaultTimeZone() +``` + +The failing test established that the application had no pre-Spring normalization boundary. + +A second mutation check temporarily removed the new call from `main` and ran: + +```text +env JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:/opt/homebrew/bin:/usr/bin:/bin ./mvnw '-Dtest=ApplicationTimeZoneIntegrationTest#mainCanonicalizesLegacyAliasBeforeStartingSpring' test +``` + +It failed `1/1` with `expected: "Asia/Ho_Chi_Minh" but was: "Asia/Saigon"`, proving the test protects the entry-point ordering rather than only the helper. + +## 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=ApplicationTimeZoneIntegrationTest test +``` + +**Observed result** + +```text +Tests run: 3, 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=ApplicationTimeZoneIntegrationTest,LabtimesheetApplicationTests,PlatformFoundationTest,TimeConfigurationTest,CalendarDevelopmentProfileWebIntegrationTest' test +Tests run: 8, 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 test +Tests run: 217, 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 ./mvnw -DskipTests -Ddoclint=all javadoc:javadoc +BUILD SUCCESS +``` + +A local Java 25 process was also started with `-Duser.timezone=Asia/Saigon` against a disposable PostgreSQL 18.4 database on port `55439`. Hikari connected, Flyway migrated the fresh database, Tomcat started on port `18080`, and Spring reported `Started LabtimesheetApplication`. The process shut down cleanly and the disposable database container was removed. + +## External-test boundaries + +The regression executes the installed pgJDBC version against PostgreSQL 18.4 and reproduces the exact rejected timezone value from the Windows report. It does not run the Windows JVM itself; the supplied Windows log is the evidence that its OS/JDK mapping produced `Asia/Saigon`. diff --git a/src/main/java/com/lab/labtimesheet/LabtimesheetApplication.java b/src/main/java/com/lab/labtimesheet/LabtimesheetApplication.java index cacb346..b33d871 100644 --- a/src/main/java/com/lab/labtimesheet/LabtimesheetApplication.java +++ b/src/main/java/com/lab/labtimesheet/LabtimesheetApplication.java @@ -1,23 +1,43 @@ package com.lab.labtimesheet; +import java.util.TimeZone; + import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import com.lab.labtimesheet.config.SecurityProperties; -/** Application entry point and root component-scan boundary for Lab Timesheet. */ +/** + * Application entry point and root component-scan boundary for Lab Timesheet. + * + *
The entry point also canonicalizes the legacy Windows Vietnam timezone alias before database
+ * drivers inspect the JVM default timezone.
+ */
@SpringBootApplication
@EnableConfigurationProperties(SecurityProperties.class)
public class LabtimesheetApplication {
+ private static final String LEGACY_VIETNAM_TIME_ZONE = "Asia/Saigon";
+ private static final String BUSINESS_TIME_ZONE = "Asia/Ho_Chi_Minh";
/**
- * Starts the standalone Spring Boot process.
+ * Canonicalizes the process timezone and starts the standalone Spring Boot process.
*
* @param args command-line arguments forwarded to Spring Boot
*/
public static void main(String[] args) {
+ normalizeDefaultTimeZone();
SpringApplication.run(LabtimesheetApplication.class, args);
}
+ /**
+ * Replaces the legacy Windows Vietnam alias before pgJDBC sends it to PostgreSQL as a startup
+ * parameter. Other supported system timezones remain unchanged.
+ */
+ static void normalizeDefaultTimeZone() {
+ if (LEGACY_VIETNAM_TIME_ZONE.equals(TimeZone.getDefault().getID())) {
+ TimeZone.setDefault(TimeZone.getTimeZone(BUSINESS_TIME_ZONE));
+ }
+ }
+
}
diff --git a/src/test/java/com/lab/labtimesheet/ApplicationTimeZoneIntegrationTest.java b/src/test/java/com/lab/labtimesheet/ApplicationTimeZoneIntegrationTest.java
new file mode 100644
index 0000000..2b9a8ee
--- /dev/null
+++ b/src/test/java/com/lab/labtimesheet/ApplicationTimeZoneIntegrationTest.java
@@ -0,0 +1,80 @@
+package com.lab.labtimesheet;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mockStatic;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.TimeZone;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.springframework.boot.SpringApplication;
+import org.testcontainers.postgresql.PostgreSQLContainer;
+import org.testcontainers.utility.DockerImageName;
+
+class ApplicationTimeZoneIntegrationTest {
+
+ @Test
+ void mainCanonicalizesLegacyAliasBeforeStartingSpring() {
+ TimeZone originalTimeZone = TimeZone.getDefault();
+ String[] args = {"--spring.profiles.active=test"};
+
+ try (MockedStatic