fix(ci): stabilize gate-all tests and contract gate

This commit is contained in:
2026-02-14 19:24:17 +08:00
parent 948befe269
commit 4cd5a6a52c
9 changed files with 92 additions and 44 deletions
+44
View File
@@ -0,0 +1,44 @@
# Repository Guidelines
## Project Structure & Module Organization
This repository is a multi-module Maven project (`pom.xml`) targeting Java 21.
- `ljwx-platform-app`: Spring Boot entrypoint, controllers, facade/appservice/domain/infra layers, integration tests.
- `ljwx-platform-core`: shared result models, constants, and error codes.
- `ljwx-platform-web`: web filters and global exception handling.
- `ljwx-platform-security`: JWT, auth, and tenant context/isolation support.
- `ljwx-platform-data`: datasource/MyBatis/Flyway configuration and migrations (`src/main/resources/db/migration`).
- `contracts/`: OpenAPI and security policy contracts.
- `docs/`: architecture, requirements, design, contracts, dev guide, runbook, ADRs.
- `scripts/gates`: CI/local quality gates.
## Build, Test, and Development Commands
- `./mvnw -q -DskipTests package`: compile all modules quickly.
- `./mvnw -q test`: run unit tests (`*Test.java`).
- `./mvnw -q -PIT verify`: run integration tests (`*IT.java`, requires Docker/Testcontainers).
- `./mvnw -pl ljwx-platform-app -am spring-boot:run`: run the app locally.
- `bash scripts/gates/gate-all.sh`: run manifest, PR, tests, contract, and NFR gates.
## Coding Style & Naming Conventions
Follow layer dependency direction strictly: `controller -> facade -> appservice -> domain <- infra`.
- Do not call MyBatis mapper directly outside `infra.persistence.repository`.
- Keep domain model classes framework-agnostic.
- Package root pattern: `com.ljwx.platform.{module}`.
- Migration naming: `V{3-digit}__description.sql` (example: `V003__add_user_index.sql`).
- Use standard Java formatting (4-space indentation, clear method/class names). No formatter is enforced in CI, so keep style consistent with nearby code.
## Testing Guidelines
- Unit tests: `*Test.java` (Surefire).
- Integration tests: `*IT.java` (Failsafe under profile `IT`).
- Integration tests should reuse `BaseIT` in `ljwx-platform-app/src/test/java/.../BaseIT.java` where applicable.
- Validate Docker is available before running ITs: `docker info`.
## Commit & Pull Request Guidelines
There is no stable local commit history yet, so use Conventional Commits (for example, `feat(auth): add refresh token endpoint`) until a project-specific pattern is established.
Use `.gitea/pull_request_template.md` and complete all required sections:
- 需求号
- 契约变更说明
- 控制面影响
- 测试证据
PRs should include linked requirement docs (for example, `docs/01-requirements/R-001.md`) and clear test evidence (`./mvnw test`, `./mvnw -PIT verify`, or manual verification notes).
+1 -1
View File
@@ -3,7 +3,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class HashGen {
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
String rawPassword = "Admin @12345";
String rawPassword = "Admin@12345";
String hash = encoder.encode(rawPassword);
System.out.println("=== BCrypt Hash Generator ===");
System.out.println("Raw password: " + rawPassword);
@@ -12,7 +12,6 @@ import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -28,12 +27,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@Testcontainers
public abstract class BaseIT {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("ljwx_platform_test")
.withUsername("test")
.withPassword("test");
static {
postgres.start();
}
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
@@ -48,10 +50,10 @@ public abstract class BaseIT {
protected ObjectMapper objectMapper;
/**
* Login with the default admin credentials and return the access token.
* Login with the default admin@defaultcredentials and return the access token.
*/
protected String loginAsAdmin() throws Exception {
return login("admin @default", "Admin @12345");
return login("admin@default", "Admin@12345");
}
/**
@@ -74,11 +76,11 @@ public abstract class BaseIT {
}
/**
* Login with default admin and return the full response body as JsonNode.
* Login with default admin@defaultand return the full response body as JsonNode.
*/
protected JsonNode loginAsAdminFullResponse() throws Exception {
String requestBody = objectMapper.writeValueAsString(
java.util.Map.of("username", "admin @default", "password", "Admin @12345")
java.util.Map.of("username", "admin@default", "password", "Admin@12345")
);
MvcResult result = mockMvc.perform(post("/api/v1/auth/login")
@@ -89,4 +91,4 @@ public abstract class BaseIT {
return objectMapper.readTree(result.getResponse().getContentAsString());
}
}
}
@@ -31,7 +31,7 @@ class AuthenticationIT extends BaseIT {
@DisplayName("POST /api/v1/auth/login with valid credentials should return access_token and refresh_token")
void loginWithValidCredentials() throws Exception {
String requestBody = objectMapper.writeValueAsString(
java.util.Map.of("username", "admin @default", "password", "Admin @12345")
java.util.Map.of("username", "admin@default", "password", "Admin@12345")
);
mockMvc.perform(post("/api/v1/auth/login")
@@ -48,7 +48,7 @@ class AuthenticationIT extends BaseIT {
@DisplayName("POST /api/v1/auth/login with invalid password should return 401")
void loginWithInvalidPassword() throws Exception {
String requestBody = objectMapper.writeValueAsString(
java.util.Map.of("username", "admin @default", "password", "WrongPassword")
java.util.Map.of("username", "admin@default", "password", "WrongPassword")
);
mockMvc.perform(post("/api/v1/auth/login")
@@ -63,7 +63,7 @@ class AuthenticationIT extends BaseIT {
@DisplayName("POST /api/v1/auth/login with non-existent user should return 401")
void loginWithNonExistentUser() throws Exception {
String requestBody = objectMapper.writeValueAsString(
java.util.Map.of("username", "nobody @default", "password", "Admin @12345")
java.util.Map.of("username", "nobody@default", "password", "Admin@12345")
);
mockMvc.perform(post("/api/v1/auth/login")
@@ -100,13 +100,13 @@ class MigrationVerificationIT {
"SELECT username FROM users WHERE id = 1",
String.class
);
assertEquals("admin @default", username, "Default admin username should be 'admin @default'");
assertEquals("admin@default", username, "Default admin username should be 'admin@default'");
Long tenantId = jdbcTemplate.queryForObject(
"SELECT tenant_id FROM users WHERE id = 1",
Long.class
);
assertEquals(1L, tenantId, "Default admin should belong to tenant_id = 1");
assertEquals(1L, tenantId, "Default admin@defaultshould belong to tenant_id = 1");
// Verify BCrypt hash actually matches the documented plaintext password
String storedHash = jdbcTemplate.queryForObject(
@@ -115,9 +115,9 @@ class MigrationVerificationIT {
);
assertNotNull(storedHash, "Stored password hash should not be null");
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
assertTrue(encoder.matches("Admin @12345", storedHash),
"Stored BCrypt hash must match plaintext 'Admin @12345'. " +
"If this fails, regenerate the hash using: new BCryptPasswordEncoder(10).encode(\"Admin @12345\") " +
assertTrue(encoder.matches("Admin@12345", storedHash),
"Stored BCrypt hash must match plaintext 'Admin@12345'. " +
"If this fails, regenerate the hash using: new BCryptPasswordEncoder(10).encode(\"Admin@12345\") " +
"and update V002__seed_default_data.sql");
}
@@ -146,13 +146,13 @@ class MigrationVerificationIT {
}
@Test
@DisplayName("Admin user should have ROLE_ADMIN assigned")
@DisplayName("Admin@12345user should have ROLE_ADMIN assigned")
void adminUserShouldHaveAdminRole() {
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM user_roles WHERE tenant_id = 1 AND user_id = 1 AND role_id = 1",
Integer.class
);
assertEquals(1, count, "Admin user should have ROLE_ADMIN assigned");
assertEquals(1, count, "Admin@12345user should have ROLE_ADMIN assigned");
}
@Test
@@ -53,7 +53,7 @@ class TenantIsolationIT extends BaseIT {
// Create role for tenant B
jdbcTemplate.update(
"INSERT INTO roles (id, tenant_id, code, name) VALUES (2, 2, 'ROLE_ADMIN', 'Admin Tenant B')");
"INSERT INTO roles (id, tenant_id, code, name) VALUES (2, 2, 'ROLE_ADMIN', 'Admin@12345Tenant B')");
// Assign permissions to role
jdbcTemplate.update(
@@ -68,10 +68,10 @@ class TenantIsolationIT extends BaseIT {
"INSERT INTO role_permissions (tenant_id, role_id, permission_id) VALUES (2, 2, 5)");
// Create user for tenant B
String hashedPassword = passwordEncoder.encode("Admin @12345");
String hashedPassword = passwordEncoder.encode("Admin@12345");
jdbcTemplate.update(
"INSERT INTO users (id, tenant_id, username, password, email, status) " +
"VALUES (2, 2, 'admin @tenant-b', ?, 'admin @tenantb.com', 'ACTIVE')",
"VALUES (2, 2, 'admin@tenant-b', ?, '[email protected]', 'ACTIVE')",
hashedPassword);
// Assign role to user
@@ -80,7 +80,7 @@ class TenantIsolationIT extends BaseIT {
}
private String loginAsTenantB() throws Exception {
return login("admin @tenant-b", "Admin @12345");
return login("admin@tenant-b", "Admin@12345");
}
@Test
@@ -95,8 +95,8 @@ class TenantIsolationIT extends BaseIT {
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("SUCCESS"))
.andExpect(jsonPath("$.data.items").isArray())
.andExpect(jsonPath("$.data.items[?( @.username == 'admin @default')]").exists())
.andExpect(jsonPath("$.data.items[?( @.username == 'admin @tenant-b')]").doesNotExist());
.andExpect(jsonPath("$.data.items[?( @.username == 'admin@default')]").exists())
.andExpect(jsonPath("$.data.items[?( @.username == 'admin@tenant-b')]").doesNotExist());
}
@Test
@@ -111,8 +111,8 @@ class TenantIsolationIT extends BaseIT {
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("SUCCESS"))
.andExpect(jsonPath("$.data.items").isArray())
.andExpect(jsonPath("$.data.items[?( @.username == 'admin @tenant-b')]").exists())
.andExpect(jsonPath("$.data.items[?( @.username == 'admin @default')]").doesNotExist());
.andExpect(jsonPath("$.data.items[?( @.username == 'admin@tenant-b')]").exists())
.andExpect(jsonPath("$.data.items[?( @.username == 'admin@default')]").doesNotExist());
}
@Test
@@ -158,4 +158,4 @@ class TenantIsolationIT extends BaseIT {
"Response should contain non-empty X-Trace-Id header";
});
}
}
}
@@ -4,7 +4,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* Utility to generate BCrypt hashes for seed data verification.
* Run main() to print the hash for Admin @12345.
* Run main() to print the hash for Admin@12345.
*
* Usage: The output hash should replace the placeholder in V002__seed_default_data.sql
* if the current hash does not match.
@@ -13,7 +13,7 @@ public class BcryptHashGenerator {
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
String rawPassword = "Admin @12345";
String rawPassword = "Admin@12345";
String hash = encoder.encode(rawPassword);
System.out.println("=== BCrypt Hash Generator ===");
System.out.println("Raw password: " + rawPassword);
@@ -3,10 +3,10 @@
-- Seeds default development/testing data.
--
-- Default credentials:
-- Username: admin @default
-- Password: Admin @12345
-- Username: admin@default
-- Password: Admin@12345
-- BCrypt cost factor: 10
-- BCrypt hash generated via: new BCryptPasswordEncoder(10).encode("Admin @12345")
-- BCrypt hash generated via: new BCryptPasswordEncoder(10).encode("Admin@12345")
--
-- WARNING: This seed is for dev/test environments ONLY.
-- Production environments should use ops scripts (see docs/05-runbook/ops.md).
@@ -30,7 +30,7 @@ INSERT INTO permissions (id, code, name, description) VALUES
(5, 'permission:read', 'Read Permissions', 'Permission to view permission list');
-- -------------------------------------------------------------------
-- Default admin role (tenant-scoped, tenant_id=1)
-- Default admin@defaultrole (tenant-scoped, tenant_id=1)
-- -------------------------------------------------------------------
INSERT INTO roles (id, tenant_id, code, name, description)
VALUES (1, 1, 'ROLE_ADMIN', '管理员', 'Default administrator role with all permissions');
@@ -47,15 +47,15 @@ INSERT INTO role_permissions (tenant_id, role_id, permission_id) VALUES
-- -------------------------------------------------------------------
-- Default admin user
-- Username: admin @default
-- Password: Admin @12345
-- BCrypt hash (cost=10): $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
-- Generated via: new BCryptPasswordEncoder(10).encode("Admin @12345")
-- Username: admin@default
-- Password: Admin@12345
-- BCrypt hash (cost=10): $2b$10$6q3YOdr.JHivu1KFOvVQve.nKbNE3Airr5DKPswPDCjhTWyoboHwm
-- Generated via: new BCryptPasswordEncoder(10).encode("Admin@12345")
-- -------------------------------------------------------------------
INSERT INTO users (id, tenant_id, username, password, email, status)
VALUES (1, 1, 'admin @default',
'$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy',
'admin @ljwx.com', 'ACTIVE');
VALUES (1, 1, 'admin@default',
'$2b$10$6q3YOdr.JHivu1KFOvVQve.nKbNE3Airr5DKPswPDCjhTWyoboHwm',
'[email protected]', 'ACTIVE');
-- -------------------------------------------------------------------
-- Assign ROLE_ADMIN to admin user
@@ -67,4 +67,4 @@ VALUES (1, 1, 1);
SELECT setval('tenants_id_seq', (SELECT MAX(id) FROM tenants));
SELECT setval('permissions_id_seq', (SELECT MAX(id) FROM permissions));
SELECT setval('roles_id_seq', (SELECT MAX(id) FROM roles));
SELECT setval('users_id_seq', (SELECT MAX(id) FROM users));
SELECT setval('users_id_seq', (SELECT MAX(id) FROM users));
+4 -2
View File
@@ -33,8 +33,10 @@ fi
echo ""
echo "[gate-contract] Step 2: Running Flyway MigrationVerificationIT..."
if ./mvnw -q -PIT -pl ljwx-platform-app -am verify \
-Dtest=MigrationVerificationIT \
-Dit.test=MigrationVerificationIT \
-DfailIfNoTests=false \
-Dfailsafe.failIfNoSpecifiedTests=false \
-Dsurefire.failIfNoSpecifiedTests=false \
-Dsurefire.skip=true; then
echo " [OK] MigrationVerificationIT passed"
else
@@ -88,4 +90,4 @@ if [ $ERRORS -gt 0 ]; then
exit 1
else
echo "[gate-contract] PASSED."
fi
fi