| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- //go:build unit
- package xai
- import (
- "net/url"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
- "github.com/stretchr/testify/require"
- )
- func TestParseAuthorizationInput(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- raw string
- wantCode string
- wantState string
- wantRequiresState bool
- }{
- {
- name: "full callback url",
- raw: "http://127.0.0.1:56121/callback?code=abc123&state=state456",
- wantCode: "abc123",
- wantState: "state456",
- wantRequiresState: true,
- },
- {
- name: "query string",
- raw: "?code=abc123&state=state456",
- wantCode: "abc123",
- wantState: "state456",
- wantRequiresState: true,
- },
- {
- name: "full callback url missing state",
- raw: "http://127.0.0.1:56121/callback?code=abc123",
- wantCode: "abc123",
- wantRequiresState: true,
- },
- {
- name: "query string missing state",
- raw: "code=abc123",
- wantCode: "abc123",
- wantRequiresState: true,
- },
- {
- name: "bare code",
- raw: "abc123",
- wantCode: "abc123",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- got := ParseAuthorizationInput(tt.raw)
- require.Equal(t, tt.wantCode, got.Code)
- require.Equal(t, tt.wantState, got.State)
- require.Equal(t, tt.wantRequiresState, got.RequiresState)
- })
- }
- }
- func TestBuildAuthorizationURLIncludesHermesCompatibleParameters(t *testing.T) {
- t.Setenv(EnvAuthorizeURL, "https://auth.example.test/oauth2/authorize")
- t.Setenv(EnvClientID, "client-id")
- t.Setenv(EnvScope, "openid profile offline_access api:access")
- t.Setenv(EnvAllowUnsafeURLOverrides, "true")
- authURL, err := BuildAuthorizationURL("state", "challenge", "http://127.0.0.1:56121/callback", "nonce")
- require.NoError(t, err)
- parsed, err := url.Parse(authURL)
- require.NoError(t, err)
- values := parsed.Query()
- require.Equal(t, "https", parsed.Scheme)
- require.Equal(t, "auth.example.test", parsed.Host)
- require.Equal(t, "/oauth2/authorize", parsed.Path)
- require.Equal(t, "code", values.Get("response_type"))
- require.Equal(t, "client-id", values.Get("client_id"))
- require.Equal(t, "http://127.0.0.1:56121/callback", values.Get("redirect_uri"))
- require.Equal(t, "openid profile offline_access api:access", values.Get("scope"))
- require.Equal(t, "state", values.Get("state"))
- require.Equal(t, "nonce", values.Get("nonce"))
- require.Equal(t, "challenge", values.Get("code_challenge"))
- require.Equal(t, "S256", values.Get("code_challenge_method"))
- require.Equal(t, "generic", values.Get("plan"))
- require.Equal(t, "sub2api", values.Get("referrer"))
- }
- func TestValidateXAIURLsAllowOfficialOAuthAndGatewayHosts(t *testing.T) {
- authorizeURL, err := ValidateOAuthEndpointURL(DefaultAuthorizeURL)
- require.NoError(t, err)
- require.Equal(t, DefaultAuthorizeURL, authorizeURL)
- tokenURL, err := ValidateOAuthEndpointURL(DefaultTokenURL)
- require.NoError(t, err)
- require.Equal(t, DefaultTokenURL, tokenURL)
- baseURL, err := ValidateBaseURL(DefaultBaseURL)
- require.NoError(t, err)
- require.Equal(t, DefaultBaseURL, baseURL)
- cliBaseURL, err := ValidateBaseURL(DefaultCLIBaseURL)
- require.NoError(t, err)
- require.Equal(t, DefaultCLIBaseURL, cliBaseURL)
- baseURLNoPath, err := ValidateBaseURL("https://api.x.ai")
- require.NoError(t, err)
- require.Equal(t, DefaultBaseURL, baseURLNoPath)
- chatURL, err := BuildChatCompletionsURL(DefaultCLIBaseURL + "/")
- require.NoError(t, err)
- require.Equal(t, DefaultCLIBaseURL+"/chat/completions", chatURL)
- }
- func TestBuildGrokMediaURLs(t *testing.T) {
- imagesURL, err := BuildImagesGenerationsURL(DefaultBaseURL + "/")
- require.NoError(t, err)
- require.Equal(t, DefaultBaseURL+"/images/generations", imagesURL)
- editsURL, err := BuildImagesEditsURL(DefaultBaseURL)
- require.NoError(t, err)
- require.Equal(t, DefaultBaseURL+"/images/edits", editsURL)
- videosURL, err := BuildVideosGenerationsURL(DefaultBaseURL)
- require.NoError(t, err)
- require.Equal(t, DefaultBaseURL+"/videos/generations", videosURL)
- videoEditsURL, err := BuildVideosEditsURL(DefaultBaseURL)
- require.NoError(t, err)
- require.Equal(t, DefaultBaseURL+"/videos/edits", videoEditsURL)
- videoExtensionsURL, err := BuildVideosExtensionsURL(DefaultBaseURL)
- require.NoError(t, err)
- require.Equal(t, DefaultBaseURL+"/videos/extensions", videoExtensionsURL)
- videoURL, err := BuildVideoURL(DefaultBaseURL, "req 123")
- require.NoError(t, err)
- require.Equal(t, DefaultBaseURL+"/videos/req%20123", videoURL)
- _, err = BuildVideoURL(DefaultBaseURL, " ")
- require.Error(t, err)
- }
- func TestValidateXAIURLsRejectUntrustedOAuthAndUnsafeBaseURLsByDefault(t *testing.T) {
- _, err := ValidateOAuthEndpointURL("https://auth.example.test/oauth2/token")
- require.Error(t, err)
- _, err = ValidateBaseURL("http://127.0.0.1:8080/v1")
- require.Error(t, err)
- _, err = ValidateBaseURL("https://api.x.ai/custom")
- require.Error(t, err)
- }
- func TestValidateBaseURLAllowsPublicThirdPartyGrokAPI(t *testing.T) {
- baseURL, err := ValidateBaseURL("https://grok.example.test/v1/")
- require.NoError(t, err)
- require.Equal(t, "https://grok.example.test/v1", baseURL)
- _, err = ValidateTrustedBaseURL("https://grok.example.test/v1")
- require.Error(t, err)
- }
- func TestValidateBaseURLPathPrefixPolicy(t *testing.T) {
- // 非官方主机保留管理员配置的任意 path 前缀。
- prefixed, err := ValidateBaseURL("https://relay.example.test/xai/v1/")
- require.NoError(t, err)
- require.Equal(t, "https://relay.example.test/xai/v1", prefixed)
- deepPrefixed, err := ValidateBaseURL("https://relay.example.test/tenant-a/proxy")
- require.NoError(t, err)
- require.Equal(t, "https://relay.example.test/tenant-a/proxy", deepPrefixed)
- // 空 path 仍按惯例补 /v1,保持既有配置兼容。
- rootOnly, err := ValidateBaseURL("https://relay.example.test")
- require.NoError(t, err)
- require.Equal(t, "https://relay.example.test/v1", rootOnly)
- // 官方主机固定 /v1 前缀。
- _, err = ValidateBaseURL("https://api.x.ai/xai/v1")
- require.Error(t, err)
- _, err = ValidateBaseURL("https://cli-chat-proxy.grok.com/other")
- require.Error(t, err)
- }
- func TestIsOfficialBaseURL(t *testing.T) {
- official := []string{
- "",
- " ",
- DefaultBaseURL,
- DefaultCLIBaseURL,
- "https://api.x.ai",
- "HTTPS://API.X.AI:443/",
- "https://api.x.ai:0443/v1",
- "https://api.x.ai/%76%31",
- "https://api.x.ai:8443/v1",
- "HTTPS://CLI-CHAT-PROXY.GROK.COM:443/%76%31/",
- "::invalid::url", // 无法解析的值按官方处理,回落默认端点
- }
- for _, raw := range official {
- require.True(t, IsOfficialBaseURL(raw), "expected official: %q", raw)
- }
- custom := []string{
- "https://relay.example.test/v1",
- "https://relay.example.test/xai/v1",
- "http://relay.example.test/v1",
- "https://grok.com.evil.example.test/v1",
- "https://api.x.ai.evil.example.test/v1", // 后缀伪装不属于 *.api.x.ai
- }
- for _, raw := range custom {
- require.False(t, IsOfficialBaseURL(raw), "expected custom: %q", raw)
- }
- }
- func TestRegionalAPIEndpointsAreOfficialAndTrusted(t *testing.T) {
- regional := []string{
- "https://us-east-1.api.x.ai/v1",
- "https://us-west-2.api.x.ai/v1",
- "https://eu-west-1.api.x.ai/v1",
- }
- for _, raw := range regional {
- require.True(t, IsOfficialBaseURL(raw), "expected official: %q", raw)
- validated, err := ValidateTrustedBaseURL(raw)
- require.NoError(t, err, "trusted validation should accept regional endpoint %q", raw)
- require.Equal(t, raw, validated)
- }
- // 区域端点作为官方主机同样强制 /v1 path
- _, err := ValidateTrustedBaseURL("https://us-east-1.api.x.ai/other")
- require.Error(t, err)
- }
- func TestValidateBaseURLsRejectEmptyQueryDelimiter(t *testing.T) {
- _, err := ValidateBaseURL("https://grok.example.test/v1?")
- require.Error(t, err)
- _, err = ValidateTrustedBaseURL("https://api.x.ai/v1?")
- require.Error(t, err)
- }
- func TestBuildResponsesURLWithValidatorUsesCallerPolicy(t *testing.T) {
- validator := func(raw string) (string, error) {
- return urlvalidator.ValidateURLFormat(raw, true)
- }
- target, err := BuildResponsesURLWithValidator("http://grok.example.test/v1/", validator)
- require.NoError(t, err)
- require.Equal(t, "http://grok.example.test/v1/responses", target)
- }
- func TestValidateTrustedBaseURLAcceptsOfficialRegionalHosts(t *testing.T) {
- for _, raw := range []string{DefaultUSEast1BaseURL, DefaultUSWest2BaseURL, DefaultEUWest1BaseURL} {
- got, err := ValidateTrustedBaseURL(raw)
- require.NoError(t, err, raw)
- require.Equal(t, raw, got)
- }
- }
- func TestBuildResponsesURLPreservesUnsafeOverrideCustomPath(t *testing.T) {
- t.Setenv(EnvAllowUnsafeURLOverrides, "true")
- target, err := BuildResponsesURL("http://localhost:8080/custom")
- require.NoError(t, err)
- require.Equal(t, "http://localhost:8080/custom/responses", target)
- }
- func TestBuildResponsesURLWithValidatorRejectsBaseURLComponents(t *testing.T) {
- permissive := func(raw string) (string, error) { return raw, nil }
- tests := []struct {
- name string
- raw string
- }{
- {name: "userinfo", raw: "https://user:secret@grok.example.test/v1"},
- {name: "query", raw: "https://grok.example.test/v1?token=secret"},
- {name: "empty query delimiter", raw: "https://grok.example.test/v1?"},
- {name: "fragment", raw: "https://grok.example.test/v1#secret"},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- _, err := BuildResponsesURLWithValidator(tt.raw, permissive)
- require.Error(t, err)
- require.NotContains(t, err.Error(), "secret")
- })
- }
- }
- func TestValidateXAIURLsAllowUnsafeDevOverride(t *testing.T) {
- t.Setenv(EnvAllowUnsafeURLOverrides, "true")
- tokenURL, err := ValidateOAuthEndpointURL("http://127.0.0.1:8080/oauth2/token")
- require.NoError(t, err)
- require.Equal(t, "http://127.0.0.1:8080/oauth2/token", tokenURL)
- baseURL, err := ValidateBaseURL("http://127.0.0.1:8080/v1/")
- require.NoError(t, err)
- require.Equal(t, "http://127.0.0.1:8080/v1", baseURL)
- }
- func TestRuntimeSanityReportsSafeDefaults(t *testing.T) {
- t.Setenv(EnvBaseURL, "")
- t.Setenv(EnvAuthorizeURL, "")
- t.Setenv(EnvTokenURL, "")
- t.Setenv(EnvRedirectURI, "")
- t.Setenv(EnvAllowUnsafeURLOverrides, "")
- t.Setenv(EnvUnsafeAllowHighConcurrency, "")
- report := RuntimeSanity()
- require.True(t, report.BaseURL.Valid)
- require.Equal(t, DefaultBaseURL, report.BaseURL.Value)
- require.True(t, report.BaseURL.IsDefault)
- require.True(t, report.OAuthAuthorizeURL.Valid)
- require.True(t, report.OAuthTokenURL.Valid)
- require.True(t, report.OAuthRedirectURI.Valid)
- require.False(t, report.UnsafeURLOverrides)
- require.False(t, report.UnsafeHighConcurrency)
- require.Equal(t, "responses_only", report.PublicGatewayScope)
- require.Contains(t, report.ProxyPolicy, "account_proxy_optional")
- require.Contains(t, report.ProxyPolicy, "API-key base URLs require public HTTPS")
- }
- func TestRuntimeSanityReportsInvalidOverridesWithoutSecrets(t *testing.T) {
- t.Setenv(EnvBaseURL, "http://127.0.0.1:8080/v1?access_token=secret")
- t.Setenv(EnvAuthorizeURL, "https://auth.example.test/oauth2/authorize")
- t.Setenv(EnvTokenURL, "https://auth.example.test/oauth2/token")
- t.Setenv(EnvRedirectURI, "not a url")
- t.Setenv(EnvClientID, "client-secret-like-value")
- t.Setenv(EnvAllowUnsafeURLOverrides, "")
- report := RuntimeSanity()
- require.False(t, report.BaseURL.Valid)
- require.False(t, report.BaseURL.IsDefault)
- require.Contains(t, report.BaseURL.Error, "invalid url")
- require.NotContains(t, report.BaseURL.Value, "secret")
- require.False(t, report.OAuthAuthorizeURL.Valid)
- require.False(t, report.OAuthTokenURL.Valid)
- require.False(t, report.OAuthRedirectURI.Valid)
- require.NotContains(t, report.ProxyPolicy, "client-secret-like-value")
- }
- func TestDefaultModelMappingIncludesGrokAliases(t *testing.T) {
- original := RuntimeModelMappingOptions()
- t.Cleanup(func() { SetRuntimeModelMappingOptions(original) })
- SetRuntimeModelMappingOptions(ModelMappingOptions{})
- mapping := DefaultModelMapping()
- require.Equal(t, "grok-4.6", mapping["grok"])
- require.Equal(t, "grok-4.6", mapping["grok-latest"])
- require.Equal(t, "grok-4.6", mapping["grok-4.6"])
- require.Equal(t, "grok-4.6", mapping["grok-4.6-latest"])
- require.Equal(t, "grok-4.5", mapping["grok-4.5"])
- require.Equal(t, "grok-4.5", mapping["grok-4.5-latest"])
- require.Equal(t, "grok-build-0.1", mapping["grok-build"])
- require.Equal(t, "grok-build-0.1", mapping["grok-build-latest"])
- require.Equal(t, "grok-composer-2.5-fast", mapping["grok-composer"])
- require.Equal(t, "grok-composer-2.5-fast", mapping["composer-2.5"])
- require.Equal(t, "grok-4.20-0309-reasoning", mapping["grok-4.20-reasoning"])
- require.Equal(t, "grok-4.20-0309-non-reasoning", mapping["grok-4.20-non-reasoning"])
- require.Equal(t, "grok-4.20-multi-agent-0309", mapping["grok-4.20-multi-agent-0309"])
- require.Equal(t, DefaultImagineImageQualityModel, mapping["grok-imagine"])
- require.Equal(t, DefaultImagineImageFastModel, mapping["grok-imagine-image"])
- require.Equal(t, DefaultImagineImageQualityModel, mapping["grok-imagine-image-quality"])
- require.Equal(t, DefaultImagineImageQualityModel, mapping["grok-imagine-edit"])
- require.Equal(t, DefaultImagineVideoModel, mapping["grok-imagine-video"])
- require.Equal(t, DefaultImagineVideo15Model, mapping["grok-imagine-video-1.5"])
- require.Equal(t, DefaultImagineVideo15Model, mapping["grok-imagine-video-1.5-preview"])
- _, hasGPT := mapping["gpt-*"]
- require.False(t, hasGPT, "cross-client wildcards must be opt-in")
- }
|