* 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 只是 编译期常量,两个测试之间不存在任何运行时共享状态
436 lines
16 KiB
Go
436 lines
16 KiB
Go
package control
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"gpt-load/internal/health"
|
|
"gpt-load/internal/platform/config"
|
|
app_errors "gpt-load/internal/platform/errors"
|
|
"gpt-load/internal/state"
|
|
"gpt-load/internal/storage/models"
|
|
)
|
|
|
|
func TestDeleteGroupRejectsActiveAndDisabledExplicitAccessKeyReferences(t *testing.T) {
|
|
t.Parallel()
|
|
fixture := newServiceFixture(t)
|
|
groupID := createGroupWithCredentials(t, fixture, "sk-in-use")
|
|
other := validControlGroup("other-reference")
|
|
if err := fixture.db.Create(other).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, row := range []models.AccessKey{
|
|
{
|
|
Name: "Active", KeyValue: "cipher-client-one", KeyHash: "client-hash-one",
|
|
KeySuffix: "0001",
|
|
Status: string(state.AccessKeyStatusActive),
|
|
Filters: models.JSON(fmt.Sprintf(`{"groups":[%d],"protocols":[],"models":[]}`, groupID)),
|
|
},
|
|
{
|
|
Name: "Disabled", KeyValue: "cipher-client-two", KeyHash: "client-hash-two",
|
|
KeySuffix: "0002",
|
|
Status: string(state.AccessKeyStatusDisabled),
|
|
Filters: models.JSON(fmt.Sprintf(`{"groups":[%d,%d],"protocols":[],"models":[]}`, other.ID, groupID)),
|
|
},
|
|
{
|
|
Name: "Unrestricted", KeyValue: "cipher-client-three", KeyHash: "client-hash-three",
|
|
KeySuffix: "0003",
|
|
Status: string(state.AccessKeyStatusActive),
|
|
Filters: models.JSON(`{"groups":[],"protocols":["openai-completions"],"models":["gpt-4o"]}`),
|
|
},
|
|
} {
|
|
row := row
|
|
if err := fixture.db.Create(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
beforeSnapshot := fixture.manager.Current()
|
|
beforeRegistry := fixture.registry.Snapshot()
|
|
|
|
err := fixture.service.DeleteGroup(t.Context(), groupID)
|
|
var apiErr *app_errors.APIError
|
|
if !errors.As(err, &apiErr) || apiErr.Code != app_errors.ErrGroupInUse.Code {
|
|
t.Fatalf("DeleteGroup() error = %#v", err)
|
|
}
|
|
data := apiErr.Data.(GroupInUseData)
|
|
if len(data.AccessKeys) != 2 ||
|
|
data.AccessKeys[0].Name != "Active" ||
|
|
data.AccessKeys[1].Name != "Disabled" {
|
|
t.Fatalf("references = %#v", data)
|
|
}
|
|
if fixture.manager.Current() != beforeSnapshot ||
|
|
!reflect.DeepEqual(fixture.registry.Snapshot(), beforeRegistry) {
|
|
t.Fatal("rejected delete mutated runtime state")
|
|
}
|
|
var count int64
|
|
if err := fixture.db.Model(&models.Group{}).Where("id = ?", groupID).Count(&count).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("group count = %d, want 1", count)
|
|
}
|
|
encoded, _ := json.Marshal(apiErr.Data)
|
|
for _, forbidden := range []string{"cipher-client", "client-hash", `"status"`, `"filters"`} {
|
|
if bytes.Contains(encoded, []byte(forbidden)) {
|
|
t.Fatalf("GROUP_IN_USE data exposes %q: %s", forbidden, encoded)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeleteGroupCommitsCascadeThenRemovesRegistryThenPublishes(t *testing.T) {
|
|
t.Parallel()
|
|
fixture := newServiceFixture(t)
|
|
runtime := &recordingCredentialRuntimeExecutor{}
|
|
fixture.service.executor = runtime
|
|
groupID := createGroupWithCredentials(t, fixture, "sk-delete-a\nsk-delete-b")
|
|
beforeRevision := fixture.manager.Current().Revision
|
|
var keyRows []models.Credential
|
|
if err := fixture.db.Where("group_id = ?", groupID).Order("id ASC").Find(&keyRows).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := fixture.service.DeleteGroup(t.Context(), groupID); err != nil {
|
|
t.Fatalf("DeleteGroup() error = %v", err)
|
|
}
|
|
for _, row := range keyRows {
|
|
if value, ok := fixture.registry.EncryptedCredentialData(row.ID); ok || value != "" {
|
|
t.Fatalf("Registry key %d remains = %q, %t", row.ID, value, ok)
|
|
}
|
|
}
|
|
if got := fixture.manager.Current(); got.Revision != beforeRevision+1 {
|
|
t.Fatalf("revision = %d, want %d", got.Revision, beforeRevision+1)
|
|
} else if _, exists := got.GroupCatalog[groupID]; exists {
|
|
t.Fatalf("published Snapshot retains Group %d", groupID)
|
|
}
|
|
var groupCount, keyCount int64
|
|
if err := fixture.db.Model(&models.Group{}).Where("id = ?", groupID).Count(&groupCount).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := fixture.db.Model(&models.Credential{}).Where("group_id = ?", groupID).Count(&keyCount).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if groupCount != 0 || keyCount != 0 {
|
|
t.Fatalf("DB counts = group:%d key:%d", groupCount, keyCount)
|
|
}
|
|
wantRetired := []uint{keyRows[0].ID, keyRows[1].ID}
|
|
if got := runtime.retiredCredentialIDs(); !reflect.DeepEqual(got, wantRetired) {
|
|
t.Fatalf("retired credential runtimes = %#v, want %#v", got, wantRetired)
|
|
}
|
|
}
|
|
|
|
func TestDeleteGroupSerializesWithCredentialSecretMutation(t *testing.T) {
|
|
t.Parallel()
|
|
fixture := newServiceFixture(t)
|
|
groupID := createGroupWithCredentials(t, fixture, "sk-delete-serialized")
|
|
var credential models.Credential
|
|
if err := fixture.db.Where("group_id = ?", groupID).Take(&credential).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entered := make(chan struct{})
|
|
release := make(chan struct{})
|
|
go fixture.mutations.Do(credential.ID, func() {
|
|
close(entered)
|
|
<-release
|
|
})
|
|
<-entered
|
|
done := make(chan error, 1)
|
|
go func() { done <- fixture.service.DeleteGroup(context.Background(), groupID) }()
|
|
select {
|
|
case callErr := <-done:
|
|
close(release)
|
|
t.Fatalf("group delete completed inside credential mutation: %v", callErr)
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
var count int64
|
|
if err := fixture.db.Model(&models.Group{}).Where("id = ?", groupID).Count(&count).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
close(release)
|
|
t.Fatalf("group was deleted before credential mutation released")
|
|
}
|
|
close(release)
|
|
select {
|
|
case callErr := <-done:
|
|
if callErr != nil {
|
|
t.Fatalf("DeleteGroup() error = %v", callErr)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("group delete did not finish")
|
|
}
|
|
}
|
|
|
|
func TestDeleteGroupCompileFailurePreservesDatabaseRegistryAndSnapshot(t *testing.T) {
|
|
t.Parallel()
|
|
fixture := newServiceFixture(t)
|
|
groupID := createGroupWithCredentials(t, fixture, "sk-delete-rollback")
|
|
corrupt := validControlGroup("delete-corrupt-other")
|
|
if err := fixture.db.Create(corrupt).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := fixture.db.Exec(
|
|
"UPDATE groups SET channel_id = ? WHERE id = ?",
|
|
"unknown-channel",
|
|
corrupt.ID,
|
|
).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
beforeSnapshot := fixture.manager.Current()
|
|
beforeRegistry := fixture.registry.Snapshot()
|
|
|
|
err := fixture.service.DeleteGroup(t.Context(), groupID)
|
|
if err == nil {
|
|
t.Fatal("DeleteGroup() error = nil")
|
|
}
|
|
if fixture.manager.Current() != beforeSnapshot ||
|
|
!reflect.DeepEqual(fixture.registry.Snapshot(), beforeRegistry) {
|
|
t.Fatal("failed delete mutated runtime state")
|
|
}
|
|
var count int64
|
|
if err := fixture.db.Model(&models.Group{}).Where("id = ?", groupID).Count(&count).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("target group count = %d, want 1", count)
|
|
}
|
|
}
|
|
|
|
func TestDeleteGroupCommitFailurePreservesDatabaseRegistryAndSnapshot(t *testing.T) {
|
|
t.Parallel()
|
|
fixture, dsn := newFileServiceFixture(t)
|
|
groupID := createGroupWithCredentials(t, fixture, "sk-delete-commit-busy")
|
|
var row models.Credential
|
|
if err := fixture.db.Where("group_id = ?", groupID).Take(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
beforeSnapshot := fixture.manager.Current()
|
|
beforeRegistry := fixture.registry.Snapshot()
|
|
releaseReader := holdRollbackJournalReadLock(t, fixture.db, dsn)
|
|
|
|
err := fixture.service.DeleteGroup(t.Context(), groupID)
|
|
var apiErr *app_errors.APIError
|
|
if !errors.As(err, &apiErr) || apiErr.Code != app_errors.ErrDatabase.Code {
|
|
t.Fatalf("DeleteGroup() error = %#v, want DATABASE_ERROR", err)
|
|
}
|
|
if fixture.manager.Current() != beforeSnapshot ||
|
|
!reflect.DeepEqual(fixture.registry.Snapshot(), beforeRegistry) {
|
|
t.Fatal("failed COMMIT changed runtime state")
|
|
}
|
|
|
|
releaseReader()
|
|
var groupCount, keyCount int64
|
|
if err := fixture.db.Model(&models.Group{}).
|
|
Where("id = ?", groupID).Count(&groupCount).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := fixture.db.Model(&models.Credential{}).
|
|
Where("id = ?", row.ID).Count(&keyCount).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if groupCount != 1 || keyCount != 1 {
|
|
t.Fatalf("rollback counts = group:%d key:%d", groupCount, keyCount)
|
|
}
|
|
if value, exists := fixture.registry.EncryptedCredentialData(row.ID); !exists || value != row.Data {
|
|
t.Fatalf("Registry credential = %q, %t", value, exists)
|
|
}
|
|
}
|
|
|
|
func TestDeleteGroupCorruptAccessKeyFiltersPreservesDatabaseRegistryAndSnapshot(t *testing.T) {
|
|
t.Parallel()
|
|
fixture := newServiceFixture(t)
|
|
groupID := createGroupWithCredentials(t, fixture, "sk-delete-corrupt-filter")
|
|
corrupt := models.AccessKey{
|
|
Name: "Corrupt filters", KeyValue: "cipher-corrupt-filter", KeyHash: "hash-corrupt-filter",
|
|
KeySuffix: "0004", Status: string(state.AccessKeyStatusDisabled), Filters: models.JSON(`{}`),
|
|
}
|
|
if err := fixture.db.Create(&corrupt).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := fixture.db.Exec("UPDATE access_keys SET filters = ? WHERE id = ?", "not-json", corrupt.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
beforeSnapshot := fixture.manager.Current()
|
|
beforeRegistry := fixture.registry.Snapshot()
|
|
|
|
err := fixture.service.DeleteGroup(t.Context(), groupID)
|
|
var apiErr *app_errors.APIError
|
|
if !errors.As(err, &apiErr) || apiErr.Code != app_errors.ErrInternalServer.Code {
|
|
t.Fatalf("DeleteGroup() error = %#v, want INTERNAL_SERVER_ERROR", err)
|
|
}
|
|
if fixture.manager.Current() != beforeSnapshot ||
|
|
!reflect.DeepEqual(fixture.registry.Snapshot(), beforeRegistry) {
|
|
t.Fatal("corrupt filters delete mutated runtime state")
|
|
}
|
|
var count int64
|
|
if err := fixture.db.Model(&models.Group{}).Where("id = ?", groupID).Count(&count).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("target group count = %d, want 1", count)
|
|
}
|
|
}
|
|
|
|
func TestDeleteGroupAcceptsRegistryAlreadyAtTargetStateAndRetainsHistory(t *testing.T) {
|
|
t.Parallel()
|
|
fixture := newServiceFixture(t)
|
|
groupID := createGroupWithCredentials(t, fixture, "sk-delete-target-state")
|
|
var key models.Credential
|
|
if err := fixture.db.Where("group_id = ?", groupID).Take(&key).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Date(2026, time.July, 24, 16, 0, 0, 0, time.UTC)
|
|
fixture.stats.RecordFailure(key.ID, health.FailureCategoryAmbiguous, 0, now)
|
|
beforeStats := fixture.stats.Snapshot(key.ID, now)
|
|
if err := fixture.db.Create(&models.RequestLog{
|
|
ID: "delete-history-log", CompletedAtMS: now.UnixMilli(), AccessKeyID: 1,
|
|
Protocol: "openai-completions", ClientModel: "model", UpstreamModel: "model",
|
|
Status: "success", StatusCode: http.StatusOK,
|
|
}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := fixture.db.Create(&models.UsageStat{
|
|
BucketStartMS: now.UnixMilli(), AccessKeyID: 1,
|
|
GroupID: groupID, Model: "model", RequestCount: 1, SuccessCount: 1,
|
|
}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if removed := fixture.registry.RemoveGroup(groupID); !removed {
|
|
t.Fatal("precondition registry RemoveGroup() = false")
|
|
}
|
|
|
|
if err := fixture.service.DeleteGroup(t.Context(), groupID); err != nil {
|
|
t.Fatalf("DeleteGroup() with registry already removed error = %v", err)
|
|
}
|
|
if got := fixture.stats.Snapshot(key.ID, now); got != beforeStats {
|
|
t.Fatalf("StatsStore Snapshot = %#v, want %#v", got, beforeStats)
|
|
}
|
|
var requestLogCount, usageStatCount int64
|
|
if err := fixture.db.Model(&models.RequestLog{}).Where("id = ?", "delete-history-log").Count(&requestLogCount).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := fixture.db.Model(&models.UsageStat{}).Where("group_id = ?", groupID).Count(&usageStatCount).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if requestLogCount != 1 || usageStatCount != 1 {
|
|
t.Fatalf("historical records = request_logs:%d usage_stats:%d", requestLogCount, usageStatCount)
|
|
}
|
|
}
|
|
|
|
func TestDeleteGroupEndpointAuthenticationValidationNotFoundConflictAndSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
initControlI18n(t)
|
|
fixture := newServiceFixture(t)
|
|
groupID := createGroupWithCredentials(t, fixture, "sk-delete-http")
|
|
engine := gin.New()
|
|
NewServer(&config.Config{AuthKey: "test-auth-key"}, fixture.service).RegisterRoutes(engine)
|
|
path := "/api/groups/" + strconv.FormatUint(uint64(groupID), 10)
|
|
|
|
unauthorized := httptest.NewRecorder()
|
|
engine.ServeHTTP(unauthorized, httptest.NewRequest(http.MethodDelete, path, nil))
|
|
if unauthorized.Code != http.StatusUnauthorized {
|
|
t.Fatalf("unauthorized response = %d, want 401", unauthorized.Code)
|
|
}
|
|
|
|
for _, rawID := range []string{"0", "not-a-number", "18446744073709551616"} {
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodDelete, "/api/groups/"+rawID, nil)
|
|
request.Header.Set("Authorization", "Bearer test-auth-key")
|
|
engine.ServeHTTP(recorder, request)
|
|
if recorder.Code != http.StatusBadRequest {
|
|
t.Fatalf("bad ID %q response = %d, want 400", rawID, recorder.Code)
|
|
}
|
|
}
|
|
|
|
notFound := httptest.NewRecorder()
|
|
notFoundRequest := httptest.NewRequest(http.MethodDelete, "/api/groups/999", nil)
|
|
notFoundRequest.Header.Set("Authorization", "Bearer test-auth-key")
|
|
notFoundRequest.Header.Set("Accept-Language", "ja-JP")
|
|
engine.ServeHTTP(notFound, notFoundRequest)
|
|
assertDeleteGroupEnvelope(t, notFound, http.StatusNotFound, "NOT_FOUND", "グループが存在しません")
|
|
|
|
for _, test := range []struct {
|
|
language string
|
|
message string
|
|
}{
|
|
{language: "zh-CN", message: "分组仍被访问密钥引用"},
|
|
{language: "en-US", message: "The group is still referenced by access keys"},
|
|
{language: "ja-JP", message: "グループはアクセスキーから参照されています"},
|
|
} {
|
|
row := models.AccessKey{
|
|
Name: "HTTP reference " + test.language,
|
|
KeyValue: "cipher-http-" + test.language,
|
|
KeyHash: "hash-http-" + test.language,
|
|
KeySuffix: "0005",
|
|
Status: string(state.AccessKeyStatusActive),
|
|
Filters: models.JSON(fmt.Sprintf(`{"groups":[%d]}`, groupID)),
|
|
}
|
|
if err := fixture.db.Create(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodDelete, path, nil)
|
|
request.Header.Set("Authorization", "Bearer test-auth-key")
|
|
request.Header.Set("Accept-Language", test.language)
|
|
engine.ServeHTTP(recorder, request)
|
|
assertDeleteGroupEnvelope(t, recorder, http.StatusConflict, "GROUP_IN_USE", test.message)
|
|
var cleanup models.AccessKey
|
|
if err := fixture.db.First(&cleanup, row.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := fixture.db.Delete(&cleanup).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
success := httptest.NewRecorder()
|
|
successRequest := httptest.NewRequest(http.MethodDelete, path, nil)
|
|
successRequest.Header.Set("Authorization", "Bearer test-auth-key")
|
|
engine.ServeHTTP(success, successRequest)
|
|
if success.Code != http.StatusOK {
|
|
t.Fatalf("success response = %d: %s", success.Code, success.Body.String())
|
|
}
|
|
var envelope map[string]json.RawMessage
|
|
if err := json.Unmarshal(success.Body.Bytes(), &envelope); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(envelope["code"]) != "0" || string(envelope["message"]) != `"操作成功"` {
|
|
t.Fatalf("success envelope = %s", success.Body.Bytes())
|
|
}
|
|
if _, exists := envelope["data"]; exists {
|
|
t.Fatalf("success envelope has unexpected data: %s", success.Body.Bytes())
|
|
}
|
|
}
|
|
|
|
func assertDeleteGroupEnvelope(t *testing.T, recorder *httptest.ResponseRecorder, wantStatus int, wantCode, wantMessage string) {
|
|
t.Helper()
|
|
var envelope struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Data struct {
|
|
AccessKeys []AccessKeyReferenceSummary `json:"access_keys"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if recorder.Code != wantStatus || envelope.Code != wantCode || envelope.Message != wantMessage {
|
|
t.Fatalf("response = %d %#v, want %d %q %q", recorder.Code, envelope, wantStatus, wantCode, wantMessage)
|
|
}
|
|
if wantCode == "GROUP_IN_USE" {
|
|
if len(envelope.Data.AccessKeys) != 1 || envelope.Data.AccessKeys[0].ID == 0 || envelope.Data.AccessKeys[0].Name == "" {
|
|
t.Fatalf("GROUP_IN_USE data = %#v", envelope.Data)
|
|
}
|
|
}
|
|
}
|