openai_live_test.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. package handler
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "mime/multipart"
  6. "net/http"
  7. "net/http/httptest"
  8. "testing"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/gin-gonic/gin"
  11. "github.com/stretchr/testify/require"
  12. )
  13. func TestParseLiveCallRequestMultipartPreservesSession(t *testing.T) {
  14. gin.SetMode(gin.TestMode)
  15. session := `{"model":"gpt-live-test","delegation":{"type":"client"},"instructions":"你好"}`
  16. var body bytes.Buffer
  17. writer := multipart.NewWriter(&body)
  18. require.NoError(t, writer.WriteField("sdp", "v=0\r\n"))
  19. require.NoError(t, writer.WriteField("session", session))
  20. require.NoError(t, writer.Close())
  21. request := httptest.NewRequest("POST", "/v1/live", &body)
  22. request.Header.Set("Content-Type", writer.FormDataContentType())
  23. context, _ := gin.CreateTestContext(httptest.NewRecorder())
  24. context.Request = request
  25. parsed, err := parseLiveCallRequest(context)
  26. require.NoError(t, err)
  27. require.Equal(t, "v=0\r\n", parsed.SDP)
  28. require.JSONEq(t, session, string(parsed.Session))
  29. require.Equal(t, "client", jsonPathString(t, parsed.Session, "delegation", "type"))
  30. }
  31. func TestParseLiveCallRequestJSONPreservesSessionWithoutDelegation(t *testing.T) {
  32. gin.SetMode(gin.TestMode)
  33. body := `{"sdp":"v=0\\r\\n","session":{"model":"gpt-live-test","instructions":"standalone"}}`
  34. request := httptest.NewRequest("POST", "/backend-api/codex/realtime/calls", bytes.NewBufferString(body))
  35. request.Header.Set("Content-Type", "application/json")
  36. context, _ := gin.CreateTestContext(httptest.NewRecorder())
  37. context.Request = request
  38. parsed, err := parseLiveCallRequest(context)
  39. require.NoError(t, err)
  40. require.NotContains(t, string(parsed.Session), "delegation")
  41. require.Equal(t, "standalone", jsonPathString(t, parsed.Session, "instructions"))
  42. }
  43. func TestParseLiveCallRequestRejectsInvalidJSONShape(t *testing.T) {
  44. gin.SetMode(gin.TestMode)
  45. testCases := []string{
  46. `{"session":{"type":"quicksilver"}}`,
  47. `{"sdp":"v=0\\r\\n","session":[]}`,
  48. `{"sdp":"v=0\\r\\n","session":null}`,
  49. `{"sdp":"v=0\\r\\n","session":{"type":"quicksilver"}} {}`,
  50. }
  51. for _, body := range testCases {
  52. request := httptest.NewRequest("POST", "/backend-api/codex/realtime/calls", bytes.NewBufferString(body))
  53. request.Header.Set("Content-Type", "application/json")
  54. context, _ := gin.CreateTestContext(httptest.NewRecorder())
  55. context.Request = request
  56. _, err := parseLiveCallRequest(context)
  57. require.Error(t, err)
  58. }
  59. }
  60. func TestLiveSidebandLocationMatchesCreateRoute(t *testing.T) {
  61. require.Equal(t, "/v1/live/call_123", liveSidebandLocation("/v1/live", "call_123"))
  62. require.Equal(
  63. t,
  64. "/backend-api/codex/call_123",
  65. liveSidebandLocation("/backend-api/codex/realtime/calls", "call_123"),
  66. )
  67. }
  68. func TestLiveEnabledForAPIKey(t *testing.T) {
  69. require.False(t, liveEnabledForAPIKey(nil))
  70. require.False(t, liveEnabledForAPIKey(&service.APIKey{}))
  71. require.False(t, liveEnabledForAPIKey(&service.APIKey{
  72. Group: &service.Group{Platform: service.PlatformOpenAI},
  73. }))
  74. require.False(t, liveEnabledForAPIKey(&service.APIKey{
  75. Group: &service.Group{Platform: service.PlatformAnthropic, AllowLive: true},
  76. }))
  77. require.True(t, liveEnabledForAPIKey(&service.APIKey{
  78. Group: &service.Group{Platform: service.PlatformOpenAI, AllowLive: true},
  79. }))
  80. require.True(t, liveEnabledForAPIKey(&service.APIKey{
  81. Group: &service.Group{Platform: service.PlatformComposite, AllowLive: true},
  82. }))
  83. }
  84. func TestLiveAttestationErrorIsExplicit(t *testing.T) {
  85. gin.SetMode(gin.TestMode)
  86. recorder := httptest.NewRecorder()
  87. context, _ := gin.CreateTestContext(recorder)
  88. (&OpenAIGatewayHandler{}).writeLiveCreateError(context, &service.LiveAttestationUnavailableError{
  89. Reason: "Live attestation is only supported when Sub2API runs on macOS",
  90. })
  91. require.Equal(t, http.StatusServiceUnavailable, recorder.Code)
  92. require.Contains(t, recorder.Body.String(), "Sub2API runs on macOS")
  93. }
  94. func jsonPathString(t *testing.T, raw json.RawMessage, keys ...string) string {
  95. t.Helper()
  96. var value any
  97. require.NoError(t, json.Unmarshal(raw, &value))
  98. current := value
  99. for _, key := range keys {
  100. object, ok := current.(map[string]any)
  101. require.True(t, ok)
  102. current = object[key]
  103. }
  104. result, ok := current.(string)
  105. require.True(t, ok)
  106. return result
  107. }