decompress_response_test.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. package repository
  2. import (
  3. "bytes"
  4. "compress/flate"
  5. "compress/gzip"
  6. "io"
  7. "log/slog"
  8. "net/http"
  9. "testing"
  10. "github.com/andybalholm/brotli"
  11. "github.com/klauspost/compress/zstd"
  12. "github.com/stretchr/testify/require"
  13. "github.com/tidwall/gjson"
  14. )
  15. func TestDecompressResponseBodyZstdUsage(t *testing.T) {
  16. payload := []byte(`{"usage":{"input_tokens":123,"output_tokens":45,"cache_read_input_tokens":67}}`)
  17. compressed := compressZstd(t, payload)
  18. resp := newEncodedResponse("zstd", compressed)
  19. decompressResponseBody(resp)
  20. body, err := io.ReadAll(resp.Body)
  21. require.NoError(t, err)
  22. require.Equal(t, payload, body)
  23. require.Equal(t, int64(123), gjson.GetBytes(body, "usage.input_tokens").Int())
  24. require.Equal(t, int64(45), gjson.GetBytes(body, "usage.output_tokens").Int())
  25. require.Equal(t, int64(67), gjson.GetBytes(body, "usage.cache_read_input_tokens").Int())
  26. require.Empty(t, resp.Header.Get("Content-Encoding"))
  27. require.Empty(t, resp.Header.Get("Content-Length"))
  28. require.Equal(t, int64(-1), resp.ContentLength)
  29. require.NoError(t, resp.Body.Close())
  30. }
  31. func TestDecompressResponseBodyExistingEncodings(t *testing.T) {
  32. payload := []byte(`{"ok":true}`)
  33. tests := []struct {
  34. name string
  35. encoding string
  36. compress func(*testing.T, []byte) []byte
  37. }{
  38. {name: "gzip", encoding: "gzip", compress: compressGzip},
  39. {name: "brotli", encoding: "br", compress: compressBrotli},
  40. {name: "deflate", encoding: "deflate", compress: compressDeflate},
  41. }
  42. for _, tt := range tests {
  43. t.Run(tt.name, func(t *testing.T) {
  44. resp := newEncodedResponse(tt.encoding, tt.compress(t, payload))
  45. decompressResponseBody(resp)
  46. body, err := io.ReadAll(resp.Body)
  47. require.NoError(t, err)
  48. require.Equal(t, payload, body)
  49. require.Empty(t, resp.Header.Get("Content-Encoding"))
  50. require.Empty(t, resp.Header.Get("Content-Length"))
  51. require.Equal(t, int64(-1), resp.ContentLength)
  52. require.NoError(t, resp.Body.Close())
  53. })
  54. }
  55. }
  56. func TestDecompressResponseBodyWithoutEncodingLeavesBodyUntouched(t *testing.T) {
  57. originalBody := &responseTestBody{Reader: bytes.NewReader([]byte("plain"))}
  58. resp := &http.Response{
  59. Header: make(http.Header),
  60. Body: originalBody,
  61. ContentLength: 5,
  62. }
  63. decompressResponseBody(resp)
  64. require.Same(t, originalBody, resp.Body)
  65. require.Equal(t, int64(5), resp.ContentLength)
  66. body, err := io.ReadAll(resp.Body)
  67. require.NoError(t, err)
  68. require.Equal(t, "plain", string(body))
  69. require.NoError(t, resp.Body.Close())
  70. }
  71. func TestDecompressResponseBodyInvalidZstdWarnsAndPreservesBody(t *testing.T) {
  72. previousLogger := slog.Default()
  73. var logOutput bytes.Buffer
  74. slog.SetDefault(slog.New(slog.NewTextHandler(&logOutput, nil)))
  75. t.Cleanup(func() {
  76. slog.SetDefault(previousLogger)
  77. })
  78. payload := []byte("not a zstd response")
  79. resp := newEncodedResponse("zstd", payload)
  80. require.NotPanics(t, func() {
  81. decompressResponseBody(resp)
  82. })
  83. body, err := io.ReadAll(resp.Body)
  84. require.NoError(t, err)
  85. require.Equal(t, payload, body)
  86. require.Equal(t, "zstd", resp.Header.Get("Content-Encoding"))
  87. require.Equal(t, int64(len(payload)), resp.ContentLength)
  88. require.Contains(t, logOutput.String(), "msg=zstd_decompress_failed")
  89. require.NoError(t, resp.Body.Close())
  90. }
  91. func TestDecompressResponseBodyEmptyZstdWarnsAndPreservesBody(t *testing.T) {
  92. previousLogger := slog.Default()
  93. var logOutput bytes.Buffer
  94. slog.SetDefault(slog.New(slog.NewTextHandler(&logOutput, nil)))
  95. t.Cleanup(func() {
  96. slog.SetDefault(previousLogger)
  97. })
  98. resp := newEncodedResponse("zstd", nil)
  99. require.NotPanics(t, func() {
  100. decompressResponseBody(resp)
  101. })
  102. body, err := io.ReadAll(resp.Body)
  103. require.NoError(t, err)
  104. require.Empty(t, body)
  105. require.Equal(t, "zstd", resp.Header.Get("Content-Encoding"))
  106. require.Equal(t, int64(0), resp.ContentLength)
  107. require.Contains(t, logOutput.String(), "msg=zstd_decompress_failed")
  108. require.NoError(t, resp.Body.Close())
  109. }
  110. type responseTestBody struct {
  111. io.Reader
  112. }
  113. func (b *responseTestBody) Close() error {
  114. return nil
  115. }
  116. func newEncodedResponse(encoding string, body []byte) *http.Response {
  117. header := make(http.Header)
  118. header.Set("Content-Encoding", encoding)
  119. header.Set("Content-Length", "123")
  120. return &http.Response{
  121. Header: header,
  122. Body: io.NopCloser(bytes.NewReader(body)),
  123. ContentLength: int64(len(body)),
  124. }
  125. }
  126. func compressZstd(t *testing.T, payload []byte) []byte {
  127. t.Helper()
  128. var buf bytes.Buffer
  129. zw, err := zstd.NewWriter(&buf)
  130. require.NoError(t, err)
  131. _, err = zw.Write(payload)
  132. require.NoError(t, err)
  133. require.NoError(t, zw.Close())
  134. return buf.Bytes()
  135. }
  136. func compressGzip(t *testing.T, payload []byte) []byte {
  137. t.Helper()
  138. var buf bytes.Buffer
  139. zw := gzip.NewWriter(&buf)
  140. _, err := zw.Write(payload)
  141. require.NoError(t, err)
  142. require.NoError(t, zw.Close())
  143. return buf.Bytes()
  144. }
  145. func compressBrotli(t *testing.T, payload []byte) []byte {
  146. t.Helper()
  147. var buf bytes.Buffer
  148. zw := brotli.NewWriter(&buf)
  149. _, err := zw.Write(payload)
  150. require.NoError(t, err)
  151. require.NoError(t, zw.Close())
  152. return buf.Bytes()
  153. }
  154. func compressDeflate(t *testing.T, payload []byte) []byte {
  155. t.Helper()
  156. var buf bytes.Buffer
  157. zw, err := flate.NewWriter(&buf, flate.DefaultCompression)
  158. require.NoError(t, err)
  159. _, err = zw.Write(payload)
  160. require.NoError(t, err)
  161. require.NoError(t, zw.Close())
  162. return buf.Bytes()
  163. }