grok_oauth_client_test.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. //go:build unit
  2. package repository
  3. import (
  4. "context"
  5. "encoding/json"
  6. "net/http"
  7. "net/http/httptest"
  8. "strings"
  9. "testing"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
  11. "github.com/stretchr/testify/require"
  12. )
  13. func TestGrokOAuthClientExchangeAndRefreshUseFormFields(t *testing.T) {
  14. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  15. require.Equal(t, http.MethodPost, r.Method)
  16. require.NoError(t, r.ParseForm())
  17. require.Equal(t, "client-id", r.Form.Get("client_id"))
  18. switch r.Form.Get("grant_type") {
  19. case "authorization_code":
  20. require.Equal(t, "auth-code", r.Form.Get("code"))
  21. require.Equal(t, "http://127.0.0.1:56121/callback", r.Form.Get("redirect_uri"))
  22. require.Equal(t, "verifier", r.Form.Get("code_verifier"))
  23. require.Empty(t, r.Form.Get("code_challenge"))
  24. require.Empty(t, r.Form.Get("code_challenge_method"))
  25. _ = json.NewEncoder(w).Encode(map[string]any{
  26. "access_token": "exchange-access",
  27. "refresh_token": "exchange-refresh",
  28. "token_type": "Bearer",
  29. "expires_in": 3600,
  30. "scope": "openid api:access",
  31. })
  32. case "refresh_token":
  33. require.Equal(t, "refresh-token", r.Form.Get("refresh_token"))
  34. _ = json.NewEncoder(w).Encode(map[string]any{
  35. "access_token": "refresh-access",
  36. "refresh_token": "refresh-rotated",
  37. "token_type": "Bearer",
  38. "expires_in": 7200,
  39. })
  40. default:
  41. http.Error(w, "unexpected grant_type", http.StatusBadRequest)
  42. }
  43. }))
  44. defer server.Close()
  45. // Tests inject a loopback token endpoint; allowlist requires unsafe override.
  46. t.Setenv(xai.EnvAllowUnsafeURLOverrides, "true")
  47. t.Setenv(xai.EnvTokenURL, server.URL)
  48. client := NewGrokOAuthClient()
  49. exchanged, err := client.ExchangeCode(context.Background(), "auth-code", "verifier", "http://127.0.0.1:56121/callback", "", "client-id")
  50. require.NoError(t, err)
  51. require.Equal(t, "exchange-access", exchanged.AccessToken)
  52. require.Equal(t, "exchange-refresh", exchanged.RefreshToken)
  53. require.Equal(t, int64(3600), exchanged.ExpiresIn)
  54. require.Equal(t, "openid api:access", exchanged.Scope)
  55. refreshed, err := client.RefreshToken(context.Background(), "refresh-token", "", "client-id")
  56. require.NoError(t, err)
  57. require.Equal(t, "refresh-access", refreshed.AccessToken)
  58. require.Equal(t, "refresh-rotated", refreshed.RefreshToken)
  59. require.Equal(t, int64(7200), refreshed.ExpiresIn)
  60. }
  61. func TestGrokOAuthClientRefreshForbiddenClassifiesOnlyExplicitEntitlement(t *testing.T) {
  62. tests := []struct {
  63. name string
  64. body string
  65. wantReason string
  66. }{
  67. {name: "explicit entitlement", body: `{"error":"access_denied"}`, wantReason: "GROK_OAUTH_ENTITLEMENT_DENIED"},
  68. {name: "generic forbidden", body: `{"error":"forbidden"}`, wantReason: "GROK_OAUTH_TOKEN_REFRESH_FAILED"},
  69. }
  70. for _, tt := range tests {
  71. t.Run(tt.name, func(t *testing.T) {
  72. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
  73. w.WriteHeader(http.StatusForbidden)
  74. _, _ = w.Write([]byte(tt.body))
  75. }))
  76. defer server.Close()
  77. t.Setenv(xai.EnvAllowUnsafeURLOverrides, "true")
  78. t.Setenv(xai.EnvTokenURL, server.URL)
  79. client := NewGrokOAuthClient()
  80. _, err := client.RefreshToken(context.Background(), "refresh-token", "", "client-id")
  81. require.Error(t, err)
  82. require.Contains(t, strings.ToUpper(err.Error()), tt.wantReason)
  83. })
  84. }
  85. }
  86. func TestGrokOAuthClientStatusErrorRedactsSensitiveResponseBody(t *testing.T) {
  87. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
  88. w.WriteHeader(http.StatusBadRequest)
  89. _, _ = w.Write([]byte(`{"error":"invalid_grant","access_token":"access-secret","refresh_token":"refresh-secret","code_verifier":"verifier-secret"}`))
  90. }))
  91. defer server.Close()
  92. t.Setenv(xai.EnvAllowUnsafeURLOverrides, "true")
  93. t.Setenv(xai.EnvTokenURL, server.URL)
  94. client := NewGrokOAuthClient()
  95. _, err := client.RefreshToken(context.Background(), "refresh-secret", "", "client-id")
  96. require.Error(t, err)
  97. errText := err.Error()
  98. require.Contains(t, errText, "status 400")
  99. require.Contains(t, errText, `\"refresh_token\":\"***\"`)
  100. require.NotContains(t, errText, "access-secret")
  101. require.NotContains(t, errText, "refresh-secret")
  102. require.NotContains(t, errText, "verifier-secret")
  103. }
  104. func TestGrokOAuthEntitlementDenialRequiresExplicitEvidence(t *testing.T) {
  105. t.Parallel()
  106. require.True(t, grokOAuthHasExplicitEntitlementDenial(`{"error":"access_denied"}`))
  107. require.True(t, grokOAuthHasExplicitEntitlementDenial(`{"code":"entitlement_denied"}`))
  108. require.True(t, grokOAuthHasExplicitEntitlementDenial(`{"message":"no active Grok subscription"}`))
  109. require.False(t, grokOAuthHasExplicitEntitlementDenial(`{"error":"forbidden","message":"request forbidden"}`))
  110. require.False(t, grokOAuthHasExplicitEntitlementDenial(`<html>403 Forbidden</html>`))
  111. require.False(t, grokOAuthHasExplicitEntitlementDenial(`{"error":"access_denied","message":"You have run out of credits"}`))
  112. require.False(t, grokOAuthHasExplicitEntitlementDenial(`{"code":"subscription_required","message":"included free usage exhausted"}`))
  113. }
  114. func TestNewGrokOAuthClient_UnvalidatedTokenURLFallsBackToDefault(t *testing.T) {
  115. // Without unsafe overrides, a random env TokenURL must not be used (fail-closed).
  116. t.Setenv(xai.EnvAllowUnsafeURLOverrides, "")
  117. t.Setenv(xai.EnvTokenURL, "https://evil.example/oauth/token")
  118. client := NewGrokOAuthClient().(*grokOAuthClient)
  119. require.Equal(t, xai.DefaultTokenURL, client.tokenURL)
  120. }