Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b13c547068 | ||
|
|
a9fb948769 |
@@ -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:** `a9fb9487692e84f5a7e7923570cbded58c362a2f`
|
||||||
|
|
||||||
|
## 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`.
|
||||||
@@ -1,23 +1,43 @@
|
|||||||
package com.lab.labtimesheet;
|
package com.lab.labtimesheet;
|
||||||
|
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
|
||||||
import com.lab.labtimesheet.config.SecurityProperties;
|
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.
|
||||||
|
*
|
||||||
|
* <p>The entry point also canonicalizes the legacy Windows Vietnam timezone alias before database
|
||||||
|
* drivers inspect the JVM default timezone.
|
||||||
|
*/
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableConfigurationProperties(SecurityProperties.class)
|
@EnableConfigurationProperties(SecurityProperties.class)
|
||||||
public class LabtimesheetApplication {
|
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
|
* @param args command-line arguments forwarded to Spring Boot
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
normalizeDefaultTimeZone();
|
||||||
SpringApplication.run(LabtimesheetApplication.class, args);
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<SpringApplication> springApplication = mockStatic(SpringApplication.class)) {
|
||||||
|
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Saigon"));
|
||||||
|
|
||||||
|
LabtimesheetApplication.main(args);
|
||||||
|
|
||||||
|
assertThat(TimeZone.getDefault().getID()).isEqualTo("Asia/Ho_Chi_Minh");
|
||||||
|
springApplication.verify(() -> SpringApplication.run(LabtimesheetApplication.class, args));
|
||||||
|
} finally {
|
||||||
|
TimeZone.setDefault(originalTimeZone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void canonicalizesLegacyVietnamAliasBeforePostgresConnects() throws SQLException {
|
||||||
|
TimeZone originalTimeZone = TimeZone.getDefault();
|
||||||
|
|
||||||
|
try (PostgreSQLContainer postgres =
|
||||||
|
new PostgreSQLContainer(DockerImageName.parse("postgres:18.4"))) {
|
||||||
|
postgres.start();
|
||||||
|
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Saigon"));
|
||||||
|
|
||||||
|
assertThat(TimeZone.getDefault().getID()).isEqualTo("Asia/Saigon");
|
||||||
|
assertThatThrownBy(() -> openConnection(postgres))
|
||||||
|
.isInstanceOf(SQLException.class)
|
||||||
|
.hasMessageContaining("Asia/Saigon");
|
||||||
|
|
||||||
|
LabtimesheetApplication.normalizeDefaultTimeZone();
|
||||||
|
|
||||||
|
assertThat(TimeZone.getDefault().getID()).isEqualTo("Asia/Ho_Chi_Minh");
|
||||||
|
try (Connection connection = openConnection(postgres)) {
|
||||||
|
assertThat(connection.isValid(1)).isTrue();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
TimeZone.setDefault(originalTimeZone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void leavesSupportedSystemTimeZoneUnchanged() {
|
||||||
|
TimeZone originalTimeZone = TimeZone.getDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
TimeZone.setDefault(TimeZone.getTimeZone("Europe/Paris"));
|
||||||
|
|
||||||
|
LabtimesheetApplication.normalizeDefaultTimeZone();
|
||||||
|
|
||||||
|
assertThat(TimeZone.getDefault().getID()).isEqualTo("Europe/Paris");
|
||||||
|
} finally {
|
||||||
|
TimeZone.setDefault(originalTimeZone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Connection openConnection(PostgreSQLContainer postgres) throws SQLException {
|
||||||
|
return DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user