processing_handler_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. package integration
  2. import (
  3. "fmt"
  4. "net/http"
  5. "net/http/httptest"
  6. "regexp"
  7. "testing"
  8. "time"
  9. "github.com/imgproxy/imgproxy/v3/config"
  10. "github.com/imgproxy/imgproxy/v3/config/configurators"
  11. "github.com/imgproxy/imgproxy/v3/fetcher"
  12. "github.com/imgproxy/imgproxy/v3/httpheaders"
  13. "github.com/imgproxy/imgproxy/v3/imagedata"
  14. "github.com/imgproxy/imgproxy/v3/imagetype"
  15. "github.com/imgproxy/imgproxy/v3/svg"
  16. "github.com/imgproxy/imgproxy/v3/testutil"
  17. "github.com/imgproxy/imgproxy/v3/vips"
  18. "github.com/stretchr/testify/suite"
  19. )
  20. // ProcessingHandlerTestSuite is a test suite for testing image processing handler
  21. type ProcessingHandlerTestSuite struct {
  22. Suite
  23. }
  24. func (s *ProcessingHandlerTestSuite) SetupTest() {
  25. config.Reset() // We reset config only at the start of each test
  26. // NOTE: This must be moved to security config
  27. config.AllowLoopbackSourceAddresses = true
  28. // NOTE: end note
  29. }
  30. func (s *ProcessingHandlerTestSuite) SetupSubTest() {
  31. // We use t.Run() a lot, so we need to reset lazy objects at the beginning of each subtest
  32. s.ResetLazyObjects()
  33. }
  34. func (s *ProcessingHandlerTestSuite) TestSignatureValidationFailure() {
  35. config.Keys = [][]byte{[]byte("test-key")}
  36. config.Salts = [][]byte{[]byte("test-salt")}
  37. tt := []struct {
  38. name string
  39. url string
  40. statusCode int
  41. }{
  42. {
  43. name: "NoSignature",
  44. url: "/unsafe/rs:fill:4:4/plain/local:///test1.png",
  45. statusCode: http.StatusForbidden,
  46. },
  47. {
  48. name: "BadSignature",
  49. url: "/bad-signature/rs:fill:4:4/plain/local:///test1.png",
  50. statusCode: http.StatusForbidden,
  51. },
  52. {
  53. name: "ValidSignature",
  54. url: "/My9d3xq_PYpVHsPrCyww0Kh1w5KZeZhIlWhsa4az1TI/rs:fill:4:4/plain/local:///test1.png",
  55. statusCode: http.StatusOK,
  56. },
  57. }
  58. for _, tc := range tt {
  59. s.Run(tc.name, func() {
  60. res := s.GET(tc.url)
  61. s.Require().Equal(tc.statusCode, res.StatusCode)
  62. })
  63. }
  64. }
  65. func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
  66. imagedata.RedirectAllRequestsTo("local:///test1.png")
  67. defer imagedata.StopRedirectingRequests()
  68. tt := []struct {
  69. name string
  70. allowedSources []string
  71. requestPath string
  72. expectedError bool
  73. }{
  74. {
  75. name: "match http URL without wildcard",
  76. allowedSources: []string{"local://", "http://images.dev/"},
  77. requestPath: "/unsafe/plain/http://images.dev/lorem/ipsum.jpg",
  78. },
  79. {
  80. name: "match http URL with wildcard in hostname single level",
  81. allowedSources: []string{"local://", "http://*.mycdn.dev/"},
  82. requestPath: "/unsafe/plain/http://a-1.mycdn.dev/lorem/ipsum.jpg",
  83. },
  84. {
  85. name: "match http URL with wildcard in hostname multiple levels",
  86. allowedSources: []string{"local://", "http://*.mycdn.dev/"},
  87. requestPath: "/unsafe/plain/http://a-1.b-2.mycdn.dev/lorem/ipsum.jpg",
  88. },
  89. {
  90. name: "no match s3 URL with allowed local and http URLs",
  91. allowedSources: []string{"local://", "http://images.dev/"},
  92. requestPath: "/unsafe/plain/s3://images/lorem/ipsum.jpg",
  93. expectedError: true,
  94. },
  95. {
  96. name: "no match http URL with wildcard in hostname including slash",
  97. allowedSources: []string{"local://", "http://*.mycdn.dev/"},
  98. requestPath: "/unsafe/plain/http://other.dev/.mycdn.dev/lorem/ipsum.jpg",
  99. expectedError: true,
  100. },
  101. }
  102. for _, tc := range tt {
  103. s.Run(tc.name, func() {
  104. config.AllowedSources = make([]*regexp.Regexp, len(tc.allowedSources))
  105. for i, pattern := range tc.allowedSources {
  106. config.AllowedSources[i] = configurators.RegexpFromPattern(pattern)
  107. }
  108. res := s.GET(tc.requestPath)
  109. if tc.expectedError {
  110. s.Require().Equal(http.StatusNotFound, res.StatusCode)
  111. } else {
  112. s.Require().Equal(http.StatusOK, res.StatusCode)
  113. }
  114. })
  115. }
  116. }
  117. func (s *ProcessingHandlerTestSuite) TestSourceNetworkValidation() {
  118. data := s.TestData.Read("test1.png")
  119. server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  120. rw.WriteHeader(200)
  121. rw.Write(data)
  122. }))
  123. defer server.Close()
  124. url := fmt.Sprintf("/unsafe/rs:fill:4:4/plain/%s/test1.png", server.URL)
  125. // We wrap this in a subtest to reset s.router()
  126. s.Run("AllowLoopbackSourceAddressesTrue", func() {
  127. config.AllowLoopbackSourceAddresses = true
  128. res := s.GET(url)
  129. s.Require().Equal(http.StatusOK, res.StatusCode)
  130. })
  131. s.Run("AllowLoopbackSourceAddressesFalse", func() {
  132. config.AllowLoopbackSourceAddresses = false
  133. res := s.GET(url)
  134. s.Require().Equal(http.StatusNotFound, res.StatusCode)
  135. })
  136. }
  137. func (s *ProcessingHandlerTestSuite) TestSourceFormatNotSupported() {
  138. vips.DisableLoadSupport(imagetype.PNG)
  139. defer vips.ResetLoadSupport()
  140. res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
  141. s.Require().Equal(http.StatusUnprocessableEntity, res.StatusCode)
  142. }
  143. func (s *ProcessingHandlerTestSuite) TestResultingFormatNotSupported() {
  144. vips.DisableSaveSupport(imagetype.PNG)
  145. defer vips.ResetSaveSupport()
  146. res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
  147. s.Require().Equal(http.StatusUnprocessableEntity, res.StatusCode)
  148. }
  149. func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
  150. config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
  151. res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
  152. s.Require().Equal(http.StatusOK, res.StatusCode)
  153. s.Require().True(s.TestData.FileEqualsToReader("test1.png", res.Body))
  154. }
  155. func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
  156. res := s.GET("/unsafe/rs:fill:4:4/skp:png/plain/local:///test1.png")
  157. s.Require().Equal(http.StatusOK, res.StatusCode)
  158. s.Require().True(s.TestData.FileEqualsToReader("test1.png", res.Body))
  159. }
  160. func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
  161. config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
  162. res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
  163. s.Require().Equal(http.StatusOK, res.StatusCode)
  164. s.Require().True(s.TestData.FileEqualsToReader("test1.png", res.Body))
  165. }
  166. func (s *ProcessingHandlerTestSuite) TestSkipProcessingDifferentFormat() {
  167. config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
  168. res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@jpg")
  169. s.Require().Equal(http.StatusOK, res.StatusCode)
  170. s.Require().False(s.TestData.FileEqualsToReader("test1.png", res.Body))
  171. }
  172. func (s *ProcessingHandlerTestSuite) TestSkipProcessingSVG() {
  173. res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.svg")
  174. s.Require().Equal(http.StatusOK, res.StatusCode)
  175. c := fetcher.NewDefaultConfig()
  176. f, err := fetcher.New(&c)
  177. s.Require().NoError(err)
  178. idf := imagedata.NewFactory(f)
  179. data, err := idf.NewFromBytes(s.TestData.Read("test1.svg"))
  180. s.Require().NoError(err)
  181. expected, err := svg.Sanitize(data)
  182. s.Require().NoError(err)
  183. s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
  184. }
  185. func (s *ProcessingHandlerTestSuite) TestNotSkipProcessingSVGToJPG() {
  186. res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.svg@jpg")
  187. s.Require().Equal(http.StatusOK, res.StatusCode)
  188. s.Require().False(s.TestData.FileEqualsToReader("test1.svg", res.Body))
  189. }
  190. func (s *ProcessingHandlerTestSuite) TestErrorSavingToSVG() {
  191. res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@svg")
  192. s.Require().Equal(http.StatusUnprocessableEntity, res.StatusCode)
  193. }
  194. func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughCacheControl() {
  195. s.Config().HeaderWriter.CacheControlPassthrough = true
  196. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  197. rw.Header().Set(httpheaders.CacheControl, "max-age=1234, public")
  198. rw.Header().Set(httpheaders.Expires, time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
  199. rw.WriteHeader(200)
  200. rw.Write(s.TestData.Read("test1.png"))
  201. }))
  202. defer ts.Close()
  203. res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
  204. s.Require().Equal(http.StatusOK, res.StatusCode)
  205. s.Require().Equal("max-age=1234, public", res.Header.Get(httpheaders.CacheControl))
  206. s.Require().Empty(res.Header.Get(httpheaders.Expires))
  207. }
  208. func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughExpires() {
  209. config.CacheControlPassthrough = true
  210. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  211. rw.Header().Set(httpheaders.Expires, time.Now().Add(1239*time.Second).UTC().Format(http.TimeFormat))
  212. rw.WriteHeader(200)
  213. rw.Write(s.TestData.Read("test1.png"))
  214. }))
  215. defer ts.Close()
  216. res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
  217. // Use regex to allow some delay
  218. s.Require().Regexp("max-age=123[0-9], public", res.Header.Get(httpheaders.CacheControl))
  219. s.Require().Empty(res.Header.Get(httpheaders.Expires))
  220. }
  221. func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughDisabled() {
  222. config.CacheControlPassthrough = false
  223. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  224. rw.Header().Set(httpheaders.CacheControl, "max-age=1234, public")
  225. rw.Header().Set(httpheaders.Expires, time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
  226. rw.WriteHeader(200)
  227. rw.Write(s.TestData.Read("test1.png"))
  228. }))
  229. defer ts.Close()
  230. res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
  231. s.Require().NotEqual("max-age=1234, public", res.Header.Get(httpheaders.CacheControl))
  232. s.Require().Empty(res.Header.Get(httpheaders.Expires))
  233. }
  234. func (s *ProcessingHandlerTestSuite) TestETagDisabled() {
  235. config.ETagEnabled = false
  236. res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
  237. s.Require().Equal(200, res.StatusCode)
  238. s.Require().Empty(res.Header.Get(httpheaders.Etag))
  239. }
  240. func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
  241. config.ETagEnabled = true
  242. etag := `"loremipsumdolor"`
  243. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  244. s.NotEmpty(r.Header.Get(httpheaders.IfNoneMatch))
  245. rw.Header().Set(httpheaders.Etag, etag)
  246. rw.WriteHeader(http.StatusNotModified)
  247. }))
  248. defer ts.Close()
  249. header := make(http.Header)
  250. header.Set(httpheaders.IfNoneMatch, etag)
  251. res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
  252. s.Require().Equal(304, res.StatusCode)
  253. s.Require().Equal(etag, res.Header.Get(httpheaders.Etag))
  254. }
  255. func (s *ProcessingHandlerTestSuite) TestLastModifiedEnabled() {
  256. config.LastModifiedEnabled = true
  257. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  258. rw.Header().Set(httpheaders.LastModified, "Wed, 21 Oct 2015 07:28:00 GMT")
  259. rw.WriteHeader(200)
  260. rw.Write(s.TestData.Read("test1.png"))
  261. }))
  262. defer ts.Close()
  263. res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
  264. s.Require().Equal("Wed, 21 Oct 2015 07:28:00 GMT", res.Header.Get(httpheaders.LastModified))
  265. }
  266. func (s *ProcessingHandlerTestSuite) TestLastModifiedDisabled() {
  267. config.LastModifiedEnabled = false
  268. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  269. rw.Header().Set(httpheaders.LastModified, "Wed, 21 Oct 2015 07:28:00 GMT")
  270. rw.WriteHeader(200)
  271. rw.Write(s.TestData.Read("test1.png"))
  272. }))
  273. defer ts.Close()
  274. res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
  275. s.Require().Empty(res.Header.Get(httpheaders.LastModified))
  276. }
  277. func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqExactMatchLastModifiedDisabled() {
  278. config.LastModifiedEnabled = false
  279. data := s.TestData.Read("test1.png")
  280. lastModified := "Wed, 21 Oct 2015 07:28:00 GMT"
  281. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  282. modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
  283. s.Empty(modifiedSince)
  284. rw.WriteHeader(200)
  285. rw.Write(data)
  286. }))
  287. defer ts.Close()
  288. header := make(http.Header)
  289. header.Set(httpheaders.IfModifiedSince, lastModified)
  290. res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
  291. s.Require().Equal(200, res.StatusCode)
  292. }
  293. func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqExactMatchLastModifiedEnabled() {
  294. config.LastModifiedEnabled = true
  295. lastModified := "Wed, 21 Oct 2015 07:28:00 GMT"
  296. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  297. modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
  298. s.Equal(lastModified, modifiedSince)
  299. rw.WriteHeader(304)
  300. }))
  301. defer ts.Close()
  302. header := make(http.Header)
  303. header.Set(httpheaders.IfModifiedSince, lastModified)
  304. res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
  305. s.Require().Equal(304, res.StatusCode)
  306. }
  307. func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastModifiedDisabled() {
  308. data := s.TestData.Read("test1.png")
  309. config.LastModifiedEnabled = false
  310. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  311. modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
  312. s.Empty(modifiedSince)
  313. rw.WriteHeader(200)
  314. rw.Write(data)
  315. }))
  316. defer ts.Close()
  317. recentTimestamp := "Thu, 25 Feb 2021 01:45:00 GMT"
  318. header := make(http.Header)
  319. header.Set(httpheaders.IfModifiedSince, recentTimestamp)
  320. res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
  321. s.Require().Equal(200, res.StatusCode)
  322. }
  323. func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastModifiedEnabled() {
  324. config.LastModifiedEnabled = true
  325. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  326. fileLastModified, _ := time.Parse(http.TimeFormat, "Wed, 21 Oct 2015 07:28:00 GMT")
  327. modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
  328. parsedModifiedSince, err := time.Parse(http.TimeFormat, modifiedSince)
  329. s.NoError(err)
  330. s.True(fileLastModified.Before(parsedModifiedSince))
  331. rw.WriteHeader(304)
  332. }))
  333. defer ts.Close()
  334. recentTimestamp := "Thu, 25 Feb 2021 01:45:00 GMT"
  335. header := make(http.Header)
  336. header.Set(httpheaders.IfModifiedSince, recentTimestamp)
  337. res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
  338. s.Require().Equal(304, res.StatusCode)
  339. }
  340. func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifiedDisabled() {
  341. s.Config().ProcessingHandler.LastModifiedEnabled = false
  342. data := s.TestData.Read("test1.png")
  343. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  344. modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
  345. s.Empty(modifiedSince)
  346. rw.WriteHeader(200)
  347. rw.Write(data)
  348. }))
  349. defer ts.Close()
  350. oldTimestamp := "Tue, 01 Oct 2013 17:31:00 GMT"
  351. header := make(http.Header)
  352. header.Set(httpheaders.IfModifiedSince, oldTimestamp)
  353. res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
  354. s.Require().Equal(200, res.StatusCode)
  355. }
  356. func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifiedEnabled() {
  357. config.LastModifiedEnabled = true
  358. data := s.TestData.Read("test1.png")
  359. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  360. fileLastModified, _ := time.Parse(http.TimeFormat, "Wed, 21 Oct 2015 07:28:00 GMT")
  361. modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
  362. parsedModifiedSince, err := time.Parse(http.TimeFormat, modifiedSince)
  363. s.NoError(err)
  364. s.True(fileLastModified.After(parsedModifiedSince))
  365. rw.WriteHeader(200)
  366. rw.Write(data)
  367. }))
  368. defer ts.Close()
  369. oldTimestamp := "Tue, 01 Oct 2013 17:31:00 GMT"
  370. header := make(http.Header)
  371. header.Set(httpheaders.IfModifiedSince, oldTimestamp)
  372. res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
  373. s.Require().Equal(200, res.StatusCode)
  374. }
  375. func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvg() {
  376. config.AlwaysRasterizeSvg = true
  377. res := s.GET("/unsafe/rs:fill:40:40/plain/local:///test1.svg")
  378. s.Require().Equal(200, res.StatusCode)
  379. s.Require().Equal("image/png", res.Header.Get(httpheaders.ContentType))
  380. }
  381. func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() {
  382. config.AlwaysRasterizeSvg = true
  383. config.EnforceWebp = true
  384. res := s.GET("/unsafe/plain/local:///test1.svg", http.Header{"Accept": []string{"image/webp"}})
  385. s.Require().Equal(200, res.StatusCode)
  386. s.Require().Equal("image/webp", res.Header.Get(httpheaders.ContentType))
  387. }
  388. func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() {
  389. config.AlwaysRasterizeSvg = false
  390. config.EnforceWebp = true
  391. res := s.GET("/unsafe/plain/local:///test1.svg")
  392. s.Require().Equal(200, res.StatusCode)
  393. s.Require().Equal("image/svg+xml", res.Header.Get(httpheaders.ContentType))
  394. }
  395. func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
  396. config.AlwaysRasterizeSvg = true
  397. config.SkipProcessingFormats = []imagetype.Type{imagetype.SVG}
  398. res := s.GET("/unsafe/plain/local:///test1.svg@svg")
  399. s.Require().Equal(200, res.StatusCode)
  400. s.Require().Equal("image/svg+xml", res.Header.Get(httpheaders.ContentType))
  401. }
  402. func (s *ProcessingHandlerTestSuite) TestMaxSrcFileSizeGlobal() {
  403. config.MaxSrcFileSize = 1
  404. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  405. rw.WriteHeader(200)
  406. rw.Write(s.TestData.Read("test1.png"))
  407. }))
  408. defer ts.Close()
  409. res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
  410. s.Require().Equal(422, res.StatusCode)
  411. }
  412. func TestProcessingHandler(t *testing.T) {
  413. suite.Run(t, new(ProcessingHandlerTestSuite))
  414. }