| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632 |
- package config
- import (
- "fmt"
- "math"
- "os"
- "path/filepath"
- "strings"
- "sync"
- "testing"
- "time"
- "github.com/spf13/viper"
- "github.com/stretchr/testify/require"
- )
- func resetViperWithJWTSecret(t *testing.T) {
- t.Helper()
- viper.Reset()
- t.Cleanup(viper.Reset)
- t.Setenv("CONFIG_FILE", "")
- t.Setenv("DATA_DIR", "")
- t.Setenv("JWT_SECRET", strings.Repeat("x", 32))
- }
- func TestLoadDefaultModelsListReadMaxBytes(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, DefaultModelsListReadMaxBytes, cfg.Gateway.ModelsListReadMaxBytes)
- }
- func TestLoadTimezonePrecedence(t *testing.T) {
- tests := []struct {
- name string
- fileTimezone string
- timezoneEnv string
- tzEnv string
- want string
- }{
- {name: "default", want: "Asia/Shanghai"},
- {name: "config_file", fileTimezone: "Europe/London", want: "Europe/London"},
- {name: "timezone_env", fileTimezone: "Europe/London", timezoneEnv: "UTC", want: "UTC"},
- {name: "tz_env", fileTimezone: "Europe/London", timezoneEnv: "UTC", tzEnv: "America/New_York", want: "America/New_York"},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("TIMEZONE", tt.timezoneEnv)
- t.Setenv("TZ", tt.tzEnv)
- if tt.fileTimezone != "" {
- configFile := filepath.Join(t.TempDir(), "config.yaml")
- require.NoError(t, os.WriteFile(configFile, []byte("timezone: "+tt.fileTimezone+"\n"), 0o600))
- t.Setenv("CONFIG_FILE", configFile)
- }
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, tt.want, cfg.Timezone)
- })
- }
- }
- func TestLoadServerTimingConfig(t *testing.T) {
- t.Run("disabled by default", func(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- require.NoError(t, err)
- require.False(t, cfg.Server.EnableServerTiming)
- })
- t.Run("enabled by exact environment variable", func(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("ENABLE_SERVER_TIMING", "true")
- cfg, err := Load()
- require.NoError(t, err)
- require.True(t, cfg.Server.EnableServerTiming)
- })
- }
- func TestLoadRedisUsernameFromEnvironment(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("REDIS_USERNAME", "app-user")
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, "app-user", cfg.Redis.Username)
- }
- func TestLoadHTTPIngressSafetyDefaults(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, 10, cfg.Server.ReadHeaderTimeout)
- require.Equal(t, 64*1024, cfg.Server.MaxHeaderBytes)
- require.Empty(t, cfg.Server.TrustedProxies)
- require.False(t, cfg.Server.TrustedProxiesConfigured)
- require.True(t, cfg.TrustForwardedIPForAPIKeyACL())
- require.Equal(t, int64(32*1024*1024), cfg.Gateway.TextMaxBodySize)
- require.True(t, cfg.APIKeyAuth.InvalidAbuse.Enabled)
- require.Equal(t, 120, cfg.APIKeyAuth.InvalidAbuse.Threshold)
- require.Equal(t, 16384, cfg.APIKeyAuth.InvalidAbuse.Capacity)
- }
- func TestNormalizeForwardedClientIPHeaders(t *testing.T) {
- headers, err := NormalizeForwardedClientIPHeaders([]string{
- " x-cdn-client-ip ",
- "X-CDN-CLIENT-IP",
- "true-client-ip",
- })
- require.NoError(t, err)
- require.Equal(t, []string{"X-Cdn-Client-Ip", "True-Client-Ip"}, headers)
- _, err = NormalizeForwardedClientIPHeaders([]string{"X Invalid"})
- require.ErrorContains(t, err, "invalid HTTP header field name")
- }
- func TestNormalizeForwardedClientIPHeadersLimit(t *testing.T) {
- headers := make([]string, 0, MaxForwardedClientIPHeaders+1)
- for i := 0; i <= MaxForwardedClientIPHeaders; i++ {
- headers = append(headers, fmt.Sprintf("X-CDN-IP-%d", i))
- }
- _, err := NormalizeForwardedClientIPHeaders(headers)
- require.ErrorContains(t, err, "at most 16 unique names")
- }
- func TestLoadForwardedClientIPHeadersNormalizesAndSnapshots(t *testing.T) {
- resetViperWithJWTSecret(t)
- viper.Set("security.forwarded_client_ip_headers", []string{" x-cdn-ip ", "X-CDN-IP", "true-client-ip"})
- cfg, err := Load()
- require.NoError(t, err)
- snapshot := cfg.ForwardedClientIPSettings()
- require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, snapshot.Headers)
- snapshot.Headers[0] = "X-Mutated"
- require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, cfg.ForwardedClientIPSettings().Headers)
- }
- func TestForwardedClientIPSettingsConcurrentPublication(t *testing.T) {
- cfg := &Config{}
- cfg.SetForwardedClientIPSettings(true, []string{"X-Public-A"})
- const iterations = 2000
- start := make(chan struct{})
- errCh := make(chan error, 8)
- var wg sync.WaitGroup
- for _, settings := range []ForwardedClientIPSettings{
- {TrustForwardedIP: true, Headers: []string{"X-Public-A"}},
- {TrustForwardedIP: false, Headers: []string{"X-Public-B"}},
- } {
- settings := settings
- wg.Add(1)
- go func() {
- defer wg.Done()
- <-start
- for i := 0; i < iterations; i++ {
- cfg.SetForwardedClientIPSettings(settings.TrustForwardedIP, settings.Headers)
- }
- }()
- }
- for i := 0; i < cap(errCh); i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- <-start
- for j := 0; j < iterations; j++ {
- snapshot := cfg.ForwardedClientIPSettings()
- validA := snapshot.TrustForwardedIP && len(snapshot.Headers) == 1 && snapshot.Headers[0] == "X-Public-A"
- validB := !snapshot.TrustForwardedIP && len(snapshot.Headers) == 1 && snapshot.Headers[0] == "X-Public-B"
- if !validA && !validB {
- errCh <- fmt.Errorf("observed inconsistent forwarded IP settings: %+v", snapshot)
- return
- }
- }
- }()
- }
- close(start)
- wg.Wait()
- close(errCh)
- for err := range errCh {
- require.NoError(t, err)
- }
- }
- func TestLoadForwardedClientIPHeadersFromEnvironment(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("SECURITY_FORWARDED_CLIENT_IP_HEADERS", " x-cdn-ip , X-CDN-IP, true-client-ip ")
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, cfg.ForwardedClientIPSettings().Headers)
- }
- func TestLoadExplicitEmptyForwardedClientIPHeadersFromEnvironment(t *testing.T) {
- resetViperWithJWTSecret(t)
- viper.Set("security.forwarded_client_ip_headers", []string{"X-Yaml-IP"})
- t.Setenv("SECURITY_FORWARDED_CLIENT_IP_HEADERS", "")
- cfg, err := Load()
- require.NoError(t, err)
- require.Empty(t, cfg.ForwardedClientIPSettings().Headers)
- }
- func TestLoadRejectsInvalidForwardedClientIPHeaderFromEnvironment(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("SECURITY_FORWARDED_CLIENT_IP_HEADERS", "X-Valid-IP, X Invalid")
- _, err := Load()
- require.ErrorContains(t, err, "security.forwarded_client_ip_headers")
- }
- func TestLoadRejectsInvalidForwardedClientIPHeader(t *testing.T) {
- resetViperWithJWTSecret(t)
- viper.Set("security.forwarded_client_ip_headers", []string{"X Invalid"})
- _, err := Load()
- require.ErrorContains(t, err, "security.forwarded_client_ip_headers")
- }
- func TestLoadExplicitEmptyTrustedProxiesEnablesConfiguredMode(t *testing.T) {
- resetViperWithJWTSecret(t)
- viper.Set("server.trusted_proxies", []string{})
- cfg, err := Load()
- require.NoError(t, err)
- require.Empty(t, cfg.Server.TrustedProxies)
- require.True(t, cfg.Server.TrustedProxiesConfigured)
- }
- func TestLoadExplicitTrustedProxiesEnablesConfiguredMode(t *testing.T) {
- resetViperWithJWTSecret(t)
- viper.Set("server.trusted_proxies", []string{"127.0.0.1/32"})
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, []string{"127.0.0.1/32"}, cfg.Server.TrustedProxies)
- require.True(t, cfg.Server.TrustedProxiesConfigured)
- }
- func TestLoadTrustedProxiesFromEnvironment(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("SERVER_TRUSTED_PROXIES", "127.0.0.1/32, ::1/128")
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, []string{"127.0.0.1/32", "::1/128"}, cfg.Server.TrustedProxies)
- require.True(t, cfg.Server.TrustedProxiesConfigured)
- }
- func TestLoadExplicitEmptyTrustedProxiesFromEnvironment(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("SERVER_TRUSTED_PROXIES", "")
- cfg, err := Load()
- require.NoError(t, err)
- require.Empty(t, cfg.Server.TrustedProxies)
- require.True(t, cfg.Server.TrustedProxiesConfigured)
- }
- func TestLoadTrustedProxiesPresenceFromYAML(t *testing.T) {
- tests := []struct {
- name string
- yaml string
- want []string
- configured bool
- }{
- {name: "absent", yaml: "server:\n mode: debug\n", configured: false},
- {name: "explicit empty", yaml: "server:\n trusted_proxies: []\n", want: []string{}, configured: true},
- {
- name: "populated",
- yaml: "server:\n trusted_proxies:\n - 127.0.0.1/32\n - ::1/128\n",
- want: []string{"127.0.0.1/32", "::1/128"},
- configured: true,
- },
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- resetViperWithJWTSecret(t)
- configDir := t.TempDir()
- require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(test.yaml), 0o600))
- t.Setenv("DATA_DIR", configDir)
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, test.want, cfg.Server.TrustedProxies)
- require.Equal(t, test.configured, cfg.Server.TrustedProxiesConfigured)
- })
- }
- }
- func TestLoadTrustedProxiesFromConfigFile(t *testing.T) {
- resetViperWithJWTSecret(t)
- configFile := filepath.Join(t.TempDir(), "reporter-config.yaml")
- require.NoError(t, os.WriteFile(configFile, []byte(`server:
- host: 192.0.2.10
- trusted_proxies:
- - 127.0.0.1/32
- - 10.0.0.0/8
- - 172.16.0.0/12
- - 192.168.0.0/16
- `), 0o600))
- t.Setenv("CONFIG_FILE", configFile)
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, "192.0.2.10", cfg.Server.Host)
- require.Equal(t, []string{"127.0.0.1/32", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}, cfg.Server.TrustedProxies)
- require.True(t, cfg.Server.TrustedProxiesConfigured)
- }
- func TestConfigFileTakesPrecedenceOverDataDir(t *testing.T) {
- resetViperWithJWTSecret(t)
- dataDir := t.TempDir()
- require.NoError(t, os.WriteFile(filepath.Join(dataDir, "config.yaml"), []byte("server:\n host: 192.0.2.20\n"), 0o600))
- configFile := filepath.Join(t.TempDir(), "explicit.yaml")
- require.NoError(t, os.WriteFile(configFile, []byte("server:\n host: 192.0.2.30\n"), 0o600))
- t.Setenv("DATA_DIR", dataDir)
- t.Setenv("CONFIG_FILE", configFile)
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, "192.0.2.30", cfg.Server.Host)
- }
- func TestLoadReturnsErrorForMissingConfigFile(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("CONFIG_FILE", filepath.Join(t.TempDir(), "missing.yaml"))
- _, err := Load()
- require.ErrorContains(t, err, "read config error")
- }
- func TestLoadForBootstrapAllowsMissingJWTSecret(t *testing.T) {
- viper.Reset()
- t.Cleanup(viper.Reset)
- t.Setenv("CONFIG_FILE", "")
- t.Setenv("DATA_DIR", "")
- t.Setenv("JWT_SECRET", "")
- cfg, err := LoadForBootstrap()
- if err != nil {
- t.Fatalf("LoadForBootstrap() error: %v", err)
- }
- if cfg.JWT.Secret != "" {
- t.Fatalf("LoadForBootstrap() should keep empty jwt.secret during bootstrap")
- }
- }
- func TestNormalizeRunMode(t *testing.T) {
- tests := []struct {
- input string
- expected string
- }{
- {"simple", "simple"},
- {"SIMPLE", "simple"},
- {"standard", "standard"},
- {"invalid", "standard"},
- {"", "standard"},
- }
- for _, tt := range tests {
- result := NormalizeRunMode(tt.input)
- if result != tt.expected {
- t.Errorf("NormalizeRunMode(%q) = %q, want %q", tt.input, result, tt.expected)
- }
- }
- }
- func TestLoadDefaultSchedulingConfig(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if cfg.Gateway.Scheduling.StickySessionMaxWaiting != 3 {
- t.Fatalf("StickySessionMaxWaiting = %d, want 3", cfg.Gateway.Scheduling.StickySessionMaxWaiting)
- }
- if cfg.Gateway.Scheduling.StickySessionWaitTimeout != 120*time.Second {
- t.Fatalf("StickySessionWaitTimeout = %v, want 120s", cfg.Gateway.Scheduling.StickySessionWaitTimeout)
- }
- if cfg.Gateway.Scheduling.FallbackWaitTimeout != 30*time.Second {
- t.Fatalf("FallbackWaitTimeout = %v, want 30s", cfg.Gateway.Scheduling.FallbackWaitTimeout)
- }
- if cfg.Gateway.Scheduling.FallbackMaxWaiting != 100 {
- t.Fatalf("FallbackMaxWaiting = %d, want 100", cfg.Gateway.Scheduling.FallbackMaxWaiting)
- }
- if !cfg.Gateway.Scheduling.LoadBatchEnabled {
- t.Fatalf("LoadBatchEnabled = false, want true")
- }
- if cfg.Gateway.Scheduling.LoadBatchCacheTTLMS != 200 {
- t.Fatalf("LoadBatchCacheTTLMS = %d, want 200", cfg.Gateway.Scheduling.LoadBatchCacheTTLMS)
- }
- if cfg.Gateway.Scheduling.SlotCleanupInterval != 30*time.Second {
- t.Fatalf("SlotCleanupInterval = %v, want 30s", cfg.Gateway.Scheduling.SlotCleanupInterval)
- }
- }
- func TestLoadDefaultOpenAIFirstOutputTimeoutsDisabled(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- require.NoError(t, err)
- require.Zero(t, cfg.Gateway.OpenAIFirstOutputTimeoutSeconds)
- require.Zero(t, cfg.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds)
- }
- func TestLoadOpenAIFirstOutputTimeoutsFromEnv(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("GATEWAY_OPENAI_FIRST_OUTPUT_TIMEOUT_SECONDS", "90")
- t.Setenv("GATEWAY_OPENAI_HIGH_EFFORT_FIRST_OUTPUT_TIMEOUT_SECONDS", "240")
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, 90, cfg.Gateway.OpenAIFirstOutputTimeoutSeconds)
- require.Equal(t, 240, cfg.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds)
- }
- func TestValidateOpenAIFirstOutputTimeoutMinimum(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- require.NoError(t, err)
- cfg.Gateway.OpenAIFirstOutputTimeoutSeconds = 30
- require.NoError(t, cfg.Validate())
- }
- func TestLoadDefaultOpenAIWSConfig(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if !cfg.Gateway.OpenAIWS.Enabled {
- t.Fatalf("Gateway.OpenAIWS.Enabled = false, want true")
- }
- if !cfg.Gateway.OpenAIWS.ResponsesWebsocketsV2 {
- t.Fatalf("Gateway.OpenAIWS.ResponsesWebsocketsV2 = false, want true")
- }
- if cfg.Gateway.OpenAIWS.ResponsesWebsockets {
- t.Fatalf("Gateway.OpenAIWS.ResponsesWebsockets = true, want false")
- }
- if !cfg.Gateway.OpenAIWS.DynamicMaxConnsByAccountConcurrencyEnabled {
- t.Fatalf("Gateway.OpenAIWS.DynamicMaxConnsByAccountConcurrencyEnabled = false, want true")
- }
- if cfg.Gateway.OpenAIWS.OAuthMaxConnsFactor != 1.0 {
- t.Fatalf("Gateway.OpenAIWS.OAuthMaxConnsFactor = %v, want 1.0", cfg.Gateway.OpenAIWS.OAuthMaxConnsFactor)
- }
- if cfg.Gateway.OpenAIWS.APIKeyMaxConnsFactor != 1.0 {
- t.Fatalf("Gateway.OpenAIWS.APIKeyMaxConnsFactor = %v, want 1.0", cfg.Gateway.OpenAIWS.APIKeyMaxConnsFactor)
- }
- if cfg.Gateway.OpenAIWS.StickySessionTTLSeconds != 3600 {
- t.Fatalf("Gateway.OpenAIWS.StickySessionTTLSeconds = %d, want 3600", cfg.Gateway.OpenAIWS.StickySessionTTLSeconds)
- }
- if !cfg.Gateway.OpenAIScheduler.StickyEscapeEnabled {
- t.Fatalf("Gateway.OpenAIScheduler.StickyEscapeEnabled = false, want true")
- }
- if cfg.Gateway.OpenAIScheduler.StickyEscapeTTFTMs != 15000 {
- t.Fatalf("Gateway.OpenAIScheduler.StickyEscapeTTFTMs = %d, want 15000", cfg.Gateway.OpenAIScheduler.StickyEscapeTTFTMs)
- }
- if cfg.Gateway.OpenAIScheduler.StickyEscapeErrorRate != 0.5 {
- t.Fatalf("Gateway.OpenAIScheduler.StickyEscapeErrorRate = %v, want 0.5", cfg.Gateway.OpenAIScheduler.StickyEscapeErrorRate)
- }
- if !cfg.Gateway.OpenAIWS.SessionHashReadOldFallback {
- t.Fatalf("Gateway.OpenAIWS.SessionHashReadOldFallback = false, want true")
- }
- if !cfg.Gateway.OpenAIWS.SessionHashDualWriteOld {
- t.Fatalf("Gateway.OpenAIWS.SessionHashDualWriteOld = false, want true")
- }
- if !cfg.Gateway.OpenAIWS.MetadataBridgeEnabled {
- t.Fatalf("Gateway.OpenAIWS.MetadataBridgeEnabled = false, want true")
- }
- if cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds != 3600 {
- t.Fatalf("Gateway.OpenAIWS.StickyResponseIDTTLSeconds = %d, want 3600", cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds)
- }
- if cfg.Gateway.OpenAIWS.FallbackCooldownSeconds != 30 {
- t.Fatalf("Gateway.OpenAIWS.FallbackCooldownSeconds = %d, want 30", cfg.Gateway.OpenAIWS.FallbackCooldownSeconds)
- }
- if cfg.Gateway.OpenAIWS.EventFlushBatchSize != 1 {
- t.Fatalf("Gateway.OpenAIWS.EventFlushBatchSize = %d, want 1", cfg.Gateway.OpenAIWS.EventFlushBatchSize)
- }
- if cfg.Gateway.OpenAIWS.EventFlushIntervalMS != 10 {
- t.Fatalf("Gateway.OpenAIWS.EventFlushIntervalMS = %d, want 10", cfg.Gateway.OpenAIWS.EventFlushIntervalMS)
- }
- if cfg.Gateway.OpenAIWS.PrewarmCooldownMS != 300 {
- t.Fatalf("Gateway.OpenAIWS.PrewarmCooldownMS = %d, want 300", cfg.Gateway.OpenAIWS.PrewarmCooldownMS)
- }
- if cfg.Gateway.OpenAIWS.ClientReadLimitBytes != 64*1024*1024 {
- t.Fatalf("Gateway.OpenAIWS.ClientReadLimitBytes = %d, want %d", cfg.Gateway.OpenAIWS.ClientReadLimitBytes, 64*1024*1024)
- }
- if !cfg.Gateway.OpenAIWS.HTTPBridgeEnabled {
- t.Fatalf("Gateway.OpenAIWS.HTTPBridgeEnabled = false, want true")
- }
- if cfg.Gateway.OpenAIWS.HTTPBridgeThresholdBytes != 15*1024*1024 {
- t.Fatalf("Gateway.OpenAIWS.HTTPBridgeThresholdBytes = %d, want %d", cfg.Gateway.OpenAIWS.HTTPBridgeThresholdBytes, 15*1024*1024)
- }
- if cfg.Gateway.OpenAIWS.RetryBackoffInitialMS != 120 {
- t.Fatalf("Gateway.OpenAIWS.RetryBackoffInitialMS = %d, want 120", cfg.Gateway.OpenAIWS.RetryBackoffInitialMS)
- }
- if cfg.Gateway.OpenAIWS.RetryBackoffMaxMS != 2000 {
- t.Fatalf("Gateway.OpenAIWS.RetryBackoffMaxMS = %d, want 2000", cfg.Gateway.OpenAIWS.RetryBackoffMaxMS)
- }
- if cfg.Gateway.OpenAIWS.RetryJitterRatio != 0.2 {
- t.Fatalf("Gateway.OpenAIWS.RetryJitterRatio = %v, want 0.2", cfg.Gateway.OpenAIWS.RetryJitterRatio)
- }
- if cfg.Gateway.OpenAIWS.RetryTotalBudgetMS != 5000 {
- t.Fatalf("Gateway.OpenAIWS.RetryTotalBudgetMS = %d, want 5000", cfg.Gateway.OpenAIWS.RetryTotalBudgetMS)
- }
- if cfg.Gateway.OpenAIWS.PayloadLogSampleRate != 0.2 {
- t.Fatalf("Gateway.OpenAIWS.PayloadLogSampleRate = %v, want 0.2", cfg.Gateway.OpenAIWS.PayloadLogSampleRate)
- }
- if cfg.Gateway.OpenAIWS.SchedulerScoreWeights.QuotaHeadroom != 0 {
- t.Fatalf("Gateway.OpenAIWS.SchedulerScoreWeights.QuotaHeadroom = %v, want 0", cfg.Gateway.OpenAIWS.SchedulerScoreWeights.QuotaHeadroom)
- }
- if cfg.Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost != 0 {
- t.Fatalf("Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost = %v, want 0", cfg.Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost)
- }
- if !cfg.Gateway.OpenAIWS.StoreDisabledForceNewConn {
- t.Fatalf("Gateway.OpenAIWS.StoreDisabledForceNewConn = false, want true")
- }
- if cfg.Gateway.OpenAIWS.StoreDisabledConnMode != "strict" {
- t.Fatalf("Gateway.OpenAIWS.StoreDisabledConnMode = %q, want %q", cfg.Gateway.OpenAIWS.StoreDisabledConnMode, "strict")
- }
- if cfg.Gateway.OpenAIWS.ModeRouterV2Enabled {
- t.Fatalf("Gateway.OpenAIWS.ModeRouterV2Enabled = true, want false")
- }
- if cfg.Gateway.OpenAIWS.IngressModeDefault != "ctx_pool" {
- t.Fatalf("Gateway.OpenAIWS.IngressModeDefault = %q, want %q", cfg.Gateway.OpenAIWS.IngressModeDefault, "ctx_pool")
- }
- if cfg.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds != DefaultOpenAIWSClientFirstMessageTimeoutSeconds {
- t.Fatalf(
- "Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds = %d, want %d",
- cfg.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds,
- DefaultOpenAIWSClientFirstMessageTimeoutSeconds,
- )
- }
- if cfg.Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds != 300 {
- t.Fatalf("Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds = %d, want 300", cfg.Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds)
- }
- if cfg.Gateway.OpenAIWS.MaxIngressConnectionsPerAPIKey != 64 {
- t.Fatalf("Gateway.OpenAIWS.MaxIngressConnectionsPerAPIKey = %d, want 64", cfg.Gateway.OpenAIWS.MaxIngressConnectionsPerAPIKey)
- }
- }
- func TestLoadOpenAIWSClientFirstMessageTimeoutFromEnv(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("GATEWAY_OPENAI_WS_CLIENT_FIRST_MESSAGE_TIMEOUT_SECONDS", "120")
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, 120, cfg.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds)
- }
- func TestLoadOpenAIWSForceHTTPFromEnv(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("GATEWAY_OPENAI_WS_FORCE_HTTP", "true")
- cfg, err := Load()
- require.NoError(t, err)
- require.True(t, cfg.Gateway.OpenAIWS.ForceHTTP)
- }
- func TestLoadDefaultOpenAICompactModel(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, "gpt-5.4", cfg.Gateway.OpenAICompactModel)
- }
- func TestLoadOpenAICompactModelFromEnv(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("GATEWAY_OPENAI_COMPACT_MODEL", "gpt-5.3-codex")
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, "gpt-5.3-codex", cfg.Gateway.OpenAICompactModel)
- }
- func TestLoadDefaultGrokFreeQuotaSoftGate(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- require.NoError(t, err)
- require.False(t, cfg.Gateway.Grok.PasswordAuthEnabled)
- require.True(t, cfg.Gateway.Grok.FreeQuotaSoftGateEnabled)
- require.Equal(t, int64(500_000), cfg.Gateway.Grok.FreeQuotaTokenLimit)
- require.Equal(t, 95, cfg.Gateway.Grok.FreeQuotaSoftGatePercent)
- require.Equal(t, 24, cfg.Gateway.Grok.FreeQuotaWindowHours)
- require.Equal(t, 60, cfg.Gateway.Grok.FreeQuotaStatsCacheSeconds)
- }
- func TestLoadDefaultOpenAIHTTP2Enabled(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- require.NoError(t, err)
- require.True(t, cfg.Gateway.OpenAIHTTP2.Enabled)
- require.True(t, cfg.Gateway.OpenAIHTTP2.AllowProxyFallbackToHTTP1)
- require.False(t, cfg.Gateway.OpenAIProxyStreamCircuit.Disabled)
- require.Equal(t, 2, cfg.Gateway.OpenAIProxyStreamCircuit.FailureThreshold)
- require.Equal(t, 60, cfg.Gateway.OpenAIProxyStreamCircuit.WindowSeconds)
- require.Equal(t, 600, cfg.Gateway.OpenAIProxyStreamCircuit.TTLSeconds)
- }
- func TestLoadOpenAIProxyStreamCircuitFromEnv(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_DISABLED", "true")
- t.Setenv("GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_FAILURE_THRESHOLD", "3")
- t.Setenv("GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_WINDOW_SECONDS", "90")
- t.Setenv("GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_TTL_SECONDS", "420")
- cfg, err := Load()
- require.NoError(t, err)
- require.True(t, cfg.Gateway.OpenAIProxyStreamCircuit.Disabled)
- require.Equal(t, 3, cfg.Gateway.OpenAIProxyStreamCircuit.FailureThreshold)
- require.Equal(t, 90, cfg.Gateway.OpenAIProxyStreamCircuit.WindowSeconds)
- require.Equal(t, 420, cfg.Gateway.OpenAIProxyStreamCircuit.TTLSeconds)
- }
- func TestLoadOpenAIHTTP2DisabledFromEnv(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("GATEWAY_OPENAI_HTTP2_ENABLED", "false")
- cfg, err := Load()
- require.NoError(t, err)
- require.False(t, cfg.Gateway.OpenAIHTTP2.Enabled)
- }
- func TestLoadDefaultOpenAIResponseHeaderTimeoutUnlimited(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, 0, cfg.Gateway.OpenAIResponseHeaderTimeout)
- }
- func TestLoadOpenAIResponseHeaderTimeoutFromEnv(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("GATEWAY_OPENAI_RESPONSE_HEADER_TIMEOUT", "1800")
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, 1800, cfg.Gateway.OpenAIResponseHeaderTimeout)
- }
- func TestLoadImageNonstreamKeepaliveFromEnv(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("GATEWAY_IMAGE_NONSTREAM_KEEPALIVE_INTERVAL", "15")
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, 15, cfg.Gateway.ImageNonstreamKeepaliveInterval)
- }
- func TestLoadOpenAIWSStickyTTLCompatibility(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("GATEWAY_OPENAI_WS_STICKY_RESPONSE_ID_TTL_SECONDS", "0")
- t.Setenv("GATEWAY_OPENAI_WS_STICKY_PREVIOUS_RESPONSE_TTL_SECONDS", "7200")
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds != 7200 {
- t.Fatalf("StickyResponseIDTTLSeconds = %d, want 7200", cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds)
- }
- }
- func TestLoadDefaultIdempotencyConfig(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if !cfg.Idempotency.ObserveOnly {
- t.Fatalf("Idempotency.ObserveOnly = false, want true")
- }
- if cfg.Idempotency.DefaultTTLSeconds != 86400 {
- t.Fatalf("Idempotency.DefaultTTLSeconds = %d, want 86400", cfg.Idempotency.DefaultTTLSeconds)
- }
- if cfg.Idempotency.SystemOperationTTLSeconds != 3600 {
- t.Fatalf("Idempotency.SystemOperationTTLSeconds = %d, want 3600", cfg.Idempotency.SystemOperationTTLSeconds)
- }
- }
- func TestLoadDefaultBatchImageQueueDisabled(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- require.NoError(t, err)
- require.False(t, cfg.BatchImage.QueueEnabled)
- }
- func TestLoadIdempotencyConfigFromEnv(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("IDEMPOTENCY_OBSERVE_ONLY", "false")
- t.Setenv("IDEMPOTENCY_DEFAULT_TTL_SECONDS", "600")
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if cfg.Idempotency.ObserveOnly {
- t.Fatalf("Idempotency.ObserveOnly = true, want false")
- }
- if cfg.Idempotency.DefaultTTLSeconds != 600 {
- t.Fatalf("Idempotency.DefaultTTLSeconds = %d, want 600", cfg.Idempotency.DefaultTTLSeconds)
- }
- }
- func TestLoadSchedulingConfigFromEnv(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("GATEWAY_SCHEDULING_STICKY_SESSION_MAX_WAITING", "5")
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if cfg.Gateway.Scheduling.StickySessionMaxWaiting != 5 {
- t.Fatalf("StickySessionMaxWaiting = %d, want 5", cfg.Gateway.Scheduling.StickySessionMaxWaiting)
- }
- }
- func TestLoadWeChatConnectConfigFromLegacyEnv(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
- t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
- t.Setenv("WECHAT_OAUTH_MP_APP_ID", "wx-mp-app")
- t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "wx-mp-secret")
- t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/legacy-callback")
- cfg, err := Load()
- require.NoError(t, err)
- require.True(t, cfg.WeChat.Enabled)
- require.True(t, cfg.WeChat.OpenEnabled)
- require.True(t, cfg.WeChat.MPEnabled)
- require.False(t, cfg.WeChat.MobileEnabled)
- require.Equal(t, "open", cfg.WeChat.Mode)
- require.Equal(t, "wx-open-app", cfg.WeChat.OpenAppID)
- require.Equal(t, "wx-open-secret", cfg.WeChat.OpenAppSecret)
- require.Equal(t, "wx-mp-app", cfg.WeChat.MPAppID)
- require.Equal(t, "wx-mp-secret", cfg.WeChat.MPAppSecret)
- require.Equal(t, "/auth/wechat/legacy-callback", cfg.WeChat.FrontendRedirectURL)
- }
- func TestLoadDefaultOIDCSecurityDefaults(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- require.NoError(t, err)
- require.True(t, cfg.OIDC.UsePKCE)
- require.True(t, cfg.OIDC.ValidateIDToken)
- require.False(t, cfg.OIDC.UsePKCEExplicit)
- require.False(t, cfg.OIDC.ValidateIDTokenExplicit)
- }
- func TestLoadExplicitOIDCSecurityDefaultsFromEnvMarksFlagsExplicit(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("OIDC_CONNECT_USE_PKCE", "false")
- t.Setenv("OIDC_CONNECT_VALIDATE_ID_TOKEN", "false")
- cfg, err := Load()
- require.NoError(t, err)
- require.False(t, cfg.OIDC.UsePKCE)
- require.False(t, cfg.OIDC.ValidateIDToken)
- require.True(t, cfg.OIDC.UsePKCEExplicit)
- require.True(t, cfg.OIDC.ValidateIDTokenExplicit)
- }
- func TestLoadForcedCodexInstructionsTemplate(t *testing.T) {
- resetViperWithJWTSecret(t)
- tempDir := t.TempDir()
- templatePath := filepath.Join(tempDir, "codex-instructions.md.tmpl")
- configPath := filepath.Join(tempDir, "config.yaml")
- require.NoError(t, os.WriteFile(templatePath, []byte("server-prefix\n\n{{ .ExistingInstructions }}"), 0o644))
- yamlSafePath := filepath.ToSlash(templatePath)
- require.NoError(t, os.WriteFile(configPath, []byte("gateway:\n forced_codex_instructions_template_file: \""+yamlSafePath+"\"\n"), 0o644))
- t.Setenv("DATA_DIR", tempDir)
- cfg, err := Load()
- require.NoError(t, err)
- require.Equal(t, yamlSafePath, cfg.Gateway.ForcedCodexInstructionsTemplateFile)
- require.Equal(t, "server-prefix\n\n{{ .ExistingInstructions }}", cfg.Gateway.ForcedCodexInstructionsTemplate)
- }
- func TestLoadDefaultSecurityToggles(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if cfg.Security.URLAllowlist.Enabled {
- t.Fatalf("URLAllowlist.Enabled = true, want false")
- }
- if !cfg.Security.URLAllowlist.AllowInsecureHTTP {
- t.Fatalf("URLAllowlist.AllowInsecureHTTP = false, want true")
- }
- if !cfg.Security.URLAllowlist.AllowPrivateHosts {
- t.Fatalf("URLAllowlist.AllowPrivateHosts = false, want true")
- }
- if !cfg.Security.ResponseHeaders.Enabled {
- t.Fatalf("ResponseHeaders.Enabled = false, want true")
- }
- wantHosts := []string{
- "api.kimi.com",
- "api.moonshot.ai",
- "api.moonshot.cn",
- }
- hostSet := make(map[string]struct{}, len(cfg.Security.URLAllowlist.UpstreamHosts))
- for _, h := range cfg.Security.URLAllowlist.UpstreamHosts {
- hostSet[h] = struct{}{}
- }
- for _, want := range wantHosts {
- if _, ok := hostSet[want]; !ok {
- t.Fatalf("URLAllowlist.UpstreamHosts missing %q; got %v", want, cfg.Security.URLAllowlist.UpstreamHosts)
- }
- }
- }
- func TestLoadDefaultServerMode(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if cfg.Server.Mode != "release" {
- t.Fatalf("Server.Mode = %q, want %q", cfg.Server.Mode, "release")
- }
- }
- func TestLoadDefaultJWTAccessTokenExpireMinutes(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if cfg.JWT.ExpireHour != 24 {
- t.Fatalf("JWT.ExpireHour = %d, want 24", cfg.JWT.ExpireHour)
- }
- if cfg.JWT.AccessTokenExpireMinutes != 0 {
- t.Fatalf("JWT.AccessTokenExpireMinutes = %d, want 0", cfg.JWT.AccessTokenExpireMinutes)
- }
- }
- func TestLoadJWTAccessTokenExpireMinutesFromEnv(t *testing.T) {
- resetViperWithJWTSecret(t)
- t.Setenv("JWT_ACCESS_TOKEN_EXPIRE_MINUTES", "90")
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if cfg.JWT.AccessTokenExpireMinutes != 90 {
- t.Fatalf("JWT.AccessTokenExpireMinutes = %d, want 90", cfg.JWT.AccessTokenExpireMinutes)
- }
- }
- func TestLoadDefaultDatabaseSSLMode(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if cfg.Database.SSLMode != "prefer" {
- t.Fatalf("Database.SSLMode = %q, want %q", cfg.Database.SSLMode, "prefer")
- }
- }
- func TestValidateLinuxDoFrontendRedirectURL(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.LinuxDo.Enabled = true
- cfg.LinuxDo.ClientID = "test-client"
- cfg.LinuxDo.ClientSecret = "test-secret"
- cfg.LinuxDo.RedirectURL = "https://example.com/api/v1/auth/oauth/linuxdo/callback"
- cfg.LinuxDo.TokenAuthMethod = "client_secret_post"
- cfg.LinuxDo.UsePKCE = true
- cfg.LinuxDo.FrontendRedirectURL = "javascript:alert(1)"
- err = cfg.Validate()
- if err == nil {
- t.Fatalf("Validate() expected error for javascript scheme, got nil")
- }
- if !strings.Contains(err.Error(), "linuxdo_connect.frontend_redirect_url") {
- t.Fatalf("Validate() expected frontend_redirect_url error, got: %v", err)
- }
- }
- func TestValidateLinuxDoAllowsDisablingPKCEForCompatibility(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.LinuxDo.Enabled = true
- cfg.LinuxDo.ClientID = "test-client"
- cfg.LinuxDo.ClientSecret = ""
- cfg.LinuxDo.RedirectURL = "https://example.com/api/v1/auth/oauth/linuxdo/callback"
- cfg.LinuxDo.FrontendRedirectURL = "/auth/linuxdo/callback"
- cfg.LinuxDo.TokenAuthMethod = "none"
- cfg.LinuxDo.UsePKCE = false
- err = cfg.Validate()
- if err != nil {
- t.Fatalf("Validate() expected LinuxDo config without PKCE to pass for compatibility, got: %v", err)
- }
- }
- func TestValidateOIDCScopesMustContainOpenID(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.OIDC.Enabled = true
- cfg.OIDC.ClientID = "oidc-client"
- cfg.OIDC.ClientSecret = "oidc-secret"
- cfg.OIDC.IssuerURL = "https://issuer.example.com"
- cfg.OIDC.AuthorizeURL = "https://issuer.example.com/auth"
- cfg.OIDC.TokenURL = "https://issuer.example.com/token"
- cfg.OIDC.JWKSURL = "https://issuer.example.com/jwks"
- cfg.OIDC.RedirectURL = "https://example.com/api/v1/auth/oauth/oidc/callback"
- cfg.OIDC.FrontendRedirectURL = "/auth/oidc/callback"
- cfg.OIDC.Scopes = "profile email"
- cfg.OIDC.UsePKCE = true
- err = cfg.Validate()
- if err == nil {
- t.Fatalf("Validate() expected error when scopes do not include openid, got nil")
- }
- if !strings.Contains(err.Error(), "oidc_connect.scopes") {
- t.Fatalf("Validate() expected oidc_connect.scopes error, got: %v", err)
- }
- }
- func TestValidateOIDCAllowsIssuerOnlyEndpointsWithDiscoveryFallback(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.OIDC.Enabled = true
- cfg.OIDC.ClientID = "oidc-client"
- cfg.OIDC.ClientSecret = "oidc-secret"
- cfg.OIDC.IssuerURL = "https://issuer.example.com"
- cfg.OIDC.AuthorizeURL = ""
- cfg.OIDC.TokenURL = ""
- cfg.OIDC.JWKSURL = ""
- cfg.OIDC.RedirectURL = "https://example.com/api/v1/auth/oauth/oidc/callback"
- cfg.OIDC.FrontendRedirectURL = "/auth/oidc/callback"
- cfg.OIDC.Scopes = "openid email profile"
- cfg.OIDC.ValidateIDToken = true
- cfg.OIDC.UsePKCE = true
- err = cfg.Validate()
- if err != nil {
- t.Fatalf("Validate() expected issuer-only OIDC config to pass with discovery fallback, got: %v", err)
- }
- }
- func TestValidateOIDCAllowsExplicitCompatibilityOverridesForPKCEAndIDTokenValidation(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.OIDC.Enabled = true
- cfg.OIDC.ClientID = "oidc-client"
- cfg.OIDC.ClientSecret = "oidc-secret"
- cfg.OIDC.IssuerURL = "https://issuer.example.com"
- cfg.OIDC.AuthorizeURL = "https://issuer.example.com/auth"
- cfg.OIDC.TokenURL = "https://issuer.example.com/token"
- cfg.OIDC.UserInfoURL = "https://issuer.example.com/userinfo"
- cfg.OIDC.RedirectURL = "https://example.com/api/v1/auth/oauth/oidc/callback"
- cfg.OIDC.FrontendRedirectURL = "/auth/oidc/callback"
- cfg.OIDC.Scopes = "openid email profile"
- cfg.OIDC.UsePKCE = false
- cfg.OIDC.ValidateIDToken = false
- cfg.OIDC.JWKSURL = ""
- cfg.OIDC.AllowedSigningAlgs = ""
- err = cfg.Validate()
- if err != nil {
- t.Fatalf("Validate() expected OIDC config without PKCE/id_token validation to pass for compatibility, got: %v", err)
- }
- }
- func TestLoadDefaultDashboardCacheConfig(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if !cfg.Dashboard.Enabled {
- t.Fatalf("Dashboard.Enabled = false, want true")
- }
- if cfg.Dashboard.KeyPrefix != "sub2api:" {
- t.Fatalf("Dashboard.KeyPrefix = %q, want %q", cfg.Dashboard.KeyPrefix, "sub2api:")
- }
- if cfg.Dashboard.StatsFreshTTLSeconds != 15 {
- t.Fatalf("Dashboard.StatsFreshTTLSeconds = %d, want 15", cfg.Dashboard.StatsFreshTTLSeconds)
- }
- if cfg.Dashboard.StatsTTLSeconds != 30 {
- t.Fatalf("Dashboard.StatsTTLSeconds = %d, want 30", cfg.Dashboard.StatsTTLSeconds)
- }
- if cfg.Dashboard.StatsRefreshTimeoutSeconds != 30 {
- t.Fatalf("Dashboard.StatsRefreshTimeoutSeconds = %d, want 30", cfg.Dashboard.StatsRefreshTimeoutSeconds)
- }
- }
- func TestValidateDashboardCacheConfigEnabled(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.Dashboard.Enabled = true
- cfg.Dashboard.StatsFreshTTLSeconds = 10
- cfg.Dashboard.StatsTTLSeconds = 5
- err = cfg.Validate()
- if err == nil {
- t.Fatalf("Validate() expected error for stats_fresh_ttl_seconds > stats_ttl_seconds, got nil")
- }
- if !strings.Contains(err.Error(), "dashboard_cache.stats_fresh_ttl_seconds") {
- t.Fatalf("Validate() expected stats_fresh_ttl_seconds error, got: %v", err)
- }
- }
- func TestValidateDashboardCacheConfigDisabled(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.Dashboard.Enabled = false
- cfg.Dashboard.StatsTTLSeconds = -1
- err = cfg.Validate()
- if err == nil {
- t.Fatalf("Validate() expected error for negative stats_ttl_seconds, got nil")
- }
- if !strings.Contains(err.Error(), "dashboard_cache.stats_ttl_seconds") {
- t.Fatalf("Validate() expected stats_ttl_seconds error, got: %v", err)
- }
- }
- func TestLoadDefaultDashboardAggregationConfig(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if !cfg.DashboardAgg.Enabled {
- t.Fatalf("DashboardAgg.Enabled = false, want true")
- }
- if cfg.DashboardAgg.IntervalSeconds != 60 {
- t.Fatalf("DashboardAgg.IntervalSeconds = %d, want 60", cfg.DashboardAgg.IntervalSeconds)
- }
- if cfg.DashboardAgg.LookbackSeconds != 120 {
- t.Fatalf("DashboardAgg.LookbackSeconds = %d, want 120", cfg.DashboardAgg.LookbackSeconds)
- }
- if cfg.DashboardAgg.BackfillEnabled {
- t.Fatalf("DashboardAgg.BackfillEnabled = true, want false")
- }
- if cfg.DashboardAgg.BackfillMaxDays != 31 {
- t.Fatalf("DashboardAgg.BackfillMaxDays = %d, want 31", cfg.DashboardAgg.BackfillMaxDays)
- }
- if cfg.DashboardAgg.Retention.UsageLogsDays != 90 {
- t.Fatalf("DashboardAgg.Retention.UsageLogsDays = %d, want 90", cfg.DashboardAgg.Retention.UsageLogsDays)
- }
- if cfg.DashboardAgg.Retention.UsageBillingDedupDays != 365 {
- t.Fatalf("DashboardAgg.Retention.UsageBillingDedupDays = %d, want 365", cfg.DashboardAgg.Retention.UsageBillingDedupDays)
- }
- if cfg.DashboardAgg.Retention.HourlyDays != 180 {
- t.Fatalf("DashboardAgg.Retention.HourlyDays = %d, want 180", cfg.DashboardAgg.Retention.HourlyDays)
- }
- if cfg.DashboardAgg.Retention.DailyDays != 730 {
- t.Fatalf("DashboardAgg.Retention.DailyDays = %d, want 730", cfg.DashboardAgg.Retention.DailyDays)
- }
- if cfg.DashboardAgg.RecomputeDays != 2 {
- t.Fatalf("DashboardAgg.RecomputeDays = %d, want 2", cfg.DashboardAgg.RecomputeDays)
- }
- }
- func TestValidateDashboardAggregationConfigDisabled(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.DashboardAgg.Enabled = false
- cfg.DashboardAgg.IntervalSeconds = -1
- err = cfg.Validate()
- if err == nil {
- t.Fatalf("Validate() expected error for negative dashboard_aggregation.interval_seconds, got nil")
- }
- if !strings.Contains(err.Error(), "dashboard_aggregation.interval_seconds") {
- t.Fatalf("Validate() expected interval_seconds error, got: %v", err)
- }
- }
- func TestValidateDashboardAggregationBackfillMaxDays(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.DashboardAgg.BackfillEnabled = true
- cfg.DashboardAgg.BackfillMaxDays = 0
- err = cfg.Validate()
- if err == nil {
- t.Fatalf("Validate() expected error for dashboard_aggregation.backfill_max_days, got nil")
- }
- if !strings.Contains(err.Error(), "dashboard_aggregation.backfill_max_days") {
- t.Fatalf("Validate() expected backfill_max_days error, got: %v", err)
- }
- }
- func TestLoadDefaultUsageCleanupConfig(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if !cfg.UsageCleanup.Enabled {
- t.Fatalf("UsageCleanup.Enabled = false, want true")
- }
- if cfg.UsageCleanup.MaxRangeDays != 31 {
- t.Fatalf("UsageCleanup.MaxRangeDays = %d, want 31", cfg.UsageCleanup.MaxRangeDays)
- }
- if cfg.UsageCleanup.BatchSize != 5000 {
- t.Fatalf("UsageCleanup.BatchSize = %d, want 5000", cfg.UsageCleanup.BatchSize)
- }
- if cfg.UsageCleanup.WorkerIntervalSeconds != 10 {
- t.Fatalf("UsageCleanup.WorkerIntervalSeconds = %d, want 10", cfg.UsageCleanup.WorkerIntervalSeconds)
- }
- if cfg.UsageCleanup.TaskTimeoutSeconds != 1800 {
- t.Fatalf("UsageCleanup.TaskTimeoutSeconds = %d, want 1800", cfg.UsageCleanup.TaskTimeoutSeconds)
- }
- }
- func TestLoadDefaultOpsCleanupConfig(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if !cfg.Ops.Cleanup.Enabled {
- t.Fatal("Ops.Cleanup.Enabled = false, want true")
- }
- if cfg.Ops.Cleanup.SystemLogRetentionDays != 30 {
- t.Fatalf("Ops.Cleanup.SystemLogRetentionDays = %d, want 30", cfg.Ops.Cleanup.SystemLogRetentionDays)
- }
- }
- func TestValidateUsageCleanupConfigEnabled(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.UsageCleanup.Enabled = true
- cfg.UsageCleanup.MaxRangeDays = 0
- err = cfg.Validate()
- if err == nil {
- t.Fatalf("Validate() expected error for usage_cleanup.max_range_days, got nil")
- }
- if !strings.Contains(err.Error(), "usage_cleanup.max_range_days") {
- t.Fatalf("Validate() expected max_range_days error, got: %v", err)
- }
- }
- func TestValidateUsageCleanupConfigDisabled(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.UsageCleanup.Enabled = false
- cfg.UsageCleanup.BatchSize = -1
- err = cfg.Validate()
- if err == nil {
- t.Fatalf("Validate() expected error for usage_cleanup.batch_size, got nil")
- }
- if !strings.Contains(err.Error(), "usage_cleanup.batch_size") {
- t.Fatalf("Validate() expected batch_size error, got: %v", err)
- }
- }
- func TestConfigAddressHelpers(t *testing.T) {
- server := ServerConfig{Host: "127.0.0.1", Port: 9000}
- if server.Address() != "127.0.0.1:9000" {
- t.Fatalf("ServerConfig.Address() = %q", server.Address())
- }
- dbCfg := DatabaseConfig{
- Host: "localhost",
- Port: 5432,
- User: "postgres",
- Password: "",
- DBName: "sub2api",
- SSLMode: "disable",
- }
- if !strings.Contains(dbCfg.DSN(), "password=") {
- } else {
- t.Fatalf("DatabaseConfig.DSN() should not include password when empty")
- }
- dbCfg.Password = "secret"
- if !strings.Contains(dbCfg.DSN(), "password=secret") {
- t.Fatalf("DatabaseConfig.DSN() missing password")
- }
- dbCfg.Password = ""
- if strings.Contains(dbCfg.DSNWithTimezone("UTC"), "password=") {
- t.Fatalf("DatabaseConfig.DSNWithTimezone() should omit password when empty")
- }
- if !strings.Contains(dbCfg.DSNWithTimezone(""), "TimeZone=Asia/Shanghai") {
- t.Fatalf("DatabaseConfig.DSNWithTimezone() should use default timezone")
- }
- if !strings.Contains(dbCfg.DSNWithTimezone("UTC"), "TimeZone=UTC") {
- t.Fatalf("DatabaseConfig.DSNWithTimezone() should use provided timezone")
- }
- redis := RedisConfig{Host: "redis", Port: 6379}
- if redis.Address() != "redis:6379" {
- t.Fatalf("RedisConfig.Address() = %q", redis.Address())
- }
- }
- func TestNormalizeStringSlice(t *testing.T) {
- values := normalizeStringSlice([]string{" a ", "", "b", " ", "c"})
- if len(values) != 3 || values[0] != "a" || values[1] != "b" || values[2] != "c" {
- t.Fatalf("normalizeStringSlice() unexpected result: %#v", values)
- }
- if normalizeStringSlice(nil) != nil {
- t.Fatalf("normalizeStringSlice(nil) expected nil slice")
- }
- }
- func TestGetServerAddressFromEnv(t *testing.T) {
- t.Setenv("CONFIG_FILE", "")
- t.Setenv("DATA_DIR", "")
- t.Setenv("SERVER_HOST", "127.0.0.1")
- t.Setenv("SERVER_PORT", "9090")
- address := GetServerAddress()
- if address != "127.0.0.1:9090" {
- t.Fatalf("GetServerAddress() = %q", address)
- }
- }
- func TestGetServerAddressFromConfigFile(t *testing.T) {
- configFile := filepath.Join(t.TempDir(), "setup.yaml")
- require.NoError(t, os.WriteFile(configFile, []byte("server:\n host: 192.0.2.40\n port: 9191\n"), 0o600))
- t.Setenv("CONFIG_FILE", configFile)
- t.Setenv("DATA_DIR", "")
- t.Setenv("SERVER_HOST", "")
- t.Setenv("SERVER_PORT", "")
- require.Equal(t, "192.0.2.40:9191", GetServerAddress())
- }
- func TestValidateAbsoluteHTTPURL(t *testing.T) {
- if err := ValidateAbsoluteHTTPURL("https://example.com/path"); err != nil {
- t.Fatalf("ValidateAbsoluteHTTPURL valid url error: %v", err)
- }
- if err := ValidateAbsoluteHTTPURL(""); err == nil {
- t.Fatalf("ValidateAbsoluteHTTPURL should reject empty url")
- }
- if err := ValidateAbsoluteHTTPURL("/relative"); err == nil {
- t.Fatalf("ValidateAbsoluteHTTPURL should reject relative url")
- }
- if err := ValidateAbsoluteHTTPURL("ftp://example.com"); err == nil {
- t.Fatalf("ValidateAbsoluteHTTPURL should reject ftp scheme")
- }
- if err := ValidateAbsoluteHTTPURL("https://example.com/#frag"); err == nil {
- t.Fatalf("ValidateAbsoluteHTTPURL should reject fragment")
- }
- }
- func TestValidateServerFrontendURL(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.Server.FrontendURL = "https://example.com"
- if err := cfg.Validate(); err != nil {
- t.Fatalf("Validate() frontend_url valid error: %v", err)
- }
- cfg.Server.FrontendURL = "https://example.com/path"
- if err := cfg.Validate(); err != nil {
- t.Fatalf("Validate() frontend_url with path valid error: %v", err)
- }
- cfg.Server.FrontendURL = "https://example.com?utm=1"
- if err := cfg.Validate(); err == nil {
- t.Fatalf("Validate() should reject server.frontend_url with query")
- }
- cfg.Server.FrontendURL = "https://user:pass@example.com"
- if err := cfg.Validate(); err == nil {
- t.Fatalf("Validate() should reject server.frontend_url with userinfo")
- }
- cfg.Server.FrontendURL = "/relative"
- if err := cfg.Validate(); err == nil {
- t.Fatalf("Validate() should reject relative server.frontend_url")
- }
- }
- func TestValidateFrontendRedirectURL(t *testing.T) {
- if err := ValidateFrontendRedirectURL("/auth/callback"); err != nil {
- t.Fatalf("ValidateFrontendRedirectURL relative error: %v", err)
- }
- if err := ValidateFrontendRedirectURL("https://example.com/auth"); err != nil {
- t.Fatalf("ValidateFrontendRedirectURL absolute error: %v", err)
- }
- if err := ValidateFrontendRedirectURL("example.com/path"); err == nil {
- t.Fatalf("ValidateFrontendRedirectURL should reject non-absolute url")
- }
- if err := ValidateFrontendRedirectURL("//evil.com"); err == nil {
- t.Fatalf("ValidateFrontendRedirectURL should reject // prefix")
- }
- if err := ValidateFrontendRedirectURL("javascript:alert(1)"); err == nil {
- t.Fatalf("ValidateFrontendRedirectURL should reject javascript scheme")
- }
- }
- func TestWarnIfInsecureURL(t *testing.T) {
- warnIfInsecureURL("test", "http://example.com")
- warnIfInsecureURL("test", "bad://url")
- warnIfInsecureURL("test", "://invalid")
- }
- func TestGenerateJWTSecretDefaultLength(t *testing.T) {
- secret, err := generateJWTSecret(0)
- if err != nil {
- t.Fatalf("generateJWTSecret error: %v", err)
- }
- if len(secret) == 0 {
- t.Fatalf("generateJWTSecret returned empty string")
- }
- }
- func TestValidateOpsCleanupScheduleRequired(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.Ops.Cleanup.Enabled = true
- cfg.Ops.Cleanup.Schedule = ""
- err = cfg.Validate()
- if err == nil {
- t.Fatalf("Validate() expected error for ops.cleanup.schedule")
- }
- if !strings.Contains(err.Error(), "ops.cleanup.schedule") {
- t.Fatalf("Validate() expected ops.cleanup.schedule error, got: %v", err)
- }
- }
- func TestValidateConcurrencyPingInterval(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.Concurrency.PingInterval = 3
- err = cfg.Validate()
- if err == nil {
- t.Fatalf("Validate() expected error for concurrency.ping_interval")
- }
- if !strings.Contains(err.Error(), "concurrency.ping_interval") {
- t.Fatalf("Validate() expected concurrency.ping_interval error, got: %v", err)
- }
- }
- func TestProvideConfig(t *testing.T) {
- resetViperWithJWTSecret(t)
- if _, err := ProvideConfig(); err != nil {
- t.Fatalf("ProvideConfig() error: %v", err)
- }
- }
- func TestValidateConfigWithLinuxDoEnabled(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.Security.CSP.Enabled = true
- cfg.Security.CSP.Policy = "default-src 'self'"
- cfg.LinuxDo.Enabled = true
- cfg.LinuxDo.ClientID = "client"
- cfg.LinuxDo.ClientSecret = "secret"
- cfg.LinuxDo.AuthorizeURL = "https://example.com/oauth2/authorize"
- cfg.LinuxDo.TokenURL = "https://example.com/oauth2/token"
- cfg.LinuxDo.UserInfoURL = "https://example.com/oauth2/userinfo"
- cfg.LinuxDo.RedirectURL = "https://example.com/api/v1/auth/oauth/linuxdo/callback"
- cfg.LinuxDo.FrontendRedirectURL = "/auth/linuxdo/callback"
- cfg.LinuxDo.TokenAuthMethod = "client_secret_post"
- cfg.LinuxDo.UsePKCE = true
- if err := cfg.Validate(); err != nil {
- t.Fatalf("Validate() unexpected error: %v", err)
- }
- }
- func TestValidateJWTSecretStrength(t *testing.T) {
- if !isWeakJWTSecret("change-me-in-production") {
- t.Fatalf("isWeakJWTSecret should detect weak secret")
- }
- if isWeakJWTSecret("StrongSecretValue") {
- t.Fatalf("isWeakJWTSecret should accept strong secret")
- }
- }
- func TestGenerateJWTSecretWithLength(t *testing.T) {
- secret, err := generateJWTSecret(16)
- if err != nil {
- t.Fatalf("generateJWTSecret error: %v", err)
- }
- if len(secret) == 0 {
- t.Fatalf("generateJWTSecret returned empty string")
- }
- }
- func TestDatabaseDSNWithTimezone_WithPassword(t *testing.T) {
- d := &DatabaseConfig{
- Host: "localhost",
- Port: 5432,
- User: "u",
- Password: "p",
- DBName: "db",
- SSLMode: "prefer",
- }
- got := d.DSNWithTimezone("UTC")
- if !strings.Contains(got, "password=p") {
- t.Fatalf("DSNWithTimezone should include password: %q", got)
- }
- if !strings.Contains(got, "TimeZone=UTC") {
- t.Fatalf("DSNWithTimezone should include TimeZone=UTC: %q", got)
- }
- }
- func TestValidateAbsoluteHTTPURLMissingHost(t *testing.T) {
- if err := ValidateAbsoluteHTTPURL("https://"); err == nil {
- t.Fatalf("ValidateAbsoluteHTTPURL should reject missing host")
- }
- }
- func TestValidateFrontendRedirectURLInvalidChars(t *testing.T) {
- if err := ValidateFrontendRedirectURL("/auth/\ncallback"); err == nil {
- t.Fatalf("ValidateFrontendRedirectURL should reject invalid chars")
- }
- if err := ValidateFrontendRedirectURL("http://"); err == nil {
- t.Fatalf("ValidateFrontendRedirectURL should reject missing host")
- }
- if err := ValidateFrontendRedirectURL("mailto:user@example.com"); err == nil {
- t.Fatalf("ValidateFrontendRedirectURL should reject mailto")
- }
- }
- func TestWarnIfInsecureURLHTTPS(t *testing.T) {
- warnIfInsecureURL("secure", "https://example.com")
- }
- func TestValidateJWTSecret_UTF8Bytes(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- // 31 bytes (< 32) even though it's 31 characters.
- cfg.JWT.Secret = strings.Repeat("a", 31)
- err = cfg.Validate()
- if err == nil {
- t.Fatalf("Validate() should reject 31-byte secret")
- }
- if !strings.Contains(err.Error(), "at least 32 bytes") {
- t.Fatalf("Validate() error = %v", err)
- }
- // 32 bytes OK.
- cfg.JWT.Secret = strings.Repeat("a", 32)
- err = cfg.Validate()
- if err != nil {
- t.Fatalf("Validate() should accept 32-byte secret: %v", err)
- }
- }
- func TestValidateConfigErrors(t *testing.T) {
- buildValid := func(t *testing.T) *Config {
- t.Helper()
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- return cfg
- }
- cases := []struct {
- name string
- mutate func(*Config)
- wantErr string
- }{
- {
- name: "server read header timeout",
- mutate: func(c *Config) { c.Server.ReadHeaderTimeout = 0 },
- wantErr: "server.read_header_timeout",
- },
- {
- name: "server max header bytes too small",
- mutate: func(c *Config) { c.Server.MaxHeaderBytes = 4096 },
- wantErr: "server.max_header_bytes",
- },
- {
- name: "server max request body size",
- mutate: func(c *Config) { c.Server.MaxRequestBodySize = -1 },
- wantErr: "server.max_request_body_size",
- },
- {
- name: "h2c zero concurrent streams",
- mutate: func(c *Config) {
- c.Server.H2C.Enabled = true
- c.Server.H2C.MaxConcurrentStreams = 0
- },
- wantErr: "server.h2c.max_concurrent_streams",
- },
- {
- name: "h2c oversized read frame",
- mutate: func(c *Config) {
- c.Server.H2C.Enabled = true
- c.Server.H2C.MaxReadFrameSize = 16 * 1024 * 1024
- },
- wantErr: "server.h2c.max_read_frame_size",
- },
- {
- name: "invalid auth abuse threshold too small",
- mutate: func(c *Config) {
- c.APIKeyAuth.InvalidAbuse.Threshold = 9
- },
- wantErr: "api_key_auth_cache.invalid_abuse.threshold",
- },
- {
- name: "invalid auth abuse capacity too small",
- mutate: func(c *Config) {
- c.APIKeyAuth.InvalidAbuse.Capacity = 255
- },
- wantErr: "api_key_auth_cache.invalid_abuse.capacity",
- },
- {
- name: "jwt secret required",
- mutate: func(c *Config) { c.JWT.Secret = "" },
- wantErr: "jwt.secret is required",
- },
- {
- name: "jwt secret min bytes",
- mutate: func(c *Config) { c.JWT.Secret = strings.Repeat("a", 31) },
- wantErr: "jwt.secret must be at least 32 bytes",
- },
- {
- name: "subscription maintenance worker_count non-negative",
- mutate: func(c *Config) { c.SubscriptionMaintenance.WorkerCount = -1 },
- wantErr: "subscription_maintenance.worker_count",
- },
- {
- name: "subscription maintenance queue_size non-negative",
- mutate: func(c *Config) { c.SubscriptionMaintenance.QueueSize = -1 },
- wantErr: "subscription_maintenance.queue_size",
- },
- {
- name: "jwt expire hour positive",
- mutate: func(c *Config) { c.JWT.ExpireHour = 0 },
- wantErr: "jwt.expire_hour must be positive",
- },
- {
- name: "jwt expire hour max",
- mutate: func(c *Config) { c.JWT.ExpireHour = 200 },
- wantErr: "jwt.expire_hour must be <= 168",
- },
- {
- name: "jwt access token expire minutes non-negative",
- mutate: func(c *Config) { c.JWT.AccessTokenExpireMinutes = -1 },
- wantErr: "jwt.access_token_expire_minutes must be non-negative",
- },
- {
- name: "csp policy required",
- mutate: func(c *Config) { c.Security.CSP.Enabled = true; c.Security.CSP.Policy = "" },
- wantErr: "security.csp.policy",
- },
- {
- name: "linuxdo client id required",
- mutate: func(c *Config) {
- c.LinuxDo.Enabled = true
- c.LinuxDo.UsePKCE = true
- c.LinuxDo.ClientID = ""
- },
- wantErr: "linuxdo_connect.client_id",
- },
- {
- name: "linuxdo token auth method",
- mutate: func(c *Config) {
- c.LinuxDo.Enabled = true
- c.LinuxDo.UsePKCE = true
- c.LinuxDo.ClientID = "client"
- c.LinuxDo.ClientSecret = "secret"
- c.LinuxDo.AuthorizeURL = "https://example.com/authorize"
- c.LinuxDo.TokenURL = "https://example.com/token"
- c.LinuxDo.UserInfoURL = "https://example.com/userinfo"
- c.LinuxDo.RedirectURL = "https://example.com/callback"
- c.LinuxDo.FrontendRedirectURL = "/auth/callback"
- c.LinuxDo.TokenAuthMethod = "invalid"
- },
- wantErr: "linuxdo_connect.token_auth_method",
- },
- {
- name: "billing circuit breaker threshold",
- mutate: func(c *Config) { c.Billing.CircuitBreaker.FailureThreshold = 0 },
- wantErr: "billing.circuit_breaker.failure_threshold",
- },
- {
- name: "billing circuit breaker reset",
- mutate: func(c *Config) { c.Billing.CircuitBreaker.ResetTimeoutSeconds = 0 },
- wantErr: "billing.circuit_breaker.reset_timeout_seconds",
- },
- {
- name: "billing circuit breaker half open",
- mutate: func(c *Config) { c.Billing.CircuitBreaker.HalfOpenRequests = 0 },
- wantErr: "billing.circuit_breaker.half_open_requests",
- },
- {
- name: "billing minimum balance reserve",
- mutate: func(c *Config) { c.Billing.MinimumBalanceReserve = -0.01 },
- wantErr: "billing.minimum_balance_reserve",
- },
- {
- name: "database max open conns",
- mutate: func(c *Config) { c.Database.MaxOpenConns = 0 },
- wantErr: "database.max_open_conns",
- },
- {
- name: "database max lifetime",
- mutate: func(c *Config) { c.Database.ConnMaxLifetimeMinutes = -1 },
- wantErr: "database.conn_max_lifetime_minutes",
- },
- {
- name: "database idle exceeds open",
- mutate: func(c *Config) { c.Database.MaxIdleConns = c.Database.MaxOpenConns + 1 },
- wantErr: "database.max_idle_conns cannot exceed",
- },
- {
- name: "redis dial timeout",
- mutate: func(c *Config) { c.Redis.DialTimeoutSeconds = 0 },
- wantErr: "redis.dial_timeout_seconds",
- },
- {
- name: "redis read timeout",
- mutate: func(c *Config) { c.Redis.ReadTimeoutSeconds = 0 },
- wantErr: "redis.read_timeout_seconds",
- },
- {
- name: "redis write timeout",
- mutate: func(c *Config) { c.Redis.WriteTimeoutSeconds = 0 },
- wantErr: "redis.write_timeout_seconds",
- },
- {
- name: "redis pool size",
- mutate: func(c *Config) { c.Redis.PoolSize = 0 },
- wantErr: "redis.pool_size",
- },
- {
- name: "redis idle exceeds pool",
- mutate: func(c *Config) { c.Redis.MinIdleConns = c.Redis.PoolSize + 1 },
- wantErr: "redis.min_idle_conns cannot exceed",
- },
- {
- name: "dashboard cache disabled negative",
- mutate: func(c *Config) { c.Dashboard.Enabled = false; c.Dashboard.StatsTTLSeconds = -1 },
- wantErr: "dashboard_cache.stats_ttl_seconds",
- },
- {
- name: "dashboard cache fresh ttl positive",
- mutate: func(c *Config) { c.Dashboard.Enabled = true; c.Dashboard.StatsFreshTTLSeconds = 0 },
- wantErr: "dashboard_cache.stats_fresh_ttl_seconds",
- },
- {
- name: "dashboard aggregation enabled interval",
- mutate: func(c *Config) { c.DashboardAgg.Enabled = true; c.DashboardAgg.IntervalSeconds = 0 },
- wantErr: "dashboard_aggregation.interval_seconds",
- },
- {
- name: "dashboard aggregation backfill positive",
- mutate: func(c *Config) {
- c.DashboardAgg.Enabled = true
- c.DashboardAgg.BackfillEnabled = true
- c.DashboardAgg.BackfillMaxDays = 0
- },
- wantErr: "dashboard_aggregation.backfill_max_days",
- },
- {
- name: "dashboard aggregation retention",
- mutate: func(c *Config) { c.DashboardAgg.Enabled = true; c.DashboardAgg.Retention.UsageLogsDays = 0 },
- wantErr: "dashboard_aggregation.retention.usage_logs_days",
- },
- {
- name: "dashboard aggregation dedup retention",
- mutate: func(c *Config) {
- c.DashboardAgg.Enabled = true
- c.DashboardAgg.Retention.UsageBillingDedupDays = 0
- },
- wantErr: "dashboard_aggregation.retention.usage_billing_dedup_days",
- },
- {
- name: "dashboard aggregation dedup retention smaller than usage logs",
- mutate: func(c *Config) {
- c.DashboardAgg.Enabled = true
- c.DashboardAgg.Retention.UsageLogsDays = 30
- c.DashboardAgg.Retention.UsageBillingDedupDays = 29
- },
- wantErr: "dashboard_aggregation.retention.usage_billing_dedup_days",
- },
- {
- name: "dashboard aggregation disabled interval",
- mutate: func(c *Config) { c.DashboardAgg.Enabled = false; c.DashboardAgg.IntervalSeconds = -1 },
- wantErr: "dashboard_aggregation.interval_seconds",
- },
- {
- name: "usage cleanup max range",
- mutate: func(c *Config) { c.UsageCleanup.Enabled = true; c.UsageCleanup.MaxRangeDays = 0 },
- wantErr: "usage_cleanup.max_range_days",
- },
- {
- name: "usage cleanup worker interval",
- mutate: func(c *Config) { c.UsageCleanup.Enabled = true; c.UsageCleanup.WorkerIntervalSeconds = 0 },
- wantErr: "usage_cleanup.worker_interval_seconds",
- },
- {
- name: "usage cleanup batch size",
- mutate: func(c *Config) { c.UsageCleanup.Enabled = true; c.UsageCleanup.BatchSize = 0 },
- wantErr: "usage_cleanup.batch_size",
- },
- {
- name: "usage cleanup disabled negative",
- mutate: func(c *Config) { c.UsageCleanup.Enabled = false; c.UsageCleanup.BatchSize = -1 },
- wantErr: "usage_cleanup.batch_size",
- },
- {
- name: "gateway max body size",
- mutate: func(c *Config) { c.Gateway.MaxBodySize = 0 },
- wantErr: "gateway.max_body_size",
- },
- {
- name: "gateway text body exceeds media body",
- mutate: func(c *Config) { c.Gateway.TextMaxBodySize = c.Gateway.MaxBodySize + 1 },
- wantErr: "gateway.text_max_body_size",
- },
- {
- name: "gateway models list read limit",
- mutate: func(c *Config) { c.Gateway.ModelsListReadMaxBytes = 0 },
- wantErr: "gateway.models_list_read_max_bytes",
- },
- {
- name: "gateway response header timeout",
- mutate: func(c *Config) { c.Gateway.ResponseHeaderTimeout = -1 },
- wantErr: "gateway.response_header_timeout",
- },
- {
- name: "gateway openai response header timeout",
- mutate: func(c *Config) { c.Gateway.OpenAIResponseHeaderTimeout = -1 },
- wantErr: "gateway.openai_response_header_timeout",
- },
- {
- name: "gateway openai first output timeout below minimum",
- mutate: func(c *Config) { c.Gateway.OpenAIFirstOutputTimeoutSeconds = 29 },
- wantErr: "gateway.openai_first_output_timeout_seconds",
- },
- {
- name: "gateway openai high effort first output timeout too large",
- mutate: func(c *Config) { c.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds = 1801 },
- wantErr: "gateway.openai_high_effort_first_output_timeout_seconds",
- },
- {
- name: "gateway max idle conns",
- mutate: func(c *Config) { c.Gateway.MaxIdleConns = 0 },
- wantErr: "gateway.max_idle_conns",
- },
- {
- name: "gateway max idle conns per host",
- mutate: func(c *Config) { c.Gateway.MaxIdleConnsPerHost = 0 },
- wantErr: "gateway.max_idle_conns_per_host",
- },
- {
- name: "gateway idle timeout",
- mutate: func(c *Config) { c.Gateway.IdleConnTimeoutSeconds = 0 },
- wantErr: "gateway.idle_conn_timeout_seconds",
- },
- {
- name: "gateway max upstream clients",
- mutate: func(c *Config) { c.Gateway.MaxUpstreamClients = 0 },
- wantErr: "gateway.max_upstream_clients",
- },
- {
- name: "gateway client idle ttl",
- mutate: func(c *Config) { c.Gateway.ClientIdleTTLSeconds = 0 },
- wantErr: "gateway.client_idle_ttl_seconds",
- },
- {
- name: "gateway concurrency slot ttl",
- mutate: func(c *Config) { c.Gateway.ConcurrencySlotTTLMinutes = 0 },
- wantErr: "gateway.concurrency_slot_ttl_minutes",
- },
- {
- name: "gateway max conns per host",
- mutate: func(c *Config) { c.Gateway.MaxConnsPerHost = -1 },
- wantErr: "gateway.max_conns_per_host",
- },
- {
- name: "gateway connection isolation",
- mutate: func(c *Config) { c.Gateway.ConnectionPoolIsolation = "invalid" },
- wantErr: "gateway.connection_pool_isolation",
- },
- {
- name: "gateway stream keepalive range",
- mutate: func(c *Config) { c.Gateway.StreamKeepaliveInterval = 4 },
- wantErr: "gateway.stream_keepalive_interval",
- },
- {
- name: "gateway openai ws oauth max conns factor",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.OAuthMaxConnsFactor = 0 },
- wantErr: "gateway.openai_ws.oauth_max_conns_factor",
- },
- {
- name: "gateway openai ws apikey max conns factor",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.APIKeyMaxConnsFactor = 0 },
- wantErr: "gateway.openai_ws.apikey_max_conns_factor",
- },
- {
- name: "gateway openai http2 fallback threshold",
- mutate: func(c *Config) { c.Gateway.OpenAIHTTP2.FallbackErrorThreshold = -1 },
- wantErr: "gateway.openai_http2.fallback_error_threshold",
- },
- {
- name: "gateway openai http2 fallback window",
- mutate: func(c *Config) { c.Gateway.OpenAIHTTP2.FallbackWindowSeconds = -1 },
- wantErr: "gateway.openai_http2.fallback_window_seconds",
- },
- {
- name: "gateway openai http2 fallback ttl",
- mutate: func(c *Config) { c.Gateway.OpenAIHTTP2.FallbackTTLSeconds = -1 },
- wantErr: "gateway.openai_http2.fallback_ttl_seconds",
- },
- {
- name: "gateway openai proxy stream circuit threshold",
- mutate: func(c *Config) { c.Gateway.OpenAIProxyStreamCircuit.FailureThreshold = -1 },
- wantErr: "gateway.openai_proxy_stream_circuit.failure_threshold",
- },
- {
- name: "gateway openai proxy stream circuit window",
- mutate: func(c *Config) { c.Gateway.OpenAIProxyStreamCircuit.WindowSeconds = -1 },
- wantErr: "gateway.openai_proxy_stream_circuit.window_seconds",
- },
- {
- name: "gateway openai proxy stream circuit ttl",
- mutate: func(c *Config) { c.Gateway.OpenAIProxyStreamCircuit.TTLSeconds = -1 },
- wantErr: "gateway.openai_proxy_stream_circuit.ttl_seconds",
- },
- {
- name: "gateway stream data interval range",
- mutate: func(c *Config) { c.Gateway.StreamDataIntervalTimeout = 5 },
- wantErr: "gateway.stream_data_interval_timeout",
- },
- {
- name: "gateway stream data interval negative",
- mutate: func(c *Config) { c.Gateway.StreamDataIntervalTimeout = -1 },
- wantErr: "gateway.stream_data_interval_timeout must be non-negative",
- },
- {
- name: "gateway image stream keepalive range",
- mutate: func(c *Config) { c.Gateway.ImageStreamKeepaliveInterval = 4 },
- wantErr: "gateway.image_stream_keepalive_interval",
- },
- {
- name: "gateway image stream keepalive negative",
- mutate: func(c *Config) { c.Gateway.ImageStreamKeepaliveInterval = -1 },
- wantErr: "gateway.image_stream_keepalive_interval must be non-negative",
- },
- {
- name: "gateway image nonstream keepalive range",
- mutate: func(c *Config) { c.Gateway.ImageNonstreamKeepaliveInterval = 4 },
- wantErr: "gateway.image_nonstream_keepalive_interval",
- },
- {
- name: "gateway image nonstream keepalive negative",
- mutate: func(c *Config) { c.Gateway.ImageNonstreamKeepaliveInterval = -1 },
- wantErr: "gateway.image_nonstream_keepalive_interval must be non-negative",
- },
- {
- name: "gateway image stream data interval range",
- mutate: func(c *Config) { c.Gateway.ImageStreamDataIntervalTimeout = 30 },
- wantErr: "gateway.image_stream_data_interval_timeout",
- },
- {
- name: "gateway image stream data interval negative",
- mutate: func(c *Config) { c.Gateway.ImageStreamDataIntervalTimeout = -1 },
- wantErr: "gateway.image_stream_data_interval_timeout must be non-negative",
- },
- {
- name: "gateway image concurrency max negative",
- mutate: func(c *Config) { c.Gateway.ImageConcurrency.MaxConcurrentRequests = -1 },
- wantErr: "gateway.image_concurrency.max_concurrent_requests must be non-negative",
- },
- {
- name: "gateway image concurrency overflow mode invalid",
- mutate: func(c *Config) { c.Gateway.ImageConcurrency.OverflowMode = "queue" },
- wantErr: "gateway.image_concurrency.overflow_mode",
- },
- {
- name: "gateway image concurrency wait timeout negative",
- mutate: func(c *Config) { c.Gateway.ImageConcurrency.WaitTimeoutSeconds = -1 },
- wantErr: "gateway.image_concurrency.wait_timeout_seconds must be non-negative",
- },
- {
- name: "gateway image concurrency max waiting negative",
- mutate: func(c *Config) { c.Gateway.ImageConcurrency.MaxWaitingRequests = -1 },
- wantErr: "gateway.image_concurrency.max_waiting_requests must be non-negative",
- },
- {
- name: "gateway max line size",
- mutate: func(c *Config) { c.Gateway.MaxLineSize = 1024 },
- wantErr: "gateway.max_line_size must be at least",
- },
- {
- name: "gateway max line size negative",
- mutate: func(c *Config) { c.Gateway.MaxLineSize = -1 },
- wantErr: "gateway.max_line_size must be non-negative",
- },
- {
- name: "gateway usage record worker count",
- mutate: func(c *Config) { c.Gateway.UsageRecord.WorkerCount = 0 },
- wantErr: "gateway.usage_record.worker_count",
- },
- {
- name: "gateway usage record queue size",
- mutate: func(c *Config) { c.Gateway.UsageRecord.QueueSize = 0 },
- wantErr: "gateway.usage_record.queue_size",
- },
- {
- name: "gateway usage record timeout",
- mutate: func(c *Config) { c.Gateway.UsageRecord.TaskTimeoutSeconds = 0 },
- wantErr: "gateway.usage_record.task_timeout_seconds",
- },
- {
- name: "gateway usage record overflow policy",
- mutate: func(c *Config) { c.Gateway.UsageRecord.OverflowPolicy = "invalid" },
- wantErr: "gateway.usage_record.overflow_policy",
- },
- {
- name: "gateway usage record sample percent range",
- mutate: func(c *Config) { c.Gateway.UsageRecord.OverflowSamplePercent = 101 },
- wantErr: "gateway.usage_record.overflow_sample_percent",
- },
- {
- name: "gateway usage record sample percent required for sample policy",
- mutate: func(c *Config) {
- c.Gateway.UsageRecord.OverflowPolicy = UsageRecordOverflowPolicySample
- c.Gateway.UsageRecord.OverflowSamplePercent = 0
- },
- wantErr: "gateway.usage_record.overflow_sample_percent must be positive",
- },
- {
- name: "gateway usage record auto scale max gte min",
- mutate: func(c *Config) {
- c.Gateway.UsageRecord.AutoScaleMinWorkers = 256
- c.Gateway.UsageRecord.AutoScaleMaxWorkers = 128
- },
- wantErr: "gateway.usage_record.auto_scale_max_workers",
- },
- {
- name: "gateway usage record worker in auto scale range",
- mutate: func(c *Config) {
- c.Gateway.UsageRecord.AutoScaleMinWorkers = 200
- c.Gateway.UsageRecord.AutoScaleMaxWorkers = 300
- c.Gateway.UsageRecord.WorkerCount = 128
- },
- wantErr: "gateway.usage_record.worker_count must be between auto_scale_min_workers and auto_scale_max_workers",
- },
- {
- name: "gateway usage record auto scale queue thresholds order",
- mutate: func(c *Config) {
- c.Gateway.UsageRecord.AutoScaleUpQueuePercent = 50
- c.Gateway.UsageRecord.AutoScaleDownQueuePercent = 50
- },
- wantErr: "gateway.usage_record.auto_scale_down_queue_percent must be less",
- },
- {
- name: "gateway usage record auto scale up step",
- mutate: func(c *Config) { c.Gateway.UsageRecord.AutoScaleUpStep = 0 },
- wantErr: "gateway.usage_record.auto_scale_up_step",
- },
- {
- name: "gateway usage record auto scale interval",
- mutate: func(c *Config) { c.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds = 0 },
- wantErr: "gateway.usage_record.auto_scale_check_interval_seconds",
- },
- {
- name: "gateway user group rate cache ttl",
- mutate: func(c *Config) { c.Gateway.UserGroupRateCacheTTLSeconds = 0 },
- wantErr: "gateway.user_group_rate_cache_ttl_seconds",
- },
- {
- name: "gateway models list cache ttl range",
- mutate: func(c *Config) { c.Gateway.ModelsListCacheTTLSeconds = 31 },
- wantErr: "gateway.models_list_cache_ttl_seconds",
- },
- {
- name: "gateway scheduling sticky waiting",
- mutate: func(c *Config) { c.Gateway.Scheduling.StickySessionMaxWaiting = 0 },
- wantErr: "gateway.scheduling.sticky_session_max_waiting",
- },
- {
- name: "gateway scheduling load batch cache ttl",
- mutate: func(c *Config) { c.Gateway.Scheduling.LoadBatchCacheTTLMS = -1 },
- wantErr: "gateway.scheduling.load_batch_cache_ttl_ms",
- },
- {
- name: "gateway scheduling outbox poll",
- mutate: func(c *Config) { c.Gateway.Scheduling.OutboxPollIntervalSeconds = 0 },
- wantErr: "gateway.scheduling.outbox_poll_interval_seconds",
- },
- {
- name: "gateway scheduling outbox failures",
- mutate: func(c *Config) { c.Gateway.Scheduling.OutboxLagRebuildFailures = 0 },
- wantErr: "gateway.scheduling.outbox_lag_rebuild_failures",
- },
- {
- name: "gateway outbox lag rebuild",
- mutate: func(c *Config) {
- c.Gateway.Scheduling.OutboxLagWarnSeconds = 10
- c.Gateway.Scheduling.OutboxLagRebuildSeconds = 5
- },
- wantErr: "gateway.scheduling.outbox_lag_rebuild_seconds",
- },
- {
- name: "log level invalid",
- mutate: func(c *Config) { c.Log.Level = "trace" },
- wantErr: "log.level",
- },
- {
- name: "log format invalid",
- mutate: func(c *Config) { c.Log.Format = "plain" },
- wantErr: "log.format",
- },
- {
- name: "log output disabled",
- mutate: func(c *Config) {
- c.Log.Output.ToStdout = false
- c.Log.Output.ToFile = false
- },
- wantErr: "log.output.to_stdout and log.output.to_file cannot both be false",
- },
- {
- name: "log rotation size",
- mutate: func(c *Config) { c.Log.Rotation.MaxSizeMB = 0 },
- wantErr: "log.rotation.max_size_mb",
- },
- {
- name: "log sampling enabled invalid",
- mutate: func(c *Config) {
- c.Log.Sampling.Enabled = true
- c.Log.Sampling.Initial = 0
- },
- wantErr: "log.sampling.initial",
- },
- {
- name: "ops metrics collector ttl",
- mutate: func(c *Config) { c.Ops.MetricsCollectorCache.TTL = -1 },
- wantErr: "ops.metrics_collector_cache.ttl",
- },
- {
- name: "ops cleanup retention",
- mutate: func(c *Config) { c.Ops.Cleanup.ErrorLogRetentionDays = -1 },
- wantErr: "ops.cleanup.error_log_retention_days",
- },
- {
- name: "ops cleanup system log retention",
- mutate: func(c *Config) { c.Ops.Cleanup.SystemLogRetentionDays = 0 },
- wantErr: "ops.cleanup.system_log_retention_days",
- },
- {
- name: "ops cleanup minute retention",
- mutate: func(c *Config) { c.Ops.Cleanup.MinuteMetricsRetentionDays = -1 },
- wantErr: "ops.cleanup.minute_metrics_retention_days",
- },
- }
- for _, tt := range cases {
- t.Run(tt.name, func(t *testing.T) {
- cfg := buildValid(t)
- tt.mutate(cfg)
- err := cfg.Validate()
- if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
- t.Fatalf("Validate() error = %v, want %q", err, tt.wantErr)
- }
- })
- }
- }
- func TestValidateConfig_OpenAIWSRules(t *testing.T) {
- buildValid := func(t *testing.T) *Config {
- t.Helper()
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- require.NoError(t, err)
- return cfg
- }
- t.Run("sticky response id ttl 兼容旧键回填", func(t *testing.T) {
- cfg := buildValid(t)
- cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds = 0
- cfg.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds = 7200
- require.NoError(t, cfg.Validate())
- require.Equal(t, 7200, cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds)
- })
- cases := []struct {
- name string
- mutate func(*Config)
- wantErr string
- }{
- {
- name: "max_conns_per_account 必须为正数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.MaxConnsPerAccount = 0 },
- wantErr: "gateway.openai_ws.max_conns_per_account",
- },
- {
- name: "client_first_message_timeout_seconds 必须为正数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds = 0 },
- wantErr: "gateway.openai_ws.client_first_message_timeout_seconds",
- },
- {
- name: "client_first_message_timeout_seconds 不能为负数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds = -1 },
- wantErr: "gateway.openai_ws.client_first_message_timeout_seconds",
- },
- {
- name: "ingress_inter_turn_idle_timeout_seconds 不能为负数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds = -1 },
- wantErr: "gateway.openai_ws.ingress_inter_turn_idle_timeout_seconds",
- },
- {
- name: "max_ingress_connections_per_api_key 不能为负数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.MaxIngressConnectionsPerAPIKey = -1 },
- wantErr: "gateway.openai_ws.max_ingress_connections_per_api_key",
- },
- {
- name: "min_idle_per_account 不能为负数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.MinIdlePerAccount = -1 },
- wantErr: "gateway.openai_ws.min_idle_per_account",
- },
- {
- name: "max_idle_per_account 不能为负数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.MaxIdlePerAccount = -1 },
- wantErr: "gateway.openai_ws.max_idle_per_account",
- },
- {
- name: "min_idle_per_account 不能大于 max_idle_per_account",
- mutate: func(c *Config) {
- c.Gateway.OpenAIWS.MinIdlePerAccount = 3
- c.Gateway.OpenAIWS.MaxIdlePerAccount = 2
- },
- wantErr: "gateway.openai_ws.min_idle_per_account must be <= max_idle_per_account",
- },
- {
- name: "max_idle_per_account 不能大于 max_conns_per_account",
- mutate: func(c *Config) {
- c.Gateway.OpenAIWS.MaxConnsPerAccount = 2
- c.Gateway.OpenAIWS.MinIdlePerAccount = 1
- c.Gateway.OpenAIWS.MaxIdlePerAccount = 3
- },
- wantErr: "gateway.openai_ws.max_idle_per_account must be <= max_conns_per_account",
- },
- {
- name: "dial_timeout_seconds 必须为正数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.DialTimeoutSeconds = 0 },
- wantErr: "gateway.openai_ws.dial_timeout_seconds",
- },
- {
- name: "read_timeout_seconds 必须为正数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.ReadTimeoutSeconds = 0 },
- wantErr: "gateway.openai_ws.read_timeout_seconds",
- },
- {
- name: "write_timeout_seconds 必须为正数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.WriteTimeoutSeconds = 0 },
- wantErr: "gateway.openai_ws.write_timeout_seconds",
- },
- {
- name: "pool_target_utilization 必须在 (0,1]",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.PoolTargetUtilization = 0 },
- wantErr: "gateway.openai_ws.pool_target_utilization",
- },
- {
- name: "queue_limit_per_conn 必须为正数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.QueueLimitPerConn = 0 },
- wantErr: "gateway.openai_ws.queue_limit_per_conn",
- },
- {
- name: "fallback_cooldown_seconds 不能为负数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.FallbackCooldownSeconds = -1 },
- wantErr: "gateway.openai_ws.fallback_cooldown_seconds",
- },
- {
- name: "store_disabled_conn_mode 必须为 strict|adaptive|off",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.StoreDisabledConnMode = "invalid" },
- wantErr: "gateway.openai_ws.store_disabled_conn_mode",
- },
- {
- name: "ingress_mode_default 必须为 off|ctx_pool|passthrough|http_bridge",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.IngressModeDefault = "invalid" },
- wantErr: "gateway.openai_ws.ingress_mode_default",
- },
- {
- name: "payload_log_sample_rate 必须在 [0,1] 范围内",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.PayloadLogSampleRate = 1.2 },
- wantErr: "gateway.openai_ws.payload_log_sample_rate",
- },
- {
- name: "retry_total_budget_ms 不能为负数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.RetryTotalBudgetMS = -1 },
- wantErr: "gateway.openai_ws.retry_total_budget_ms",
- },
- {
- name: "lb_top_k 必须为正数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.LBTopK = 0 },
- wantErr: "gateway.openai_ws.lb_top_k",
- },
- {
- name: "sticky_session_ttl_seconds 必须为正数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.StickySessionTTLSeconds = 0 },
- wantErr: "gateway.openai_ws.sticky_session_ttl_seconds",
- },
- {
- name: "sticky_response_id_ttl_seconds 必须为正数",
- mutate: func(c *Config) {
- c.Gateway.OpenAIWS.StickyResponseIDTTLSeconds = 0
- c.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds = 0
- },
- wantErr: "gateway.openai_ws.sticky_response_id_ttl_seconds",
- },
- {
- name: "sticky_previous_response_ttl_seconds 不能为负数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds = -1 },
- wantErr: "gateway.openai_ws.sticky_previous_response_ttl_seconds",
- },
- {
- name: "scheduler_score_weights 不能为负数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = -0.1 },
- wantErr: "gateway.openai_ws.scheduler_score_weights.* must be non-negative",
- },
- {
- name: "scheduler_score_weights quota_headroom 不能为负数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.SchedulerScoreWeights.QuotaHeadroom = -0.1 },
- wantErr: "gateway.openai_ws.scheduler_score_weights.* must be non-negative",
- },
- {
- name: "scheduler_score_weights upstream_cost 不能为负数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost = -0.1 },
- wantErr: "gateway.openai_ws.scheduler_score_weights.* must be non-negative",
- },
- {
- name: "scheduler_score_weights reset 不能为负数",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.SchedulerScoreWeights.Reset = -0.1 },
- wantErr: "gateway.openai_ws.scheduler_score_weights.* must be non-negative",
- },
- {
- name: "scheduler_score_weights 不能为 NaN",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.SchedulerScoreWeights.PreviousResponse = math.NaN() },
- wantErr: "gateway.openai_ws.scheduler_score_weights.* must be non-negative and finite",
- },
- {
- name: "scheduler_score_weights 不能为 Inf",
- mutate: func(c *Config) { c.Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost = math.Inf(1) },
- wantErr: "gateway.openai_ws.scheduler_score_weights.* must be non-negative and finite",
- },
- {
- name: "scheduler_score_weights 总和不能溢出",
- mutate: func(c *Config) {
- c.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = math.MaxFloat64
- c.Gateway.OpenAIWS.SchedulerScoreWeights.Load = math.MaxFloat64
- },
- wantErr: "gateway.openai_ws.scheduler_score_weights base-weight sum must be finite",
- },
- {
- name: "scheduler_score_weights 含 sticky 总和不能溢出",
- mutate: func(c *Config) {
- c.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = math.MaxFloat64
- c.Gateway.OpenAIWS.SchedulerScoreWeights.PreviousResponse = math.MaxFloat64
- },
- wantErr: "gateway.openai_ws.scheduler_score_weights total-weight sum must be finite",
- },
- {
- name: "scheduler_score_weights 不能全为 0",
- mutate: func(c *Config) {
- c.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = 0
- c.Gateway.OpenAIWS.SchedulerScoreWeights.Load = 0
- c.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = 0
- c.Gateway.OpenAIWS.SchedulerScoreWeights.ErrorRate = 0
- c.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT = 0
- },
- wantErr: "gateway.openai_ws.scheduler_score_weights must not all be zero",
- },
- {
- name: "sticky_escape_ttft_ms 必须为正数",
- mutate: func(c *Config) { c.Gateway.OpenAIScheduler.StickyEscapeTTFTMs = 0 },
- wantErr: "gateway.openai_scheduler.sticky_escape_ttft_ms",
- },
- {
- name: "sticky_escape_error_rate 不能小于 0",
- mutate: func(c *Config) { c.Gateway.OpenAIScheduler.StickyEscapeErrorRate = -0.1 },
- wantErr: "gateway.openai_scheduler.sticky_escape_error_rate",
- },
- {
- name: "sticky_escape_error_rate 不能大于 1",
- mutate: func(c *Config) { c.Gateway.OpenAIScheduler.StickyEscapeErrorRate = 1.1 },
- wantErr: "gateway.openai_scheduler.sticky_escape_error_rate",
- },
- }
- for _, tc := range cases {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- cfg := buildValid(t)
- tc.mutate(cfg)
- err := cfg.Validate()
- require.Error(t, err)
- require.Contains(t, err.Error(), tc.wantErr)
- })
- }
- t.Run("quota_headroom 可作为唯一有效调度权重", func(t *testing.T) {
- cfg := buildValid(t)
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Load = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.ErrorRate = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.QuotaHeadroom = 0.1
- require.NoError(t, cfg.Validate())
- })
- t.Run("upstream_cost 可作为唯一有效调度权重", func(t *testing.T) {
- cfg := buildValid(t)
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Load = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.ErrorRate = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost = 0.1
- require.NoError(t, cfg.Validate())
- })
- t.Run("reset 可作为唯一有效调度权重", func(t *testing.T) {
- cfg := buildValid(t)
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Load = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.ErrorRate = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT = 0
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Reset = 0.1
- require.NoError(t, cfg.Validate())
- })
- }
- func TestValidateConfig_AutoScaleDisabledIgnoreAutoScaleFields(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- cfg.Gateway.UsageRecord.AutoScaleEnabled = false
- cfg.Gateway.UsageRecord.WorkerCount = 64
- // 自动扩缩容关闭时,这些字段应被忽略,不应导致校验失败。
- cfg.Gateway.UsageRecord.AutoScaleMinWorkers = 0
- cfg.Gateway.UsageRecord.AutoScaleMaxWorkers = 0
- cfg.Gateway.UsageRecord.AutoScaleUpQueuePercent = 0
- cfg.Gateway.UsageRecord.AutoScaleDownQueuePercent = 100
- cfg.Gateway.UsageRecord.AutoScaleUpStep = 0
- cfg.Gateway.UsageRecord.AutoScaleDownStep = 0
- cfg.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds = 0
- cfg.Gateway.UsageRecord.AutoScaleCooldownSeconds = -1
- if err := cfg.Validate(); err != nil {
- t.Fatalf("Validate() should ignore auto scale fields when disabled: %v", err)
- }
- }
- func TestValidateConfig_LogRequiredAndRotationBounds(t *testing.T) {
- resetViperWithJWTSecret(t)
- cases := []struct {
- name string
- mutate func(*Config)
- wantErr string
- }{
- {
- name: "log level required",
- mutate: func(c *Config) {
- c.Log.Level = ""
- },
- wantErr: "log.level is required",
- },
- {
- name: "log format required",
- mutate: func(c *Config) {
- c.Log.Format = ""
- },
- wantErr: "log.format is required",
- },
- {
- name: "log stacktrace required",
- mutate: func(c *Config) {
- c.Log.StacktraceLevel = ""
- },
- wantErr: "log.stacktrace_level is required",
- },
- {
- name: "log max backups non-negative",
- mutate: func(c *Config) {
- c.Log.Rotation.MaxBackups = -1
- },
- wantErr: "log.rotation.max_backups must be non-negative",
- },
- {
- name: "log max age non-negative",
- mutate: func(c *Config) {
- c.Log.Rotation.MaxAgeDays = -1
- },
- wantErr: "log.rotation.max_age_days must be non-negative",
- },
- {
- name: "sampling thereafter non-negative when disabled",
- mutate: func(c *Config) {
- c.Log.Sampling.Enabled = false
- c.Log.Sampling.Thereafter = -1
- },
- wantErr: "log.sampling.thereafter must be non-negative",
- },
- }
- for _, tt := range cases {
- t.Run(tt.name, func(t *testing.T) {
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- tt.mutate(cfg)
- err = cfg.Validate()
- if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
- t.Fatalf("Validate() error = %v, want %q", err, tt.wantErr)
- }
- })
- }
- }
- func TestLoad_DefaultGatewayUsageRecordConfig(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if cfg.Gateway.UsageRecord.WorkerCount != 128 {
- t.Fatalf("worker_count = %d, want 128", cfg.Gateway.UsageRecord.WorkerCount)
- }
- if cfg.Gateway.UsageRecord.QueueSize != 16384 {
- t.Fatalf("queue_size = %d, want 16384", cfg.Gateway.UsageRecord.QueueSize)
- }
- if cfg.Gateway.UsageRecord.TaskTimeoutSeconds != 5 {
- t.Fatalf("task_timeout_seconds = %d, want 5", cfg.Gateway.UsageRecord.TaskTimeoutSeconds)
- }
- if cfg.Gateway.UsageRecord.OverflowPolicy != UsageRecordOverflowPolicySync {
- t.Fatalf("overflow_policy = %s, want %s", cfg.Gateway.UsageRecord.OverflowPolicy, UsageRecordOverflowPolicySync)
- }
- if cfg.Gateway.UsageRecord.OverflowSamplePercent != 10 {
- t.Fatalf("overflow_sample_percent = %d, want 10", cfg.Gateway.UsageRecord.OverflowSamplePercent)
- }
- if !cfg.Gateway.UsageRecord.AutoScaleEnabled {
- t.Fatalf("auto_scale_enabled = false, want true")
- }
- if cfg.Gateway.UsageRecord.AutoScaleMinWorkers != 128 {
- t.Fatalf("auto_scale_min_workers = %d, want 128", cfg.Gateway.UsageRecord.AutoScaleMinWorkers)
- }
- if cfg.Gateway.UsageRecord.AutoScaleMaxWorkers != 512 {
- t.Fatalf("auto_scale_max_workers = %d, want 512", cfg.Gateway.UsageRecord.AutoScaleMaxWorkers)
- }
- if cfg.Gateway.UsageRecord.AutoScaleUpQueuePercent != 70 {
- t.Fatalf("auto_scale_up_queue_percent = %d, want 70", cfg.Gateway.UsageRecord.AutoScaleUpQueuePercent)
- }
- if cfg.Gateway.UsageRecord.AutoScaleDownQueuePercent != 15 {
- t.Fatalf("auto_scale_down_queue_percent = %d, want 15", cfg.Gateway.UsageRecord.AutoScaleDownQueuePercent)
- }
- if cfg.Gateway.UsageRecord.AutoScaleUpStep != 32 {
- t.Fatalf("auto_scale_up_step = %d, want 32", cfg.Gateway.UsageRecord.AutoScaleUpStep)
- }
- if cfg.Gateway.UsageRecord.AutoScaleDownStep != 16 {
- t.Fatalf("auto_scale_down_step = %d, want 16", cfg.Gateway.UsageRecord.AutoScaleDownStep)
- }
- if cfg.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds != 3 {
- t.Fatalf("auto_scale_check_interval_seconds = %d, want 3", cfg.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds)
- }
- if cfg.Gateway.UsageRecord.AutoScaleCooldownSeconds != 10 {
- t.Fatalf("auto_scale_cooldown_seconds = %d, want 10", cfg.Gateway.UsageRecord.AutoScaleCooldownSeconds)
- }
- }
- func TestLoad_DefaultGatewayImageStreamConfig(t *testing.T) {
- resetViperWithJWTSecret(t)
- cfg, err := Load()
- if err != nil {
- t.Fatalf("Load() error: %v", err)
- }
- if cfg.Gateway.StreamDataIntervalTimeout != 180 {
- t.Fatalf("stream_data_interval_timeout = %d, want 180", cfg.Gateway.StreamDataIntervalTimeout)
- }
- if cfg.Gateway.StreamKeepaliveInterval != 10 {
- t.Fatalf("stream_keepalive_interval = %d, want 10", cfg.Gateway.StreamKeepaliveInterval)
- }
- if cfg.Gateway.ImageStreamDataIntervalTimeout != 900 {
- t.Fatalf("image_stream_data_interval_timeout = %d, want 900", cfg.Gateway.ImageStreamDataIntervalTimeout)
- }
- if cfg.Gateway.ImageStreamKeepaliveInterval != 10 {
- t.Fatalf("image_stream_keepalive_interval = %d, want 10", cfg.Gateway.ImageStreamKeepaliveInterval)
- }
- if cfg.Gateway.ImageNonstreamKeepaliveInterval != 0 {
- t.Fatalf("image_nonstream_keepalive_interval = %d, want 0", cfg.Gateway.ImageNonstreamKeepaliveInterval)
- }
- if cfg.Gateway.ImageConcurrency.Enabled {
- t.Fatalf("image_concurrency.enabled = true, want false")
- }
- if cfg.Gateway.ImageConcurrency.MaxConcurrentRequests != 0 {
- t.Fatalf("image_concurrency.max_concurrent_requests = %d, want 0", cfg.Gateway.ImageConcurrency.MaxConcurrentRequests)
- }
- if cfg.Gateway.ImageConcurrency.OverflowMode != ImageConcurrencyOverflowModeReject {
- t.Fatalf("image_concurrency.overflow_mode = %q, want %q", cfg.Gateway.ImageConcurrency.OverflowMode, ImageConcurrencyOverflowModeReject)
- }
- if cfg.Gateway.ImageConcurrency.WaitTimeoutSeconds != 30 {
- t.Fatalf("image_concurrency.wait_timeout_seconds = %d, want 30", cfg.Gateway.ImageConcurrency.WaitTimeoutSeconds)
- }
- if cfg.Gateway.ImageConcurrency.MaxWaitingRequests != 100 {
- t.Fatalf("image_concurrency.max_waiting_requests = %d, want 100", cfg.Gateway.ImageConcurrency.MaxWaitingRequests)
- }
- if cfg.Gateway.ImageStreamDataIntervalTimeout <= cfg.Gateway.StreamDataIntervalTimeout {
- t.Fatalf("image stream timeout = %d, want greater than ordinary stream timeout %d", cfg.Gateway.ImageStreamDataIntervalTimeout, cfg.Gateway.StreamDataIntervalTimeout)
- }
- }
|