openai_gateway_messages_transport_failover_test.go 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. //go:build unit
  2. package service
  3. import (
  4. "bytes"
  5. "context"
  6. "errors"
  7. "net/http"
  8. "net/http/httptest"
  9. "testing"
  10. "github.com/gin-gonic/gin"
  11. "github.com/stretchr/testify/require"
  12. )
  13. func TestForwardAsAnthropic_TransportError_ReturnsFailoverError(t *testing.T) {
  14. gin.SetMode(gin.TestMode)
  15. body := []byte(`{"model":"gpt-5.4","max_tokens":32,"messages":[{"role":"user","content":"hello"}],"stream":false}`)
  16. rec := httptest.NewRecorder()
  17. c, _ := gin.CreateTestContext(rec)
  18. c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
  19. c.Request.Header.Set("Content-Type", "application/json")
  20. upstream := &httpUpstreamRecorder{
  21. err: errors.New(`dial tcp 1.2.3.4:443: connect: connection refused`),
  22. }
  23. svc := &OpenAIGatewayService{
  24. cfg: rawChatCompletionsTestConfig(),
  25. httpUpstream: upstream,
  26. }
  27. account := rawChatCompletionsTestAccount()
  28. _, err := svc.ForwardAsAnthropic(context.Background(), c, account, body, "", "")
  29. require.Error(t, err)
  30. var failoverErr *UpstreamFailoverError
  31. require.True(t, errors.As(err, &failoverErr), "transport error should return UpstreamFailoverError for handler failover, got: %T", err)
  32. require.Equal(t, http.StatusBadGateway, failoverErr.StatusCode)
  33. }
  34. func TestForwardAsAnthropic_TransportError_DoesNotWriteResponse(t *testing.T) {
  35. gin.SetMode(gin.TestMode)
  36. body := []byte(`{"model":"gpt-5.4","max_tokens":32,"messages":[{"role":"user","content":"hello"}],"stream":false}`)
  37. rec := httptest.NewRecorder()
  38. c, _ := gin.CreateTestContext(rec)
  39. c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
  40. c.Request.Header.Set("Content-Type", "application/json")
  41. upstream := &httpUpstreamRecorder{
  42. err: errors.New(`read tcp: connection reset by peer`),
  43. }
  44. svc := &OpenAIGatewayService{
  45. cfg: rawChatCompletionsTestConfig(),
  46. httpUpstream: upstream,
  47. }
  48. account := rawChatCompletionsTestAccount()
  49. _, _ = svc.ForwardAsAnthropic(context.Background(), c, account, body, "", "")
  50. require.Equal(t, http.StatusOK, rec.Code, "transport error must not write HTTP response — handler owns the response for failover")
  51. require.Empty(t, rec.Body.String(), "response body must be empty so handler can write the correct error or failover")
  52. }
  53. func TestForwardAsAnthropic_TransportError_ClientCanceled_NoFailover(t *testing.T) {
  54. gin.SetMode(gin.TestMode)
  55. body := []byte(`{"model":"gpt-5.4","max_tokens":32,"messages":[{"role":"user","content":"hello"}],"stream":false}`)
  56. rec := httptest.NewRecorder()
  57. cancelCtx, cancel := context.WithCancel(context.Background())
  58. cancel()
  59. c, _ := gin.CreateTestContext(rec)
  60. c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body)).WithContext(cancelCtx)
  61. c.Request.Header.Set("Content-Type", "application/json")
  62. upstream := &httpUpstreamRecorder{
  63. err: context.Canceled,
  64. }
  65. svc := &OpenAIGatewayService{
  66. cfg: rawChatCompletionsTestConfig(),
  67. httpUpstream: upstream,
  68. }
  69. account := rawChatCompletionsTestAccount()
  70. _, err := svc.ForwardAsAnthropic(context.Background(), c, account, body, "", "")
  71. require.Error(t, err)
  72. var failoverErr *UpstreamFailoverError
  73. require.False(t, errors.As(err, &failoverErr), "client-canceled transport error should NOT trigger failover")
  74. }