Merge commit '4b37f8fd05804d2d76e11cec1afce52919f2eb59' into work/tasks
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
# Test Evidence: Platform foundation
|
||||||
|
|
||||||
|
- **Test type:** Integration
|
||||||
|
- **Requirement IDs:** `ARC-001–ARC-008, DB-003–DB-012, OPS-003, TST-001–TST-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 Monday–Friday 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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,6 @@ spring:
|
|||||||
docker:
|
docker:
|
||||||
compose:
|
compose:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
lab:
|
||||||
|
security:
|
||||||
|
master-key: AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
|
||||||
|
|||||||
Reference in New Issue
Block a user