fix(ci): stabilize gate-all tests and contract gate
This commit is contained in:
@@ -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
@@ -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")
|
||||
|
||||
+7
-7
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user