Files
gpt-load/internal/control/settings_occ_test.go
T

290 lines
9.3 KiB
Go

package control
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"gpt-load/internal/channel"
"gpt-load/internal/platform/config"
"gpt-load/internal/state"
stateloader "gpt-load/internal/state/loader"
)
func TestSettingsHTTPUsesCanonicalLocalizedStrongRepresentation(t *testing.T) {
initControlI18n(t)
fixture := newServiceFixture(t)
engine := gin.New()
NewServer(&config.Config{AuthKey: "test-auth-key"}, fixture.service).RegisterRoutes(engine)
english := serveSettingsOCCRequest(
t, engine, http.MethodGet, "test-auth-key", "en-US", "", "",
)
if english.Code != http.StatusOK {
t.Fatalf("English GET = %d %s", english.Code, english.Body.String())
}
assertSettingsRepresentationHeaders(t, english, "en-US")
const wantEnglish = `{"code":0,"data":{"overrides":[],"values":{"first_byte_timeout":120,"header_rules":{"remove":[],"set":{}},"inject_usage_options":true,"models_dev_auto_sync_enabled":true,"request_log_retention_days":7,"request_timeout":600,"stream_idle_timeout":300,"validation_interval":600}},"message":"Success"}`
if english.Body.String() != wantEnglish {
t.Fatalf("English body = %s, want %s", english.Body.String(), wantEnglish)
}
chinese := serveSettingsOCCRequest(
t, engine, http.MethodGet, "test-auth-key", "zh-CN", "", "",
)
if chinese.Code != http.StatusOK {
t.Fatalf("Chinese GET = %d %s", chinese.Code, chinese.Body.String())
}
assertSettingsRepresentationHeaders(t, chinese, "zh-CN")
if chinese.Header().Get("ETag") == english.Header().Get("ETag") {
t.Fatalf("localized representations share ETag %q", english.Header().Get("ETag"))
}
if !strings.Contains(chinese.Body.String(), `"message":"操作成功"`) {
t.Fatalf("Chinese body = %s", chinese.Body.String())
}
}
func TestSettingsHTTPRequiresStrongIfMatchAndReturnsLatestConflict(t *testing.T) {
initControlI18n(t)
fixture := newServiceFixture(t)
engine := gin.New()
NewServer(&config.Config{AuthKey: "test-auth-key"}, fixture.service).RegisterRoutes(engine)
before := fixture.manager.Current()
missing := serveSettingsOCCRequest(
t,
engine,
http.MethodPut,
"test-auth-key",
"en-US",
"",
`{"settings":{"request_timeout":`,
)
if missing.Code != http.StatusPreconditionRequired ||
!strings.Contains(missing.Body.String(), `"code":"SETTINGS_PRECONDITION_REQUIRED"`) {
t.Fatalf("missing If-Match = %d %s", missing.Code, missing.Body.String())
}
if fixture.manager.Current() != before {
t.Fatal("missing If-Match published a snapshot")
}
malformed := serveSettingsOCCRequest(
t,
engine,
http.MethodPut,
"test-auth-key",
"en-US",
`W/"sha256-`+strings.Repeat("0", 64)+`"`,
`{"settings":{"request_timeout":900}}`,
)
if malformed.Code != http.StatusBadRequest ||
!strings.Contains(malformed.Body.String(), `"code":"BAD_REQUEST"`) {
t.Fatalf("weak If-Match = %d %s", malformed.Code, malformed.Body.String())
}
latest := serveSettingsOCCRequest(
t, engine, http.MethodGet, "test-auth-key", "en-US", "", "",
)
conflict := serveSettingsOCCRequest(
t,
engine,
http.MethodPut,
"test-auth-key",
"en-US",
`"sha256-`+strings.Repeat("0", 64)+`"`,
`{"settings":{"request_timeout":900}}`,
)
if conflict.Code != http.StatusPreconditionFailed {
t.Fatalf("conflict = %d %s", conflict.Code, conflict.Body.String())
}
var envelope struct {
Code string `json:"code"`
Data struct {
Settings SettingsDTO `json:"settings"`
SettingsETag string `json:"settings_etag"`
} `json:"data"`
}
if err := json.Unmarshal(conflict.Body.Bytes(), &envelope); err != nil {
t.Fatal(err)
}
if envelope.Code != "SETTINGS_VERSION_CONFLICT" ||
envelope.Data.SettingsETag != strings.Trim(latest.Header().Get("ETag"), `"`) ||
envelope.Data.Settings.Values.RequestTimeout != 600 ||
len(envelope.Data.Settings.Overrides) != 0 {
t.Fatalf("conflict body = %s", conflict.Body.String())
}
if fixture.manager.Current() != before {
t.Fatal("version conflict published a snapshot")
}
}
func TestSettingsHTTPMatchingWriteReturnsCurrentGETAndAllowsABA(t *testing.T) {
initControlI18n(t)
fixture := newServiceFixture(t)
engine := gin.New()
NewServer(&config.Config{AuthKey: "test-auth-key"}, fixture.service).RegisterRoutes(engine)
initial := serveSettingsOCCRequest(
t, engine, http.MethodGet, "test-auth-key", "en-US", "", "",
)
updated := serveSettingsOCCRequest(
t,
engine,
http.MethodPut,
"test-auth-key",
"en-US",
initial.Header().Get("ETag"),
`{"settings":{"request_timeout":900}}`,
)
if updated.Code != http.StatusOK {
t.Fatalf("update = %d %s", updated.Code, updated.Body.String())
}
assertSettingsRepresentationHeaders(t, updated, "en-US")
if updated.Header().Get("ETag") == initial.Header().Get("ETag") {
t.Fatalf("changed settings retained ETag %q", updated.Header().Get("ETag"))
}
current := serveSettingsOCCRequest(
t, engine, http.MethodGet, "test-auth-key", "en-US", "", "",
)
if current.Header().Get("ETag") != updated.Header().Get("ETag") ||
!bytes.Equal(current.Body.Bytes(), updated.Body.Bytes()) {
t.Fatalf("GET/PUT representation differs:\nGET %q %s\nPUT %q %s",
current.Header().Get("ETag"), current.Body.String(),
updated.Header().Get("ETag"), updated.Body.String())
}
reset := serveSettingsOCCRequest(
t,
engine,
http.MethodPut,
"test-auth-key",
"en-US",
updated.Header().Get("ETag"),
`{"settings":{"request_timeout":null}}`,
)
if reset.Code != http.StatusOK ||
reset.Header().Get("ETag") != initial.Header().Get("ETag") ||
!bytes.Equal(reset.Body.Bytes(), initial.Body.Bytes()) {
t.Fatalf("A-B-A response = %d %q %s, want %q %s",
reset.Code, reset.Header().Get("ETag"), reset.Body.String(),
initial.Header().Get("ETag"), initial.Body.String())
}
}
func TestSettingsETagIgnoresOtherResourcesAndSurvivesRuntimeReload(t *testing.T) {
initControlI18n(t)
fixture := newServiceFixture(t)
engine := gin.New()
NewServer(&config.Config{AuthKey: "test-auth-key"}, fixture.service).RegisterRoutes(engine)
initial := serveSettingsOCCRequest(
t, engine, http.MethodGet, "test-auth-key", "en-US", "", "",
)
groupName := "ETag unrelated group"
if _, err := fixture.service.CreateGroup(t.Context(), GroupCreateRequest{
Name: &groupName, ChannelID: channel.OpenAICompatible,
Params: json.RawMessage(`{"base_url":"https://example.com/v1"}`),
Models: optionalGroupModels{Set: true, Values: []GroupModel{}},
Credentials: "sk-unrelated-etag-key",
}); err != nil {
t.Fatal(err)
}
afterGroup := serveSettingsOCCRequest(
t, engine, http.MethodGet, "test-auth-key", "en-US", "", "",
)
if afterGroup.Header().Get("ETag") != initial.Header().Get("ETag") ||
!bytes.Equal(afterGroup.Body.Bytes(), initial.Body.Bytes()) {
t.Fatalf("unrelated mutation changed Settings representation")
}
reloadedManager := state.NewManager()
reloadedRegistry := state.NewCredentialRegistry()
if err := stateloader.New(fixture.db, reloadedManager, reloadedRegistry).Load(t.Context()); err != nil {
t.Fatal(err)
}
reloadedService := NewService(
fixture.db,
reloadedManager,
reloadedRegistry,
fixture.priceRuntime,
fixture.catalogRuntime,
nil,
fixture.encryption,
fixture.service.executor,
fixture.service.requestLogs,
fixture.service.usageStats,
fixture.service.homeStatistics,
fixture.stats,
fixture.mutations,
fixture.requestLogStats,
)
reloadedEngine := gin.New()
NewServer(&config.Config{AuthKey: "test-auth-key"}, reloadedService).RegisterRoutes(reloadedEngine)
afterReload := serveSettingsOCCRequest(
t, reloadedEngine, http.MethodGet, "test-auth-key", "en-US", "", "",
)
if afterReload.Header().Get("ETag") != initial.Header().Get("ETag") ||
!bytes.Equal(afterReload.Body.Bytes(), initial.Body.Bytes()) {
t.Fatalf("reload changed Settings representation:\ninitial %q %s\nreload %q %s",
initial.Header().Get("ETag"), initial.Body.String(),
afterReload.Header().Get("ETag"), afterReload.Body.String())
}
}
func assertSettingsRepresentationHeaders(
t *testing.T,
recorder *httptest.ResponseRecorder,
language string,
) {
t.Helper()
tag := recorder.Header().Get("ETag")
if len(tag) != len(`"sha256-`)+64+len(`"`) ||
!strings.HasPrefix(tag, `"sha256-`) ||
!strings.HasSuffix(tag, `"`) {
t.Fatalf("ETag = %q", tag)
}
if got := recorder.Header().Get("Content-Type"); got != "application/json; charset=utf-8" {
t.Fatalf("Content-Type = %q", got)
}
if got := recorder.Header().Get("Content-Encoding"); got != "" {
t.Fatalf("Content-Encoding = %q, want header omitted", got)
}
if got := recorder.Header().Get("Content-Language"); got != language {
t.Fatalf("Content-Language = %q, want %q", got, language)
}
if got := recorder.Header().Get("Vary"); got != "Accept-Language" {
t.Fatalf("Vary = %q", got)
}
}
func serveSettingsOCCRequest(
t *testing.T,
engine *gin.Engine,
method string,
authKey string,
language string,
ifMatch string,
body string,
) *httptest.ResponseRecorder {
t.Helper()
recorder := httptest.NewRecorder()
request := httptest.NewRequest(method, "/api/settings", strings.NewReader(body))
if authKey != "" {
request.Header.Set("Authorization", "Bearer "+authKey)
}
if language != "" {
request.Header.Set("Accept-Language", language)
}
if ifMatch != "" {
request.Header.Set("If-Match", ifMatch)
}
request.Header.Set("Content-Type", "application/json")
engine.ServeHTTP(recorder, request)
return recorder
}