config_test.go 88 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632
  1. package config
  2. import (
  3. "fmt"
  4. "math"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "sync"
  9. "testing"
  10. "time"
  11. "github.com/spf13/viper"
  12. "github.com/stretchr/testify/require"
  13. )
  14. func resetViperWithJWTSecret(t *testing.T) {
  15. t.Helper()
  16. viper.Reset()
  17. t.Cleanup(viper.Reset)
  18. t.Setenv("CONFIG_FILE", "")
  19. t.Setenv("DATA_DIR", "")
  20. t.Setenv("JWT_SECRET", strings.Repeat("x", 32))
  21. }
  22. func TestLoadDefaultModelsListReadMaxBytes(t *testing.T) {
  23. resetViperWithJWTSecret(t)
  24. cfg, err := Load()
  25. require.NoError(t, err)
  26. require.Equal(t, DefaultModelsListReadMaxBytes, cfg.Gateway.ModelsListReadMaxBytes)
  27. }
  28. func TestLoadTimezonePrecedence(t *testing.T) {
  29. tests := []struct {
  30. name string
  31. fileTimezone string
  32. timezoneEnv string
  33. tzEnv string
  34. want string
  35. }{
  36. {name: "default", want: "Asia/Shanghai"},
  37. {name: "config_file", fileTimezone: "Europe/London", want: "Europe/London"},
  38. {name: "timezone_env", fileTimezone: "Europe/London", timezoneEnv: "UTC", want: "UTC"},
  39. {name: "tz_env", fileTimezone: "Europe/London", timezoneEnv: "UTC", tzEnv: "America/New_York", want: "America/New_York"},
  40. }
  41. for _, tt := range tests {
  42. t.Run(tt.name, func(t *testing.T) {
  43. resetViperWithJWTSecret(t)
  44. t.Setenv("TIMEZONE", tt.timezoneEnv)
  45. t.Setenv("TZ", tt.tzEnv)
  46. if tt.fileTimezone != "" {
  47. configFile := filepath.Join(t.TempDir(), "config.yaml")
  48. require.NoError(t, os.WriteFile(configFile, []byte("timezone: "+tt.fileTimezone+"\n"), 0o600))
  49. t.Setenv("CONFIG_FILE", configFile)
  50. }
  51. cfg, err := Load()
  52. require.NoError(t, err)
  53. require.Equal(t, tt.want, cfg.Timezone)
  54. })
  55. }
  56. }
  57. func TestLoadServerTimingConfig(t *testing.T) {
  58. t.Run("disabled by default", func(t *testing.T) {
  59. resetViperWithJWTSecret(t)
  60. cfg, err := Load()
  61. require.NoError(t, err)
  62. require.False(t, cfg.Server.EnableServerTiming)
  63. })
  64. t.Run("enabled by exact environment variable", func(t *testing.T) {
  65. resetViperWithJWTSecret(t)
  66. t.Setenv("ENABLE_SERVER_TIMING", "true")
  67. cfg, err := Load()
  68. require.NoError(t, err)
  69. require.True(t, cfg.Server.EnableServerTiming)
  70. })
  71. }
  72. func TestLoadRedisUsernameFromEnvironment(t *testing.T) {
  73. resetViperWithJWTSecret(t)
  74. t.Setenv("REDIS_USERNAME", "app-user")
  75. cfg, err := Load()
  76. require.NoError(t, err)
  77. require.Equal(t, "app-user", cfg.Redis.Username)
  78. }
  79. func TestLoadHTTPIngressSafetyDefaults(t *testing.T) {
  80. resetViperWithJWTSecret(t)
  81. cfg, err := Load()
  82. require.NoError(t, err)
  83. require.Equal(t, 10, cfg.Server.ReadHeaderTimeout)
  84. require.Equal(t, 64*1024, cfg.Server.MaxHeaderBytes)
  85. require.Empty(t, cfg.Server.TrustedProxies)
  86. require.False(t, cfg.Server.TrustedProxiesConfigured)
  87. require.True(t, cfg.TrustForwardedIPForAPIKeyACL())
  88. require.Equal(t, int64(32*1024*1024), cfg.Gateway.TextMaxBodySize)
  89. require.True(t, cfg.APIKeyAuth.InvalidAbuse.Enabled)
  90. require.Equal(t, 120, cfg.APIKeyAuth.InvalidAbuse.Threshold)
  91. require.Equal(t, 16384, cfg.APIKeyAuth.InvalidAbuse.Capacity)
  92. }
  93. func TestNormalizeForwardedClientIPHeaders(t *testing.T) {
  94. headers, err := NormalizeForwardedClientIPHeaders([]string{
  95. " x-cdn-client-ip ",
  96. "X-CDN-CLIENT-IP",
  97. "true-client-ip",
  98. })
  99. require.NoError(t, err)
  100. require.Equal(t, []string{"X-Cdn-Client-Ip", "True-Client-Ip"}, headers)
  101. _, err = NormalizeForwardedClientIPHeaders([]string{"X Invalid"})
  102. require.ErrorContains(t, err, "invalid HTTP header field name")
  103. }
  104. func TestNormalizeForwardedClientIPHeadersLimit(t *testing.T) {
  105. headers := make([]string, 0, MaxForwardedClientIPHeaders+1)
  106. for i := 0; i <= MaxForwardedClientIPHeaders; i++ {
  107. headers = append(headers, fmt.Sprintf("X-CDN-IP-%d", i))
  108. }
  109. _, err := NormalizeForwardedClientIPHeaders(headers)
  110. require.ErrorContains(t, err, "at most 16 unique names")
  111. }
  112. func TestLoadForwardedClientIPHeadersNormalizesAndSnapshots(t *testing.T) {
  113. resetViperWithJWTSecret(t)
  114. viper.Set("security.forwarded_client_ip_headers", []string{" x-cdn-ip ", "X-CDN-IP", "true-client-ip"})
  115. cfg, err := Load()
  116. require.NoError(t, err)
  117. snapshot := cfg.ForwardedClientIPSettings()
  118. require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, snapshot.Headers)
  119. snapshot.Headers[0] = "X-Mutated"
  120. require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, cfg.ForwardedClientIPSettings().Headers)
  121. }
  122. func TestForwardedClientIPSettingsConcurrentPublication(t *testing.T) {
  123. cfg := &Config{}
  124. cfg.SetForwardedClientIPSettings(true, []string{"X-Public-A"})
  125. const iterations = 2000
  126. start := make(chan struct{})
  127. errCh := make(chan error, 8)
  128. var wg sync.WaitGroup
  129. for _, settings := range []ForwardedClientIPSettings{
  130. {TrustForwardedIP: true, Headers: []string{"X-Public-A"}},
  131. {TrustForwardedIP: false, Headers: []string{"X-Public-B"}},
  132. } {
  133. settings := settings
  134. wg.Add(1)
  135. go func() {
  136. defer wg.Done()
  137. <-start
  138. for i := 0; i < iterations; i++ {
  139. cfg.SetForwardedClientIPSettings(settings.TrustForwardedIP, settings.Headers)
  140. }
  141. }()
  142. }
  143. for i := 0; i < cap(errCh); i++ {
  144. wg.Add(1)
  145. go func() {
  146. defer wg.Done()
  147. <-start
  148. for j := 0; j < iterations; j++ {
  149. snapshot := cfg.ForwardedClientIPSettings()
  150. validA := snapshot.TrustForwardedIP && len(snapshot.Headers) == 1 && snapshot.Headers[0] == "X-Public-A"
  151. validB := !snapshot.TrustForwardedIP && len(snapshot.Headers) == 1 && snapshot.Headers[0] == "X-Public-B"
  152. if !validA && !validB {
  153. errCh <- fmt.Errorf("observed inconsistent forwarded IP settings: %+v", snapshot)
  154. return
  155. }
  156. }
  157. }()
  158. }
  159. close(start)
  160. wg.Wait()
  161. close(errCh)
  162. for err := range errCh {
  163. require.NoError(t, err)
  164. }
  165. }
  166. func TestLoadForwardedClientIPHeadersFromEnvironment(t *testing.T) {
  167. resetViperWithJWTSecret(t)
  168. t.Setenv("SECURITY_FORWARDED_CLIENT_IP_HEADERS", " x-cdn-ip , X-CDN-IP, true-client-ip ")
  169. cfg, err := Load()
  170. require.NoError(t, err)
  171. require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, cfg.ForwardedClientIPSettings().Headers)
  172. }
  173. func TestLoadExplicitEmptyForwardedClientIPHeadersFromEnvironment(t *testing.T) {
  174. resetViperWithJWTSecret(t)
  175. viper.Set("security.forwarded_client_ip_headers", []string{"X-Yaml-IP"})
  176. t.Setenv("SECURITY_FORWARDED_CLIENT_IP_HEADERS", "")
  177. cfg, err := Load()
  178. require.NoError(t, err)
  179. require.Empty(t, cfg.ForwardedClientIPSettings().Headers)
  180. }
  181. func TestLoadRejectsInvalidForwardedClientIPHeaderFromEnvironment(t *testing.T) {
  182. resetViperWithJWTSecret(t)
  183. t.Setenv("SECURITY_FORWARDED_CLIENT_IP_HEADERS", "X-Valid-IP, X Invalid")
  184. _, err := Load()
  185. require.ErrorContains(t, err, "security.forwarded_client_ip_headers")
  186. }
  187. func TestLoadRejectsInvalidForwardedClientIPHeader(t *testing.T) {
  188. resetViperWithJWTSecret(t)
  189. viper.Set("security.forwarded_client_ip_headers", []string{"X Invalid"})
  190. _, err := Load()
  191. require.ErrorContains(t, err, "security.forwarded_client_ip_headers")
  192. }
  193. func TestLoadExplicitEmptyTrustedProxiesEnablesConfiguredMode(t *testing.T) {
  194. resetViperWithJWTSecret(t)
  195. viper.Set("server.trusted_proxies", []string{})
  196. cfg, err := Load()
  197. require.NoError(t, err)
  198. require.Empty(t, cfg.Server.TrustedProxies)
  199. require.True(t, cfg.Server.TrustedProxiesConfigured)
  200. }
  201. func TestLoadExplicitTrustedProxiesEnablesConfiguredMode(t *testing.T) {
  202. resetViperWithJWTSecret(t)
  203. viper.Set("server.trusted_proxies", []string{"127.0.0.1/32"})
  204. cfg, err := Load()
  205. require.NoError(t, err)
  206. require.Equal(t, []string{"127.0.0.1/32"}, cfg.Server.TrustedProxies)
  207. require.True(t, cfg.Server.TrustedProxiesConfigured)
  208. }
  209. func TestLoadTrustedProxiesFromEnvironment(t *testing.T) {
  210. resetViperWithJWTSecret(t)
  211. t.Setenv("SERVER_TRUSTED_PROXIES", "127.0.0.1/32, ::1/128")
  212. cfg, err := Load()
  213. require.NoError(t, err)
  214. require.Equal(t, []string{"127.0.0.1/32", "::1/128"}, cfg.Server.TrustedProxies)
  215. require.True(t, cfg.Server.TrustedProxiesConfigured)
  216. }
  217. func TestLoadExplicitEmptyTrustedProxiesFromEnvironment(t *testing.T) {
  218. resetViperWithJWTSecret(t)
  219. t.Setenv("SERVER_TRUSTED_PROXIES", "")
  220. cfg, err := Load()
  221. require.NoError(t, err)
  222. require.Empty(t, cfg.Server.TrustedProxies)
  223. require.True(t, cfg.Server.TrustedProxiesConfigured)
  224. }
  225. func TestLoadTrustedProxiesPresenceFromYAML(t *testing.T) {
  226. tests := []struct {
  227. name string
  228. yaml string
  229. want []string
  230. configured bool
  231. }{
  232. {name: "absent", yaml: "server:\n mode: debug\n", configured: false},
  233. {name: "explicit empty", yaml: "server:\n trusted_proxies: []\n", want: []string{}, configured: true},
  234. {
  235. name: "populated",
  236. yaml: "server:\n trusted_proxies:\n - 127.0.0.1/32\n - ::1/128\n",
  237. want: []string{"127.0.0.1/32", "::1/128"},
  238. configured: true,
  239. },
  240. }
  241. for _, test := range tests {
  242. t.Run(test.name, func(t *testing.T) {
  243. resetViperWithJWTSecret(t)
  244. configDir := t.TempDir()
  245. require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(test.yaml), 0o600))
  246. t.Setenv("DATA_DIR", configDir)
  247. cfg, err := Load()
  248. require.NoError(t, err)
  249. require.Equal(t, test.want, cfg.Server.TrustedProxies)
  250. require.Equal(t, test.configured, cfg.Server.TrustedProxiesConfigured)
  251. })
  252. }
  253. }
  254. func TestLoadTrustedProxiesFromConfigFile(t *testing.T) {
  255. resetViperWithJWTSecret(t)
  256. configFile := filepath.Join(t.TempDir(), "reporter-config.yaml")
  257. require.NoError(t, os.WriteFile(configFile, []byte(`server:
  258. host: 192.0.2.10
  259. trusted_proxies:
  260. - 127.0.0.1/32
  261. - 10.0.0.0/8
  262. - 172.16.0.0/12
  263. - 192.168.0.0/16
  264. `), 0o600))
  265. t.Setenv("CONFIG_FILE", configFile)
  266. cfg, err := Load()
  267. require.NoError(t, err)
  268. require.Equal(t, "192.0.2.10", cfg.Server.Host)
  269. 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)
  270. require.True(t, cfg.Server.TrustedProxiesConfigured)
  271. }
  272. func TestConfigFileTakesPrecedenceOverDataDir(t *testing.T) {
  273. resetViperWithJWTSecret(t)
  274. dataDir := t.TempDir()
  275. require.NoError(t, os.WriteFile(filepath.Join(dataDir, "config.yaml"), []byte("server:\n host: 192.0.2.20\n"), 0o600))
  276. configFile := filepath.Join(t.TempDir(), "explicit.yaml")
  277. require.NoError(t, os.WriteFile(configFile, []byte("server:\n host: 192.0.2.30\n"), 0o600))
  278. t.Setenv("DATA_DIR", dataDir)
  279. t.Setenv("CONFIG_FILE", configFile)
  280. cfg, err := Load()
  281. require.NoError(t, err)
  282. require.Equal(t, "192.0.2.30", cfg.Server.Host)
  283. }
  284. func TestLoadReturnsErrorForMissingConfigFile(t *testing.T) {
  285. resetViperWithJWTSecret(t)
  286. t.Setenv("CONFIG_FILE", filepath.Join(t.TempDir(), "missing.yaml"))
  287. _, err := Load()
  288. require.ErrorContains(t, err, "read config error")
  289. }
  290. func TestLoadForBootstrapAllowsMissingJWTSecret(t *testing.T) {
  291. viper.Reset()
  292. t.Cleanup(viper.Reset)
  293. t.Setenv("CONFIG_FILE", "")
  294. t.Setenv("DATA_DIR", "")
  295. t.Setenv("JWT_SECRET", "")
  296. cfg, err := LoadForBootstrap()
  297. if err != nil {
  298. t.Fatalf("LoadForBootstrap() error: %v", err)
  299. }
  300. if cfg.JWT.Secret != "" {
  301. t.Fatalf("LoadForBootstrap() should keep empty jwt.secret during bootstrap")
  302. }
  303. }
  304. func TestNormalizeRunMode(t *testing.T) {
  305. tests := []struct {
  306. input string
  307. expected string
  308. }{
  309. {"simple", "simple"},
  310. {"SIMPLE", "simple"},
  311. {"standard", "standard"},
  312. {"invalid", "standard"},
  313. {"", "standard"},
  314. }
  315. for _, tt := range tests {
  316. result := NormalizeRunMode(tt.input)
  317. if result != tt.expected {
  318. t.Errorf("NormalizeRunMode(%q) = %q, want %q", tt.input, result, tt.expected)
  319. }
  320. }
  321. }
  322. func TestLoadDefaultSchedulingConfig(t *testing.T) {
  323. resetViperWithJWTSecret(t)
  324. cfg, err := Load()
  325. if err != nil {
  326. t.Fatalf("Load() error: %v", err)
  327. }
  328. if cfg.Gateway.Scheduling.StickySessionMaxWaiting != 3 {
  329. t.Fatalf("StickySessionMaxWaiting = %d, want 3", cfg.Gateway.Scheduling.StickySessionMaxWaiting)
  330. }
  331. if cfg.Gateway.Scheduling.StickySessionWaitTimeout != 120*time.Second {
  332. t.Fatalf("StickySessionWaitTimeout = %v, want 120s", cfg.Gateway.Scheduling.StickySessionWaitTimeout)
  333. }
  334. if cfg.Gateway.Scheduling.FallbackWaitTimeout != 30*time.Second {
  335. t.Fatalf("FallbackWaitTimeout = %v, want 30s", cfg.Gateway.Scheduling.FallbackWaitTimeout)
  336. }
  337. if cfg.Gateway.Scheduling.FallbackMaxWaiting != 100 {
  338. t.Fatalf("FallbackMaxWaiting = %d, want 100", cfg.Gateway.Scheduling.FallbackMaxWaiting)
  339. }
  340. if !cfg.Gateway.Scheduling.LoadBatchEnabled {
  341. t.Fatalf("LoadBatchEnabled = false, want true")
  342. }
  343. if cfg.Gateway.Scheduling.LoadBatchCacheTTLMS != 200 {
  344. t.Fatalf("LoadBatchCacheTTLMS = %d, want 200", cfg.Gateway.Scheduling.LoadBatchCacheTTLMS)
  345. }
  346. if cfg.Gateway.Scheduling.SlotCleanupInterval != 30*time.Second {
  347. t.Fatalf("SlotCleanupInterval = %v, want 30s", cfg.Gateway.Scheduling.SlotCleanupInterval)
  348. }
  349. }
  350. func TestLoadDefaultOpenAIFirstOutputTimeoutsDisabled(t *testing.T) {
  351. resetViperWithJWTSecret(t)
  352. cfg, err := Load()
  353. require.NoError(t, err)
  354. require.Zero(t, cfg.Gateway.OpenAIFirstOutputTimeoutSeconds)
  355. require.Zero(t, cfg.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds)
  356. }
  357. func TestLoadOpenAIFirstOutputTimeoutsFromEnv(t *testing.T) {
  358. resetViperWithJWTSecret(t)
  359. t.Setenv("GATEWAY_OPENAI_FIRST_OUTPUT_TIMEOUT_SECONDS", "90")
  360. t.Setenv("GATEWAY_OPENAI_HIGH_EFFORT_FIRST_OUTPUT_TIMEOUT_SECONDS", "240")
  361. cfg, err := Load()
  362. require.NoError(t, err)
  363. require.Equal(t, 90, cfg.Gateway.OpenAIFirstOutputTimeoutSeconds)
  364. require.Equal(t, 240, cfg.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds)
  365. }
  366. func TestValidateOpenAIFirstOutputTimeoutMinimum(t *testing.T) {
  367. resetViperWithJWTSecret(t)
  368. cfg, err := Load()
  369. require.NoError(t, err)
  370. cfg.Gateway.OpenAIFirstOutputTimeoutSeconds = 30
  371. require.NoError(t, cfg.Validate())
  372. }
  373. func TestLoadDefaultOpenAIWSConfig(t *testing.T) {
  374. resetViperWithJWTSecret(t)
  375. cfg, err := Load()
  376. if err != nil {
  377. t.Fatalf("Load() error: %v", err)
  378. }
  379. if !cfg.Gateway.OpenAIWS.Enabled {
  380. t.Fatalf("Gateway.OpenAIWS.Enabled = false, want true")
  381. }
  382. if !cfg.Gateway.OpenAIWS.ResponsesWebsocketsV2 {
  383. t.Fatalf("Gateway.OpenAIWS.ResponsesWebsocketsV2 = false, want true")
  384. }
  385. if cfg.Gateway.OpenAIWS.ResponsesWebsockets {
  386. t.Fatalf("Gateway.OpenAIWS.ResponsesWebsockets = true, want false")
  387. }
  388. if !cfg.Gateway.OpenAIWS.DynamicMaxConnsByAccountConcurrencyEnabled {
  389. t.Fatalf("Gateway.OpenAIWS.DynamicMaxConnsByAccountConcurrencyEnabled = false, want true")
  390. }
  391. if cfg.Gateway.OpenAIWS.OAuthMaxConnsFactor != 1.0 {
  392. t.Fatalf("Gateway.OpenAIWS.OAuthMaxConnsFactor = %v, want 1.0", cfg.Gateway.OpenAIWS.OAuthMaxConnsFactor)
  393. }
  394. if cfg.Gateway.OpenAIWS.APIKeyMaxConnsFactor != 1.0 {
  395. t.Fatalf("Gateway.OpenAIWS.APIKeyMaxConnsFactor = %v, want 1.0", cfg.Gateway.OpenAIWS.APIKeyMaxConnsFactor)
  396. }
  397. if cfg.Gateway.OpenAIWS.StickySessionTTLSeconds != 3600 {
  398. t.Fatalf("Gateway.OpenAIWS.StickySessionTTLSeconds = %d, want 3600", cfg.Gateway.OpenAIWS.StickySessionTTLSeconds)
  399. }
  400. if !cfg.Gateway.OpenAIScheduler.StickyEscapeEnabled {
  401. t.Fatalf("Gateway.OpenAIScheduler.StickyEscapeEnabled = false, want true")
  402. }
  403. if cfg.Gateway.OpenAIScheduler.StickyEscapeTTFTMs != 15000 {
  404. t.Fatalf("Gateway.OpenAIScheduler.StickyEscapeTTFTMs = %d, want 15000", cfg.Gateway.OpenAIScheduler.StickyEscapeTTFTMs)
  405. }
  406. if cfg.Gateway.OpenAIScheduler.StickyEscapeErrorRate != 0.5 {
  407. t.Fatalf("Gateway.OpenAIScheduler.StickyEscapeErrorRate = %v, want 0.5", cfg.Gateway.OpenAIScheduler.StickyEscapeErrorRate)
  408. }
  409. if !cfg.Gateway.OpenAIWS.SessionHashReadOldFallback {
  410. t.Fatalf("Gateway.OpenAIWS.SessionHashReadOldFallback = false, want true")
  411. }
  412. if !cfg.Gateway.OpenAIWS.SessionHashDualWriteOld {
  413. t.Fatalf("Gateway.OpenAIWS.SessionHashDualWriteOld = false, want true")
  414. }
  415. if !cfg.Gateway.OpenAIWS.MetadataBridgeEnabled {
  416. t.Fatalf("Gateway.OpenAIWS.MetadataBridgeEnabled = false, want true")
  417. }
  418. if cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds != 3600 {
  419. t.Fatalf("Gateway.OpenAIWS.StickyResponseIDTTLSeconds = %d, want 3600", cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds)
  420. }
  421. if cfg.Gateway.OpenAIWS.FallbackCooldownSeconds != 30 {
  422. t.Fatalf("Gateway.OpenAIWS.FallbackCooldownSeconds = %d, want 30", cfg.Gateway.OpenAIWS.FallbackCooldownSeconds)
  423. }
  424. if cfg.Gateway.OpenAIWS.EventFlushBatchSize != 1 {
  425. t.Fatalf("Gateway.OpenAIWS.EventFlushBatchSize = %d, want 1", cfg.Gateway.OpenAIWS.EventFlushBatchSize)
  426. }
  427. if cfg.Gateway.OpenAIWS.EventFlushIntervalMS != 10 {
  428. t.Fatalf("Gateway.OpenAIWS.EventFlushIntervalMS = %d, want 10", cfg.Gateway.OpenAIWS.EventFlushIntervalMS)
  429. }
  430. if cfg.Gateway.OpenAIWS.PrewarmCooldownMS != 300 {
  431. t.Fatalf("Gateway.OpenAIWS.PrewarmCooldownMS = %d, want 300", cfg.Gateway.OpenAIWS.PrewarmCooldownMS)
  432. }
  433. if cfg.Gateway.OpenAIWS.ClientReadLimitBytes != 64*1024*1024 {
  434. t.Fatalf("Gateway.OpenAIWS.ClientReadLimitBytes = %d, want %d", cfg.Gateway.OpenAIWS.ClientReadLimitBytes, 64*1024*1024)
  435. }
  436. if !cfg.Gateway.OpenAIWS.HTTPBridgeEnabled {
  437. t.Fatalf("Gateway.OpenAIWS.HTTPBridgeEnabled = false, want true")
  438. }
  439. if cfg.Gateway.OpenAIWS.HTTPBridgeThresholdBytes != 15*1024*1024 {
  440. t.Fatalf("Gateway.OpenAIWS.HTTPBridgeThresholdBytes = %d, want %d", cfg.Gateway.OpenAIWS.HTTPBridgeThresholdBytes, 15*1024*1024)
  441. }
  442. if cfg.Gateway.OpenAIWS.RetryBackoffInitialMS != 120 {
  443. t.Fatalf("Gateway.OpenAIWS.RetryBackoffInitialMS = %d, want 120", cfg.Gateway.OpenAIWS.RetryBackoffInitialMS)
  444. }
  445. if cfg.Gateway.OpenAIWS.RetryBackoffMaxMS != 2000 {
  446. t.Fatalf("Gateway.OpenAIWS.RetryBackoffMaxMS = %d, want 2000", cfg.Gateway.OpenAIWS.RetryBackoffMaxMS)
  447. }
  448. if cfg.Gateway.OpenAIWS.RetryJitterRatio != 0.2 {
  449. t.Fatalf("Gateway.OpenAIWS.RetryJitterRatio = %v, want 0.2", cfg.Gateway.OpenAIWS.RetryJitterRatio)
  450. }
  451. if cfg.Gateway.OpenAIWS.RetryTotalBudgetMS != 5000 {
  452. t.Fatalf("Gateway.OpenAIWS.RetryTotalBudgetMS = %d, want 5000", cfg.Gateway.OpenAIWS.RetryTotalBudgetMS)
  453. }
  454. if cfg.Gateway.OpenAIWS.PayloadLogSampleRate != 0.2 {
  455. t.Fatalf("Gateway.OpenAIWS.PayloadLogSampleRate = %v, want 0.2", cfg.Gateway.OpenAIWS.PayloadLogSampleRate)
  456. }
  457. if cfg.Gateway.OpenAIWS.SchedulerScoreWeights.QuotaHeadroom != 0 {
  458. t.Fatalf("Gateway.OpenAIWS.SchedulerScoreWeights.QuotaHeadroom = %v, want 0", cfg.Gateway.OpenAIWS.SchedulerScoreWeights.QuotaHeadroom)
  459. }
  460. if cfg.Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost != 0 {
  461. t.Fatalf("Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost = %v, want 0", cfg.Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost)
  462. }
  463. if !cfg.Gateway.OpenAIWS.StoreDisabledForceNewConn {
  464. t.Fatalf("Gateway.OpenAIWS.StoreDisabledForceNewConn = false, want true")
  465. }
  466. if cfg.Gateway.OpenAIWS.StoreDisabledConnMode != "strict" {
  467. t.Fatalf("Gateway.OpenAIWS.StoreDisabledConnMode = %q, want %q", cfg.Gateway.OpenAIWS.StoreDisabledConnMode, "strict")
  468. }
  469. if cfg.Gateway.OpenAIWS.ModeRouterV2Enabled {
  470. t.Fatalf("Gateway.OpenAIWS.ModeRouterV2Enabled = true, want false")
  471. }
  472. if cfg.Gateway.OpenAIWS.IngressModeDefault != "ctx_pool" {
  473. t.Fatalf("Gateway.OpenAIWS.IngressModeDefault = %q, want %q", cfg.Gateway.OpenAIWS.IngressModeDefault, "ctx_pool")
  474. }
  475. if cfg.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds != DefaultOpenAIWSClientFirstMessageTimeoutSeconds {
  476. t.Fatalf(
  477. "Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds = %d, want %d",
  478. cfg.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds,
  479. DefaultOpenAIWSClientFirstMessageTimeoutSeconds,
  480. )
  481. }
  482. if cfg.Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds != 300 {
  483. t.Fatalf("Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds = %d, want 300", cfg.Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds)
  484. }
  485. if cfg.Gateway.OpenAIWS.MaxIngressConnectionsPerAPIKey != 64 {
  486. t.Fatalf("Gateway.OpenAIWS.MaxIngressConnectionsPerAPIKey = %d, want 64", cfg.Gateway.OpenAIWS.MaxIngressConnectionsPerAPIKey)
  487. }
  488. }
  489. func TestLoadOpenAIWSClientFirstMessageTimeoutFromEnv(t *testing.T) {
  490. resetViperWithJWTSecret(t)
  491. t.Setenv("GATEWAY_OPENAI_WS_CLIENT_FIRST_MESSAGE_TIMEOUT_SECONDS", "120")
  492. cfg, err := Load()
  493. require.NoError(t, err)
  494. require.Equal(t, 120, cfg.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds)
  495. }
  496. func TestLoadOpenAIWSForceHTTPFromEnv(t *testing.T) {
  497. resetViperWithJWTSecret(t)
  498. t.Setenv("GATEWAY_OPENAI_WS_FORCE_HTTP", "true")
  499. cfg, err := Load()
  500. require.NoError(t, err)
  501. require.True(t, cfg.Gateway.OpenAIWS.ForceHTTP)
  502. }
  503. func TestLoadDefaultOpenAICompactModel(t *testing.T) {
  504. resetViperWithJWTSecret(t)
  505. cfg, err := Load()
  506. require.NoError(t, err)
  507. require.Equal(t, "gpt-5.4", cfg.Gateway.OpenAICompactModel)
  508. }
  509. func TestLoadOpenAICompactModelFromEnv(t *testing.T) {
  510. resetViperWithJWTSecret(t)
  511. t.Setenv("GATEWAY_OPENAI_COMPACT_MODEL", "gpt-5.3-codex")
  512. cfg, err := Load()
  513. require.NoError(t, err)
  514. require.Equal(t, "gpt-5.3-codex", cfg.Gateway.OpenAICompactModel)
  515. }
  516. func TestLoadDefaultGrokFreeQuotaSoftGate(t *testing.T) {
  517. resetViperWithJWTSecret(t)
  518. cfg, err := Load()
  519. require.NoError(t, err)
  520. require.False(t, cfg.Gateway.Grok.PasswordAuthEnabled)
  521. require.True(t, cfg.Gateway.Grok.FreeQuotaSoftGateEnabled)
  522. require.Equal(t, int64(500_000), cfg.Gateway.Grok.FreeQuotaTokenLimit)
  523. require.Equal(t, 95, cfg.Gateway.Grok.FreeQuotaSoftGatePercent)
  524. require.Equal(t, 24, cfg.Gateway.Grok.FreeQuotaWindowHours)
  525. require.Equal(t, 60, cfg.Gateway.Grok.FreeQuotaStatsCacheSeconds)
  526. }
  527. func TestLoadDefaultOpenAIHTTP2Enabled(t *testing.T) {
  528. resetViperWithJWTSecret(t)
  529. cfg, err := Load()
  530. require.NoError(t, err)
  531. require.True(t, cfg.Gateway.OpenAIHTTP2.Enabled)
  532. require.True(t, cfg.Gateway.OpenAIHTTP2.AllowProxyFallbackToHTTP1)
  533. require.False(t, cfg.Gateway.OpenAIProxyStreamCircuit.Disabled)
  534. require.Equal(t, 2, cfg.Gateway.OpenAIProxyStreamCircuit.FailureThreshold)
  535. require.Equal(t, 60, cfg.Gateway.OpenAIProxyStreamCircuit.WindowSeconds)
  536. require.Equal(t, 600, cfg.Gateway.OpenAIProxyStreamCircuit.TTLSeconds)
  537. }
  538. func TestLoadOpenAIProxyStreamCircuitFromEnv(t *testing.T) {
  539. resetViperWithJWTSecret(t)
  540. t.Setenv("GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_DISABLED", "true")
  541. t.Setenv("GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_FAILURE_THRESHOLD", "3")
  542. t.Setenv("GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_WINDOW_SECONDS", "90")
  543. t.Setenv("GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_TTL_SECONDS", "420")
  544. cfg, err := Load()
  545. require.NoError(t, err)
  546. require.True(t, cfg.Gateway.OpenAIProxyStreamCircuit.Disabled)
  547. require.Equal(t, 3, cfg.Gateway.OpenAIProxyStreamCircuit.FailureThreshold)
  548. require.Equal(t, 90, cfg.Gateway.OpenAIProxyStreamCircuit.WindowSeconds)
  549. require.Equal(t, 420, cfg.Gateway.OpenAIProxyStreamCircuit.TTLSeconds)
  550. }
  551. func TestLoadOpenAIHTTP2DisabledFromEnv(t *testing.T) {
  552. resetViperWithJWTSecret(t)
  553. t.Setenv("GATEWAY_OPENAI_HTTP2_ENABLED", "false")
  554. cfg, err := Load()
  555. require.NoError(t, err)
  556. require.False(t, cfg.Gateway.OpenAIHTTP2.Enabled)
  557. }
  558. func TestLoadDefaultOpenAIResponseHeaderTimeoutUnlimited(t *testing.T) {
  559. resetViperWithJWTSecret(t)
  560. cfg, err := Load()
  561. require.NoError(t, err)
  562. require.Equal(t, 0, cfg.Gateway.OpenAIResponseHeaderTimeout)
  563. }
  564. func TestLoadOpenAIResponseHeaderTimeoutFromEnv(t *testing.T) {
  565. resetViperWithJWTSecret(t)
  566. t.Setenv("GATEWAY_OPENAI_RESPONSE_HEADER_TIMEOUT", "1800")
  567. cfg, err := Load()
  568. require.NoError(t, err)
  569. require.Equal(t, 1800, cfg.Gateway.OpenAIResponseHeaderTimeout)
  570. }
  571. func TestLoadImageNonstreamKeepaliveFromEnv(t *testing.T) {
  572. resetViperWithJWTSecret(t)
  573. t.Setenv("GATEWAY_IMAGE_NONSTREAM_KEEPALIVE_INTERVAL", "15")
  574. cfg, err := Load()
  575. require.NoError(t, err)
  576. require.Equal(t, 15, cfg.Gateway.ImageNonstreamKeepaliveInterval)
  577. }
  578. func TestLoadOpenAIWSStickyTTLCompatibility(t *testing.T) {
  579. resetViperWithJWTSecret(t)
  580. t.Setenv("GATEWAY_OPENAI_WS_STICKY_RESPONSE_ID_TTL_SECONDS", "0")
  581. t.Setenv("GATEWAY_OPENAI_WS_STICKY_PREVIOUS_RESPONSE_TTL_SECONDS", "7200")
  582. cfg, err := Load()
  583. if err != nil {
  584. t.Fatalf("Load() error: %v", err)
  585. }
  586. if cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds != 7200 {
  587. t.Fatalf("StickyResponseIDTTLSeconds = %d, want 7200", cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds)
  588. }
  589. }
  590. func TestLoadDefaultIdempotencyConfig(t *testing.T) {
  591. resetViperWithJWTSecret(t)
  592. cfg, err := Load()
  593. if err != nil {
  594. t.Fatalf("Load() error: %v", err)
  595. }
  596. if !cfg.Idempotency.ObserveOnly {
  597. t.Fatalf("Idempotency.ObserveOnly = false, want true")
  598. }
  599. if cfg.Idempotency.DefaultTTLSeconds != 86400 {
  600. t.Fatalf("Idempotency.DefaultTTLSeconds = %d, want 86400", cfg.Idempotency.DefaultTTLSeconds)
  601. }
  602. if cfg.Idempotency.SystemOperationTTLSeconds != 3600 {
  603. t.Fatalf("Idempotency.SystemOperationTTLSeconds = %d, want 3600", cfg.Idempotency.SystemOperationTTLSeconds)
  604. }
  605. }
  606. func TestLoadDefaultBatchImageQueueDisabled(t *testing.T) {
  607. resetViperWithJWTSecret(t)
  608. cfg, err := Load()
  609. require.NoError(t, err)
  610. require.False(t, cfg.BatchImage.QueueEnabled)
  611. }
  612. func TestLoadIdempotencyConfigFromEnv(t *testing.T) {
  613. resetViperWithJWTSecret(t)
  614. t.Setenv("IDEMPOTENCY_OBSERVE_ONLY", "false")
  615. t.Setenv("IDEMPOTENCY_DEFAULT_TTL_SECONDS", "600")
  616. cfg, err := Load()
  617. if err != nil {
  618. t.Fatalf("Load() error: %v", err)
  619. }
  620. if cfg.Idempotency.ObserveOnly {
  621. t.Fatalf("Idempotency.ObserveOnly = true, want false")
  622. }
  623. if cfg.Idempotency.DefaultTTLSeconds != 600 {
  624. t.Fatalf("Idempotency.DefaultTTLSeconds = %d, want 600", cfg.Idempotency.DefaultTTLSeconds)
  625. }
  626. }
  627. func TestLoadSchedulingConfigFromEnv(t *testing.T) {
  628. resetViperWithJWTSecret(t)
  629. t.Setenv("GATEWAY_SCHEDULING_STICKY_SESSION_MAX_WAITING", "5")
  630. cfg, err := Load()
  631. if err != nil {
  632. t.Fatalf("Load() error: %v", err)
  633. }
  634. if cfg.Gateway.Scheduling.StickySessionMaxWaiting != 5 {
  635. t.Fatalf("StickySessionMaxWaiting = %d, want 5", cfg.Gateway.Scheduling.StickySessionMaxWaiting)
  636. }
  637. }
  638. func TestLoadWeChatConnectConfigFromLegacyEnv(t *testing.T) {
  639. resetViperWithJWTSecret(t)
  640. t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
  641. t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
  642. t.Setenv("WECHAT_OAUTH_MP_APP_ID", "wx-mp-app")
  643. t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "wx-mp-secret")
  644. t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/legacy-callback")
  645. cfg, err := Load()
  646. require.NoError(t, err)
  647. require.True(t, cfg.WeChat.Enabled)
  648. require.True(t, cfg.WeChat.OpenEnabled)
  649. require.True(t, cfg.WeChat.MPEnabled)
  650. require.False(t, cfg.WeChat.MobileEnabled)
  651. require.Equal(t, "open", cfg.WeChat.Mode)
  652. require.Equal(t, "wx-open-app", cfg.WeChat.OpenAppID)
  653. require.Equal(t, "wx-open-secret", cfg.WeChat.OpenAppSecret)
  654. require.Equal(t, "wx-mp-app", cfg.WeChat.MPAppID)
  655. require.Equal(t, "wx-mp-secret", cfg.WeChat.MPAppSecret)
  656. require.Equal(t, "/auth/wechat/legacy-callback", cfg.WeChat.FrontendRedirectURL)
  657. }
  658. func TestLoadDefaultOIDCSecurityDefaults(t *testing.T) {
  659. resetViperWithJWTSecret(t)
  660. cfg, err := Load()
  661. require.NoError(t, err)
  662. require.True(t, cfg.OIDC.UsePKCE)
  663. require.True(t, cfg.OIDC.ValidateIDToken)
  664. require.False(t, cfg.OIDC.UsePKCEExplicit)
  665. require.False(t, cfg.OIDC.ValidateIDTokenExplicit)
  666. }
  667. func TestLoadExplicitOIDCSecurityDefaultsFromEnvMarksFlagsExplicit(t *testing.T) {
  668. resetViperWithJWTSecret(t)
  669. t.Setenv("OIDC_CONNECT_USE_PKCE", "false")
  670. t.Setenv("OIDC_CONNECT_VALIDATE_ID_TOKEN", "false")
  671. cfg, err := Load()
  672. require.NoError(t, err)
  673. require.False(t, cfg.OIDC.UsePKCE)
  674. require.False(t, cfg.OIDC.ValidateIDToken)
  675. require.True(t, cfg.OIDC.UsePKCEExplicit)
  676. require.True(t, cfg.OIDC.ValidateIDTokenExplicit)
  677. }
  678. func TestLoadForcedCodexInstructionsTemplate(t *testing.T) {
  679. resetViperWithJWTSecret(t)
  680. tempDir := t.TempDir()
  681. templatePath := filepath.Join(tempDir, "codex-instructions.md.tmpl")
  682. configPath := filepath.Join(tempDir, "config.yaml")
  683. require.NoError(t, os.WriteFile(templatePath, []byte("server-prefix\n\n{{ .ExistingInstructions }}"), 0o644))
  684. yamlSafePath := filepath.ToSlash(templatePath)
  685. require.NoError(t, os.WriteFile(configPath, []byte("gateway:\n forced_codex_instructions_template_file: \""+yamlSafePath+"\"\n"), 0o644))
  686. t.Setenv("DATA_DIR", tempDir)
  687. cfg, err := Load()
  688. require.NoError(t, err)
  689. require.Equal(t, yamlSafePath, cfg.Gateway.ForcedCodexInstructionsTemplateFile)
  690. require.Equal(t, "server-prefix\n\n{{ .ExistingInstructions }}", cfg.Gateway.ForcedCodexInstructionsTemplate)
  691. }
  692. func TestLoadDefaultSecurityToggles(t *testing.T) {
  693. resetViperWithJWTSecret(t)
  694. cfg, err := Load()
  695. if err != nil {
  696. t.Fatalf("Load() error: %v", err)
  697. }
  698. if cfg.Security.URLAllowlist.Enabled {
  699. t.Fatalf("URLAllowlist.Enabled = true, want false")
  700. }
  701. if !cfg.Security.URLAllowlist.AllowInsecureHTTP {
  702. t.Fatalf("URLAllowlist.AllowInsecureHTTP = false, want true")
  703. }
  704. if !cfg.Security.URLAllowlist.AllowPrivateHosts {
  705. t.Fatalf("URLAllowlist.AllowPrivateHosts = false, want true")
  706. }
  707. if !cfg.Security.ResponseHeaders.Enabled {
  708. t.Fatalf("ResponseHeaders.Enabled = false, want true")
  709. }
  710. wantHosts := []string{
  711. "api.kimi.com",
  712. "api.moonshot.ai",
  713. "api.moonshot.cn",
  714. }
  715. hostSet := make(map[string]struct{}, len(cfg.Security.URLAllowlist.UpstreamHosts))
  716. for _, h := range cfg.Security.URLAllowlist.UpstreamHosts {
  717. hostSet[h] = struct{}{}
  718. }
  719. for _, want := range wantHosts {
  720. if _, ok := hostSet[want]; !ok {
  721. t.Fatalf("URLAllowlist.UpstreamHosts missing %q; got %v", want, cfg.Security.URLAllowlist.UpstreamHosts)
  722. }
  723. }
  724. }
  725. func TestLoadDefaultServerMode(t *testing.T) {
  726. resetViperWithJWTSecret(t)
  727. cfg, err := Load()
  728. if err != nil {
  729. t.Fatalf("Load() error: %v", err)
  730. }
  731. if cfg.Server.Mode != "release" {
  732. t.Fatalf("Server.Mode = %q, want %q", cfg.Server.Mode, "release")
  733. }
  734. }
  735. func TestLoadDefaultJWTAccessTokenExpireMinutes(t *testing.T) {
  736. resetViperWithJWTSecret(t)
  737. cfg, err := Load()
  738. if err != nil {
  739. t.Fatalf("Load() error: %v", err)
  740. }
  741. if cfg.JWT.ExpireHour != 24 {
  742. t.Fatalf("JWT.ExpireHour = %d, want 24", cfg.JWT.ExpireHour)
  743. }
  744. if cfg.JWT.AccessTokenExpireMinutes != 0 {
  745. t.Fatalf("JWT.AccessTokenExpireMinutes = %d, want 0", cfg.JWT.AccessTokenExpireMinutes)
  746. }
  747. }
  748. func TestLoadJWTAccessTokenExpireMinutesFromEnv(t *testing.T) {
  749. resetViperWithJWTSecret(t)
  750. t.Setenv("JWT_ACCESS_TOKEN_EXPIRE_MINUTES", "90")
  751. cfg, err := Load()
  752. if err != nil {
  753. t.Fatalf("Load() error: %v", err)
  754. }
  755. if cfg.JWT.AccessTokenExpireMinutes != 90 {
  756. t.Fatalf("JWT.AccessTokenExpireMinutes = %d, want 90", cfg.JWT.AccessTokenExpireMinutes)
  757. }
  758. }
  759. func TestLoadDefaultDatabaseSSLMode(t *testing.T) {
  760. resetViperWithJWTSecret(t)
  761. cfg, err := Load()
  762. if err != nil {
  763. t.Fatalf("Load() error: %v", err)
  764. }
  765. if cfg.Database.SSLMode != "prefer" {
  766. t.Fatalf("Database.SSLMode = %q, want %q", cfg.Database.SSLMode, "prefer")
  767. }
  768. }
  769. func TestValidateLinuxDoFrontendRedirectURL(t *testing.T) {
  770. resetViperWithJWTSecret(t)
  771. cfg, err := Load()
  772. if err != nil {
  773. t.Fatalf("Load() error: %v", err)
  774. }
  775. cfg.LinuxDo.Enabled = true
  776. cfg.LinuxDo.ClientID = "test-client"
  777. cfg.LinuxDo.ClientSecret = "test-secret"
  778. cfg.LinuxDo.RedirectURL = "https://example.com/api/v1/auth/oauth/linuxdo/callback"
  779. cfg.LinuxDo.TokenAuthMethod = "client_secret_post"
  780. cfg.LinuxDo.UsePKCE = true
  781. cfg.LinuxDo.FrontendRedirectURL = "javascript:alert(1)"
  782. err = cfg.Validate()
  783. if err == nil {
  784. t.Fatalf("Validate() expected error for javascript scheme, got nil")
  785. }
  786. if !strings.Contains(err.Error(), "linuxdo_connect.frontend_redirect_url") {
  787. t.Fatalf("Validate() expected frontend_redirect_url error, got: %v", err)
  788. }
  789. }
  790. func TestValidateLinuxDoAllowsDisablingPKCEForCompatibility(t *testing.T) {
  791. resetViperWithJWTSecret(t)
  792. cfg, err := Load()
  793. if err != nil {
  794. t.Fatalf("Load() error: %v", err)
  795. }
  796. cfg.LinuxDo.Enabled = true
  797. cfg.LinuxDo.ClientID = "test-client"
  798. cfg.LinuxDo.ClientSecret = ""
  799. cfg.LinuxDo.RedirectURL = "https://example.com/api/v1/auth/oauth/linuxdo/callback"
  800. cfg.LinuxDo.FrontendRedirectURL = "/auth/linuxdo/callback"
  801. cfg.LinuxDo.TokenAuthMethod = "none"
  802. cfg.LinuxDo.UsePKCE = false
  803. err = cfg.Validate()
  804. if err != nil {
  805. t.Fatalf("Validate() expected LinuxDo config without PKCE to pass for compatibility, got: %v", err)
  806. }
  807. }
  808. func TestValidateOIDCScopesMustContainOpenID(t *testing.T) {
  809. resetViperWithJWTSecret(t)
  810. cfg, err := Load()
  811. if err != nil {
  812. t.Fatalf("Load() error: %v", err)
  813. }
  814. cfg.OIDC.Enabled = true
  815. cfg.OIDC.ClientID = "oidc-client"
  816. cfg.OIDC.ClientSecret = "oidc-secret"
  817. cfg.OIDC.IssuerURL = "https://issuer.example.com"
  818. cfg.OIDC.AuthorizeURL = "https://issuer.example.com/auth"
  819. cfg.OIDC.TokenURL = "https://issuer.example.com/token"
  820. cfg.OIDC.JWKSURL = "https://issuer.example.com/jwks"
  821. cfg.OIDC.RedirectURL = "https://example.com/api/v1/auth/oauth/oidc/callback"
  822. cfg.OIDC.FrontendRedirectURL = "/auth/oidc/callback"
  823. cfg.OIDC.Scopes = "profile email"
  824. cfg.OIDC.UsePKCE = true
  825. err = cfg.Validate()
  826. if err == nil {
  827. t.Fatalf("Validate() expected error when scopes do not include openid, got nil")
  828. }
  829. if !strings.Contains(err.Error(), "oidc_connect.scopes") {
  830. t.Fatalf("Validate() expected oidc_connect.scopes error, got: %v", err)
  831. }
  832. }
  833. func TestValidateOIDCAllowsIssuerOnlyEndpointsWithDiscoveryFallback(t *testing.T) {
  834. resetViperWithJWTSecret(t)
  835. cfg, err := Load()
  836. if err != nil {
  837. t.Fatalf("Load() error: %v", err)
  838. }
  839. cfg.OIDC.Enabled = true
  840. cfg.OIDC.ClientID = "oidc-client"
  841. cfg.OIDC.ClientSecret = "oidc-secret"
  842. cfg.OIDC.IssuerURL = "https://issuer.example.com"
  843. cfg.OIDC.AuthorizeURL = ""
  844. cfg.OIDC.TokenURL = ""
  845. cfg.OIDC.JWKSURL = ""
  846. cfg.OIDC.RedirectURL = "https://example.com/api/v1/auth/oauth/oidc/callback"
  847. cfg.OIDC.FrontendRedirectURL = "/auth/oidc/callback"
  848. cfg.OIDC.Scopes = "openid email profile"
  849. cfg.OIDC.ValidateIDToken = true
  850. cfg.OIDC.UsePKCE = true
  851. err = cfg.Validate()
  852. if err != nil {
  853. t.Fatalf("Validate() expected issuer-only OIDC config to pass with discovery fallback, got: %v", err)
  854. }
  855. }
  856. func TestValidateOIDCAllowsExplicitCompatibilityOverridesForPKCEAndIDTokenValidation(t *testing.T) {
  857. resetViperWithJWTSecret(t)
  858. cfg, err := Load()
  859. if err != nil {
  860. t.Fatalf("Load() error: %v", err)
  861. }
  862. cfg.OIDC.Enabled = true
  863. cfg.OIDC.ClientID = "oidc-client"
  864. cfg.OIDC.ClientSecret = "oidc-secret"
  865. cfg.OIDC.IssuerURL = "https://issuer.example.com"
  866. cfg.OIDC.AuthorizeURL = "https://issuer.example.com/auth"
  867. cfg.OIDC.TokenURL = "https://issuer.example.com/token"
  868. cfg.OIDC.UserInfoURL = "https://issuer.example.com/userinfo"
  869. cfg.OIDC.RedirectURL = "https://example.com/api/v1/auth/oauth/oidc/callback"
  870. cfg.OIDC.FrontendRedirectURL = "/auth/oidc/callback"
  871. cfg.OIDC.Scopes = "openid email profile"
  872. cfg.OIDC.UsePKCE = false
  873. cfg.OIDC.ValidateIDToken = false
  874. cfg.OIDC.JWKSURL = ""
  875. cfg.OIDC.AllowedSigningAlgs = ""
  876. err = cfg.Validate()
  877. if err != nil {
  878. t.Fatalf("Validate() expected OIDC config without PKCE/id_token validation to pass for compatibility, got: %v", err)
  879. }
  880. }
  881. func TestLoadDefaultDashboardCacheConfig(t *testing.T) {
  882. resetViperWithJWTSecret(t)
  883. cfg, err := Load()
  884. if err != nil {
  885. t.Fatalf("Load() error: %v", err)
  886. }
  887. if !cfg.Dashboard.Enabled {
  888. t.Fatalf("Dashboard.Enabled = false, want true")
  889. }
  890. if cfg.Dashboard.KeyPrefix != "sub2api:" {
  891. t.Fatalf("Dashboard.KeyPrefix = %q, want %q", cfg.Dashboard.KeyPrefix, "sub2api:")
  892. }
  893. if cfg.Dashboard.StatsFreshTTLSeconds != 15 {
  894. t.Fatalf("Dashboard.StatsFreshTTLSeconds = %d, want 15", cfg.Dashboard.StatsFreshTTLSeconds)
  895. }
  896. if cfg.Dashboard.StatsTTLSeconds != 30 {
  897. t.Fatalf("Dashboard.StatsTTLSeconds = %d, want 30", cfg.Dashboard.StatsTTLSeconds)
  898. }
  899. if cfg.Dashboard.StatsRefreshTimeoutSeconds != 30 {
  900. t.Fatalf("Dashboard.StatsRefreshTimeoutSeconds = %d, want 30", cfg.Dashboard.StatsRefreshTimeoutSeconds)
  901. }
  902. }
  903. func TestValidateDashboardCacheConfigEnabled(t *testing.T) {
  904. resetViperWithJWTSecret(t)
  905. cfg, err := Load()
  906. if err != nil {
  907. t.Fatalf("Load() error: %v", err)
  908. }
  909. cfg.Dashboard.Enabled = true
  910. cfg.Dashboard.StatsFreshTTLSeconds = 10
  911. cfg.Dashboard.StatsTTLSeconds = 5
  912. err = cfg.Validate()
  913. if err == nil {
  914. t.Fatalf("Validate() expected error for stats_fresh_ttl_seconds > stats_ttl_seconds, got nil")
  915. }
  916. if !strings.Contains(err.Error(), "dashboard_cache.stats_fresh_ttl_seconds") {
  917. t.Fatalf("Validate() expected stats_fresh_ttl_seconds error, got: %v", err)
  918. }
  919. }
  920. func TestValidateDashboardCacheConfigDisabled(t *testing.T) {
  921. resetViperWithJWTSecret(t)
  922. cfg, err := Load()
  923. if err != nil {
  924. t.Fatalf("Load() error: %v", err)
  925. }
  926. cfg.Dashboard.Enabled = false
  927. cfg.Dashboard.StatsTTLSeconds = -1
  928. err = cfg.Validate()
  929. if err == nil {
  930. t.Fatalf("Validate() expected error for negative stats_ttl_seconds, got nil")
  931. }
  932. if !strings.Contains(err.Error(), "dashboard_cache.stats_ttl_seconds") {
  933. t.Fatalf("Validate() expected stats_ttl_seconds error, got: %v", err)
  934. }
  935. }
  936. func TestLoadDefaultDashboardAggregationConfig(t *testing.T) {
  937. resetViperWithJWTSecret(t)
  938. cfg, err := Load()
  939. if err != nil {
  940. t.Fatalf("Load() error: %v", err)
  941. }
  942. if !cfg.DashboardAgg.Enabled {
  943. t.Fatalf("DashboardAgg.Enabled = false, want true")
  944. }
  945. if cfg.DashboardAgg.IntervalSeconds != 60 {
  946. t.Fatalf("DashboardAgg.IntervalSeconds = %d, want 60", cfg.DashboardAgg.IntervalSeconds)
  947. }
  948. if cfg.DashboardAgg.LookbackSeconds != 120 {
  949. t.Fatalf("DashboardAgg.LookbackSeconds = %d, want 120", cfg.DashboardAgg.LookbackSeconds)
  950. }
  951. if cfg.DashboardAgg.BackfillEnabled {
  952. t.Fatalf("DashboardAgg.BackfillEnabled = true, want false")
  953. }
  954. if cfg.DashboardAgg.BackfillMaxDays != 31 {
  955. t.Fatalf("DashboardAgg.BackfillMaxDays = %d, want 31", cfg.DashboardAgg.BackfillMaxDays)
  956. }
  957. if cfg.DashboardAgg.Retention.UsageLogsDays != 90 {
  958. t.Fatalf("DashboardAgg.Retention.UsageLogsDays = %d, want 90", cfg.DashboardAgg.Retention.UsageLogsDays)
  959. }
  960. if cfg.DashboardAgg.Retention.UsageBillingDedupDays != 365 {
  961. t.Fatalf("DashboardAgg.Retention.UsageBillingDedupDays = %d, want 365", cfg.DashboardAgg.Retention.UsageBillingDedupDays)
  962. }
  963. if cfg.DashboardAgg.Retention.HourlyDays != 180 {
  964. t.Fatalf("DashboardAgg.Retention.HourlyDays = %d, want 180", cfg.DashboardAgg.Retention.HourlyDays)
  965. }
  966. if cfg.DashboardAgg.Retention.DailyDays != 730 {
  967. t.Fatalf("DashboardAgg.Retention.DailyDays = %d, want 730", cfg.DashboardAgg.Retention.DailyDays)
  968. }
  969. if cfg.DashboardAgg.RecomputeDays != 2 {
  970. t.Fatalf("DashboardAgg.RecomputeDays = %d, want 2", cfg.DashboardAgg.RecomputeDays)
  971. }
  972. }
  973. func TestValidateDashboardAggregationConfigDisabled(t *testing.T) {
  974. resetViperWithJWTSecret(t)
  975. cfg, err := Load()
  976. if err != nil {
  977. t.Fatalf("Load() error: %v", err)
  978. }
  979. cfg.DashboardAgg.Enabled = false
  980. cfg.DashboardAgg.IntervalSeconds = -1
  981. err = cfg.Validate()
  982. if err == nil {
  983. t.Fatalf("Validate() expected error for negative dashboard_aggregation.interval_seconds, got nil")
  984. }
  985. if !strings.Contains(err.Error(), "dashboard_aggregation.interval_seconds") {
  986. t.Fatalf("Validate() expected interval_seconds error, got: %v", err)
  987. }
  988. }
  989. func TestValidateDashboardAggregationBackfillMaxDays(t *testing.T) {
  990. resetViperWithJWTSecret(t)
  991. cfg, err := Load()
  992. if err != nil {
  993. t.Fatalf("Load() error: %v", err)
  994. }
  995. cfg.DashboardAgg.BackfillEnabled = true
  996. cfg.DashboardAgg.BackfillMaxDays = 0
  997. err = cfg.Validate()
  998. if err == nil {
  999. t.Fatalf("Validate() expected error for dashboard_aggregation.backfill_max_days, got nil")
  1000. }
  1001. if !strings.Contains(err.Error(), "dashboard_aggregation.backfill_max_days") {
  1002. t.Fatalf("Validate() expected backfill_max_days error, got: %v", err)
  1003. }
  1004. }
  1005. func TestLoadDefaultUsageCleanupConfig(t *testing.T) {
  1006. resetViperWithJWTSecret(t)
  1007. cfg, err := Load()
  1008. if err != nil {
  1009. t.Fatalf("Load() error: %v", err)
  1010. }
  1011. if !cfg.UsageCleanup.Enabled {
  1012. t.Fatalf("UsageCleanup.Enabled = false, want true")
  1013. }
  1014. if cfg.UsageCleanup.MaxRangeDays != 31 {
  1015. t.Fatalf("UsageCleanup.MaxRangeDays = %d, want 31", cfg.UsageCleanup.MaxRangeDays)
  1016. }
  1017. if cfg.UsageCleanup.BatchSize != 5000 {
  1018. t.Fatalf("UsageCleanup.BatchSize = %d, want 5000", cfg.UsageCleanup.BatchSize)
  1019. }
  1020. if cfg.UsageCleanup.WorkerIntervalSeconds != 10 {
  1021. t.Fatalf("UsageCleanup.WorkerIntervalSeconds = %d, want 10", cfg.UsageCleanup.WorkerIntervalSeconds)
  1022. }
  1023. if cfg.UsageCleanup.TaskTimeoutSeconds != 1800 {
  1024. t.Fatalf("UsageCleanup.TaskTimeoutSeconds = %d, want 1800", cfg.UsageCleanup.TaskTimeoutSeconds)
  1025. }
  1026. }
  1027. func TestLoadDefaultOpsCleanupConfig(t *testing.T) {
  1028. resetViperWithJWTSecret(t)
  1029. cfg, err := Load()
  1030. if err != nil {
  1031. t.Fatalf("Load() error: %v", err)
  1032. }
  1033. if !cfg.Ops.Cleanup.Enabled {
  1034. t.Fatal("Ops.Cleanup.Enabled = false, want true")
  1035. }
  1036. if cfg.Ops.Cleanup.SystemLogRetentionDays != 30 {
  1037. t.Fatalf("Ops.Cleanup.SystemLogRetentionDays = %d, want 30", cfg.Ops.Cleanup.SystemLogRetentionDays)
  1038. }
  1039. }
  1040. func TestValidateUsageCleanupConfigEnabled(t *testing.T) {
  1041. resetViperWithJWTSecret(t)
  1042. cfg, err := Load()
  1043. if err != nil {
  1044. t.Fatalf("Load() error: %v", err)
  1045. }
  1046. cfg.UsageCleanup.Enabled = true
  1047. cfg.UsageCleanup.MaxRangeDays = 0
  1048. err = cfg.Validate()
  1049. if err == nil {
  1050. t.Fatalf("Validate() expected error for usage_cleanup.max_range_days, got nil")
  1051. }
  1052. if !strings.Contains(err.Error(), "usage_cleanup.max_range_days") {
  1053. t.Fatalf("Validate() expected max_range_days error, got: %v", err)
  1054. }
  1055. }
  1056. func TestValidateUsageCleanupConfigDisabled(t *testing.T) {
  1057. resetViperWithJWTSecret(t)
  1058. cfg, err := Load()
  1059. if err != nil {
  1060. t.Fatalf("Load() error: %v", err)
  1061. }
  1062. cfg.UsageCleanup.Enabled = false
  1063. cfg.UsageCleanup.BatchSize = -1
  1064. err = cfg.Validate()
  1065. if err == nil {
  1066. t.Fatalf("Validate() expected error for usage_cleanup.batch_size, got nil")
  1067. }
  1068. if !strings.Contains(err.Error(), "usage_cleanup.batch_size") {
  1069. t.Fatalf("Validate() expected batch_size error, got: %v", err)
  1070. }
  1071. }
  1072. func TestConfigAddressHelpers(t *testing.T) {
  1073. server := ServerConfig{Host: "127.0.0.1", Port: 9000}
  1074. if server.Address() != "127.0.0.1:9000" {
  1075. t.Fatalf("ServerConfig.Address() = %q", server.Address())
  1076. }
  1077. dbCfg := DatabaseConfig{
  1078. Host: "localhost",
  1079. Port: 5432,
  1080. User: "postgres",
  1081. Password: "",
  1082. DBName: "sub2api",
  1083. SSLMode: "disable",
  1084. }
  1085. if !strings.Contains(dbCfg.DSN(), "password=") {
  1086. } else {
  1087. t.Fatalf("DatabaseConfig.DSN() should not include password when empty")
  1088. }
  1089. dbCfg.Password = "secret"
  1090. if !strings.Contains(dbCfg.DSN(), "password=secret") {
  1091. t.Fatalf("DatabaseConfig.DSN() missing password")
  1092. }
  1093. dbCfg.Password = ""
  1094. if strings.Contains(dbCfg.DSNWithTimezone("UTC"), "password=") {
  1095. t.Fatalf("DatabaseConfig.DSNWithTimezone() should omit password when empty")
  1096. }
  1097. if !strings.Contains(dbCfg.DSNWithTimezone(""), "TimeZone=Asia/Shanghai") {
  1098. t.Fatalf("DatabaseConfig.DSNWithTimezone() should use default timezone")
  1099. }
  1100. if !strings.Contains(dbCfg.DSNWithTimezone("UTC"), "TimeZone=UTC") {
  1101. t.Fatalf("DatabaseConfig.DSNWithTimezone() should use provided timezone")
  1102. }
  1103. redis := RedisConfig{Host: "redis", Port: 6379}
  1104. if redis.Address() != "redis:6379" {
  1105. t.Fatalf("RedisConfig.Address() = %q", redis.Address())
  1106. }
  1107. }
  1108. func TestNormalizeStringSlice(t *testing.T) {
  1109. values := normalizeStringSlice([]string{" a ", "", "b", " ", "c"})
  1110. if len(values) != 3 || values[0] != "a" || values[1] != "b" || values[2] != "c" {
  1111. t.Fatalf("normalizeStringSlice() unexpected result: %#v", values)
  1112. }
  1113. if normalizeStringSlice(nil) != nil {
  1114. t.Fatalf("normalizeStringSlice(nil) expected nil slice")
  1115. }
  1116. }
  1117. func TestGetServerAddressFromEnv(t *testing.T) {
  1118. t.Setenv("CONFIG_FILE", "")
  1119. t.Setenv("DATA_DIR", "")
  1120. t.Setenv("SERVER_HOST", "127.0.0.1")
  1121. t.Setenv("SERVER_PORT", "9090")
  1122. address := GetServerAddress()
  1123. if address != "127.0.0.1:9090" {
  1124. t.Fatalf("GetServerAddress() = %q", address)
  1125. }
  1126. }
  1127. func TestGetServerAddressFromConfigFile(t *testing.T) {
  1128. configFile := filepath.Join(t.TempDir(), "setup.yaml")
  1129. require.NoError(t, os.WriteFile(configFile, []byte("server:\n host: 192.0.2.40\n port: 9191\n"), 0o600))
  1130. t.Setenv("CONFIG_FILE", configFile)
  1131. t.Setenv("DATA_DIR", "")
  1132. t.Setenv("SERVER_HOST", "")
  1133. t.Setenv("SERVER_PORT", "")
  1134. require.Equal(t, "192.0.2.40:9191", GetServerAddress())
  1135. }
  1136. func TestValidateAbsoluteHTTPURL(t *testing.T) {
  1137. if err := ValidateAbsoluteHTTPURL("https://example.com/path"); err != nil {
  1138. t.Fatalf("ValidateAbsoluteHTTPURL valid url error: %v", err)
  1139. }
  1140. if err := ValidateAbsoluteHTTPURL(""); err == nil {
  1141. t.Fatalf("ValidateAbsoluteHTTPURL should reject empty url")
  1142. }
  1143. if err := ValidateAbsoluteHTTPURL("/relative"); err == nil {
  1144. t.Fatalf("ValidateAbsoluteHTTPURL should reject relative url")
  1145. }
  1146. if err := ValidateAbsoluteHTTPURL("ftp://example.com"); err == nil {
  1147. t.Fatalf("ValidateAbsoluteHTTPURL should reject ftp scheme")
  1148. }
  1149. if err := ValidateAbsoluteHTTPURL("https://example.com/#frag"); err == nil {
  1150. t.Fatalf("ValidateAbsoluteHTTPURL should reject fragment")
  1151. }
  1152. }
  1153. func TestValidateServerFrontendURL(t *testing.T) {
  1154. resetViperWithJWTSecret(t)
  1155. cfg, err := Load()
  1156. if err != nil {
  1157. t.Fatalf("Load() error: %v", err)
  1158. }
  1159. cfg.Server.FrontendURL = "https://example.com"
  1160. if err := cfg.Validate(); err != nil {
  1161. t.Fatalf("Validate() frontend_url valid error: %v", err)
  1162. }
  1163. cfg.Server.FrontendURL = "https://example.com/path"
  1164. if err := cfg.Validate(); err != nil {
  1165. t.Fatalf("Validate() frontend_url with path valid error: %v", err)
  1166. }
  1167. cfg.Server.FrontendURL = "https://example.com?utm=1"
  1168. if err := cfg.Validate(); err == nil {
  1169. t.Fatalf("Validate() should reject server.frontend_url with query")
  1170. }
  1171. cfg.Server.FrontendURL = "https://user:pass@example.com"
  1172. if err := cfg.Validate(); err == nil {
  1173. t.Fatalf("Validate() should reject server.frontend_url with userinfo")
  1174. }
  1175. cfg.Server.FrontendURL = "/relative"
  1176. if err := cfg.Validate(); err == nil {
  1177. t.Fatalf("Validate() should reject relative server.frontend_url")
  1178. }
  1179. }
  1180. func TestValidateFrontendRedirectURL(t *testing.T) {
  1181. if err := ValidateFrontendRedirectURL("/auth/callback"); err != nil {
  1182. t.Fatalf("ValidateFrontendRedirectURL relative error: %v", err)
  1183. }
  1184. if err := ValidateFrontendRedirectURL("https://example.com/auth"); err != nil {
  1185. t.Fatalf("ValidateFrontendRedirectURL absolute error: %v", err)
  1186. }
  1187. if err := ValidateFrontendRedirectURL("example.com/path"); err == nil {
  1188. t.Fatalf("ValidateFrontendRedirectURL should reject non-absolute url")
  1189. }
  1190. if err := ValidateFrontendRedirectURL("//evil.com"); err == nil {
  1191. t.Fatalf("ValidateFrontendRedirectURL should reject // prefix")
  1192. }
  1193. if err := ValidateFrontendRedirectURL("javascript:alert(1)"); err == nil {
  1194. t.Fatalf("ValidateFrontendRedirectURL should reject javascript scheme")
  1195. }
  1196. }
  1197. func TestWarnIfInsecureURL(t *testing.T) {
  1198. warnIfInsecureURL("test", "http://example.com")
  1199. warnIfInsecureURL("test", "bad://url")
  1200. warnIfInsecureURL("test", "://invalid")
  1201. }
  1202. func TestGenerateJWTSecretDefaultLength(t *testing.T) {
  1203. secret, err := generateJWTSecret(0)
  1204. if err != nil {
  1205. t.Fatalf("generateJWTSecret error: %v", err)
  1206. }
  1207. if len(secret) == 0 {
  1208. t.Fatalf("generateJWTSecret returned empty string")
  1209. }
  1210. }
  1211. func TestValidateOpsCleanupScheduleRequired(t *testing.T) {
  1212. resetViperWithJWTSecret(t)
  1213. cfg, err := Load()
  1214. if err != nil {
  1215. t.Fatalf("Load() error: %v", err)
  1216. }
  1217. cfg.Ops.Cleanup.Enabled = true
  1218. cfg.Ops.Cleanup.Schedule = ""
  1219. err = cfg.Validate()
  1220. if err == nil {
  1221. t.Fatalf("Validate() expected error for ops.cleanup.schedule")
  1222. }
  1223. if !strings.Contains(err.Error(), "ops.cleanup.schedule") {
  1224. t.Fatalf("Validate() expected ops.cleanup.schedule error, got: %v", err)
  1225. }
  1226. }
  1227. func TestValidateConcurrencyPingInterval(t *testing.T) {
  1228. resetViperWithJWTSecret(t)
  1229. cfg, err := Load()
  1230. if err != nil {
  1231. t.Fatalf("Load() error: %v", err)
  1232. }
  1233. cfg.Concurrency.PingInterval = 3
  1234. err = cfg.Validate()
  1235. if err == nil {
  1236. t.Fatalf("Validate() expected error for concurrency.ping_interval")
  1237. }
  1238. if !strings.Contains(err.Error(), "concurrency.ping_interval") {
  1239. t.Fatalf("Validate() expected concurrency.ping_interval error, got: %v", err)
  1240. }
  1241. }
  1242. func TestProvideConfig(t *testing.T) {
  1243. resetViperWithJWTSecret(t)
  1244. if _, err := ProvideConfig(); err != nil {
  1245. t.Fatalf("ProvideConfig() error: %v", err)
  1246. }
  1247. }
  1248. func TestValidateConfigWithLinuxDoEnabled(t *testing.T) {
  1249. resetViperWithJWTSecret(t)
  1250. cfg, err := Load()
  1251. if err != nil {
  1252. t.Fatalf("Load() error: %v", err)
  1253. }
  1254. cfg.Security.CSP.Enabled = true
  1255. cfg.Security.CSP.Policy = "default-src 'self'"
  1256. cfg.LinuxDo.Enabled = true
  1257. cfg.LinuxDo.ClientID = "client"
  1258. cfg.LinuxDo.ClientSecret = "secret"
  1259. cfg.LinuxDo.AuthorizeURL = "https://example.com/oauth2/authorize"
  1260. cfg.LinuxDo.TokenURL = "https://example.com/oauth2/token"
  1261. cfg.LinuxDo.UserInfoURL = "https://example.com/oauth2/userinfo"
  1262. cfg.LinuxDo.RedirectURL = "https://example.com/api/v1/auth/oauth/linuxdo/callback"
  1263. cfg.LinuxDo.FrontendRedirectURL = "/auth/linuxdo/callback"
  1264. cfg.LinuxDo.TokenAuthMethod = "client_secret_post"
  1265. cfg.LinuxDo.UsePKCE = true
  1266. if err := cfg.Validate(); err != nil {
  1267. t.Fatalf("Validate() unexpected error: %v", err)
  1268. }
  1269. }
  1270. func TestValidateJWTSecretStrength(t *testing.T) {
  1271. if !isWeakJWTSecret("change-me-in-production") {
  1272. t.Fatalf("isWeakJWTSecret should detect weak secret")
  1273. }
  1274. if isWeakJWTSecret("StrongSecretValue") {
  1275. t.Fatalf("isWeakJWTSecret should accept strong secret")
  1276. }
  1277. }
  1278. func TestGenerateJWTSecretWithLength(t *testing.T) {
  1279. secret, err := generateJWTSecret(16)
  1280. if err != nil {
  1281. t.Fatalf("generateJWTSecret error: %v", err)
  1282. }
  1283. if len(secret) == 0 {
  1284. t.Fatalf("generateJWTSecret returned empty string")
  1285. }
  1286. }
  1287. func TestDatabaseDSNWithTimezone_WithPassword(t *testing.T) {
  1288. d := &DatabaseConfig{
  1289. Host: "localhost",
  1290. Port: 5432,
  1291. User: "u",
  1292. Password: "p",
  1293. DBName: "db",
  1294. SSLMode: "prefer",
  1295. }
  1296. got := d.DSNWithTimezone("UTC")
  1297. if !strings.Contains(got, "password=p") {
  1298. t.Fatalf("DSNWithTimezone should include password: %q", got)
  1299. }
  1300. if !strings.Contains(got, "TimeZone=UTC") {
  1301. t.Fatalf("DSNWithTimezone should include TimeZone=UTC: %q", got)
  1302. }
  1303. }
  1304. func TestValidateAbsoluteHTTPURLMissingHost(t *testing.T) {
  1305. if err := ValidateAbsoluteHTTPURL("https://"); err == nil {
  1306. t.Fatalf("ValidateAbsoluteHTTPURL should reject missing host")
  1307. }
  1308. }
  1309. func TestValidateFrontendRedirectURLInvalidChars(t *testing.T) {
  1310. if err := ValidateFrontendRedirectURL("/auth/\ncallback"); err == nil {
  1311. t.Fatalf("ValidateFrontendRedirectURL should reject invalid chars")
  1312. }
  1313. if err := ValidateFrontendRedirectURL("http://"); err == nil {
  1314. t.Fatalf("ValidateFrontendRedirectURL should reject missing host")
  1315. }
  1316. if err := ValidateFrontendRedirectURL("mailto:user@example.com"); err == nil {
  1317. t.Fatalf("ValidateFrontendRedirectURL should reject mailto")
  1318. }
  1319. }
  1320. func TestWarnIfInsecureURLHTTPS(t *testing.T) {
  1321. warnIfInsecureURL("secure", "https://example.com")
  1322. }
  1323. func TestValidateJWTSecret_UTF8Bytes(t *testing.T) {
  1324. resetViperWithJWTSecret(t)
  1325. cfg, err := Load()
  1326. if err != nil {
  1327. t.Fatalf("Load() error: %v", err)
  1328. }
  1329. // 31 bytes (< 32) even though it's 31 characters.
  1330. cfg.JWT.Secret = strings.Repeat("a", 31)
  1331. err = cfg.Validate()
  1332. if err == nil {
  1333. t.Fatalf("Validate() should reject 31-byte secret")
  1334. }
  1335. if !strings.Contains(err.Error(), "at least 32 bytes") {
  1336. t.Fatalf("Validate() error = %v", err)
  1337. }
  1338. // 32 bytes OK.
  1339. cfg.JWT.Secret = strings.Repeat("a", 32)
  1340. err = cfg.Validate()
  1341. if err != nil {
  1342. t.Fatalf("Validate() should accept 32-byte secret: %v", err)
  1343. }
  1344. }
  1345. func TestValidateConfigErrors(t *testing.T) {
  1346. buildValid := func(t *testing.T) *Config {
  1347. t.Helper()
  1348. resetViperWithJWTSecret(t)
  1349. cfg, err := Load()
  1350. if err != nil {
  1351. t.Fatalf("Load() error: %v", err)
  1352. }
  1353. return cfg
  1354. }
  1355. cases := []struct {
  1356. name string
  1357. mutate func(*Config)
  1358. wantErr string
  1359. }{
  1360. {
  1361. name: "server read header timeout",
  1362. mutate: func(c *Config) { c.Server.ReadHeaderTimeout = 0 },
  1363. wantErr: "server.read_header_timeout",
  1364. },
  1365. {
  1366. name: "server max header bytes too small",
  1367. mutate: func(c *Config) { c.Server.MaxHeaderBytes = 4096 },
  1368. wantErr: "server.max_header_bytes",
  1369. },
  1370. {
  1371. name: "server max request body size",
  1372. mutate: func(c *Config) { c.Server.MaxRequestBodySize = -1 },
  1373. wantErr: "server.max_request_body_size",
  1374. },
  1375. {
  1376. name: "h2c zero concurrent streams",
  1377. mutate: func(c *Config) {
  1378. c.Server.H2C.Enabled = true
  1379. c.Server.H2C.MaxConcurrentStreams = 0
  1380. },
  1381. wantErr: "server.h2c.max_concurrent_streams",
  1382. },
  1383. {
  1384. name: "h2c oversized read frame",
  1385. mutate: func(c *Config) {
  1386. c.Server.H2C.Enabled = true
  1387. c.Server.H2C.MaxReadFrameSize = 16 * 1024 * 1024
  1388. },
  1389. wantErr: "server.h2c.max_read_frame_size",
  1390. },
  1391. {
  1392. name: "invalid auth abuse threshold too small",
  1393. mutate: func(c *Config) {
  1394. c.APIKeyAuth.InvalidAbuse.Threshold = 9
  1395. },
  1396. wantErr: "api_key_auth_cache.invalid_abuse.threshold",
  1397. },
  1398. {
  1399. name: "invalid auth abuse capacity too small",
  1400. mutate: func(c *Config) {
  1401. c.APIKeyAuth.InvalidAbuse.Capacity = 255
  1402. },
  1403. wantErr: "api_key_auth_cache.invalid_abuse.capacity",
  1404. },
  1405. {
  1406. name: "jwt secret required",
  1407. mutate: func(c *Config) { c.JWT.Secret = "" },
  1408. wantErr: "jwt.secret is required",
  1409. },
  1410. {
  1411. name: "jwt secret min bytes",
  1412. mutate: func(c *Config) { c.JWT.Secret = strings.Repeat("a", 31) },
  1413. wantErr: "jwt.secret must be at least 32 bytes",
  1414. },
  1415. {
  1416. name: "subscription maintenance worker_count non-negative",
  1417. mutate: func(c *Config) { c.SubscriptionMaintenance.WorkerCount = -1 },
  1418. wantErr: "subscription_maintenance.worker_count",
  1419. },
  1420. {
  1421. name: "subscription maintenance queue_size non-negative",
  1422. mutate: func(c *Config) { c.SubscriptionMaintenance.QueueSize = -1 },
  1423. wantErr: "subscription_maintenance.queue_size",
  1424. },
  1425. {
  1426. name: "jwt expire hour positive",
  1427. mutate: func(c *Config) { c.JWT.ExpireHour = 0 },
  1428. wantErr: "jwt.expire_hour must be positive",
  1429. },
  1430. {
  1431. name: "jwt expire hour max",
  1432. mutate: func(c *Config) { c.JWT.ExpireHour = 200 },
  1433. wantErr: "jwt.expire_hour must be <= 168",
  1434. },
  1435. {
  1436. name: "jwt access token expire minutes non-negative",
  1437. mutate: func(c *Config) { c.JWT.AccessTokenExpireMinutes = -1 },
  1438. wantErr: "jwt.access_token_expire_minutes must be non-negative",
  1439. },
  1440. {
  1441. name: "csp policy required",
  1442. mutate: func(c *Config) { c.Security.CSP.Enabled = true; c.Security.CSP.Policy = "" },
  1443. wantErr: "security.csp.policy",
  1444. },
  1445. {
  1446. name: "linuxdo client id required",
  1447. mutate: func(c *Config) {
  1448. c.LinuxDo.Enabled = true
  1449. c.LinuxDo.UsePKCE = true
  1450. c.LinuxDo.ClientID = ""
  1451. },
  1452. wantErr: "linuxdo_connect.client_id",
  1453. },
  1454. {
  1455. name: "linuxdo token auth method",
  1456. mutate: func(c *Config) {
  1457. c.LinuxDo.Enabled = true
  1458. c.LinuxDo.UsePKCE = true
  1459. c.LinuxDo.ClientID = "client"
  1460. c.LinuxDo.ClientSecret = "secret"
  1461. c.LinuxDo.AuthorizeURL = "https://example.com/authorize"
  1462. c.LinuxDo.TokenURL = "https://example.com/token"
  1463. c.LinuxDo.UserInfoURL = "https://example.com/userinfo"
  1464. c.LinuxDo.RedirectURL = "https://example.com/callback"
  1465. c.LinuxDo.FrontendRedirectURL = "/auth/callback"
  1466. c.LinuxDo.TokenAuthMethod = "invalid"
  1467. },
  1468. wantErr: "linuxdo_connect.token_auth_method",
  1469. },
  1470. {
  1471. name: "billing circuit breaker threshold",
  1472. mutate: func(c *Config) { c.Billing.CircuitBreaker.FailureThreshold = 0 },
  1473. wantErr: "billing.circuit_breaker.failure_threshold",
  1474. },
  1475. {
  1476. name: "billing circuit breaker reset",
  1477. mutate: func(c *Config) { c.Billing.CircuitBreaker.ResetTimeoutSeconds = 0 },
  1478. wantErr: "billing.circuit_breaker.reset_timeout_seconds",
  1479. },
  1480. {
  1481. name: "billing circuit breaker half open",
  1482. mutate: func(c *Config) { c.Billing.CircuitBreaker.HalfOpenRequests = 0 },
  1483. wantErr: "billing.circuit_breaker.half_open_requests",
  1484. },
  1485. {
  1486. name: "billing minimum balance reserve",
  1487. mutate: func(c *Config) { c.Billing.MinimumBalanceReserve = -0.01 },
  1488. wantErr: "billing.minimum_balance_reserve",
  1489. },
  1490. {
  1491. name: "database max open conns",
  1492. mutate: func(c *Config) { c.Database.MaxOpenConns = 0 },
  1493. wantErr: "database.max_open_conns",
  1494. },
  1495. {
  1496. name: "database max lifetime",
  1497. mutate: func(c *Config) { c.Database.ConnMaxLifetimeMinutes = -1 },
  1498. wantErr: "database.conn_max_lifetime_minutes",
  1499. },
  1500. {
  1501. name: "database idle exceeds open",
  1502. mutate: func(c *Config) { c.Database.MaxIdleConns = c.Database.MaxOpenConns + 1 },
  1503. wantErr: "database.max_idle_conns cannot exceed",
  1504. },
  1505. {
  1506. name: "redis dial timeout",
  1507. mutate: func(c *Config) { c.Redis.DialTimeoutSeconds = 0 },
  1508. wantErr: "redis.dial_timeout_seconds",
  1509. },
  1510. {
  1511. name: "redis read timeout",
  1512. mutate: func(c *Config) { c.Redis.ReadTimeoutSeconds = 0 },
  1513. wantErr: "redis.read_timeout_seconds",
  1514. },
  1515. {
  1516. name: "redis write timeout",
  1517. mutate: func(c *Config) { c.Redis.WriteTimeoutSeconds = 0 },
  1518. wantErr: "redis.write_timeout_seconds",
  1519. },
  1520. {
  1521. name: "redis pool size",
  1522. mutate: func(c *Config) { c.Redis.PoolSize = 0 },
  1523. wantErr: "redis.pool_size",
  1524. },
  1525. {
  1526. name: "redis idle exceeds pool",
  1527. mutate: func(c *Config) { c.Redis.MinIdleConns = c.Redis.PoolSize + 1 },
  1528. wantErr: "redis.min_idle_conns cannot exceed",
  1529. },
  1530. {
  1531. name: "dashboard cache disabled negative",
  1532. mutate: func(c *Config) { c.Dashboard.Enabled = false; c.Dashboard.StatsTTLSeconds = -1 },
  1533. wantErr: "dashboard_cache.stats_ttl_seconds",
  1534. },
  1535. {
  1536. name: "dashboard cache fresh ttl positive",
  1537. mutate: func(c *Config) { c.Dashboard.Enabled = true; c.Dashboard.StatsFreshTTLSeconds = 0 },
  1538. wantErr: "dashboard_cache.stats_fresh_ttl_seconds",
  1539. },
  1540. {
  1541. name: "dashboard aggregation enabled interval",
  1542. mutate: func(c *Config) { c.DashboardAgg.Enabled = true; c.DashboardAgg.IntervalSeconds = 0 },
  1543. wantErr: "dashboard_aggregation.interval_seconds",
  1544. },
  1545. {
  1546. name: "dashboard aggregation backfill positive",
  1547. mutate: func(c *Config) {
  1548. c.DashboardAgg.Enabled = true
  1549. c.DashboardAgg.BackfillEnabled = true
  1550. c.DashboardAgg.BackfillMaxDays = 0
  1551. },
  1552. wantErr: "dashboard_aggregation.backfill_max_days",
  1553. },
  1554. {
  1555. name: "dashboard aggregation retention",
  1556. mutate: func(c *Config) { c.DashboardAgg.Enabled = true; c.DashboardAgg.Retention.UsageLogsDays = 0 },
  1557. wantErr: "dashboard_aggregation.retention.usage_logs_days",
  1558. },
  1559. {
  1560. name: "dashboard aggregation dedup retention",
  1561. mutate: func(c *Config) {
  1562. c.DashboardAgg.Enabled = true
  1563. c.DashboardAgg.Retention.UsageBillingDedupDays = 0
  1564. },
  1565. wantErr: "dashboard_aggregation.retention.usage_billing_dedup_days",
  1566. },
  1567. {
  1568. name: "dashboard aggregation dedup retention smaller than usage logs",
  1569. mutate: func(c *Config) {
  1570. c.DashboardAgg.Enabled = true
  1571. c.DashboardAgg.Retention.UsageLogsDays = 30
  1572. c.DashboardAgg.Retention.UsageBillingDedupDays = 29
  1573. },
  1574. wantErr: "dashboard_aggregation.retention.usage_billing_dedup_days",
  1575. },
  1576. {
  1577. name: "dashboard aggregation disabled interval",
  1578. mutate: func(c *Config) { c.DashboardAgg.Enabled = false; c.DashboardAgg.IntervalSeconds = -1 },
  1579. wantErr: "dashboard_aggregation.interval_seconds",
  1580. },
  1581. {
  1582. name: "usage cleanup max range",
  1583. mutate: func(c *Config) { c.UsageCleanup.Enabled = true; c.UsageCleanup.MaxRangeDays = 0 },
  1584. wantErr: "usage_cleanup.max_range_days",
  1585. },
  1586. {
  1587. name: "usage cleanup worker interval",
  1588. mutate: func(c *Config) { c.UsageCleanup.Enabled = true; c.UsageCleanup.WorkerIntervalSeconds = 0 },
  1589. wantErr: "usage_cleanup.worker_interval_seconds",
  1590. },
  1591. {
  1592. name: "usage cleanup batch size",
  1593. mutate: func(c *Config) { c.UsageCleanup.Enabled = true; c.UsageCleanup.BatchSize = 0 },
  1594. wantErr: "usage_cleanup.batch_size",
  1595. },
  1596. {
  1597. name: "usage cleanup disabled negative",
  1598. mutate: func(c *Config) { c.UsageCleanup.Enabled = false; c.UsageCleanup.BatchSize = -1 },
  1599. wantErr: "usage_cleanup.batch_size",
  1600. },
  1601. {
  1602. name: "gateway max body size",
  1603. mutate: func(c *Config) { c.Gateway.MaxBodySize = 0 },
  1604. wantErr: "gateway.max_body_size",
  1605. },
  1606. {
  1607. name: "gateway text body exceeds media body",
  1608. mutate: func(c *Config) { c.Gateway.TextMaxBodySize = c.Gateway.MaxBodySize + 1 },
  1609. wantErr: "gateway.text_max_body_size",
  1610. },
  1611. {
  1612. name: "gateway models list read limit",
  1613. mutate: func(c *Config) { c.Gateway.ModelsListReadMaxBytes = 0 },
  1614. wantErr: "gateway.models_list_read_max_bytes",
  1615. },
  1616. {
  1617. name: "gateway response header timeout",
  1618. mutate: func(c *Config) { c.Gateway.ResponseHeaderTimeout = -1 },
  1619. wantErr: "gateway.response_header_timeout",
  1620. },
  1621. {
  1622. name: "gateway openai response header timeout",
  1623. mutate: func(c *Config) { c.Gateway.OpenAIResponseHeaderTimeout = -1 },
  1624. wantErr: "gateway.openai_response_header_timeout",
  1625. },
  1626. {
  1627. name: "gateway openai first output timeout below minimum",
  1628. mutate: func(c *Config) { c.Gateway.OpenAIFirstOutputTimeoutSeconds = 29 },
  1629. wantErr: "gateway.openai_first_output_timeout_seconds",
  1630. },
  1631. {
  1632. name: "gateway openai high effort first output timeout too large",
  1633. mutate: func(c *Config) { c.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds = 1801 },
  1634. wantErr: "gateway.openai_high_effort_first_output_timeout_seconds",
  1635. },
  1636. {
  1637. name: "gateway max idle conns",
  1638. mutate: func(c *Config) { c.Gateway.MaxIdleConns = 0 },
  1639. wantErr: "gateway.max_idle_conns",
  1640. },
  1641. {
  1642. name: "gateway max idle conns per host",
  1643. mutate: func(c *Config) { c.Gateway.MaxIdleConnsPerHost = 0 },
  1644. wantErr: "gateway.max_idle_conns_per_host",
  1645. },
  1646. {
  1647. name: "gateway idle timeout",
  1648. mutate: func(c *Config) { c.Gateway.IdleConnTimeoutSeconds = 0 },
  1649. wantErr: "gateway.idle_conn_timeout_seconds",
  1650. },
  1651. {
  1652. name: "gateway max upstream clients",
  1653. mutate: func(c *Config) { c.Gateway.MaxUpstreamClients = 0 },
  1654. wantErr: "gateway.max_upstream_clients",
  1655. },
  1656. {
  1657. name: "gateway client idle ttl",
  1658. mutate: func(c *Config) { c.Gateway.ClientIdleTTLSeconds = 0 },
  1659. wantErr: "gateway.client_idle_ttl_seconds",
  1660. },
  1661. {
  1662. name: "gateway concurrency slot ttl",
  1663. mutate: func(c *Config) { c.Gateway.ConcurrencySlotTTLMinutes = 0 },
  1664. wantErr: "gateway.concurrency_slot_ttl_minutes",
  1665. },
  1666. {
  1667. name: "gateway max conns per host",
  1668. mutate: func(c *Config) { c.Gateway.MaxConnsPerHost = -1 },
  1669. wantErr: "gateway.max_conns_per_host",
  1670. },
  1671. {
  1672. name: "gateway connection isolation",
  1673. mutate: func(c *Config) { c.Gateway.ConnectionPoolIsolation = "invalid" },
  1674. wantErr: "gateway.connection_pool_isolation",
  1675. },
  1676. {
  1677. name: "gateway stream keepalive range",
  1678. mutate: func(c *Config) { c.Gateway.StreamKeepaliveInterval = 4 },
  1679. wantErr: "gateway.stream_keepalive_interval",
  1680. },
  1681. {
  1682. name: "gateway openai ws oauth max conns factor",
  1683. mutate: func(c *Config) { c.Gateway.OpenAIWS.OAuthMaxConnsFactor = 0 },
  1684. wantErr: "gateway.openai_ws.oauth_max_conns_factor",
  1685. },
  1686. {
  1687. name: "gateway openai ws apikey max conns factor",
  1688. mutate: func(c *Config) { c.Gateway.OpenAIWS.APIKeyMaxConnsFactor = 0 },
  1689. wantErr: "gateway.openai_ws.apikey_max_conns_factor",
  1690. },
  1691. {
  1692. name: "gateway openai http2 fallback threshold",
  1693. mutate: func(c *Config) { c.Gateway.OpenAIHTTP2.FallbackErrorThreshold = -1 },
  1694. wantErr: "gateway.openai_http2.fallback_error_threshold",
  1695. },
  1696. {
  1697. name: "gateway openai http2 fallback window",
  1698. mutate: func(c *Config) { c.Gateway.OpenAIHTTP2.FallbackWindowSeconds = -1 },
  1699. wantErr: "gateway.openai_http2.fallback_window_seconds",
  1700. },
  1701. {
  1702. name: "gateway openai http2 fallback ttl",
  1703. mutate: func(c *Config) { c.Gateway.OpenAIHTTP2.FallbackTTLSeconds = -1 },
  1704. wantErr: "gateway.openai_http2.fallback_ttl_seconds",
  1705. },
  1706. {
  1707. name: "gateway openai proxy stream circuit threshold",
  1708. mutate: func(c *Config) { c.Gateway.OpenAIProxyStreamCircuit.FailureThreshold = -1 },
  1709. wantErr: "gateway.openai_proxy_stream_circuit.failure_threshold",
  1710. },
  1711. {
  1712. name: "gateway openai proxy stream circuit window",
  1713. mutate: func(c *Config) { c.Gateway.OpenAIProxyStreamCircuit.WindowSeconds = -1 },
  1714. wantErr: "gateway.openai_proxy_stream_circuit.window_seconds",
  1715. },
  1716. {
  1717. name: "gateway openai proxy stream circuit ttl",
  1718. mutate: func(c *Config) { c.Gateway.OpenAIProxyStreamCircuit.TTLSeconds = -1 },
  1719. wantErr: "gateway.openai_proxy_stream_circuit.ttl_seconds",
  1720. },
  1721. {
  1722. name: "gateway stream data interval range",
  1723. mutate: func(c *Config) { c.Gateway.StreamDataIntervalTimeout = 5 },
  1724. wantErr: "gateway.stream_data_interval_timeout",
  1725. },
  1726. {
  1727. name: "gateway stream data interval negative",
  1728. mutate: func(c *Config) { c.Gateway.StreamDataIntervalTimeout = -1 },
  1729. wantErr: "gateway.stream_data_interval_timeout must be non-negative",
  1730. },
  1731. {
  1732. name: "gateway image stream keepalive range",
  1733. mutate: func(c *Config) { c.Gateway.ImageStreamKeepaliveInterval = 4 },
  1734. wantErr: "gateway.image_stream_keepalive_interval",
  1735. },
  1736. {
  1737. name: "gateway image stream keepalive negative",
  1738. mutate: func(c *Config) { c.Gateway.ImageStreamKeepaliveInterval = -1 },
  1739. wantErr: "gateway.image_stream_keepalive_interval must be non-negative",
  1740. },
  1741. {
  1742. name: "gateway image nonstream keepalive range",
  1743. mutate: func(c *Config) { c.Gateway.ImageNonstreamKeepaliveInterval = 4 },
  1744. wantErr: "gateway.image_nonstream_keepalive_interval",
  1745. },
  1746. {
  1747. name: "gateway image nonstream keepalive negative",
  1748. mutate: func(c *Config) { c.Gateway.ImageNonstreamKeepaliveInterval = -1 },
  1749. wantErr: "gateway.image_nonstream_keepalive_interval must be non-negative",
  1750. },
  1751. {
  1752. name: "gateway image stream data interval range",
  1753. mutate: func(c *Config) { c.Gateway.ImageStreamDataIntervalTimeout = 30 },
  1754. wantErr: "gateway.image_stream_data_interval_timeout",
  1755. },
  1756. {
  1757. name: "gateway image stream data interval negative",
  1758. mutate: func(c *Config) { c.Gateway.ImageStreamDataIntervalTimeout = -1 },
  1759. wantErr: "gateway.image_stream_data_interval_timeout must be non-negative",
  1760. },
  1761. {
  1762. name: "gateway image concurrency max negative",
  1763. mutate: func(c *Config) { c.Gateway.ImageConcurrency.MaxConcurrentRequests = -1 },
  1764. wantErr: "gateway.image_concurrency.max_concurrent_requests must be non-negative",
  1765. },
  1766. {
  1767. name: "gateway image concurrency overflow mode invalid",
  1768. mutate: func(c *Config) { c.Gateway.ImageConcurrency.OverflowMode = "queue" },
  1769. wantErr: "gateway.image_concurrency.overflow_mode",
  1770. },
  1771. {
  1772. name: "gateway image concurrency wait timeout negative",
  1773. mutate: func(c *Config) { c.Gateway.ImageConcurrency.WaitTimeoutSeconds = -1 },
  1774. wantErr: "gateway.image_concurrency.wait_timeout_seconds must be non-negative",
  1775. },
  1776. {
  1777. name: "gateway image concurrency max waiting negative",
  1778. mutate: func(c *Config) { c.Gateway.ImageConcurrency.MaxWaitingRequests = -1 },
  1779. wantErr: "gateway.image_concurrency.max_waiting_requests must be non-negative",
  1780. },
  1781. {
  1782. name: "gateway max line size",
  1783. mutate: func(c *Config) { c.Gateway.MaxLineSize = 1024 },
  1784. wantErr: "gateway.max_line_size must be at least",
  1785. },
  1786. {
  1787. name: "gateway max line size negative",
  1788. mutate: func(c *Config) { c.Gateway.MaxLineSize = -1 },
  1789. wantErr: "gateway.max_line_size must be non-negative",
  1790. },
  1791. {
  1792. name: "gateway usage record worker count",
  1793. mutate: func(c *Config) { c.Gateway.UsageRecord.WorkerCount = 0 },
  1794. wantErr: "gateway.usage_record.worker_count",
  1795. },
  1796. {
  1797. name: "gateway usage record queue size",
  1798. mutate: func(c *Config) { c.Gateway.UsageRecord.QueueSize = 0 },
  1799. wantErr: "gateway.usage_record.queue_size",
  1800. },
  1801. {
  1802. name: "gateway usage record timeout",
  1803. mutate: func(c *Config) { c.Gateway.UsageRecord.TaskTimeoutSeconds = 0 },
  1804. wantErr: "gateway.usage_record.task_timeout_seconds",
  1805. },
  1806. {
  1807. name: "gateway usage record overflow policy",
  1808. mutate: func(c *Config) { c.Gateway.UsageRecord.OverflowPolicy = "invalid" },
  1809. wantErr: "gateway.usage_record.overflow_policy",
  1810. },
  1811. {
  1812. name: "gateway usage record sample percent range",
  1813. mutate: func(c *Config) { c.Gateway.UsageRecord.OverflowSamplePercent = 101 },
  1814. wantErr: "gateway.usage_record.overflow_sample_percent",
  1815. },
  1816. {
  1817. name: "gateway usage record sample percent required for sample policy",
  1818. mutate: func(c *Config) {
  1819. c.Gateway.UsageRecord.OverflowPolicy = UsageRecordOverflowPolicySample
  1820. c.Gateway.UsageRecord.OverflowSamplePercent = 0
  1821. },
  1822. wantErr: "gateway.usage_record.overflow_sample_percent must be positive",
  1823. },
  1824. {
  1825. name: "gateway usage record auto scale max gte min",
  1826. mutate: func(c *Config) {
  1827. c.Gateway.UsageRecord.AutoScaleMinWorkers = 256
  1828. c.Gateway.UsageRecord.AutoScaleMaxWorkers = 128
  1829. },
  1830. wantErr: "gateway.usage_record.auto_scale_max_workers",
  1831. },
  1832. {
  1833. name: "gateway usage record worker in auto scale range",
  1834. mutate: func(c *Config) {
  1835. c.Gateway.UsageRecord.AutoScaleMinWorkers = 200
  1836. c.Gateway.UsageRecord.AutoScaleMaxWorkers = 300
  1837. c.Gateway.UsageRecord.WorkerCount = 128
  1838. },
  1839. wantErr: "gateway.usage_record.worker_count must be between auto_scale_min_workers and auto_scale_max_workers",
  1840. },
  1841. {
  1842. name: "gateway usage record auto scale queue thresholds order",
  1843. mutate: func(c *Config) {
  1844. c.Gateway.UsageRecord.AutoScaleUpQueuePercent = 50
  1845. c.Gateway.UsageRecord.AutoScaleDownQueuePercent = 50
  1846. },
  1847. wantErr: "gateway.usage_record.auto_scale_down_queue_percent must be less",
  1848. },
  1849. {
  1850. name: "gateway usage record auto scale up step",
  1851. mutate: func(c *Config) { c.Gateway.UsageRecord.AutoScaleUpStep = 0 },
  1852. wantErr: "gateway.usage_record.auto_scale_up_step",
  1853. },
  1854. {
  1855. name: "gateway usage record auto scale interval",
  1856. mutate: func(c *Config) { c.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds = 0 },
  1857. wantErr: "gateway.usage_record.auto_scale_check_interval_seconds",
  1858. },
  1859. {
  1860. name: "gateway user group rate cache ttl",
  1861. mutate: func(c *Config) { c.Gateway.UserGroupRateCacheTTLSeconds = 0 },
  1862. wantErr: "gateway.user_group_rate_cache_ttl_seconds",
  1863. },
  1864. {
  1865. name: "gateway models list cache ttl range",
  1866. mutate: func(c *Config) { c.Gateway.ModelsListCacheTTLSeconds = 31 },
  1867. wantErr: "gateway.models_list_cache_ttl_seconds",
  1868. },
  1869. {
  1870. name: "gateway scheduling sticky waiting",
  1871. mutate: func(c *Config) { c.Gateway.Scheduling.StickySessionMaxWaiting = 0 },
  1872. wantErr: "gateway.scheduling.sticky_session_max_waiting",
  1873. },
  1874. {
  1875. name: "gateway scheduling load batch cache ttl",
  1876. mutate: func(c *Config) { c.Gateway.Scheduling.LoadBatchCacheTTLMS = -1 },
  1877. wantErr: "gateway.scheduling.load_batch_cache_ttl_ms",
  1878. },
  1879. {
  1880. name: "gateway scheduling outbox poll",
  1881. mutate: func(c *Config) { c.Gateway.Scheduling.OutboxPollIntervalSeconds = 0 },
  1882. wantErr: "gateway.scheduling.outbox_poll_interval_seconds",
  1883. },
  1884. {
  1885. name: "gateway scheduling outbox failures",
  1886. mutate: func(c *Config) { c.Gateway.Scheduling.OutboxLagRebuildFailures = 0 },
  1887. wantErr: "gateway.scheduling.outbox_lag_rebuild_failures",
  1888. },
  1889. {
  1890. name: "gateway outbox lag rebuild",
  1891. mutate: func(c *Config) {
  1892. c.Gateway.Scheduling.OutboxLagWarnSeconds = 10
  1893. c.Gateway.Scheduling.OutboxLagRebuildSeconds = 5
  1894. },
  1895. wantErr: "gateway.scheduling.outbox_lag_rebuild_seconds",
  1896. },
  1897. {
  1898. name: "log level invalid",
  1899. mutate: func(c *Config) { c.Log.Level = "trace" },
  1900. wantErr: "log.level",
  1901. },
  1902. {
  1903. name: "log format invalid",
  1904. mutate: func(c *Config) { c.Log.Format = "plain" },
  1905. wantErr: "log.format",
  1906. },
  1907. {
  1908. name: "log output disabled",
  1909. mutate: func(c *Config) {
  1910. c.Log.Output.ToStdout = false
  1911. c.Log.Output.ToFile = false
  1912. },
  1913. wantErr: "log.output.to_stdout and log.output.to_file cannot both be false",
  1914. },
  1915. {
  1916. name: "log rotation size",
  1917. mutate: func(c *Config) { c.Log.Rotation.MaxSizeMB = 0 },
  1918. wantErr: "log.rotation.max_size_mb",
  1919. },
  1920. {
  1921. name: "log sampling enabled invalid",
  1922. mutate: func(c *Config) {
  1923. c.Log.Sampling.Enabled = true
  1924. c.Log.Sampling.Initial = 0
  1925. },
  1926. wantErr: "log.sampling.initial",
  1927. },
  1928. {
  1929. name: "ops metrics collector ttl",
  1930. mutate: func(c *Config) { c.Ops.MetricsCollectorCache.TTL = -1 },
  1931. wantErr: "ops.metrics_collector_cache.ttl",
  1932. },
  1933. {
  1934. name: "ops cleanup retention",
  1935. mutate: func(c *Config) { c.Ops.Cleanup.ErrorLogRetentionDays = -1 },
  1936. wantErr: "ops.cleanup.error_log_retention_days",
  1937. },
  1938. {
  1939. name: "ops cleanup system log retention",
  1940. mutate: func(c *Config) { c.Ops.Cleanup.SystemLogRetentionDays = 0 },
  1941. wantErr: "ops.cleanup.system_log_retention_days",
  1942. },
  1943. {
  1944. name: "ops cleanup minute retention",
  1945. mutate: func(c *Config) { c.Ops.Cleanup.MinuteMetricsRetentionDays = -1 },
  1946. wantErr: "ops.cleanup.minute_metrics_retention_days",
  1947. },
  1948. }
  1949. for _, tt := range cases {
  1950. t.Run(tt.name, func(t *testing.T) {
  1951. cfg := buildValid(t)
  1952. tt.mutate(cfg)
  1953. err := cfg.Validate()
  1954. if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
  1955. t.Fatalf("Validate() error = %v, want %q", err, tt.wantErr)
  1956. }
  1957. })
  1958. }
  1959. }
  1960. func TestValidateConfig_OpenAIWSRules(t *testing.T) {
  1961. buildValid := func(t *testing.T) *Config {
  1962. t.Helper()
  1963. resetViperWithJWTSecret(t)
  1964. cfg, err := Load()
  1965. require.NoError(t, err)
  1966. return cfg
  1967. }
  1968. t.Run("sticky response id ttl 兼容旧键回填", func(t *testing.T) {
  1969. cfg := buildValid(t)
  1970. cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds = 0
  1971. cfg.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds = 7200
  1972. require.NoError(t, cfg.Validate())
  1973. require.Equal(t, 7200, cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds)
  1974. })
  1975. cases := []struct {
  1976. name string
  1977. mutate func(*Config)
  1978. wantErr string
  1979. }{
  1980. {
  1981. name: "max_conns_per_account 必须为正数",
  1982. mutate: func(c *Config) { c.Gateway.OpenAIWS.MaxConnsPerAccount = 0 },
  1983. wantErr: "gateway.openai_ws.max_conns_per_account",
  1984. },
  1985. {
  1986. name: "client_first_message_timeout_seconds 必须为正数",
  1987. mutate: func(c *Config) { c.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds = 0 },
  1988. wantErr: "gateway.openai_ws.client_first_message_timeout_seconds",
  1989. },
  1990. {
  1991. name: "client_first_message_timeout_seconds 不能为负数",
  1992. mutate: func(c *Config) { c.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds = -1 },
  1993. wantErr: "gateway.openai_ws.client_first_message_timeout_seconds",
  1994. },
  1995. {
  1996. name: "ingress_inter_turn_idle_timeout_seconds 不能为负数",
  1997. mutate: func(c *Config) { c.Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds = -1 },
  1998. wantErr: "gateway.openai_ws.ingress_inter_turn_idle_timeout_seconds",
  1999. },
  2000. {
  2001. name: "max_ingress_connections_per_api_key 不能为负数",
  2002. mutate: func(c *Config) { c.Gateway.OpenAIWS.MaxIngressConnectionsPerAPIKey = -1 },
  2003. wantErr: "gateway.openai_ws.max_ingress_connections_per_api_key",
  2004. },
  2005. {
  2006. name: "min_idle_per_account 不能为负数",
  2007. mutate: func(c *Config) { c.Gateway.OpenAIWS.MinIdlePerAccount = -1 },
  2008. wantErr: "gateway.openai_ws.min_idle_per_account",
  2009. },
  2010. {
  2011. name: "max_idle_per_account 不能为负数",
  2012. mutate: func(c *Config) { c.Gateway.OpenAIWS.MaxIdlePerAccount = -1 },
  2013. wantErr: "gateway.openai_ws.max_idle_per_account",
  2014. },
  2015. {
  2016. name: "min_idle_per_account 不能大于 max_idle_per_account",
  2017. mutate: func(c *Config) {
  2018. c.Gateway.OpenAIWS.MinIdlePerAccount = 3
  2019. c.Gateway.OpenAIWS.MaxIdlePerAccount = 2
  2020. },
  2021. wantErr: "gateway.openai_ws.min_idle_per_account must be <= max_idle_per_account",
  2022. },
  2023. {
  2024. name: "max_idle_per_account 不能大于 max_conns_per_account",
  2025. mutate: func(c *Config) {
  2026. c.Gateway.OpenAIWS.MaxConnsPerAccount = 2
  2027. c.Gateway.OpenAIWS.MinIdlePerAccount = 1
  2028. c.Gateway.OpenAIWS.MaxIdlePerAccount = 3
  2029. },
  2030. wantErr: "gateway.openai_ws.max_idle_per_account must be <= max_conns_per_account",
  2031. },
  2032. {
  2033. name: "dial_timeout_seconds 必须为正数",
  2034. mutate: func(c *Config) { c.Gateway.OpenAIWS.DialTimeoutSeconds = 0 },
  2035. wantErr: "gateway.openai_ws.dial_timeout_seconds",
  2036. },
  2037. {
  2038. name: "read_timeout_seconds 必须为正数",
  2039. mutate: func(c *Config) { c.Gateway.OpenAIWS.ReadTimeoutSeconds = 0 },
  2040. wantErr: "gateway.openai_ws.read_timeout_seconds",
  2041. },
  2042. {
  2043. name: "write_timeout_seconds 必须为正数",
  2044. mutate: func(c *Config) { c.Gateway.OpenAIWS.WriteTimeoutSeconds = 0 },
  2045. wantErr: "gateway.openai_ws.write_timeout_seconds",
  2046. },
  2047. {
  2048. name: "pool_target_utilization 必须在 (0,1]",
  2049. mutate: func(c *Config) { c.Gateway.OpenAIWS.PoolTargetUtilization = 0 },
  2050. wantErr: "gateway.openai_ws.pool_target_utilization",
  2051. },
  2052. {
  2053. name: "queue_limit_per_conn 必须为正数",
  2054. mutate: func(c *Config) { c.Gateway.OpenAIWS.QueueLimitPerConn = 0 },
  2055. wantErr: "gateway.openai_ws.queue_limit_per_conn",
  2056. },
  2057. {
  2058. name: "fallback_cooldown_seconds 不能为负数",
  2059. mutate: func(c *Config) { c.Gateway.OpenAIWS.FallbackCooldownSeconds = -1 },
  2060. wantErr: "gateway.openai_ws.fallback_cooldown_seconds",
  2061. },
  2062. {
  2063. name: "store_disabled_conn_mode 必须为 strict|adaptive|off",
  2064. mutate: func(c *Config) { c.Gateway.OpenAIWS.StoreDisabledConnMode = "invalid" },
  2065. wantErr: "gateway.openai_ws.store_disabled_conn_mode",
  2066. },
  2067. {
  2068. name: "ingress_mode_default 必须为 off|ctx_pool|passthrough|http_bridge",
  2069. mutate: func(c *Config) { c.Gateway.OpenAIWS.IngressModeDefault = "invalid" },
  2070. wantErr: "gateway.openai_ws.ingress_mode_default",
  2071. },
  2072. {
  2073. name: "payload_log_sample_rate 必须在 [0,1] 范围内",
  2074. mutate: func(c *Config) { c.Gateway.OpenAIWS.PayloadLogSampleRate = 1.2 },
  2075. wantErr: "gateway.openai_ws.payload_log_sample_rate",
  2076. },
  2077. {
  2078. name: "retry_total_budget_ms 不能为负数",
  2079. mutate: func(c *Config) { c.Gateway.OpenAIWS.RetryTotalBudgetMS = -1 },
  2080. wantErr: "gateway.openai_ws.retry_total_budget_ms",
  2081. },
  2082. {
  2083. name: "lb_top_k 必须为正数",
  2084. mutate: func(c *Config) { c.Gateway.OpenAIWS.LBTopK = 0 },
  2085. wantErr: "gateway.openai_ws.lb_top_k",
  2086. },
  2087. {
  2088. name: "sticky_session_ttl_seconds 必须为正数",
  2089. mutate: func(c *Config) { c.Gateway.OpenAIWS.StickySessionTTLSeconds = 0 },
  2090. wantErr: "gateway.openai_ws.sticky_session_ttl_seconds",
  2091. },
  2092. {
  2093. name: "sticky_response_id_ttl_seconds 必须为正数",
  2094. mutate: func(c *Config) {
  2095. c.Gateway.OpenAIWS.StickyResponseIDTTLSeconds = 0
  2096. c.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds = 0
  2097. },
  2098. wantErr: "gateway.openai_ws.sticky_response_id_ttl_seconds",
  2099. },
  2100. {
  2101. name: "sticky_previous_response_ttl_seconds 不能为负数",
  2102. mutate: func(c *Config) { c.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds = -1 },
  2103. wantErr: "gateway.openai_ws.sticky_previous_response_ttl_seconds",
  2104. },
  2105. {
  2106. name: "scheduler_score_weights 不能为负数",
  2107. mutate: func(c *Config) { c.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = -0.1 },
  2108. wantErr: "gateway.openai_ws.scheduler_score_weights.* must be non-negative",
  2109. },
  2110. {
  2111. name: "scheduler_score_weights quota_headroom 不能为负数",
  2112. mutate: func(c *Config) { c.Gateway.OpenAIWS.SchedulerScoreWeights.QuotaHeadroom = -0.1 },
  2113. wantErr: "gateway.openai_ws.scheduler_score_weights.* must be non-negative",
  2114. },
  2115. {
  2116. name: "scheduler_score_weights upstream_cost 不能为负数",
  2117. mutate: func(c *Config) { c.Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost = -0.1 },
  2118. wantErr: "gateway.openai_ws.scheduler_score_weights.* must be non-negative",
  2119. },
  2120. {
  2121. name: "scheduler_score_weights reset 不能为负数",
  2122. mutate: func(c *Config) { c.Gateway.OpenAIWS.SchedulerScoreWeights.Reset = -0.1 },
  2123. wantErr: "gateway.openai_ws.scheduler_score_weights.* must be non-negative",
  2124. },
  2125. {
  2126. name: "scheduler_score_weights 不能为 NaN",
  2127. mutate: func(c *Config) { c.Gateway.OpenAIWS.SchedulerScoreWeights.PreviousResponse = math.NaN() },
  2128. wantErr: "gateway.openai_ws.scheduler_score_weights.* must be non-negative and finite",
  2129. },
  2130. {
  2131. name: "scheduler_score_weights 不能为 Inf",
  2132. mutate: func(c *Config) { c.Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost = math.Inf(1) },
  2133. wantErr: "gateway.openai_ws.scheduler_score_weights.* must be non-negative and finite",
  2134. },
  2135. {
  2136. name: "scheduler_score_weights 总和不能溢出",
  2137. mutate: func(c *Config) {
  2138. c.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = math.MaxFloat64
  2139. c.Gateway.OpenAIWS.SchedulerScoreWeights.Load = math.MaxFloat64
  2140. },
  2141. wantErr: "gateway.openai_ws.scheduler_score_weights base-weight sum must be finite",
  2142. },
  2143. {
  2144. name: "scheduler_score_weights 含 sticky 总和不能溢出",
  2145. mutate: func(c *Config) {
  2146. c.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = math.MaxFloat64
  2147. c.Gateway.OpenAIWS.SchedulerScoreWeights.PreviousResponse = math.MaxFloat64
  2148. },
  2149. wantErr: "gateway.openai_ws.scheduler_score_weights total-weight sum must be finite",
  2150. },
  2151. {
  2152. name: "scheduler_score_weights 不能全为 0",
  2153. mutate: func(c *Config) {
  2154. c.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = 0
  2155. c.Gateway.OpenAIWS.SchedulerScoreWeights.Load = 0
  2156. c.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = 0
  2157. c.Gateway.OpenAIWS.SchedulerScoreWeights.ErrorRate = 0
  2158. c.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT = 0
  2159. },
  2160. wantErr: "gateway.openai_ws.scheduler_score_weights must not all be zero",
  2161. },
  2162. {
  2163. name: "sticky_escape_ttft_ms 必须为正数",
  2164. mutate: func(c *Config) { c.Gateway.OpenAIScheduler.StickyEscapeTTFTMs = 0 },
  2165. wantErr: "gateway.openai_scheduler.sticky_escape_ttft_ms",
  2166. },
  2167. {
  2168. name: "sticky_escape_error_rate 不能小于 0",
  2169. mutate: func(c *Config) { c.Gateway.OpenAIScheduler.StickyEscapeErrorRate = -0.1 },
  2170. wantErr: "gateway.openai_scheduler.sticky_escape_error_rate",
  2171. },
  2172. {
  2173. name: "sticky_escape_error_rate 不能大于 1",
  2174. mutate: func(c *Config) { c.Gateway.OpenAIScheduler.StickyEscapeErrorRate = 1.1 },
  2175. wantErr: "gateway.openai_scheduler.sticky_escape_error_rate",
  2176. },
  2177. }
  2178. for _, tc := range cases {
  2179. tc := tc
  2180. t.Run(tc.name, func(t *testing.T) {
  2181. cfg := buildValid(t)
  2182. tc.mutate(cfg)
  2183. err := cfg.Validate()
  2184. require.Error(t, err)
  2185. require.Contains(t, err.Error(), tc.wantErr)
  2186. })
  2187. }
  2188. t.Run("quota_headroom 可作为唯一有效调度权重", func(t *testing.T) {
  2189. cfg := buildValid(t)
  2190. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = 0
  2191. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Load = 0
  2192. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = 0
  2193. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.ErrorRate = 0
  2194. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT = 0
  2195. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.QuotaHeadroom = 0.1
  2196. require.NoError(t, cfg.Validate())
  2197. })
  2198. t.Run("upstream_cost 可作为唯一有效调度权重", func(t *testing.T) {
  2199. cfg := buildValid(t)
  2200. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = 0
  2201. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Load = 0
  2202. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = 0
  2203. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.ErrorRate = 0
  2204. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT = 0
  2205. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost = 0.1
  2206. require.NoError(t, cfg.Validate())
  2207. })
  2208. t.Run("reset 可作为唯一有效调度权重", func(t *testing.T) {
  2209. cfg := buildValid(t)
  2210. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = 0
  2211. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Load = 0
  2212. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = 0
  2213. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.ErrorRate = 0
  2214. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT = 0
  2215. cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Reset = 0.1
  2216. require.NoError(t, cfg.Validate())
  2217. })
  2218. }
  2219. func TestValidateConfig_AutoScaleDisabledIgnoreAutoScaleFields(t *testing.T) {
  2220. resetViperWithJWTSecret(t)
  2221. cfg, err := Load()
  2222. if err != nil {
  2223. t.Fatalf("Load() error: %v", err)
  2224. }
  2225. cfg.Gateway.UsageRecord.AutoScaleEnabled = false
  2226. cfg.Gateway.UsageRecord.WorkerCount = 64
  2227. // 自动扩缩容关闭时,这些字段应被忽略,不应导致校验失败。
  2228. cfg.Gateway.UsageRecord.AutoScaleMinWorkers = 0
  2229. cfg.Gateway.UsageRecord.AutoScaleMaxWorkers = 0
  2230. cfg.Gateway.UsageRecord.AutoScaleUpQueuePercent = 0
  2231. cfg.Gateway.UsageRecord.AutoScaleDownQueuePercent = 100
  2232. cfg.Gateway.UsageRecord.AutoScaleUpStep = 0
  2233. cfg.Gateway.UsageRecord.AutoScaleDownStep = 0
  2234. cfg.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds = 0
  2235. cfg.Gateway.UsageRecord.AutoScaleCooldownSeconds = -1
  2236. if err := cfg.Validate(); err != nil {
  2237. t.Fatalf("Validate() should ignore auto scale fields when disabled: %v", err)
  2238. }
  2239. }
  2240. func TestValidateConfig_LogRequiredAndRotationBounds(t *testing.T) {
  2241. resetViperWithJWTSecret(t)
  2242. cases := []struct {
  2243. name string
  2244. mutate func(*Config)
  2245. wantErr string
  2246. }{
  2247. {
  2248. name: "log level required",
  2249. mutate: func(c *Config) {
  2250. c.Log.Level = ""
  2251. },
  2252. wantErr: "log.level is required",
  2253. },
  2254. {
  2255. name: "log format required",
  2256. mutate: func(c *Config) {
  2257. c.Log.Format = ""
  2258. },
  2259. wantErr: "log.format is required",
  2260. },
  2261. {
  2262. name: "log stacktrace required",
  2263. mutate: func(c *Config) {
  2264. c.Log.StacktraceLevel = ""
  2265. },
  2266. wantErr: "log.stacktrace_level is required",
  2267. },
  2268. {
  2269. name: "log max backups non-negative",
  2270. mutate: func(c *Config) {
  2271. c.Log.Rotation.MaxBackups = -1
  2272. },
  2273. wantErr: "log.rotation.max_backups must be non-negative",
  2274. },
  2275. {
  2276. name: "log max age non-negative",
  2277. mutate: func(c *Config) {
  2278. c.Log.Rotation.MaxAgeDays = -1
  2279. },
  2280. wantErr: "log.rotation.max_age_days must be non-negative",
  2281. },
  2282. {
  2283. name: "sampling thereafter non-negative when disabled",
  2284. mutate: func(c *Config) {
  2285. c.Log.Sampling.Enabled = false
  2286. c.Log.Sampling.Thereafter = -1
  2287. },
  2288. wantErr: "log.sampling.thereafter must be non-negative",
  2289. },
  2290. }
  2291. for _, tt := range cases {
  2292. t.Run(tt.name, func(t *testing.T) {
  2293. cfg, err := Load()
  2294. if err != nil {
  2295. t.Fatalf("Load() error: %v", err)
  2296. }
  2297. tt.mutate(cfg)
  2298. err = cfg.Validate()
  2299. if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
  2300. t.Fatalf("Validate() error = %v, want %q", err, tt.wantErr)
  2301. }
  2302. })
  2303. }
  2304. }
  2305. func TestLoad_DefaultGatewayUsageRecordConfig(t *testing.T) {
  2306. resetViperWithJWTSecret(t)
  2307. cfg, err := Load()
  2308. if err != nil {
  2309. t.Fatalf("Load() error: %v", err)
  2310. }
  2311. if cfg.Gateway.UsageRecord.WorkerCount != 128 {
  2312. t.Fatalf("worker_count = %d, want 128", cfg.Gateway.UsageRecord.WorkerCount)
  2313. }
  2314. if cfg.Gateway.UsageRecord.QueueSize != 16384 {
  2315. t.Fatalf("queue_size = %d, want 16384", cfg.Gateway.UsageRecord.QueueSize)
  2316. }
  2317. if cfg.Gateway.UsageRecord.TaskTimeoutSeconds != 5 {
  2318. t.Fatalf("task_timeout_seconds = %d, want 5", cfg.Gateway.UsageRecord.TaskTimeoutSeconds)
  2319. }
  2320. if cfg.Gateway.UsageRecord.OverflowPolicy != UsageRecordOverflowPolicySync {
  2321. t.Fatalf("overflow_policy = %s, want %s", cfg.Gateway.UsageRecord.OverflowPolicy, UsageRecordOverflowPolicySync)
  2322. }
  2323. if cfg.Gateway.UsageRecord.OverflowSamplePercent != 10 {
  2324. t.Fatalf("overflow_sample_percent = %d, want 10", cfg.Gateway.UsageRecord.OverflowSamplePercent)
  2325. }
  2326. if !cfg.Gateway.UsageRecord.AutoScaleEnabled {
  2327. t.Fatalf("auto_scale_enabled = false, want true")
  2328. }
  2329. if cfg.Gateway.UsageRecord.AutoScaleMinWorkers != 128 {
  2330. t.Fatalf("auto_scale_min_workers = %d, want 128", cfg.Gateway.UsageRecord.AutoScaleMinWorkers)
  2331. }
  2332. if cfg.Gateway.UsageRecord.AutoScaleMaxWorkers != 512 {
  2333. t.Fatalf("auto_scale_max_workers = %d, want 512", cfg.Gateway.UsageRecord.AutoScaleMaxWorkers)
  2334. }
  2335. if cfg.Gateway.UsageRecord.AutoScaleUpQueuePercent != 70 {
  2336. t.Fatalf("auto_scale_up_queue_percent = %d, want 70", cfg.Gateway.UsageRecord.AutoScaleUpQueuePercent)
  2337. }
  2338. if cfg.Gateway.UsageRecord.AutoScaleDownQueuePercent != 15 {
  2339. t.Fatalf("auto_scale_down_queue_percent = %d, want 15", cfg.Gateway.UsageRecord.AutoScaleDownQueuePercent)
  2340. }
  2341. if cfg.Gateway.UsageRecord.AutoScaleUpStep != 32 {
  2342. t.Fatalf("auto_scale_up_step = %d, want 32", cfg.Gateway.UsageRecord.AutoScaleUpStep)
  2343. }
  2344. if cfg.Gateway.UsageRecord.AutoScaleDownStep != 16 {
  2345. t.Fatalf("auto_scale_down_step = %d, want 16", cfg.Gateway.UsageRecord.AutoScaleDownStep)
  2346. }
  2347. if cfg.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds != 3 {
  2348. t.Fatalf("auto_scale_check_interval_seconds = %d, want 3", cfg.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds)
  2349. }
  2350. if cfg.Gateway.UsageRecord.AutoScaleCooldownSeconds != 10 {
  2351. t.Fatalf("auto_scale_cooldown_seconds = %d, want 10", cfg.Gateway.UsageRecord.AutoScaleCooldownSeconds)
  2352. }
  2353. }
  2354. func TestLoad_DefaultGatewayImageStreamConfig(t *testing.T) {
  2355. resetViperWithJWTSecret(t)
  2356. cfg, err := Load()
  2357. if err != nil {
  2358. t.Fatalf("Load() error: %v", err)
  2359. }
  2360. if cfg.Gateway.StreamDataIntervalTimeout != 180 {
  2361. t.Fatalf("stream_data_interval_timeout = %d, want 180", cfg.Gateway.StreamDataIntervalTimeout)
  2362. }
  2363. if cfg.Gateway.StreamKeepaliveInterval != 10 {
  2364. t.Fatalf("stream_keepalive_interval = %d, want 10", cfg.Gateway.StreamKeepaliveInterval)
  2365. }
  2366. if cfg.Gateway.ImageStreamDataIntervalTimeout != 900 {
  2367. t.Fatalf("image_stream_data_interval_timeout = %d, want 900", cfg.Gateway.ImageStreamDataIntervalTimeout)
  2368. }
  2369. if cfg.Gateway.ImageStreamKeepaliveInterval != 10 {
  2370. t.Fatalf("image_stream_keepalive_interval = %d, want 10", cfg.Gateway.ImageStreamKeepaliveInterval)
  2371. }
  2372. if cfg.Gateway.ImageNonstreamKeepaliveInterval != 0 {
  2373. t.Fatalf("image_nonstream_keepalive_interval = %d, want 0", cfg.Gateway.ImageNonstreamKeepaliveInterval)
  2374. }
  2375. if cfg.Gateway.ImageConcurrency.Enabled {
  2376. t.Fatalf("image_concurrency.enabled = true, want false")
  2377. }
  2378. if cfg.Gateway.ImageConcurrency.MaxConcurrentRequests != 0 {
  2379. t.Fatalf("image_concurrency.max_concurrent_requests = %d, want 0", cfg.Gateway.ImageConcurrency.MaxConcurrentRequests)
  2380. }
  2381. if cfg.Gateway.ImageConcurrency.OverflowMode != ImageConcurrencyOverflowModeReject {
  2382. t.Fatalf("image_concurrency.overflow_mode = %q, want %q", cfg.Gateway.ImageConcurrency.OverflowMode, ImageConcurrencyOverflowModeReject)
  2383. }
  2384. if cfg.Gateway.ImageConcurrency.WaitTimeoutSeconds != 30 {
  2385. t.Fatalf("image_concurrency.wait_timeout_seconds = %d, want 30", cfg.Gateway.ImageConcurrency.WaitTimeoutSeconds)
  2386. }
  2387. if cfg.Gateway.ImageConcurrency.MaxWaitingRequests != 100 {
  2388. t.Fatalf("image_concurrency.max_waiting_requests = %d, want 100", cfg.Gateway.ImageConcurrency.MaxWaitingRequests)
  2389. }
  2390. if cfg.Gateway.ImageStreamDataIntervalTimeout <= cfg.Gateway.StreamDataIntervalTimeout {
  2391. t.Fatalf("image stream timeout = %d, want greater than ordinary stream timeout %d", cfg.Gateway.ImageStreamDataIntervalTimeout, cfg.Gateway.StreamDataIntervalTimeout)
  2392. }
  2393. }