writer_test.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. package headerwriter
  2. import (
  3. "fmt"
  4. "math"
  5. "net/http"
  6. "net/http/httptest"
  7. "strconv"
  8. "testing"
  9. "time"
  10. "github.com/stretchr/testify/suite"
  11. "go.withmatt.com/httpheaders"
  12. )
  13. type HeaderWriterSuite struct {
  14. suite.Suite
  15. }
  16. type writerTestCase struct {
  17. name string
  18. url string
  19. req http.Header
  20. res http.Header
  21. config Config
  22. fn func(*Writer)
  23. }
  24. func (s *HeaderWriterSuite) TestHeaderCases() {
  25. expires := time.Date(2030, 8, 1, 0, 0, 0, 0, time.UTC)
  26. expiresSeconds := strconv.Itoa(int(time.Until(expires).Seconds()))
  27. tt := []writerTestCase{
  28. {
  29. name: "MinimalHeaders",
  30. req: http.Header{},
  31. res: http.Header{
  32. httpheaders.CacheControl: []string{"no-cache"},
  33. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  34. },
  35. config: Config{
  36. SetCanonicalHeader: false,
  37. DefaultTTL: 0,
  38. CacheControlPassthrough: false,
  39. LastModifiedEnabled: false,
  40. EnableClientHints: false,
  41. SetVaryAccept: false,
  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. },
  57. },
  58. {
  59. name: "PassthroughCacheControlExpires",
  60. req: http.Header{
  61. httpheaders.Expires: []string{expires.Format(http.TimeFormat)},
  62. },
  63. res: http.Header{
  64. httpheaders.CacheControl: []string{fmt.Sprintf("max-age=%s, public", expiresSeconds)},
  65. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  66. },
  67. config: Config{
  68. CacheControlPassthrough: true,
  69. DefaultTTL: 3600,
  70. },
  71. },
  72. {
  73. name: "Canonical_ValidURL",
  74. req: http.Header{},
  75. url: "https://example.com/image.jpg",
  76. res: http.Header{
  77. httpheaders.Link: []string{"<https://example.com/image.jpg>; rel=\"canonical\""},
  78. httpheaders.CacheControl: []string{"max-age=3600, public"},
  79. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  80. },
  81. config: Config{
  82. SetCanonicalHeader: true,
  83. DefaultTTL: 3600,
  84. },
  85. fn: func(w *Writer) {
  86. w.SetCanonical()
  87. },
  88. },
  89. {
  90. name: "Canonical_InvalidURL",
  91. url: "ftp://example.com/image.jpg",
  92. req: http.Header{},
  93. res: http.Header{
  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. },
  101. },
  102. {
  103. name: "WriteCanonical_Disabled",
  104. req: http.Header{},
  105. url: "https://example.com/image.jpg",
  106. res: http.Header{
  107. httpheaders.CacheControl: []string{"max-age=3600, public"},
  108. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  109. },
  110. config: Config{
  111. SetCanonicalHeader: false,
  112. DefaultTTL: 3600,
  113. },
  114. fn: func(w *Writer) {
  115. w.SetCanonical()
  116. },
  117. },
  118. {
  119. name: "LastModified",
  120. req: http.Header{
  121. httpheaders.LastModified: []string{expires.Format(http.TimeFormat)},
  122. },
  123. res: http.Header{
  124. httpheaders.LastModified: []string{expires.Format(http.TimeFormat)},
  125. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  126. httpheaders.CacheControl: []string{"max-age=3600, public"},
  127. },
  128. config: Config{
  129. LastModifiedEnabled: true,
  130. DefaultTTL: 3600,
  131. },
  132. fn: func(w *Writer) {
  133. w.SetLastModified()
  134. },
  135. },
  136. {
  137. name: "SetMaxAgeExplicit",
  138. req: http.Header{},
  139. res: http.Header{
  140. httpheaders.CacheControl: []string{"max-age=1, public"},
  141. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  142. },
  143. config: Config{
  144. DefaultTTL: 3600,
  145. },
  146. fn: func(w *Writer) {
  147. w.SetMaxAge(1)
  148. },
  149. },
  150. {
  151. name: "SetMaxAgeFromTime",
  152. req: http.Header{},
  153. res: http.Header{
  154. httpheaders.CacheControl: []string{fmt.Sprintf("max-age=%s, public", expiresSeconds)},
  155. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  156. },
  157. config: Config{
  158. DefaultTTL: math.MaxInt32,
  159. },
  160. fn: func(w *Writer) {
  161. w.SetMaxAgeFromExpires(&expires)
  162. },
  163. },
  164. {
  165. name: "SetVaryHeader",
  166. req: http.Header{},
  167. res: http.Header{
  168. httpheaders.Vary: []string{"Accept, Sec-CH-DPR, DPR, Sec-CH-Width, Width"},
  169. httpheaders.CacheControl: []string{"no-cache"},
  170. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  171. },
  172. config: Config{
  173. EnableClientHints: true,
  174. SetVaryAccept: true,
  175. },
  176. fn: func(w *Writer) {
  177. w.SetVary()
  178. },
  179. },
  180. {
  181. name: "CopyHeaders",
  182. req: http.Header{
  183. "X-Test": []string{"foo", "bar"},
  184. },
  185. res: http.Header{
  186. "X-Test": []string{"foo", "bar"},
  187. httpheaders.CacheControl: []string{"no-cache"},
  188. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  189. },
  190. config: Config{},
  191. fn: func(w *Writer) {
  192. w.Copy([]string{"X-Test"})
  193. },
  194. },
  195. {
  196. name: "CopyFromHeaders",
  197. req: http.Header{},
  198. res: http.Header{
  199. "X-From": []string{"baz"},
  200. httpheaders.CacheControl: []string{"no-cache"},
  201. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  202. },
  203. config: Config{},
  204. fn: func(w *Writer) {
  205. h := http.Header{}
  206. h.Set("X-From", "baz")
  207. w.CopyFrom(h, []string{"X-From"})
  208. },
  209. },
  210. {
  211. name: "WriteContentLength",
  212. req: http.Header{},
  213. res: http.Header{
  214. httpheaders.ContentLength: []string{"123"},
  215. httpheaders.CacheControl: []string{"no-cache"},
  216. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  217. },
  218. config: Config{},
  219. fn: func(w *Writer) {
  220. w.SetContentLength(123)
  221. },
  222. },
  223. {
  224. name: "WriteContentDispositionInline",
  225. req: http.Header{},
  226. res: http.Header{
  227. httpheaders.ContentDisposition: []string{"inline; filename=\"file.txt\""},
  228. httpheaders.CacheControl: []string{"no-cache"},
  229. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  230. },
  231. config: Config{},
  232. fn: func(w *Writer) {
  233. w.SetContentDisposition("file", ".txt", false)
  234. },
  235. },
  236. {
  237. name: "WriteContentDispositionAttachment",
  238. req: http.Header{},
  239. res: http.Header{
  240. httpheaders.ContentDisposition: []string{"attachment; filename=\"file.txt\""},
  241. httpheaders.CacheControl: []string{"no-cache"},
  242. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  243. },
  244. config: Config{},
  245. fn: func(w *Writer) {
  246. w.SetContentDisposition("file", ".txt", true)
  247. },
  248. },
  249. {
  250. name: "WriteContentType",
  251. req: http.Header{},
  252. res: http.Header{
  253. httpheaders.ContentType: []string{"image/png"},
  254. httpheaders.CacheControl: []string{"no-cache"},
  255. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  256. },
  257. config: Config{},
  258. fn: func(w *Writer) {
  259. w.SetContentType("image/png")
  260. },
  261. },
  262. {
  263. name: "WriteIsFallbackImage",
  264. req: http.Header{},
  265. res: http.Header{
  266. "Fallback-Image": []string{"1"},
  267. httpheaders.CacheControl: []string{"no-cache"},
  268. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  269. },
  270. config: Config{},
  271. fn: func(w *Writer) {
  272. w.SetIsFallbackImage()
  273. },
  274. },
  275. {
  276. name: "SetMaxAgeZeroOrNegative",
  277. req: http.Header{},
  278. res: http.Header{
  279. httpheaders.CacheControl: []string{"max-age=3600, public"},
  280. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  281. },
  282. config: Config{
  283. DefaultTTL: 3600,
  284. },
  285. fn: func(w *Writer) {
  286. w.SetMaxAge(0)
  287. w.SetMaxAge(-10)
  288. },
  289. },
  290. {
  291. name: "SetMaxAgeFromExpiresNil",
  292. req: http.Header{},
  293. res: http.Header{
  294. httpheaders.CacheControl: []string{"max-age=3600, public"},
  295. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  296. },
  297. config: Config{
  298. DefaultTTL: 3600,
  299. },
  300. fn: func(w *Writer) {
  301. w.SetMaxAgeFromExpires(nil)
  302. },
  303. },
  304. {
  305. name: "WriteVaryAcceptOnly",
  306. req: http.Header{},
  307. res: http.Header{
  308. httpheaders.Vary: []string{"Accept"},
  309. httpheaders.CacheControl: []string{"no-cache"},
  310. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  311. },
  312. config: Config{
  313. SetVaryAccept: true,
  314. },
  315. fn: func(w *Writer) {
  316. w.SetVary()
  317. },
  318. },
  319. {
  320. name: "WriteVaryClientHintsOnly",
  321. req: http.Header{},
  322. res: http.Header{
  323. httpheaders.Vary: []string{"Sec-CH-DPR, DPR, Sec-CH-Width, Width"},
  324. httpheaders.CacheControl: []string{"no-cache"},
  325. httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
  326. },
  327. config: Config{
  328. EnableClientHints: true,
  329. },
  330. fn: func(w *Writer) {
  331. w.SetVary()
  332. },
  333. },
  334. }
  335. for _, tc := range tt {
  336. s.Run(tc.name, func() {
  337. f := NewFactory(&tc.config)
  338. writer := f.NewHeaderWriter(tc.req, tc.url)
  339. if tc.fn != nil {
  340. tc.fn(writer)
  341. }
  342. r := httptest.NewRecorder()
  343. writer.Write(r)
  344. s.Require().Equal(tc.res, r.Header())
  345. })
  346. }
  347. }
  348. func TestHeaderWriter(t *testing.T) {
  349. suite.Run(t, new(HeaderWriterSuite))
  350. }