response_handler_test.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package fetcher // import "miniflux.app/v2/internal/reader/fetcher"
  4. import (
  5. "errors"
  6. "io"
  7. "net/http"
  8. "testing"
  9. "time"
  10. )
  11. type testReadCloser struct {
  12. closed bool
  13. }
  14. func (rc *testReadCloser) Read(_ []byte) (int, error) {
  15. return 0, io.EOF
  16. }
  17. func (rc *testReadCloser) Close() error {
  18. rc.closed = true
  19. return nil
  20. }
  21. func TestIsModified(t *testing.T) {
  22. var cachedEtag = "abc123"
  23. var cachedLastModified = "Wed, 21 Oct 2015 07:28:00 GMT"
  24. var testCases = map[string]struct {
  25. Status int
  26. LastModified string
  27. ETag string
  28. IsModified bool
  29. }{
  30. "Unmodified 304": {
  31. Status: 304,
  32. LastModified: cachedLastModified,
  33. ETag: cachedEtag,
  34. IsModified: false,
  35. },
  36. "Unmodified 200": {
  37. Status: 200,
  38. LastModified: cachedLastModified,
  39. ETag: cachedEtag,
  40. IsModified: false,
  41. },
  42. // ETag takes precedence per RFC9110 8.8.1.
  43. "Last-Modified changed only": {
  44. Status: 200,
  45. LastModified: "Thu, 22 Oct 2015 07:28:00 GMT",
  46. ETag: cachedEtag,
  47. IsModified: false,
  48. },
  49. "ETag changed only": {
  50. Status: 200,
  51. LastModified: cachedLastModified,
  52. ETag: "xyz789",
  53. IsModified: true,
  54. },
  55. "ETag and Last-Modified changed": {
  56. Status: 200,
  57. LastModified: "Thu, 22 Oct 2015 07:28:00 GMT",
  58. ETag: "xyz789",
  59. IsModified: true,
  60. },
  61. }
  62. for name, tc := range testCases {
  63. t.Run(name, func(tt *testing.T) {
  64. header := http.Header{}
  65. header.Add("Last-Modified", tc.LastModified)
  66. header.Add("ETag", tc.ETag)
  67. rh := ResponseHandler{
  68. httpResponse: &http.Response{
  69. StatusCode: tc.Status,
  70. Header: header,
  71. },
  72. }
  73. if tc.IsModified != rh.IsModified(cachedEtag, cachedLastModified) {
  74. tt.Error(name)
  75. }
  76. })
  77. }
  78. }
  79. func TestRetryDelay(t *testing.T) {
  80. var testCases = map[string]struct {
  81. RetryAfterHeader string
  82. ExpectedDelay time.Duration
  83. }{
  84. "Empty header": {
  85. RetryAfterHeader: "",
  86. ExpectedDelay: 0,
  87. },
  88. "Integer value": {
  89. RetryAfterHeader: "42",
  90. ExpectedDelay: 42 * time.Second,
  91. },
  92. "HTTP-date": {
  93. RetryAfterHeader: time.Now().Add(42 * time.Second).Format(time.RFC1123),
  94. ExpectedDelay: 41 * time.Second,
  95. },
  96. }
  97. for name, tc := range testCases {
  98. t.Run(name, func(tt *testing.T) {
  99. header := http.Header{}
  100. header.Add("Retry-After", tc.RetryAfterHeader)
  101. rh := ResponseHandler{
  102. httpResponse: &http.Response{
  103. Header: header,
  104. },
  105. }
  106. if tc.ExpectedDelay != rh.ParseRetryDelay() {
  107. tt.Errorf("Expected %d, got %d for scenario %q", tc.ExpectedDelay, rh.ParseRetryDelay(), name)
  108. }
  109. })
  110. }
  111. }
  112. func TestExpiresInMinutes(t *testing.T) {
  113. var testCases = map[string]struct {
  114. ExpiresHeader string
  115. Expected time.Duration
  116. }{
  117. "Empty header": {
  118. ExpiresHeader: "",
  119. Expected: 0,
  120. },
  121. "Valid Expires header": {
  122. ExpiresHeader: time.Now().Add(10 * time.Minute).Format(time.RFC1123),
  123. Expected: 10 * time.Minute,
  124. },
  125. "Invalid Expires header": {
  126. ExpiresHeader: "invalid-date",
  127. Expected: 0,
  128. },
  129. }
  130. for name, tc := range testCases {
  131. t.Run(name, func(tt *testing.T) {
  132. header := http.Header{}
  133. header.Add("Expires", tc.ExpiresHeader)
  134. rh := ResponseHandler{
  135. httpResponse: &http.Response{
  136. Header: header,
  137. },
  138. }
  139. if tc.Expected != rh.Expires() {
  140. t.Errorf("Expected %d, got %d for scenario %q", tc.Expected, rh.Expires(), name)
  141. }
  142. })
  143. }
  144. }
  145. func TestCacheControlMaxAgeInMinutes(t *testing.T) {
  146. var testCases = map[string]struct {
  147. CacheControlHeader string
  148. Expected time.Duration
  149. }{
  150. "Empty header": {
  151. CacheControlHeader: "",
  152. Expected: 0,
  153. },
  154. "Valid max-age": {
  155. CacheControlHeader: "max-age=600",
  156. Expected: 10 * time.Minute,
  157. },
  158. "Invalid max-age": {
  159. CacheControlHeader: "max-age=invalid",
  160. Expected: 0,
  161. },
  162. "Multiple directives": {
  163. CacheControlHeader: "no-cache, max-age=300",
  164. Expected: 5 * time.Minute,
  165. },
  166. }
  167. for name, tc := range testCases {
  168. t.Run(name, func(tt *testing.T) {
  169. header := http.Header{}
  170. header.Add("Cache-Control", tc.CacheControlHeader)
  171. rh := ResponseHandler{
  172. httpResponse: &http.Response{
  173. Header: header,
  174. },
  175. }
  176. if tc.Expected != rh.CacheControlMaxAge() {
  177. t.Errorf("Expected %d, got %d for scenario %q", tc.Expected, rh.CacheControlMaxAge(), name)
  178. }
  179. })
  180. }
  181. }
  182. func TestResponseHandlerCloseClosesBodyOnClientError(t *testing.T) {
  183. body := &testReadCloser{}
  184. rh := ResponseHandler{
  185. httpResponse: &http.Response{Body: body},
  186. clientErr: errors.New("boom"),
  187. }
  188. rh.Close()
  189. if !body.closed {
  190. t.Error("Expected response body to be closed")
  191. }
  192. }