test: 契约冻结 v1.3.0 api 侧收尾——四模块字节级快照同步 + community/media 契约矩阵入场
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:
2026-09-09 11:31:45 +08:00
parent 7f1dd33097
commit 0569585434
16 changed files with 12356 additions and 31 deletions
@@ -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");