test: 契约冻结 v1.3.0 api 侧收尾——四模块字节级快照同步 + community/media 契约矩阵入场
CI / backend-test (push) Successful in 5m4s
CI / backend-test (push) Successful in 5m4s
- 正典 v1.3.0(doc main@f848476)字节级复制为 pet/auth/community/user 四份 openapi-v1.3.0.yaml 快照(md5 与正典一致),删除旧 v1.2.0(守卫只认一份, 历史由 git 承载);pet/auth 守卫期望升版 1.3.0/31 路径/43 操作/72 schemas - CommunityContractConformanceTest:community 域 17 操作 64 单元格全响应矩阵 (Feed/帖子/评论/互动/关注,401/403/404/409/422 各格实证,零豁免) - MediaContractConformanceTest(user 模块):media 两步上传 2 操作 8 单元格 全矩阵(真实 MinIO 直传,零豁免) - ContractValidator 四副本加单分支 allOf 展平合并,修复 v1.3.0 nullable+allOf 模式(coverImage/replyToUser)被静默跳过的校验盲区, 定向 mutation 自证生效 - 实现与冻结契约零漂移;310 → 325 测试全绿;check-secrets --all 通过 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+6
-6
@@ -27,8 +27,8 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request;
|
||||
|
||||
/**
|
||||
* T2-09 契约一致性保障:对冻结契约 v1.2.0(快照
|
||||
* {@code src/test/resources/contract/openapi-v1.2.0.yaml},正典在 doc 仓
|
||||
* T2-09 契约一致性保障:对冻结契约 v1.3.0(快照
|
||||
* {@code src/test/resources/contract/openapi-v1.3.0.yaml},正典在 doc 仓
|
||||
* {@code docs/api/openapi.yaml})的 pets 域 18 个操作逐一真实起服务发请求,
|
||||
* 用 {@link ContractValidator} 严格校验响应结构:路径/方法/状态码已声明、
|
||||
* 字段名与类型、必填与 nullable、枚举与格式、信封结构、错误码值。
|
||||
@@ -703,10 +703,10 @@ class ContractConformanceTest extends PetIntegrationTestSupport {
|
||||
@Test
|
||||
@Order(98)
|
||||
void frozenSnapshotIsTheExpectedContractVersion() {
|
||||
assertThat(CONTRACT.version()).isEqualTo("1.2.0");
|
||||
assertThat(CONTRACT.paths()).hasSize(18);
|
||||
assertThat(CONTRACT.operations()).hasSize(24);
|
||||
assertThat(CONTRACT.schemas()).hasSize(45);
|
||||
assertThat(CONTRACT.version()).isEqualTo("1.3.0");
|
||||
assertThat(CONTRACT.paths()).hasSize(31);
|
||||
assertThat(CONTRACT.operations()).hasSize(43);
|
||||
assertThat(CONTRACT.schemas()).hasSize(72);
|
||||
assertThat(CONTRACT.operationsTagged(Set.of("pets", "dictionaries", "health-records")))
|
||||
.containsExactlyInAnyOrderElementsOf(PETS_OPERATIONS);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -81,7 +82,7 @@ final class ContractValidator {
|
||||
}
|
||||
|
||||
private void validate(Map<String, Object> rawSchema, JsonNode node, String loc, List<String> errors) {
|
||||
Map<String, Object> schema = contract.resolve(rawSchema);
|
||||
Map<String, Object> schema = effectiveSchema(rawSchema);
|
||||
if (node == null || node.isMissingNode()) {
|
||||
errors.add(loc + ": 字段缺失");
|
||||
return;
|
||||
@@ -130,6 +131,32 @@ final class ContractValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves $refs and flattens the v1.3.0 {@code nullable + allOf: [$ref]}
|
||||
* pattern into one plain schema (branch keys first, sibling keys — e.g.
|
||||
* the outer {@code nullable} — win). The frozen contract only ever uses
|
||||
* single-branch allOf, so a shallow merge is exact; overlapping
|
||||
* {@code properties} across branches would need a deep merge and are not
|
||||
* supported.
|
||||
*/
|
||||
private Map<String, Object> effectiveSchema(Map<String, Object> rawSchema) {
|
||||
Map<String, Object> schema = contract.resolve(rawSchema);
|
||||
List<Object> allOf = list(schema, "allOf");
|
||||
if (allOf == null) {
|
||||
return schema;
|
||||
}
|
||||
Map<String, Object> merged = new LinkedHashMap<>();
|
||||
for (Object branch : allOf) {
|
||||
merged.putAll(effectiveSchema(cast(branch)));
|
||||
}
|
||||
schema.forEach((key, value) -> {
|
||||
if (!"allOf".equals(key)) {
|
||||
merged.put(key, value);
|
||||
}
|
||||
});
|
||||
return merged;
|
||||
}
|
||||
|
||||
private void validateObject(Map<String, Object> schema, JsonNode node, String loc, List<String> errors) {
|
||||
if (!node.isObject()) {
|
||||
errors.add(loc + ": 应为 object,实际 " + node.getNodeType());
|
||||
|
||||
@@ -13,8 +13,8 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The frozen v1.2.0 OpenAPI contract, loaded from the test-resource snapshot
|
||||
* {@code /contract/openapi-v1.2.0.yaml}.
|
||||
* The frozen v1.3.0 OpenAPI contract, loaded from the test-resource snapshot
|
||||
* {@code /contract/openapi-v1.3.0.yaml}.
|
||||
*
|
||||
* <p><b>Sync discipline (T2-09)</b>: the canonical contract lives in the doc
|
||||
* repo at {@code docs/api/openapi.yaml}; this snapshot is a byte-identical
|
||||
@@ -26,11 +26,13 @@ import java.util.Set;
|
||||
*
|
||||
* <p>Only the subset of OpenAPI 3.0 this contract actually uses is supported:
|
||||
* local {@code #/} refs, plain types, {@code nullable}, {@code enum},
|
||||
* {@code required}, {@code properties}, {@code items} — no allOf/oneOf.
|
||||
* {@code required}, {@code properties}, {@code items}, and the v1.3.0
|
||||
* single-branch {@code nullable + allOf: [$ref]} pattern (merged in
|
||||
* {@link ContractValidator}) — no oneOf/anyOf.
|
||||
*/
|
||||
final class OpenApiContract {
|
||||
|
||||
static final String RESOURCE = "/contract/openapi-v1.2.0.yaml";
|
||||
static final String RESOURCE = "/contract/openapi-v1.3.0.yaml";
|
||||
|
||||
private static final Set<String> HTTP_METHODS =
|
||||
Set.of("get", "put", "post", "delete", "options", "head", "patch", "trace");
|
||||
|
||||
+1477
-3
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user