* test(control): 消除测试 fixture 中的重复密钥派生 - internal/control:fixture 构造中 388 次重复执行 PBKDF2(10 万轮 SHA256)密钥派生,改为共享单例,仅派生一次 - 新增 internal/testutil/encryptiontest,提供按 key material 值 memoize 的共享 encryption.Service,横向覆盖 gateway、subscription、 execution/cpa 中同样存在的重复派生(PBKDF2 占比最高达 79%) - aesService 构造后只读,跨 fixture(含并行测试)共享安全;container 包同类调用经排查确认属于被测装配流程本身,不适用本次改造 * test(state): 迁移测试改用共享 schema 模板 state/loader 中 openMigratedDatabase 被 21 次调用,每次都完整走一遍 迁移注册表;改为复用 sqlitetest.OpenMigrated 的模板缓存,与 control、 requestlog 部分测试已采用的方式一致。app、container、requestlog 中同 类调用点经逐点排查,均属被测对象本身或依赖真实文件/账本状态,不适用。 * ci(release): 消除 CI 与发布流水线中的耗时浪费 - race-tests 独立 setup-go 缓存命名空间,避免与 test/database-contract 共用 key 导致 race-instrumented GOCACHE 永远无法写回 - 移除 publish-images 中零命中的 GHA buildx image cache,同时消除其 失败会拖累发布状态判定为 partial/blocked 的关联风险 - 三个只读 job(仅用 buildx imagetools 查询)移除多余的 buildx 初始化 - native-artifact-smoke 改为按需下载单个 binary + 独立 checksums, 不再下载完整 release-assets * test(control): 第一批测试标记 t.Parallel(35 个文件) internal/control 651 个顶层测试里只有 5.8% 标了 t.Parallel(),其余 在包内是排队串行执行的;race-tests 总耗时现在完全由该包的最长执行时 间决定(213s),是唯一剩余的瓶颈。 本批覆盖 35 个文件、299 个函数。排除 18 个不适用的函数: - 16 个直接操作全局 logrus 状态的测试(catalog_sync_test.go 等 8 个 文件),继续保持串行;Go 的 t.Parallel() 调度语义保证未标记的测试 会在所有并行测试开始前完整跑完,两者不会交叉执行 - external_database_integration_test.go 的 2 个测试依赖 GPT_LOAD_DATABASE_TEST_DSN:race-tests 中因未设置而被 t.Skip,标记 并行无收益;database-contract job 中会共享同一个外部数据库实例真 实执行,标记并行有正确性风险(唯一索引冲突/断言假设独占) 已排查确认安全:t.Setenv/os.Setenv 均为 0 处,无硬编码端口占用,每个 fixture 走独立 :memory: 数据库;本地 -count=3 重复验证无 flaky。 剩余约 297 个函数(36 个文件)作为第二批,待本批 CI 实测确认收益后 再决定是否推进。 * test(control): 第二批测试标记 t.Parallel,补齐排除说明(36 个文件) 覆盖剩余 297 个函数、36 个文件,加上第一批的 299 个,internal/control 除 18 个明确排除的函数外全部标记 t.Parallel()。 为 18 个排除函数逐一补充一行注释说明原因,避免日后被误认为遗漏而被 顺手加上 t.Parallel(): - 16 个劫持全局 logrus 状态的测试 - 2 个依赖 GPT_LOAD_DATABASE_TEST_DSN 共享外部数据库的测试 本地 -count=3 重复验证、全量 go test -count=1 . ./internal/...(58 包 ok)均通过。 * fix(release): 采纳 Review 反馈,修正文档措辞与产物保留期 - encryptiontest.Service 的文档注释修正为明确说明缓存仅在单个包的 测试二进制内有效,不跨包共享(每个包的 go test 是独立进程) - native-artifact-smoke 依赖的 binary-*、release-checksums 保留期 从 1 天提高到与 release-assets 一致的 14 天:失败的 release run 若延迟超过 1 天才重试,此前较短的保留期会导致产物过期、下载失败, 阻塞发布流程 以下反馈经核实判定为不成立,未采纳: - validation_test.go 中两个测试被指"竞争全局并发槽位"——经查 newValidationWorkerForTest 每次调用构造全新独立实例(worker、 channel、mutation coordinator 等),validationConcurrency 只是 编译期常量,两个测试之间不存在任何运行时共享状态
192 lines
6.9 KiB
Go
192 lines
6.9 KiB
Go
package control
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestIdempotencyDigestFixtures(t *testing.T) {
|
|
t.Parallel()
|
|
type fixture struct {
|
|
Name string `json:"name"`
|
|
OperationKind operationKind `json:"operation_kind"`
|
|
PathTemplate string `json:"path_template"`
|
|
ResourceLocator string `json:"resource_locator"`
|
|
CanonicalBody string `json:"canonical_body"`
|
|
DomainHex string `json:"domain_hex"`
|
|
SeparatorHex string `json:"separator_hex"`
|
|
FramedInputHex string `json:"framed_input_hex"`
|
|
ExpectedSHA256 string `json:"expected_sha256"`
|
|
}
|
|
paths, err := filepath.Glob("testdata/idempotency/*.json")
|
|
if err != nil {
|
|
t.Fatalf("glob fixtures: %v", err)
|
|
}
|
|
if len(paths) != 3 {
|
|
t.Fatalf("fixture count = %d, want 3", len(paths))
|
|
}
|
|
digests := make(map[string]string, len(paths))
|
|
for _, path := range paths {
|
|
path := path
|
|
t.Run(filepath.Base(path), func(t *testing.T) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read fixture: %v", err)
|
|
}
|
|
var value fixture
|
|
if err := json.Unmarshal(raw, &value); err != nil {
|
|
t.Fatalf("decode fixture: %v", err)
|
|
}
|
|
result, err := buildIdempotencyDigest(idempotencyDigestInput{
|
|
Version: 1,
|
|
Method: "POST",
|
|
OperationKind: value.OperationKind,
|
|
PathTemplate: value.PathTemplate,
|
|
ResourceLocator: value.ResourceLocator,
|
|
AuthScopeID: idempotencyAuthScopeID,
|
|
CanonicalBody: []byte(value.CanonicalBody),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("buildIdempotencyDigest() error = %v", err)
|
|
}
|
|
if got := hex.EncodeToString([]byte(idempotencyDigestDomain)); got != value.DomainHex {
|
|
t.Fatalf("domain hex = %s, want %s", got, value.DomainHex)
|
|
}
|
|
if value.SeparatorHex != "00" ||
|
|
result.FramedInput[len(idempotencyDigestDomain)] != 0 {
|
|
t.Fatalf("separator fixture/runtime = %q/%02x", value.SeparatorHex, result.FramedInput[len(idempotencyDigestDomain)])
|
|
}
|
|
if got := hex.EncodeToString(result.FramedInput); got != value.FramedInputHex {
|
|
t.Fatalf("framed input hex = %s, want %s", got, value.FramedInputHex)
|
|
}
|
|
gotDigest := hex.EncodeToString(result.Digest[:])
|
|
if gotDigest != value.ExpectedSHA256 {
|
|
t.Fatalf("digest = %s, want %s", gotDigest, value.ExpectedSHA256)
|
|
}
|
|
digests[value.Name] = gotDigest
|
|
})
|
|
}
|
|
for _, pair := range [][2]string{
|
|
{"group-create-k", "group-create-k-twice"},
|
|
} {
|
|
if digests[pair[0]] == digests[pair[1]] {
|
|
t.Fatalf("%s and %s digests are equal", pair[0], pair[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIdempotencyDigestV1MatchesAccessKeyCreateFixture(t *testing.T) {
|
|
t.Parallel()
|
|
body := []byte(
|
|
`{"filters":{"groups":[],"models":[],"protocols":[]},"name":"CI client","rpm_limit":0}`,
|
|
)
|
|
result, err := buildIdempotencyDigest(idempotencyDigestInput{
|
|
Version: 1,
|
|
Method: "POST",
|
|
OperationKind: operationKindAccessKeyCreate,
|
|
PathTemplate: "/api/access-keys",
|
|
ResourceLocator: "new",
|
|
AuthScopeID: "control-admin-v1",
|
|
CanonicalBody: body,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("buildIdempotencyDigest() error = %v", err)
|
|
}
|
|
|
|
const wantDomainHex = "6770742d6c6f61642f636f6e74726f6c2d6964656d706f74656e6379"
|
|
const wantInputHex = "6770742d6c6f61642f636f6e74726f6c2d6964656d706f74656e637900000000013100000004504f5354000000116163636573735f6b65795f637265617465000000102f6170692f6163636573732d6b657973000000036e657700000010636f6e74726f6c2d61646d696e2d7631000000557b2266696c74657273223a7b2267726f757073223a5b5d2c226d6f64656c73223a5b5d2c2270726f746f636f6c73223a5b5d7d2c226e616d65223a22434920636c69656e74222c2272706d5f6c696d6974223a307d"
|
|
const wantDigestHex = "7b22877fc5ce68bde59170f4906463bbbf873abff4cda536dd36c30da3a62b81"
|
|
|
|
if got := hex.EncodeToString([]byte(idempotencyDigestDomain)); got != wantDomainHex {
|
|
t.Fatalf("domain hex = %s, want %s", got, wantDomainHex)
|
|
}
|
|
if got := hex.EncodeToString(result.FramedInput); got != wantInputHex {
|
|
t.Fatalf("framed input hex = %s, want %s", got, wantInputHex)
|
|
}
|
|
if got := hex.EncodeToString(result.Digest[:]); got != wantDigestHex {
|
|
t.Fatalf("digest hex = %s, want %s", got, wantDigestHex)
|
|
}
|
|
if result.FramedInput[len(idempotencyDigestDomain)] != 0 {
|
|
t.Fatalf(
|
|
"separator = %02x, want exactly one NUL after domain",
|
|
result.FramedInput[len(idempotencyDigestDomain)],
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeIdempotencyKeyLinesPreservesSortedMultiplicity(t *testing.T) {
|
|
t.Parallel()
|
|
one, err := normalizeIdempotencyKeyLines(" K \r\n")
|
|
if err != nil {
|
|
t.Fatalf("normalizeIdempotencyKeyLines(K) error = %v", err)
|
|
}
|
|
two, err := normalizeIdempotencyKeyLines("K\r\n K\n")
|
|
if err != nil {
|
|
t.Fatalf("normalizeIdempotencyKeyLines(K twice) error = %v", err)
|
|
}
|
|
if len(one) != 1 || one[0] != "K" {
|
|
t.Fatalf("one = %#v, want [K]", one)
|
|
}
|
|
if len(two) != 2 || two[0] != "K" || two[1] != "K" {
|
|
t.Fatalf("two = %#v, want [K K]", two)
|
|
}
|
|
|
|
oneBody, err := canonicalIdempotencyBody(map[string]any{"credentials": one})
|
|
if err != nil {
|
|
t.Fatalf("canonicalIdempotencyBody(one) error = %v", err)
|
|
}
|
|
twoBody, err := canonicalIdempotencyBody(map[string]any{"credentials": two})
|
|
if err != nil {
|
|
t.Fatalf("canonicalIdempotencyBody(two) error = %v", err)
|
|
}
|
|
oneDigest, err := buildIdempotencyDigest(idempotencyDigestInput{
|
|
Version: 1, Method: "POST", OperationKind: operationKindCredentialImport,
|
|
PathTemplate: "/api/groups/:group_id/credentials/import", ResourceLocator: "group:7",
|
|
AuthScopeID: "control-admin-v1", CanonicalBody: oneBody,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("buildIdempotencyDigest(one) error = %v", err)
|
|
}
|
|
twoDigest, err := buildIdempotencyDigest(idempotencyDigestInput{
|
|
Version: 1, Method: "POST", OperationKind: operationKindCredentialImport,
|
|
PathTemplate: "/api/groups/:group_id/credentials/import", ResourceLocator: "group:7",
|
|
AuthScopeID: "control-admin-v1", CanonicalBody: twoBody,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("buildIdempotencyDigest(two) error = %v", err)
|
|
}
|
|
if oneDigest.Digest == twoDigest.Digest {
|
|
t.Fatal("K and K\\nK produced the same digest")
|
|
}
|
|
}
|
|
|
|
func TestIdempotencyDigestRejectsUnsupportedVersionAndInvalidFields(t *testing.T) {
|
|
t.Parallel()
|
|
valid := idempotencyDigestInput{
|
|
Version: 1, Method: "POST", OperationKind: operationKindAccessKeyCreate,
|
|
PathTemplate: "/api/access-keys", ResourceLocator: "new",
|
|
AuthScopeID: "control-admin-v1", CanonicalBody: []byte(`{}`),
|
|
}
|
|
|
|
unsupported := valid
|
|
unsupported.Version = 2
|
|
if _, err := buildIdempotencyDigest(unsupported); err == nil {
|
|
t.Fatal("buildIdempotencyDigest(version 2) error = nil, want rejection")
|
|
}
|
|
|
|
lowerMethod := valid
|
|
lowerMethod.Method = "post"
|
|
if _, err := buildIdempotencyDigest(lowerMethod); err == nil {
|
|
t.Fatal("buildIdempotencyDigest(lowercase method) error = nil, want rejection")
|
|
}
|
|
|
|
implicitLocator := valid
|
|
implicitLocator.ResourceLocator = ""
|
|
if _, err := buildIdempotencyDigest(implicitLocator); err == nil {
|
|
t.Fatal("buildIdempotencyDigest(empty locator) error = nil, want rejection")
|
|
}
|
|
}
|