| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644 |
- //go:build unit
- package service
- import (
- "bytes"
- "context"
- "encoding/base64"
- "errors"
- "image"
- "image/color"
- "image/png"
- "io"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
- "time"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- "github.com/tidwall/gjson"
- )
- type grokAccountTestRateLimitRepo struct {
- *mockAccountRepoForGemini
- rateLimitedCalls int
- resetAt time.Time
- }
- func TestObserveGrokTestResponseClassifiesBodyOnlyQuotaErrors(t *testing.T) {
- account := &Account{ID: 1901, Platform: PlatformGrok, Type: AccountTypeOAuth}
- repo := &grokQuotaAccountRepo{mockAccountRepoForPlatform: &mockAccountRepoForPlatform{
- accountsByID: map[int64]*Account{account.ID: account},
- }}
- svc := &AccountTestService{accountRepo: repo}
- resp := &http.Response{
- StatusCode: http.StatusBadRequest,
- Header: make(http.Header),
- Body: io.NopCloser(strings.NewReader(`{"error":{"code":"subscription:free-usage-exhausted","message":"included free usage exhausted"}}`)),
- }
- svc.observeGrokTestResponse(context.Background(), account, resp)
- require.Equal(t, 1, repo.tempUnschedCalls)
- require.Equal(t, "grok free usage exhausted", repo.lastTempUnschedReason)
- body, err := io.ReadAll(resp.Body)
- require.NoError(t, err)
- require.Contains(t, string(body), "free-usage-exhausted")
- }
- func TestObserveGrokTestResponseDoesNotQuarantineContentPolicy(t *testing.T) {
- account := &Account{ID: 1902, Platform: PlatformGrok, Type: AccountTypeOAuth}
- repo := &grokQuotaAccountRepo{mockAccountRepoForPlatform: &mockAccountRepoForPlatform{
- accountsByID: map[int64]*Account{account.ID: account},
- }}
- svc := &AccountTestService{accountRepo: repo}
- resp := &http.Response{
- StatusCode: http.StatusForbidden,
- Header: make(http.Header),
- Body: io.NopCloser(strings.NewReader(`{"error":{"code":"new_sensitive","message":"text is sensitive"}}`)),
- }
- svc.observeGrokTestResponse(context.Background(), account, resp)
- require.Zero(t, repo.tempUnschedCalls)
- require.Zero(t, repo.rateLimitedCalls)
- }
- func TestObserveGrokTestResponseKeepsEntitlement403Cooldown(t *testing.T) {
- account := &Account{ID: 1903, Platform: PlatformGrok, Type: AccountTypeOAuth}
- repo := &grokQuotaAccountRepo{mockAccountRepoForPlatform: &mockAccountRepoForPlatform{
- accountsByID: map[int64]*Account{account.ID: account},
- }}
- svc := &AccountTestService{accountRepo: repo}
- resp := &http.Response{
- StatusCode: http.StatusForbidden,
- Header: make(http.Header),
- Body: io.NopCloser(strings.NewReader(`{"error":{"message":"subscription required"}}`)),
- }
- before := time.Now()
- svc.observeGrokTestResponse(context.Background(), account, resp)
- require.Equal(t, 1, repo.tempUnschedCalls)
- require.Equal(t, "grok entitlement or subscription tier denied", repo.lastTempUnschedReason)
- require.Greater(t, repo.lastTempUnschedUntil, before.Add(29*time.Minute))
- }
- func (r *grokAccountTestRateLimitRepo) SetRateLimited(_ context.Context, _ int64, resetAt time.Time) error {
- r.rateLimitedCalls++
- r.resetAt = resetAt
- return nil
- }
- func TestAccountTestService_TestAccountConnection_GrokUsesXAIResponses(t *testing.T) {
- gin.SetMode(gin.TestMode)
- account := &Account{
- ID: 13,
- Name: "grok-oauth",
- Platform: PlatformGrok,
- Type: AccountTypeOAuth,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "grok-access-token",
- "refresh_token": "grok-refresh-token",
- "expires_at": time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339),
- "model_mapping": map[string]any{
- "grok": "grok-4.3",
- },
- },
- }
- repo := &mockAccountRepoForGemini{
- accountsByID: map[int64]*Account{account.ID: account},
- }
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}},
- Body: io.NopCloser(strings.NewReader(
- "data: {\"type\":\"response.output_text.delta\",\"delta\":\"ok\"}\n\n" +
- "data: {\"type\":\"response.completed\"}\n\n",
- )),
- }}
- svc := &AccountTestService{
- accountRepo: repo,
- grokTokenProvider: NewGrokTokenProvider(repo, nil),
- httpUpstream: upstream,
- }
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/13/test", nil)
- err := svc.TestAccountConnection(c, account.ID, "grok", "", AccountTestModeDefault)
- require.NoError(t, err)
- require.Equal(t, "https://cli-chat-proxy.grok.com/v1/responses", upstream.lastReq.URL.String())
- require.Equal(t, "Bearer grok-access-token", upstream.lastReq.Header.Get("Authorization"))
- require.Equal(t, grokCLIVersion, upstream.lastReq.Header.Get("X-Grok-Client-Version"))
- require.Equal(t, "application/json, text/event-stream", upstream.lastReq.Header.Get("Accept"))
- require.Equal(t, "grok-4.3", gjson.GetBytes(upstream.lastBody, "model").String())
- require.Equal(t, grokQuotaProbeInput, gjson.GetBytes(upstream.lastBody, "input").String())
- require.True(t, gjson.GetBytes(upstream.lastBody, "stream").Bool())
- require.False(t, gjson.GetBytes(upstream.lastBody, "max_output_tokens").Exists())
- require.False(t, gjson.GetBytes(upstream.lastBody, "store").Exists())
- require.NotContains(t, rec.Body.String(), "claude")
- require.Contains(t, rec.Body.String(), `"model":"grok-4.3"`)
- require.Contains(t, rec.Body.String(), `"type":"test_complete"`)
- }
- func TestAccountTestService_TestAccountConnection_GrokDefaultsEmptyModelTo45(t *testing.T) {
- gin.SetMode(gin.TestMode)
- account := &Account{
- ID: 16,
- Name: "grok-oauth-default-model",
- Platform: PlatformGrok,
- Type: AccountTypeOAuth,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "grok-access-token",
- "refresh_token": "grok-refresh-token",
- "expires_at": time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339),
- },
- }
- repo := &mockAccountRepoForGemini{accountsByID: map[int64]*Account{account.ID: account}}
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}},
- Body: io.NopCloser(strings.NewReader(
- "data: {\"type\":\"response.output_text.delta\",\"delta\":\"ok\"}\n\n" +
- "data: {\"type\":\"response.completed\"}\n\n",
- )),
- }}
- svc := &AccountTestService{
- accountRepo: repo,
- grokTokenProvider: NewGrokTokenProvider(repo, nil),
- httpUpstream: upstream,
- }
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/16/test", nil)
- err := svc.TestAccountConnection(c, account.ID, "", "", AccountTestModeDefault)
- require.NoError(t, err)
- require.Equal(t, grokDefaultResponsesModel, gjson.GetBytes(upstream.lastBody, "model").String())
- require.Contains(t, recorder.Body.String(), `"model":"grok-4.5"`)
- }
- func TestAccountTestService_Grok429PersistsRateLimitReset(t *testing.T) {
- gin.SetMode(gin.TestMode)
- account := &Account{
- ID: 14,
- Name: "grok-oauth-limited",
- Platform: PlatformGrok,
- Type: AccountTypeOAuth,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "grok-access-token",
- "refresh_token": "grok-refresh-token",
- "expires_at": time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339),
- },
- }
- baseRepo := &mockAccountRepoForGemini{accountsByID: map[int64]*Account{account.ID: account}}
- repo := &grokAccountTestRateLimitRepo{mockAccountRepoForGemini: baseRepo}
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusTooManyRequests,
- Header: http.Header{"Retry-After": []string{"45"}},
- Body: io.NopCloser(strings.NewReader(`{"error":{"message":"rate limited"}}`)),
- }}
- svc := &AccountTestService{
- accountRepo: repo,
- grokTokenProvider: NewGrokTokenProvider(repo, nil),
- httpUpstream: upstream,
- }
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/14/test", nil)
- err := svc.TestAccountConnection(c, account.ID, "grok", "", AccountTestModeDefault)
- require.Error(t, err)
- require.Equal(t, 1, repo.rateLimitedCalls)
- require.WithinDuration(t, time.Now().Add(45*time.Second), repo.resetAt, time.Second)
- }
- func TestAccountTestService_Grok429WithoutQuotaHeadersUsesFallback(t *testing.T) {
- gin.SetMode(gin.TestMode)
- account := &Account{
- ID: 15, Name: "grok-oauth-limited-no-headers", Platform: PlatformGrok,
- Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true, Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "grok-access-token",
- "refresh_token": "grok-refresh-token",
- "expires_at": time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339),
- },
- }
- baseRepo := &mockAccountRepoForGemini{accountsByID: map[int64]*Account{account.ID: account}}
- repo := &grokAccountTestRateLimitRepo{mockAccountRepoForGemini: baseRepo}
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusTooManyRequests,
- Body: io.NopCloser(strings.NewReader(`{"error":{"message":"quota exhausted"}}`)),
- }}
- svc := &AccountTestService{
- accountRepo: repo, grokTokenProvider: NewGrokTokenProvider(repo, nil), httpUpstream: upstream,
- }
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/15/test", nil)
- before := time.Now()
- err := svc.TestAccountConnection(c, account.ID, "grok", "", AccountTestModeDefault)
- require.Error(t, err)
- require.Equal(t, 1, repo.rateLimitedCalls)
- require.WithinDuration(t, before.Add(grokRateLimitFallbackCooldown), repo.resetAt, time.Second)
- }
- func TestAccountTestService_GrokImageModelUsesImagesGenerations(t *testing.T) {
- gin.SetMode(gin.TestMode)
- account := &Account{
- ID: 17, Name: "grok-oauth-image", Platform: PlatformGrok,
- Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true, Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "grok-access-token",
- "refresh_token": "grok-refresh-token",
- "expires_at": time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339),
- "base_url": "https://cli-chat-proxy.grok.com/v1",
- },
- }
- repo := &mockAccountRepoForGemini{accountsByID: map[int64]*Account{account.ID: account}}
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"application/json"}},
- Body: io.NopCloser(strings.NewReader(
- `{"data":[{"b64_json":"QUJD","mime_type":"image/jpeg"}]}`,
- )),
- }}
- svc := &AccountTestService{
- accountRepo: repo,
- grokTokenProvider: NewGrokTokenProvider(repo, nil),
- httpUpstream: upstream,
- }
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/17/test", nil)
- err := svc.TestAccountConnection(c, account.ID, "grok-imagine-image", "a red apple", AccountTestModeDefault)
- require.NoError(t, err)
- require.Equal(t, "https://api.x.ai/v1/images/generations", upstream.lastReq.URL.String())
- require.Equal(t, "grok-imagine-image", gjson.GetBytes(upstream.lastBody, "model").String())
- require.Equal(t, "a red apple", gjson.GetBytes(upstream.lastBody, "prompt").String())
- require.Equal(t, "b64_json", gjson.GetBytes(upstream.lastBody, "response_format").String())
- require.Contains(t, rec.Body.String(), `"type":"image"`)
- require.Contains(t, rec.Body.String(), "data:image/jpeg;base64,QUJD")
- require.Contains(t, rec.Body.String(), `"type":"test_complete"`)
- }
- func TestAccountTestService_GrokWebSearchModeUsesResponsesWebSearchTool(t *testing.T) {
- gin.SetMode(gin.TestMode)
- account := &Account{
- ID: 18, Name: "grok-oauth-search", Platform: PlatformGrok,
- Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true, Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "grok-access-token",
- "refresh_token": "grok-refresh-token",
- "expires_at": time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339),
- },
- }
- repo := &mockAccountRepoForGemini{accountsByID: map[int64]*Account{account.ID: account}}
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Body: io.NopCloser(strings.NewReader(
- `{"id":"r1","output":[{"type":"web_search_call","id":"ws1","status":"completed"},{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Grok is built by xAI."}]}]}`,
- )),
- }}
- svc := &AccountTestService{
- accountRepo: repo,
- grokTokenProvider: NewGrokTokenProvider(repo, nil),
- httpUpstream: upstream,
- }
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/18/test", nil)
- err := svc.TestAccountConnection(c, account.ID, "grok-4.5", "xAI Grok", AccountTestModeGrokSearch)
- require.NoError(t, err)
- require.Equal(t, "https://cli-chat-proxy.grok.com/v1/responses", upstream.lastReq.URL.String())
- // Standalone web_search wraps the query in the gateway-style prompt.
- require.Contains(t, gjson.GetBytes(upstream.lastBody, "input").String(), "xAI Grok")
- require.Equal(t, "web_search", gjson.GetBytes(upstream.lastBody, "tools.0.type").String())
- require.Equal(t, "web_search_call.action.sources", gjson.GetBytes(upstream.lastBody, "include.0").String())
- require.Contains(t, rec.Body.String(), "web_search ok")
- require.Contains(t, rec.Body.String(), `"type":"test_complete"`)
- }
- func TestAccountTestService_GrokTTSIncludesLanguage(t *testing.T) {
- gin.SetMode(gin.TestMode)
- account := &Account{
- ID: 19, Name: "grok-oauth-tts", Platform: PlatformGrok,
- Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true, Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "grok-access-token",
- "refresh_token": "grok-refresh-token",
- "expires_at": time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339),
- },
- }
- repo := &mockAccountRepoForGemini{accountsByID: map[int64]*Account{account.ID: account}}
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"audio/mpeg"}},
- Body: io.NopCloser(strings.NewReader("ID3fakeaudio")),
- }}
- svc := &AccountTestService{
- accountRepo: repo,
- grokTokenProvider: NewGrokTokenProvider(repo, nil),
- httpUpstream: upstream,
- }
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/19/test", nil)
- err := svc.TestAccountConnection(c, account.ID, "", "hello voice", AccountTestModeGrokTTS)
- require.NoError(t, err)
- require.Equal(t, "https://api.x.ai/v1/tts", upstream.lastReq.URL.String())
- require.Equal(t, "hello voice", gjson.GetBytes(upstream.lastBody, "text").String())
- require.Equal(t, "en", gjson.GetBytes(upstream.lastBody, "language").String())
- require.Contains(t, rec.Body.String(), "tts ok")
- require.Contains(t, rec.Body.String(), `"type":"audio"`)
- require.Contains(t, rec.Body.String(), "data:audio/mpeg;base64,")
- require.Contains(t, rec.Body.String(), `"type":"test_complete"`)
- }
- func TestAccountTestService_GrokImageEditUsesUploadedImage(t *testing.T) {
- gin.SetMode(gin.TestMode)
- account := &Account{
- ID: 24, Name: "grok-oauth-image-edit", Platform: PlatformGrok,
- Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true, Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "grok-access-token",
- "refresh_token": "grok-refresh-token",
- "expires_at": time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339),
- },
- }
- repo := &mockAccountRepoForGemini{accountsByID: map[int64]*Account{account.ID: account}}
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"application/json"}},
- Body: io.NopCloser(strings.NewReader(
- `{"data":[{"b64_json":"QUJD","mime_type":"image/png"}]}`,
- )),
- }}
- svc := &AccountTestService{
- accountRepo: repo,
- grokTokenProvider: NewGrokTokenProvider(repo, nil),
- httpUpstream: upstream,
- }
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/24/test", nil)
- // 8x8 solid PNG (xAI min dimension is 8px).
- src := minimalAccountTestPNGDataURL(8, 8)
- err := svc.TestAccountConnection(c, account.ID, "grok-imagine-image", "edit me", AccountTestModeGrokImage, AccountTestOptions{
- ImageDataURL: src,
- })
- require.NoError(t, err)
- require.Equal(t, "https://api.x.ai/v1/images/edits", upstream.lastReq.URL.String())
- require.True(t, strings.HasPrefix(gjson.GetBytes(upstream.lastBody, "image.url").String(), "data:image/png;base64,"))
- require.Equal(t, "image_url", gjson.GetBytes(upstream.lastBody, "image.type").String())
- require.Equal(t, "b64_json", gjson.GetBytes(upstream.lastBody, "response_format").String())
- // concrete image model ids pass through; only bare "grok-imagine" is aliased.
- require.Equal(t, "grok-imagine-image", gjson.GetBytes(upstream.lastBody, "model").String())
- require.Contains(t, rec.Body.String(), `"type":"image"`)
- }
- func TestAccountTestService_GrokImageEditRejectsTinySource(t *testing.T) {
- gin.SetMode(gin.TestMode)
- account := &Account{
- ID: 25, Name: "grok-oauth-image-tiny", Platform: PlatformGrok,
- Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true, Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "grok-access-token",
- "refresh_token": "grok-refresh-token",
- "expires_at": time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339),
- },
- }
- repo := &mockAccountRepoForGemini{accountsByID: map[int64]*Account{account.ID: account}}
- svc := &AccountTestService{
- accountRepo: repo,
- grokTokenProvider: NewGrokTokenProvider(repo, nil),
- httpUpstream: &httpUpstreamRecorder{},
- }
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/25/test", nil)
- // 1x1 PNG data URL
- tiny := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
- err := svc.TestAccountConnection(c, account.ID, "grok-imagine-image-quality", "edit", AccountTestModeGrokImage, AccountTestOptions{
- ImageDataURL: tiny,
- })
- require.Error(t, err)
- require.Contains(t, rec.Body.String(), "too small")
- }
- // minimalAccountTestPNGDataURL builds a solid RGBA PNG as a data URL for tests.
- func minimalAccountTestPNGDataURL(w, h int) string {
- img := image.NewRGBA(image.Rect(0, 0, w, h))
- for y := 0; y < h; y++ {
- for x := 0; x < w; x++ {
- img.Set(x, y, color.RGBA{R: 200, G: 40, B: 40, A: 255})
- }
- }
- var buf bytes.Buffer
- _ = png.Encode(&buf, img)
- return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
- }
- func TestAccountTestService_GrokExplicitImageModeDefaultsModel(t *testing.T) {
- gin.SetMode(gin.TestMode)
- account := &Account{
- ID: 20, Name: "grok-oauth-image-mode", Platform: PlatformGrok,
- Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true, Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "grok-access-token",
- "refresh_token": "grok-refresh-token",
- "expires_at": time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339),
- },
- }
- repo := &mockAccountRepoForGemini{accountsByID: map[int64]*Account{account.ID: account}}
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"application/json"}},
- Body: io.NopCloser(strings.NewReader(
- `{"data":[{"b64_json":"QUJD","mime_type":"image/jpeg"}]}`,
- )),
- }}
- svc := &AccountTestService{
- accountRepo: repo,
- grokTokenProvider: NewGrokTokenProvider(repo, nil),
- httpUpstream: upstream,
- }
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/20/test", nil)
- err := svc.TestAccountConnection(c, account.ID, "", "", AccountTestModeGrokImage)
- require.NoError(t, err)
- require.Equal(t, "https://api.x.ai/v1/images/generations", upstream.lastReq.URL.String())
- require.Equal(t, "grok-imagine-image", gjson.GetBytes(upstream.lastBody, "model").String())
- require.Equal(t, "b64_json", gjson.GetBytes(upstream.lastBody, "response_format").String())
- require.Contains(t, rec.Body.String(), `"type":"test_complete"`)
- }
- func TestAccountTestService_GrokVideoUpstreamErrorIsNotMaskedAsSuccess(t *testing.T) {
- gin.SetMode(gin.TestMode)
- account := &Account{
- ID: 21, Name: "grok-oauth-video-err", Platform: PlatformGrok,
- Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true, Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "grok-access-token",
- "refresh_token": "grok-refresh-token",
- "expires_at": time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339),
- },
- }
- repo := &mockAccountRepoForGemini{accountsByID: map[int64]*Account{account.ID: account}}
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusBadRequest,
- Header: http.Header{"Content-Type": []string{"application/json"}},
- Body: io.NopCloser(strings.NewReader(
- `{"code":"invalid-argument","error":"bad video request"}`,
- )),
- }}
- svc := &AccountTestService{
- accountRepo: repo,
- grokTokenProvider: NewGrokTokenProvider(repo, nil),
- httpUpstream: upstream,
- }
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/21/test", nil)
- err := svc.TestAccountConnection(c, account.ID, "grok-imagine-video", "bounce ball", AccountTestModeGrokVideo)
- require.Error(t, err)
- require.Equal(t, "https://api.x.ai/v1/videos/generations", upstream.lastReq.URL.String())
- require.Contains(t, rec.Body.String(), `"type":"error"`)
- require.Contains(t, rec.Body.String(), "Grok videos API returned 400")
- require.NotContains(t, rec.Body.String(), `"success":true`)
- }
- type grokRealtimeTestConn struct {
- msg []byte
- }
- func (c *grokRealtimeTestConn) WriteJSON(context.Context, any) error { return nil }
- func (c *grokRealtimeTestConn) ReadMessage(context.Context) ([]byte, error) {
- if c == nil || len(c.msg) == 0 {
- return nil, context.DeadlineExceeded
- }
- return c.msg, nil
- }
- func (c *grokRealtimeTestConn) Ping(context.Context) error { return nil }
- func (c *grokRealtimeTestConn) Close() error { return nil }
- type grokRealtimeTestDialer struct {
- lastURL string
- lastAuth string
- lastProxy string
- conn openAIWSClientConn
- err error
- status int
- }
- func (d *grokRealtimeTestDialer) Dial(_ context.Context, wsURL string, headers http.Header, proxyURL string) (openAIWSClientConn, int, http.Header, error) {
- d.lastURL = wsURL
- d.lastAuth = headers.Get("Authorization")
- d.lastProxy = proxyURL
- if d.err != nil {
- return nil, d.status, nil, d.err
- }
- if d.conn == nil {
- d.conn = &grokRealtimeTestConn{}
- }
- return d.conn, 0, nil, nil
- }
- func TestAccountTestService_GrokRealtimeModeDialsWS(t *testing.T) {
- gin.SetMode(gin.TestMode)
- account := &Account{
- ID: 22, Name: "grok-oauth-realtime", Platform: PlatformGrok,
- Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true, Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "grok-access-token",
- "refresh_token": "grok-refresh-token",
- "expires_at": time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339),
- },
- }
- repo := &mockAccountRepoForGemini{accountsByID: map[int64]*Account{account.ID: account}}
- dialer := &grokRealtimeTestDialer{
- conn: &grokRealtimeTestConn{msg: []byte(`{"type":"session.created","session":{"id":"sess_1"}}`)},
- }
- svc := &AccountTestService{
- accountRepo: repo,
- grokTokenProvider: NewGrokTokenProvider(repo, nil),
- grokWSDialer: dialer,
- }
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/22/test", nil)
- err := svc.TestAccountConnection(c, account.ID, "", "", AccountTestModeGrokRealtime)
- require.NoError(t, err)
- require.Contains(t, dialer.lastURL, "wss://api.x.ai/v1/realtime")
- require.Contains(t, dialer.lastURL, "model=grok-voice-latest")
- require.Equal(t, "Bearer grok-access-token", dialer.lastAuth)
- require.Contains(t, rec.Body.String(), "realtime ws handshake ok")
- require.Contains(t, rec.Body.String(), "session.created")
- require.Contains(t, rec.Body.String(), `"type":"test_complete"`)
- require.Contains(t, rec.Body.String(), `"success":true`)
- }
- func TestAccountTestService_GrokRealtimeModeDialFailure(t *testing.T) {
- gin.SetMode(gin.TestMode)
- account := &Account{
- ID: 23, Name: "grok-oauth-realtime-fail", Platform: PlatformGrok,
- Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true, Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "grok-access-token",
- "refresh_token": "grok-refresh-token",
- "expires_at": time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339),
- },
- }
- repo := &mockAccountRepoForGemini{accountsByID: map[int64]*Account{account.ID: account}}
- dialer := &grokRealtimeTestDialer{
- status: 401,
- err: &openAIWSHandshakeError{Body: []byte(`{"error":"unauthorized"}`), Err: errors.New("websocket handshake failed")},
- }
- svc := &AccountTestService{
- accountRepo: repo,
- grokTokenProvider: NewGrokTokenProvider(repo, nil),
- grokWSDialer: dialer,
- }
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/23/test", nil)
- err := svc.TestAccountConnection(c, account.ID, "", "", AccountTestModeGrokRealtime)
- require.Error(t, err)
- require.Contains(t, rec.Body.String(), `"type":"error"`)
- require.Contains(t, rec.Body.String(), "Realtime")
- }
|