writer_test.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. package responsewriter
  2. import (
  3. "fmt"
  4. "math"
  5. "net/http"
  6. "net/http/httptest"
  7. "strconv"
  8. "testing"
  9. "time"
  10. "github.com/imgproxy/imgproxy/v3/httpheaders"
  11. "github.com/stretchr/testify/suite"
  12. )
  13. type ResponseWriterSuite struct {
  14. suite.Suite
  15. }
  16. type writerTestCase struct {
  17. name string
  18. req http.Header
  19. res http.Header
  20. config Config
  21. fn func(*Writer)
  22. }
  23. func (s *ResponseWriterSuite) TestHeaderCases() {
  24. expires := time.Date(2030, 8, 1, 0, 0, 0, 0, time.UTC)
  25. expiresSeconds := strconv.Itoa(int(time.Until(expires).Seconds()))
  26. shortExpires := time.Now().Add(10 * time.Second)
  27. shortExpiresSeconds := strconv.Itoa(int(time.Until(shortExpires).Seconds()))
  28. writeResponseTimeout := 10 * time.Second
  29. tt := []writerTestCase{
  30. {
  31. name: "MinimalHeaders",
  32. req: http.Header{},
  33. res: http.Header{
  34. httpheaders.CacheControl: []string{"no-cache"},
  35. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  36. },
  37. config: Config{
  38. SetCanonicalHeader: false,
  39. DefaultTTL: 0,
  40. CacheControlPassthrough: false,
  41. WriteResponseTimeout: writeResponseTimeout,
  42. },
  43. },
  44. {
  45. name: "PassthroughCacheControl",
  46. req: http.Header{
  47. httpheaders.CacheControl: []string{"no-cache, no-store, must-revalidate"},
  48. },
  49. res: http.Header{
  50. httpheaders.CacheControl: []string{"no-cache, no-store, must-revalidate"},
  51. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  52. },
  53. config: Config{
  54. CacheControlPassthrough: true,
  55. DefaultTTL: 3600,
  56. WriteResponseTimeout: writeResponseTimeout,
  57. },
  58. },
  59. {
  60. name: "PassthroughCacheControlExpires",
  61. req: http.Header{
  62. httpheaders.Expires: []string{expires.Format(http.TimeFormat)},
  63. },
  64. res: http.Header{
  65. httpheaders.CacheControl: []string{fmt.Sprintf("max-age=%s, public", expiresSeconds)},
  66. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  67. },
  68. config: Config{
  69. CacheControlPassthrough: true,
  70. DefaultTTL: 3600,
  71. WriteResponseTimeout: writeResponseTimeout,
  72. },
  73. },
  74. {
  75. name: "PassthroughCacheControlExpiredInThePast",
  76. req: http.Header{
  77. httpheaders.Expires: []string{time.Now().Add(-1 * time.Hour).UTC().Format(http.TimeFormat)},
  78. },
  79. res: http.Header{
  80. httpheaders.CacheControl: []string{"max-age=3600, public"},
  81. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  82. },
  83. config: Config{
  84. CacheControlPassthrough: true,
  85. DefaultTTL: 3600,
  86. WriteResponseTimeout: writeResponseTimeout,
  87. },
  88. },
  89. {
  90. name: "Canonical_ValidURL",
  91. req: http.Header{},
  92. res: http.Header{
  93. httpheaders.Link: []string{"<https://example.com/image.jpg>; rel=\"canonical\""},
  94. httpheaders.CacheControl: []string{"max-age=3600, public"},
  95. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  96. },
  97. config: Config{
  98. SetCanonicalHeader: true,
  99. DefaultTTL: 3600,
  100. WriteResponseTimeout: writeResponseTimeout,
  101. },
  102. fn: func(w *Writer) {
  103. w.SetCanonical("https://example.com/image.jpg")
  104. },
  105. },
  106. {
  107. name: "Canonical_InvalidURL",
  108. req: http.Header{},
  109. res: http.Header{
  110. httpheaders.CacheControl: []string{"max-age=3600, public"},
  111. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  112. },
  113. config: Config{
  114. SetCanonicalHeader: true,
  115. DefaultTTL: 3600,
  116. WriteResponseTimeout: writeResponseTimeout,
  117. },
  118. },
  119. {
  120. name: "WriteCanonical_Disabled",
  121. req: http.Header{},
  122. res: http.Header{
  123. httpheaders.CacheControl: []string{"max-age=3600, public"},
  124. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  125. },
  126. config: Config{
  127. SetCanonicalHeader: false,
  128. DefaultTTL: 3600,
  129. WriteResponseTimeout: writeResponseTimeout,
  130. },
  131. fn: func(w *Writer) {
  132. w.SetCanonical("https://example.com/image.jpg")
  133. },
  134. },
  135. {
  136. name: "SetMaxAgeTTL",
  137. req: http.Header{},
  138. res: http.Header{
  139. httpheaders.CacheControl: []string{"max-age=1, public"},
  140. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  141. },
  142. config: Config{
  143. DefaultTTL: 3600,
  144. FallbackImageTTL: 1,
  145. WriteResponseTimeout: writeResponseTimeout,
  146. },
  147. fn: func(w *Writer) {
  148. w.SetIsFallbackImage()
  149. },
  150. },
  151. {
  152. name: "SetMaxAgeExpires",
  153. req: http.Header{},
  154. res: http.Header{
  155. httpheaders.CacheControl: []string{fmt.Sprintf("max-age=%s, public", expiresSeconds)},
  156. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  157. },
  158. config: Config{
  159. DefaultTTL: math.MaxInt32,
  160. WriteResponseTimeout: writeResponseTimeout,
  161. },
  162. fn: func(w *Writer) {
  163. w.SetExpires(expires)
  164. },
  165. },
  166. {
  167. name: "SetMaxAgeTTLOutlivesExpires",
  168. req: http.Header{},
  169. res: http.Header{
  170. httpheaders.CacheControl: []string{fmt.Sprintf("max-age=%s, public", shortExpiresSeconds)},
  171. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  172. },
  173. config: Config{
  174. DefaultTTL: math.MaxInt32,
  175. FallbackImageTTL: 600,
  176. WriteResponseTimeout: writeResponseTimeout,
  177. },
  178. fn: func(w *Writer) {
  179. w.SetIsFallbackImage()
  180. w.SetExpires(shortExpires)
  181. },
  182. },
  183. {
  184. name: "SetVaryHeader",
  185. req: http.Header{},
  186. res: http.Header{
  187. httpheaders.Vary: []string{"Accept, Sec-CH-DPR, DPR, Sec-CH-Width, Width"},
  188. httpheaders.CacheControl: []string{"no-cache"},
  189. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  190. },
  191. config: Config{
  192. VaryValue: "Accept, Sec-CH-DPR, DPR, Sec-CH-Width, Width",
  193. WriteResponseTimeout: writeResponseTimeout,
  194. },
  195. fn: func(w *Writer) {
  196. w.SetVary()
  197. },
  198. },
  199. {
  200. name: "PassthroughHeaders",
  201. req: http.Header{
  202. "X-Test": []string{"foo", "bar"},
  203. },
  204. res: http.Header{
  205. "X-Test": []string{"foo", "bar"},
  206. httpheaders.CacheControl: []string{"no-cache"},
  207. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  208. },
  209. config: Config{
  210. WriteResponseTimeout: writeResponseTimeout,
  211. },
  212. fn: func(w *Writer) {
  213. w.Passthrough("X-Test")
  214. },
  215. },
  216. {
  217. name: "CopyFromHeaders",
  218. req: http.Header{},
  219. res: http.Header{
  220. "X-From": []string{"baz"},
  221. httpheaders.CacheControl: []string{"no-cache"},
  222. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  223. },
  224. config: Config{
  225. WriteResponseTimeout: writeResponseTimeout,
  226. },
  227. fn: func(w *Writer) {
  228. h := http.Header{}
  229. h.Set("X-From", "baz")
  230. w.CopyFrom(h, []string{"X-From"})
  231. },
  232. },
  233. {
  234. name: "WriteContentLength",
  235. req: http.Header{},
  236. res: http.Header{
  237. httpheaders.ContentLength: []string{"123"},
  238. httpheaders.CacheControl: []string{"no-cache"},
  239. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  240. },
  241. config: Config{
  242. WriteResponseTimeout: writeResponseTimeout,
  243. },
  244. fn: func(w *Writer) {
  245. w.SetContentLength(123)
  246. },
  247. },
  248. {
  249. name: "WriteContentType",
  250. req: http.Header{},
  251. res: http.Header{
  252. httpheaders.ContentType: []string{"image/png"},
  253. httpheaders.CacheControl: []string{"no-cache"},
  254. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  255. },
  256. config: Config{
  257. WriteResponseTimeout: writeResponseTimeout,
  258. },
  259. fn: func(w *Writer) {
  260. w.SetContentType("image/png")
  261. },
  262. },
  263. {
  264. name: "SetMaxAgeFromExpiresZero",
  265. req: http.Header{},
  266. res: http.Header{
  267. httpheaders.CacheControl: []string{"max-age=3600, public"},
  268. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  269. },
  270. config: Config{
  271. DefaultTTL: 3600,
  272. WriteResponseTimeout: writeResponseTimeout,
  273. },
  274. fn: func(w *Writer) {
  275. w.SetExpires(time.Time{})
  276. },
  277. },
  278. }
  279. for _, tc := range tt {
  280. s.Run(tc.name, func() {
  281. factory, err := NewFactory(&tc.config)
  282. s.Require().NoError(err)
  283. r := httptest.NewRecorder()
  284. writer := factory.NewWriter(r)
  285. writer.SetOriginHeaders(tc.req)
  286. if tc.fn != nil {
  287. tc.fn(writer)
  288. }
  289. writer.WriteHeader(http.StatusOK)
  290. s.Require().Equal(tc.res, r.Header())
  291. })
  292. }
  293. }
  294. func TestHeaderWriter(t *testing.T) {
  295. suite.Run(t, new(ResponseWriterSuite))
  296. }