grok_upstream_url_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. //go:build unit
  2. package service
  3. import (
  4. "testing"
  5. "github.com/Wei-Shaw/sub2api/internal/config"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
  7. "github.com/stretchr/testify/require"
  8. )
  9. func TestGrokAPIKeyURLPolicyFollowsGlobalSecurityConfig(t *testing.T) {
  10. account := &Account{
  11. Platform: PlatformGrok,
  12. Type: AccountTypeAPIKey,
  13. Credentials: map[string]any{
  14. "base_url": "http://grok.example.test/v1",
  15. },
  16. }
  17. t.Run("insecure HTTP enabled with allowlist disabled", func(t *testing.T) {
  18. cfg := &config.Config{}
  19. cfg.Security.URLAllowlist.Enabled = false
  20. cfg.Security.URLAllowlist.AllowInsecureHTTP = true
  21. responsesURL, err := buildGrokResponsesURL(account, cfg)
  22. require.NoError(t, err)
  23. require.Equal(t, "http://grok.example.test/v1/responses", responsesURL)
  24. chatURL, err := buildGrokChatCompletionsURL(account, cfg)
  25. require.NoError(t, err)
  26. require.Equal(t, "http://grok.example.test/v1/chat/completions", chatURL)
  27. mediaURL, err := buildGrokMediaURL(account, cfg, GrokMediaEndpointImagesGenerations, "")
  28. require.NoError(t, err)
  29. require.Equal(t, "http://grok.example.test/v1/images/generations", mediaURL)
  30. contentURL, err := buildGrokMediaURL(account, cfg, GrokMediaEndpointVideoContent, "request 123")
  31. require.NoError(t, err)
  32. require.Equal(t, "http://grok.example.test/v1/videos/request%20123/content", contentURL)
  33. })
  34. t.Run("insecure HTTP disabled", func(t *testing.T) {
  35. cfg := &config.Config{}
  36. cfg.Security.URLAllowlist.Enabled = false
  37. cfg.Security.URLAllowlist.AllowInsecureHTTP = false
  38. _, err := buildGrokResponsesURL(account, cfg)
  39. require.EqualError(t, err, "invalid base url: base URL rejected by URL security policy")
  40. })
  41. t.Run("enabled allowlist remains HTTPS only", func(t *testing.T) {
  42. cfg := &config.Config{}
  43. cfg.Security.URLAllowlist.Enabled = true
  44. cfg.Security.URLAllowlist.AllowInsecureHTTP = true
  45. cfg.Security.URLAllowlist.UpstreamHosts = []string{"grok.example.test"}
  46. _, err := buildGrokResponsesURL(account, cfg)
  47. require.EqualError(t, err, "invalid base url: base URL rejected by URL security policy")
  48. })
  49. }
  50. func TestGrokAPIKeyURLPolicyAppliesAllowlistAndPrivateHostControls(t *testing.T) {
  51. account := &Account{
  52. Platform: PlatformGrok,
  53. Type: AccountTypeAPIKey,
  54. Credentials: map[string]any{
  55. "base_url": "https://grok.example.test/v1",
  56. },
  57. }
  58. cfg := &config.Config{}
  59. cfg.Security.URLAllowlist.Enabled = true
  60. cfg.Security.URLAllowlist.UpstreamHosts = []string{"grok.example.test"}
  61. target, err := buildGrokResponsesURL(account, cfg)
  62. require.NoError(t, err)
  63. require.Equal(t, "https://grok.example.test/v1/responses", target)
  64. cfg.Security.URLAllowlist.UpstreamHosts = []string{"other.example.test"}
  65. _, err = buildGrokResponsesURL(account, cfg)
  66. require.EqualError(t, err, "invalid base url: base URL rejected by URL security policy")
  67. account.Credentials["base_url"] = "https://127.0.0.1/v1"
  68. cfg.Security.URLAllowlist.UpstreamHosts = []string{"127.0.0.1"}
  69. _, err = buildGrokResponsesURL(account, cfg)
  70. require.EqualError(t, err, "invalid base url: base URL rejected by URL security policy")
  71. cfg.Security.URLAllowlist.AllowPrivateHosts = true
  72. target, err = buildGrokResponsesURL(account, cfg)
  73. require.NoError(t, err)
  74. require.Equal(t, "https://127.0.0.1/v1/responses", target)
  75. }
  76. func TestGrokAPIKeyURLPolicyRedactsMalformedConfiguredURL(t *testing.T) {
  77. account := &Account{
  78. Platform: PlatformGrok,
  79. Type: AccountTypeAPIKey,
  80. Credentials: map[string]any{
  81. "base_url": "https://%zz:secret@grok.example.test/v1",
  82. },
  83. }
  84. cfg := &config.Config{}
  85. cfg.Security.URLAllowlist.AllowInsecureHTTP = true
  86. _, err := buildGrokResponsesURL(account, cfg)
  87. require.EqualError(t, err, "invalid base url: base URL rejected by URL security policy")
  88. require.NotContains(t, err.Error(), "secret")
  89. }
  90. func TestGrokOAuthURLPolicy(t *testing.T) {
  91. t.Run("default CLI gateway always allowed under restrictive allowlist", func(t *testing.T) {
  92. account := &Account{
  93. Platform: PlatformGrok,
  94. Type: AccountTypeOAuth,
  95. Credentials: map[string]any{},
  96. }
  97. cfg := &config.Config{}
  98. cfg.Security.URLAllowlist.Enabled = true
  99. cfg.Security.URLAllowlist.UpstreamHosts = []string{"other.example.test"}
  100. target, err := buildGrokResponsesURL(account, cfg)
  101. require.NoError(t, err)
  102. require.Equal(t, xai.DefaultCLIBaseURL+"/responses", target)
  103. })
  104. t.Run("stored official API endpoint is honored (manual endpoint switch)", func(t *testing.T) {
  105. account := &Account{
  106. Platform: PlatformGrok,
  107. Type: AccountTypeOAuth,
  108. Credentials: map[string]any{
  109. "base_url": xai.DefaultBaseURL,
  110. },
  111. }
  112. cfg := &config.Config{}
  113. target, err := buildGrokResponsesURL(account, cfg)
  114. require.NoError(t, err)
  115. require.Equal(t, xai.DefaultBaseURL+"/responses", target)
  116. })
  117. t.Run("stored regional API endpoint is trusted even under restrictive allowlist", func(t *testing.T) {
  118. account := &Account{
  119. Platform: PlatformGrok,
  120. Type: AccountTypeOAuth,
  121. Credentials: map[string]any{
  122. "base_url": "https://us-west-2.api.x.ai/v1",
  123. },
  124. }
  125. cfg := &config.Config{}
  126. cfg.Security.URLAllowlist.Enabled = true
  127. cfg.Security.URLAllowlist.UpstreamHosts = []string{"other.example.test"}
  128. target, err := buildGrokResponsesURL(account, cfg)
  129. require.NoError(t, err)
  130. require.Equal(t, "https://us-west-2.api.x.ai/v1/responses", target)
  131. })
  132. t.Run("custom forwarding address follows operator policy", func(t *testing.T) {
  133. account := &Account{
  134. Platform: PlatformGrok,
  135. Type: AccountTypeOAuth,
  136. Credentials: map[string]any{
  137. "base_url": "https://relay.example.test/v1",
  138. },
  139. }
  140. cfg := &config.Config{}
  141. cfg.Security.URLAllowlist.Enabled = false
  142. target, err := buildGrokResponsesURL(account, cfg)
  143. require.NoError(t, err)
  144. require.Equal(t, "https://relay.example.test/v1/responses", target)
  145. })
  146. t.Run("custom path prefix is preserved", func(t *testing.T) {
  147. account := &Account{
  148. Platform: PlatformGrok,
  149. Type: AccountTypeOAuth,
  150. Credentials: map[string]any{
  151. "base_url": "https://relay.example.test/xai/v1",
  152. },
  153. }
  154. cfg := &config.Config{}
  155. target, err := buildGrokResponsesURL(account, cfg)
  156. require.NoError(t, err)
  157. require.Equal(t, "https://relay.example.test/xai/v1/responses", target)
  158. })
  159. t.Run("custom forwarding address rejected by allowlist", func(t *testing.T) {
  160. account := &Account{
  161. Platform: PlatformGrok,
  162. Type: AccountTypeOAuth,
  163. Credentials: map[string]any{
  164. "base_url": "https://relay.example.test/v1",
  165. },
  166. }
  167. cfg := &config.Config{}
  168. cfg.Security.URLAllowlist.Enabled = true
  169. cfg.Security.URLAllowlist.UpstreamHosts = []string{"other.example.test"}
  170. _, err := buildGrokResponsesURL(account, cfg)
  171. require.EqualError(t, err, "invalid base url: base URL rejected by URL security policy")
  172. })
  173. t.Run("insecure HTTP custom address requires operator opt-in", func(t *testing.T) {
  174. account := &Account{
  175. Platform: PlatformGrok,
  176. Type: AccountTypeOAuth,
  177. Credentials: map[string]any{
  178. "base_url": "http://relay.example.test/v1",
  179. },
  180. }
  181. cfg := &config.Config{}
  182. cfg.Security.URLAllowlist.Enabled = false
  183. cfg.Security.URLAllowlist.AllowInsecureHTTP = false
  184. _, err := buildGrokResponsesURL(account, cfg)
  185. require.EqualError(t, err, "invalid base url: base URL rejected by URL security policy")
  186. cfg.Security.URLAllowlist.AllowInsecureHTTP = true
  187. target, err := buildGrokResponsesURL(account, cfg)
  188. require.NoError(t, err)
  189. require.Equal(t, "http://relay.example.test/v1/responses", target)
  190. })
  191. t.Run("unsafe override switch does not relax the operator allowlist for custom hosts", func(t *testing.T) {
  192. // XAI_ALLOW_UNSAFE_URL_OVERRIDES relaxes the trusted-host validator to
  193. // accept-any; a custom OAuth forwarding host must still be governed by
  194. // the operator allowlist so the bearer token cannot reach arbitrary hosts.
  195. t.Setenv(xai.EnvAllowUnsafeURLOverrides, "true")
  196. cfg := &config.Config{}
  197. cfg.Security.URLAllowlist.Enabled = true
  198. cfg.Security.URLAllowlist.UpstreamHosts = []string{"cli-chat-proxy.grok.com"}
  199. custom := &Account{
  200. Platform: PlatformGrok,
  201. Type: AccountTypeOAuth,
  202. Credentials: map[string]any{
  203. "base_url": "http://10.0.0.1/v1",
  204. },
  205. }
  206. _, err := buildGrokResponsesURL(custom, cfg)
  207. require.EqualError(t, err, "invalid base url: base URL rejected by URL security policy")
  208. // The official gateway still resolves even under the restrictive allowlist.
  209. official := &Account{
  210. Platform: PlatformGrok,
  211. Type: AccountTypeOAuth,
  212. Credentials: map[string]any{},
  213. }
  214. target, err := buildGrokResponsesURL(official, cfg)
  215. require.NoError(t, err)
  216. require.Equal(t, xai.DefaultCLIBaseURL+"/responses", target)
  217. })
  218. }
  219. func TestBuildGrokBillingURLUsesCLIForOfficialAPIHosts(t *testing.T) {
  220. for _, baseURL := range []string{xai.DefaultBaseURL, "https://us-west-2.api.x.ai/v1"} {
  221. account := &Account{Platform: PlatformGrok, Type: AccountTypeOAuth, Credentials: map[string]any{"base_url": baseURL}}
  222. weekly, err := buildGrokBillingURL(account, &config.Config{}, true)
  223. require.NoError(t, err)
  224. require.Equal(t, xai.DefaultCLIBaseURL+xai.BillingWeeklyPath, weekly)
  225. }
  226. }
  227. func TestBuildGrokBillingURLKeepsCustomRelay(t *testing.T) {
  228. account := &Account{Platform: PlatformGrok, Type: AccountTypeOAuth, Credentials: map[string]any{
  229. "base_url": "https://relay.example.test/xai/v1",
  230. }}
  231. monthly, err := buildGrokBillingURL(account, &config.Config{}, false)
  232. require.NoError(t, err)
  233. require.Equal(t, "https://relay.example.test/xai/v1"+xai.BillingMonthlyPath, monthly)
  234. }
  235. func TestGrokBillingURLFollowsAccountBaseURL(t *testing.T) {
  236. t.Run("oauth default stays on CLI gateway", func(t *testing.T) {
  237. account := &Account{
  238. Platform: PlatformGrok,
  239. Type: AccountTypeOAuth,
  240. Credentials: map[string]any{},
  241. }
  242. weeklyURL, err := buildGrokBillingURL(account, nil, true)
  243. require.NoError(t, err)
  244. require.Equal(t, xai.DefaultCLIBaseURL+"/billing?format=credits", weeklyURL)
  245. monthlyURL, err := buildGrokBillingURL(account, nil, false)
  246. require.NoError(t, err)
  247. require.Equal(t, xai.DefaultCLIBaseURL+"/billing", monthlyURL)
  248. })
  249. t.Run("oauth custom forwarding address carries billing probes", func(t *testing.T) {
  250. account := &Account{
  251. Platform: PlatformGrok,
  252. Type: AccountTypeOAuth,
  253. Credentials: map[string]any{
  254. "base_url": "https://relay.example.test/v1",
  255. },
  256. }
  257. weeklyURL, err := buildGrokBillingURL(account, nil, true)
  258. require.NoError(t, err)
  259. require.Equal(t, "https://relay.example.test/v1/billing?format=credits", weeklyURL)
  260. })
  261. t.Run("billing probe honors the operator allowlist like forwarding", func(t *testing.T) {
  262. // Probe paths must share the forwarding URL policy so a custom host the
  263. // allowlist rejects cannot receive the OAuth bearer via a billing probe.
  264. account := &Account{
  265. Platform: PlatformGrok,
  266. Type: AccountTypeOAuth,
  267. Credentials: map[string]any{
  268. "base_url": "https://relay.example.test/v1",
  269. },
  270. }
  271. cfg := &config.Config{}
  272. cfg.Security.URLAllowlist.Enabled = true
  273. cfg.Security.URLAllowlist.UpstreamHosts = []string{"cli-chat-proxy.grok.com"}
  274. _, err := buildGrokBillingURL(account, cfg, true)
  275. require.EqualError(t, err, "invalid base url: base URL rejected by URL security policy")
  276. })
  277. }