feat: establish iteration 1 platform foundation

This commit is contained in:
sechmachine
2026-08-14 23:31:39 +07:00
parent 5967f7f70d
commit 4b37f8fd05
13 changed files with 1349 additions and 0 deletions
@@ -0,0 +1,78 @@
# Test Evidence: Platform foundation
- **Test type:** Integration
- **Requirement IDs:** `ARC-001ARC-008, DB-003DB-012, OPS-003, TST-001TST-010`
- **Scenario IDs:** `AC-DB-001, AC-OPS-002, AC-TST-001`
- **Test class/method:** `com.lab.labtimesheet.PlatformFoundationTest`
- **Implementation commit:** `this milestone commit`
## Protected behavior
The application starts with the six required package boundaries, Flyway creates the approved 23-table/56-foreign-key PostgreSQL catalog and seed, and tests receive deterministic time without a developer database.
## Test method
A full Spring context starts against a PostgreSQL 18.4 Testcontainer. JDBC catalog queries independently count application tables and foreign keys and inspect the seed. Class loading checks the declared package boundaries, and the injected test `Clock` is asserted exactly.
## Hand-derived expected result
The approved DDL catalog contains 23 application tables and 56 foreign keys. The seed has checkout grace 30 and five MondayFriday rows. Test time is `2026-08-14T00:00:00Z` in `Asia/Ho_Chi_Minh`.
## RED
**Command**
```text
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
export PATH="$JAVA_HOME/bin:/opt/homebrew/opt/node@24/bin:$PATH"
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
./mvnw -Dtest=PlatformFoundationTest test
```
**Observed result**
```text
Tests run: 3, Failures: 1, Errors: 1, Skipped: 0
PlatformFoundationTest.flywayCreatesApprovedPostgresCatalog: expected: 23 but was: 0
PlatformFoundationTest.applicationExposesRequiredModulePackages: ClassNotFound com.lab.labtimesheet.accounts.package-info
BUILD FAILURE
```
Flyway reported zero migrations and the first required boundary class was absent, so the failure was caused by the missing foundation.
## GREEN
**Command**
```text
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
export PATH="$JAVA_HOME/bin:/opt/homebrew/opt/node@24/bin:$PATH"
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
./mvnw -Dtest=PlatformFoundationTest test
```
**Observed result**
```text
Successfully applied 1 migration to schema "public", now at version v1
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
```
## Affected suite
**Command and result**
```text
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
export PATH="$JAVA_HOME/bin:/opt/homebrew/opt/node@24/bin:$PATH"
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
./mvnw test
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
```
## External-test boundaries
This proves migration replay and catalog shape on an ephemeral local PostgreSQL 18.4 container. It does not prove application container, Compose, CI, external SMTP, browser, publication, or deployment behavior; those boundaries are deferred or owned elsewhere.
@@ -0,0 +1,7 @@
package com.lab.labtimesheet.accounts;
/** Accounts and security module boundary. */
public final class ModuleBoundary {
private ModuleBoundary() {
}
}
@@ -0,0 +1,7 @@
package com.lab.labtimesheet.attendance;
/** Attendance, leave, and corrections module boundary. */
public final class ModuleBoundary {
private ModuleBoundary() {
}
}
@@ -0,0 +1,7 @@
package com.lab.labtimesheet.configuration;
/** Configuration, integrations, and calendar module boundary. */
public final class ModuleBoundary {
private ModuleBoundary() {
}
}
@@ -0,0 +1,15 @@
package com.lab.labtimesheet.configuration;
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
class TimeConfiguration {
@Bean
Clock applicationClock() {
return Clock.systemUTC();
}
}
@@ -0,0 +1,7 @@
package com.lab.labtimesheet.notifications;
/** Notifications module boundary. */
public final class ModuleBoundary {
private ModuleBoundary() {
}
}
@@ -0,0 +1,7 @@
package com.lab.labtimesheet.projects;
/** Projects and tasks module boundary. */
public final class ModuleBoundary {
private ModuleBoundary() {
}
}
@@ -0,0 +1,7 @@
package com.lab.labtimesheet.reporting;
/** Reporting module boundary. */
public final class ModuleBoundary {
private ModuleBoundary() {
}
}
+2
View File
@@ -7,4 +7,6 @@ spring:
compose: compose:
enabled: false enabled: false
jpa: jpa:
hibernate:
ddl-auto: validate
open-in-view: false open-in-view: false
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,75 @@
package com.lab.labtimesheet;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
@Import(TestcontainersConfiguration.class)
@SpringBootTest
@ActiveProfiles("test")
class PlatformFoundationTest {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private DataSource dataSource;
@Autowired
private Clock clock;
@Test
void applicationExposesRequiredModulePackages() throws ClassNotFoundException {
assertThat(applicationContext).isNotNull();
assertThat(Class.forName("com.lab.labtimesheet.accounts.ModuleBoundary")).isNotNull();
assertThat(Class.forName("com.lab.labtimesheet.configuration.ModuleBoundary")).isNotNull();
assertThat(Class.forName("com.lab.labtimesheet.projects.ModuleBoundary")).isNotNull();
assertThat(Class.forName("com.lab.labtimesheet.attendance.ModuleBoundary")).isNotNull();
assertThat(Class.forName("com.lab.labtimesheet.notifications.ModuleBoundary")).isNotNull();
assertThat(Class.forName("com.lab.labtimesheet.reporting.ModuleBoundary")).isNotNull();
}
@Test
void flywayCreatesApprovedPostgresCatalog() {
JdbcTemplate jdbc = new JdbcTemplate(dataSource);
Integer tables = jdbc.queryForObject("""
select count(*)
from information_schema.tables
where table_schema = 'public'
and table_type = 'BASE TABLE'
and table_name <> 'flyway_schema_history'
""", Integer.class);
Integer foreignKeys = jdbc.queryForObject("""
select count(*)
from pg_constraint c
join pg_namespace n on n.oid = c.connamespace
where n.nspname = 'public' and c.contype = 'f'
""", Integer.class);
assertThat(tables).isEqualTo(23);
assertThat(foreignKeys).isEqualTo(56);
assertThat(jdbc.queryForObject(
"select checkout_grace_minutes from attendance_policy_versions where effective_from = date '1970-01-01'",
Integer.class)).isEqualTo(30);
assertThat(jdbc.queryForObject("select count(*) from attendance_policy_workdays", Integer.class)).isEqualTo(5);
}
@Test
void testClockIsDeterministic() {
assertThat(clock.instant()).isEqualTo(Instant.parse("2026-08-14T00:00:00Z"));
assertThat(clock.getZone()).isEqualTo(ZoneId.of("Asia/Ho_Chi_Minh"));
}
}
@@ -1,8 +1,13 @@
package com.lab.labtimesheet; package com.lab.labtimesheet;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.testcontainers.postgresql.PostgreSQLContainer; import org.testcontainers.postgresql.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.DockerImageName;
@@ -15,4 +20,10 @@ class TestcontainersConfiguration {
return new PostgreSQLContainer(DockerImageName.parse("postgres:18.4")); return new PostgreSQLContainer(DockerImageName.parse("postgres:18.4"));
} }
@Bean
@Primary
Clock testClock() {
return Clock.fixed(Instant.parse("2026-08-14T00:00:00Z"), ZoneId.of("Asia/Ho_Chi_Minh"));
}
} }
+3
View File
@@ -2,3 +2,6 @@ spring:
docker: docker:
compose: compose:
enabled: false enabled: false
lab:
security:
master-key: AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=