* 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 只是 编译期常量,两个测试之间不存在任何运行时共享状态
332 lines
11 KiB
Go
332 lines
11 KiB
Go
package control
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"log"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"gpt-load/internal/channel"
|
|
"gpt-load/internal/platform/encryption"
|
|
"gpt-load/internal/state"
|
|
"gpt-load/internal/storage/models"
|
|
)
|
|
|
|
const bootstrapMarkerForTest = models.InternalSystemSettingPrefix + "bootstrap.default_access_key.v1"
|
|
|
|
var errBootstrapEncryption = errors.New("forced bootstrap encryption failure")
|
|
|
|
func TestEnsureInitialStateCreatesDefaultAccessKeyAndMarker(t *testing.T) {
|
|
t.Parallel()
|
|
fixture := newServiceFixture(t)
|
|
fixture.service.random = bytes.NewReader([]byte{
|
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
|
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
|
})
|
|
beforeRevision := fixture.manager.Current().Revision
|
|
|
|
if err := fixture.service.EnsureInitialState(context.Background()); err != nil {
|
|
t.Fatalf("EnsureInitialState() error = %v", err)
|
|
}
|
|
|
|
var row models.AccessKey
|
|
if err := fixture.db.First(&row).Error; err != nil {
|
|
t.Fatalf("query default AccessKey: %v", err)
|
|
}
|
|
plaintext, err := fixture.encryption.Decrypt(row.KeyValue)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt(default key) error = %v", err)
|
|
}
|
|
if !strings.HasPrefix(plaintext, "sk-gl-") || len(plaintext) != len("sk-gl-")+32 {
|
|
t.Fatal("default plaintext shape is invalid")
|
|
}
|
|
if _, err := hex.DecodeString(strings.TrimPrefix(plaintext, "sk-gl-")); err != nil {
|
|
t.Fatalf("default plaintext suffix is not hex: %v", err)
|
|
}
|
|
if row.KeyHash != fixture.encryption.Hash(plaintext) {
|
|
t.Fatal("default access key hash does not match plaintext")
|
|
}
|
|
if row.Name != "Default" || row.Status != string(state.AccessKeyStatusActive) ||
|
|
row.RPMLimit != 0 {
|
|
t.Fatalf("default row = %#v", row)
|
|
}
|
|
var filters AccessKeyFilters
|
|
if err := json.Unmarshal(row.Filters, &filters); err != nil {
|
|
t.Fatalf("decode default filters: %v", err)
|
|
}
|
|
if filters.Groups == nil || filters.Protocols == nil || filters.Models == nil ||
|
|
len(filters.Groups) != 0 || len(filters.Protocols) != 0 || len(filters.Models) != 0 {
|
|
t.Fatalf("default filters = %#v, want three non-nil empty slices", filters)
|
|
}
|
|
|
|
var marker models.SystemSetting
|
|
if err := fixture.db.First(&marker, "key = ?", bootstrapMarkerForTest).Error; err != nil {
|
|
t.Fatalf("query bootstrap marker: %v", err)
|
|
}
|
|
if marker.Key != bootstrapMarkerForTest || marker.Value != "true" {
|
|
t.Fatalf("bootstrap marker = %#v, want exact JSON true", marker)
|
|
}
|
|
if got := fixture.manager.Current().Revision; got != beforeRevision {
|
|
t.Fatalf("snapshot revision = %d, want unchanged %d", got, beforeRevision)
|
|
}
|
|
if len(fixture.manager.Current().AccessKeysByHash) != 0 {
|
|
t.Fatal("EnsureInitialState() published a runtime snapshot")
|
|
}
|
|
}
|
|
|
|
func TestEnsureInitialStateWithExistingAccessKeyOnlyWritesMarker(t *testing.T) {
|
|
t.Parallel()
|
|
fixture := newServiceFixture(t)
|
|
const plaintext = "gl-existing-access-key"
|
|
ciphertext, err := fixture.encryption.Encrypt(plaintext)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt(existing key) error = %v", err)
|
|
}
|
|
existing := models.AccessKey{
|
|
Name: "Existing",
|
|
KeyValue: ciphertext,
|
|
KeyHash: fixture.encryption.Hash(plaintext),
|
|
KeySuffix: "0000",
|
|
Status: string(state.AccessKeyStatusActive),
|
|
Filters: models.JSON(`{"groups":[],"protocols":[],"models":[]}`),
|
|
}
|
|
if err := fixture.db.Create(&existing).Error; err != nil {
|
|
t.Fatalf("create existing AccessKey: %v", err)
|
|
}
|
|
|
|
if err := fixture.service.EnsureInitialState(context.Background()); err != nil {
|
|
t.Fatalf("EnsureInitialState() error = %v", err)
|
|
}
|
|
|
|
var rows []models.AccessKey
|
|
if err := fixture.db.Order("id ASC").Find(&rows).Error; err != nil {
|
|
t.Fatalf("query AccessKeys: %v", err)
|
|
}
|
|
if len(rows) != 1 || rows[0].ID != existing.ID {
|
|
t.Fatalf("AccessKeys = %#v, want only existing row", rows)
|
|
}
|
|
decrypted, err := fixture.encryption.Decrypt(rows[0].KeyValue)
|
|
if err != nil || decrypted != plaintext {
|
|
t.Fatalf("existing credential = %q, %v, want unchanged", decrypted, err)
|
|
}
|
|
assertBootstrapMarkerCount(t, fixture, 1)
|
|
}
|
|
|
|
func TestEnsureInitialStateDoesNotLogExpectedMarkerMissAsError(t *testing.T) {
|
|
t.Parallel()
|
|
fixture := newServiceFixture(t)
|
|
var logs bytes.Buffer
|
|
fixture.service.db = fixture.db.Session(&gorm.Session{
|
|
Logger: logger.New(log.New(&logs, "\r\n", log.LstdFlags), logger.Config{
|
|
SlowThreshold: 200 * time.Millisecond,
|
|
LogLevel: logger.Warn,
|
|
ParameterizedQueries: true,
|
|
Colorful: true,
|
|
}),
|
|
})
|
|
|
|
if err := fixture.service.EnsureInitialState(context.Background()); err != nil {
|
|
t.Fatalf("EnsureInitialState() error = %v", err)
|
|
}
|
|
if strings.Contains(logs.String(), "record not found") {
|
|
t.Fatalf("expected marker miss was logged as an error: %q", logs.String())
|
|
}
|
|
}
|
|
|
|
func TestEnsureInitialStateIsIdempotentWhenMarkerExists(t *testing.T) {
|
|
t.Parallel()
|
|
fixture := newServiceFixture(t)
|
|
if err := fixture.db.Create(&models.SystemSetting{
|
|
Key: bootstrapMarkerForTest, Value: "true",
|
|
}).Error; err != nil {
|
|
t.Fatalf("create bootstrap marker: %v", err)
|
|
}
|
|
if err := fixture.db.Exec("DROP TABLE access_keys").Error; err != nil {
|
|
t.Fatalf("drop AccessKey table: %v", err)
|
|
}
|
|
|
|
if err := fixture.service.EnsureInitialState(context.Background()); err != nil {
|
|
t.Fatalf("EnsureInitialState() with marker error = %v", err)
|
|
}
|
|
assertBootstrapMarkerCount(t, fixture, 1)
|
|
}
|
|
|
|
func TestEnsureInitialStateDoesNotRecreateDeletedFinalKey(t *testing.T) {
|
|
t.Parallel()
|
|
fixture := newServiceFixture(t)
|
|
fixture.service.random = bytes.NewReader(make([]byte, 16))
|
|
if err := fixture.service.EnsureInitialState(context.Background()); err != nil {
|
|
t.Fatalf("first EnsureInitialState() error = %v", err)
|
|
}
|
|
if err := fixture.db.Where("1 = 1").Delete(&models.AccessKey{}).Error; err != nil {
|
|
t.Fatalf("delete final AccessKey: %v", err)
|
|
}
|
|
|
|
if err := fixture.service.EnsureInitialState(context.Background()); err != nil {
|
|
t.Fatalf("second EnsureInitialState() error = %v", err)
|
|
}
|
|
assertAccessKeyCount(t, fixture, 0)
|
|
assertBootstrapMarkerCount(t, fixture, 1)
|
|
}
|
|
|
|
func TestEnsureInitialStateRollsBackWhenEncryptionFails(t *testing.T) {
|
|
t.Parallel()
|
|
fixture := newServiceFixture(t)
|
|
fixture.service.random = bytes.NewReader(make([]byte, 16))
|
|
fixture.service.encryption = bootstrapFailingEncryptService{Service: fixture.encryption}
|
|
|
|
err := fixture.service.EnsureInitialState(context.Background())
|
|
if !errors.Is(err, errBootstrapEncryption) {
|
|
t.Fatalf("EnsureInitialState() error = %v, want encryption failure", err)
|
|
}
|
|
assertAccessKeyCount(t, fixture, 0)
|
|
assertBootstrapMarkerCount(t, fixture, 0)
|
|
}
|
|
|
|
func TestEnsureInitialStateRollsBackAccessKeyWhenMarkerWriteFails(t *testing.T) {
|
|
t.Parallel()
|
|
fixture := newServiceFixture(t)
|
|
fixture.service.random = bytes.NewReader(make([]byte, 16))
|
|
if err := fixture.db.Exec(`
|
|
CREATE TRIGGER reject_bootstrap_marker
|
|
BEFORE INSERT ON system_settings
|
|
WHEN NEW.key = '_internal.bootstrap.default_access_key.v1'
|
|
BEGIN
|
|
SELECT RAISE(ABORT, 'marker rejected');
|
|
END;
|
|
`).Error; err != nil {
|
|
t.Fatalf("create marker rejection trigger: %v", err)
|
|
}
|
|
|
|
if err := fixture.service.EnsureInitialState(context.Background()); err == nil {
|
|
t.Fatal("EnsureInitialState() error = nil, want marker rejection")
|
|
}
|
|
assertAccessKeyCount(t, fixture, 0)
|
|
assertBootstrapMarkerCount(t, fixture, 0)
|
|
}
|
|
|
|
func TestEnsureInitialStateDoesNotLogPlaintext(t *testing.T) {
|
|
// 不标记 t.Parallel():本测试劫持了全局 logrus 输出/格式,与其他并行测试同时运行会互相覆盖断言。
|
|
fixture := newServiceFixture(t)
|
|
randomBytes := bytes.Repeat([]byte{0xab}, 16)
|
|
fixture.service.random = bytes.NewReader(randomBytes)
|
|
expectedPlaintext := "sk-gl-" + hex.EncodeToString(randomBytes)
|
|
|
|
var logs bytes.Buffer
|
|
logger := logrus.StandardLogger()
|
|
previousOutput, previousFormatter, previousLevel := logger.Out, logger.Formatter, logger.GetLevel()
|
|
logrus.SetOutput(&logs)
|
|
logrus.SetFormatter(&logrus.JSONFormatter{DisableTimestamp: true})
|
|
logrus.SetLevel(logrus.DebugLevel)
|
|
t.Cleanup(func() {
|
|
logrus.SetOutput(previousOutput)
|
|
logrus.SetFormatter(previousFormatter)
|
|
logrus.SetLevel(previousLevel)
|
|
})
|
|
|
|
if err := fixture.service.EnsureInitialState(context.Background()); err != nil {
|
|
t.Fatalf("EnsureInitialState() error = %v", err)
|
|
}
|
|
if strings.Contains(logs.String(), expectedPlaintext) {
|
|
t.Fatalf("bootstrap logs exposed plaintext: %q", logs.String())
|
|
}
|
|
}
|
|
|
|
func TestEnsureInitialStateRecoversInterruptedSubscriptionAuth(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fixture := newServiceFixture(t)
|
|
now := time.UnixMilli(1_800_000_000_000)
|
|
fixture.service.now = func() time.Time { return now }
|
|
group := validControlGroup("subscription-recovery")
|
|
group.ChannelID = string(channel.Codex)
|
|
group.ConnectionType = models.ConnectionTypeSubscription
|
|
if err := fixture.db.Create(group).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
credential := models.Credential{
|
|
GroupID: group.ID, Data: "cipher", Fingerprint: "secret", IdentityFingerprint: "identity",
|
|
SecretVersion: 2, AuthState: models.CredentialAuthStateRefreshing,
|
|
Status: models.CredentialStatusActive, CreatedAtMS: now.Add(-time.Hour).UnixMilli(), UpdatedAtMS: now.Add(-time.Hour).UnixMilli(),
|
|
}
|
|
if err := fixture.db.Create(&credential).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stage := models.CredentialStage{
|
|
ID: "00000000-0000-4000-8000-000000000901", ChannelID: "codex",
|
|
ConnectionType: models.ConnectionTypeSubscription, AuthorizationMethod: "browser_oauth",
|
|
Status: models.CredentialStageExchanging, EncryptedPayload: "encrypted-stage", PayloadSchemaVersion: 1,
|
|
SafeSummaryJSON: models.JSON(`{}`), ExpiresAtMS: now.Add(time.Minute).UnixMilli(),
|
|
CreatedAtMS: now.Add(-time.Minute).UnixMilli(), UpdatedAtMS: now.Add(-time.Minute).UnixMilli(),
|
|
}
|
|
if err := fixture.db.Create(&stage).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := fixture.service.EnsureInitialState(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := fixture.db.Take(&credential, credential.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if credential.AuthState != models.CredentialAuthStateOutcomeUnknown || credential.AuthErrorCode != "refresh_interrupted" {
|
|
t.Fatalf("credential = %#v", credential)
|
|
}
|
|
if err := fixture.db.Take(&stage, "id = ?", stage.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stage.Status != models.CredentialStageOutcomeUnknown || stage.EncryptedPayload != "" || stage.ErrorCode != "authorization_exchange_interrupted" {
|
|
t.Fatalf("stage = %#v", stage)
|
|
}
|
|
}
|
|
|
|
type bootstrapFailingEncryptService struct {
|
|
encryption.Service
|
|
}
|
|
|
|
func (bootstrapFailingEncryptService) Encrypt(string) (string, error) {
|
|
return "", errBootstrapEncryption
|
|
}
|
|
|
|
func assertAccessKeyCount(t *testing.T, fixture serviceFixture, want int64) {
|
|
t.Helper()
|
|
var count int64
|
|
if err := fixture.db.Model(&models.AccessKey{}).Count(&count).Error; err != nil {
|
|
t.Fatalf("count AccessKeys: %v", err)
|
|
}
|
|
if count != want {
|
|
t.Fatalf("AccessKey count = %d, want %d", count, want)
|
|
}
|
|
}
|
|
|
|
func assertBootstrapMarkerCount(t *testing.T, fixture serviceFixture, want int64) {
|
|
t.Helper()
|
|
var count int64
|
|
if err := fixture.db.Model(&models.SystemSetting{}).
|
|
Where("key = ?", bootstrapMarkerForTest).Count(&count).Error; err != nil {
|
|
t.Fatalf("count bootstrap marker: %v", err)
|
|
}
|
|
if count != want {
|
|
t.Fatalf("bootstrap marker count = %d, want %d", count, want)
|
|
}
|
|
}
|
|
|
|
func assertModelPriceCount(t *testing.T, fixture serviceFixture, want int64) {
|
|
t.Helper()
|
|
var count int64
|
|
if err := fixture.db.Model(&models.ModelPrice{}).Count(&count).Error; err != nil {
|
|
t.Fatalf("count ModelPrice rows: %v", err)
|
|
}
|
|
if count != want {
|
|
t.Fatalf("ModelPrice count = %d, want %d", count, want)
|
|
}
|
|
}
|