Files
knowledge-governance/internal/httpapi/openapi_contract_test.go
T
zheromiku dcdc8d1ddd feat: 知识治理平台(含已验证发布/回滚契约)
全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环):

- 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口)
- 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入
  parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截
- 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过):
  * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id)
  * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配
  * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数
  * 回滚 job payload 支持 prior_execution_id(strict 解码)
- 三通道发布+回滚演练实测:publish 4/4 verified,检索命中,
  rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false
- 前端 A·Data-console 风格全站统一,120 用例通过
- 门禁:go vet / go test 全包通过,gofmt 干净
2026-09-30 10:54:06 +08:00

111 lines
3.9 KiB
Go

package httpapi
import (
"encoding/json"
"os"
"regexp"
"strings"
"testing"
)
func TestOpenAPIContractMatchesRegisteredRoutesAndOmitsSecrets(t *testing.T) {
contract, err := os.ReadFile("../../api/openapi.yaml")
if err != nil {
t.Fatalf("read OpenAPI contract: %v", err)
}
var document struct {
OpenAPI string `json:"openapi"`
Info map[string]any `json:"info"`
Paths map[string]map[string]any `json:"paths"`
Components map[string]map[string]any `json:"components"`
}
if err := json.Unmarshal(contract, &document); err != nil {
t.Fatalf("OpenAPI contract must use JSON-compatible YAML: %v", err)
}
if document.OpenAPI != "3.1.0" || document.Info["title"] != "Knowledge Governance API" || len(document.Paths) == 0 {
t.Fatalf("invalid OpenAPI header: %#v", document)
}
routes, err := os.ReadFile("server.go")
if err != nil {
t.Fatalf("read registered routes: %v", err)
}
matcher := regexp.MustCompile(`mux\.HandleFunc\("(GET|POST|PUT|DELETE) ([^"]+)"`)
for _, match := range matcher.FindAllStringSubmatch(string(routes), -1) {
method, path := strings.ToLower(match[1]), match[2]
operation, ok := document.Paths[path][method].(map[string]any)
if !ok {
t.Fatalf("OpenAPI is missing %s %s", strings.ToUpper(method), path)
}
if strings.TrimSpace(stringValue(operation["operationId"])) == "" {
t.Fatalf("OpenAPI operation %s %s has no operationId", strings.ToUpper(method), path)
}
responses, ok := operation["responses"].(map[string]any)
if !ok || responses["400"] == nil || responses["500"] == nil {
t.Fatalf("OpenAPI operation %s %s must document 400 and 500 responses", strings.ToUpper(method), path)
}
}
lower := strings.ToLower(string(contract))
for _, forbidden := range []string{"secret_ref", "authorization_code", "manifest_sha256", "knowledge_id", "test_kb_id", "base_url", "download_code", "cookie"} {
if strings.Contains(lower, forbidden) {
t.Fatalf("OpenAPI contract must not expose %q", forbidden)
}
}
}
func TestOpenAPICatalogMutationsRequireIdempotencyKeyHeader(t *testing.T) {
contract, err := os.ReadFile("../../api/openapi.yaml")
if err != nil {
t.Fatal(err)
}
var document struct {
Paths map[string]map[string]any `json:"paths"`
Components struct {
Parameters map[string]map[string]any `json:"parameters"`
} `json:"components"`
}
if err := json.Unmarshal(contract, &document); err != nil {
t.Fatal(err)
}
parameter, ok := document.Components.Parameters["IdempotencyKey"]
if !ok || parameter["name"] != "Idempotency-Key" || parameter["in"] != "header" || parameter["required"] != true {
t.Fatalf("IdempotencyKey header must be required: %#v", parameter)
}
schema, ok := parameter["schema"].(map[string]any)
if !ok || schema["type"] != "string" || schema["minLength"] != float64(1) || schema["maxLength"] != float64(128) {
t.Fatalf("IdempotencyKey header schema is unsafe: %#v", parameter["schema"])
}
for _, path := range []string{"/api/v1/source-catalog/route", "/api/v1/source-catalog/sources/{id}/refresh"} {
operation, ok := document.Paths[path]["post"].(map[string]any)
if !ok {
t.Fatalf("missing POST %s", path)
}
parameters, ok := operation["parameters"].([]any)
if !ok {
t.Fatalf("POST %s must declare the idempotency header", path)
}
found := false
for _, value := range parameters {
parameterRef, _ := value.(map[string]any)
if parameterRef["$ref"] == "#/components/parameters/IdempotencyKey" {
found = true
}
}
if !found {
t.Fatalf("POST %s must reference IdempotencyKey", path)
}
responses, _ := operation["responses"].(map[string]any)
accepted, _ := responses["202"].(map[string]any)
if accepted["$ref"] != "#/components/responses/OperationEnvelope" {
t.Fatalf("POST %s must document created/replayed operation response: %#v", path, accepted)
}
}
}
func stringValue(value any) string {
result, _ := value.(string)
return result
}