]> git.arvados.org - arvados.git/blob - services/keep-web/zip_test.go
Merge branch '22978-dependabot-2' into main. Closes #22978
[arvados.git] / services / keep-web / zip_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package keepweb
6
7 import (
8         "archive/zip"
9         "bytes"
10         "context"
11         "encoding/json"
12         "io"
13         "net/http"
14         "net/url"
15         "os"
16         "strings"
17
18         "git.arvados.org/arvados.git/sdk/go/arvados"
19         "git.arvados.org/arvados.git/sdk/go/arvadosclient"
20         "git.arvados.org/arvados.git/sdk/go/arvadostest"
21         "git.arvados.org/arvados.git/sdk/go/ctxlog"
22         "git.arvados.org/arvados.git/sdk/go/keepclient"
23         "github.com/sirupsen/logrus"
24         . "gopkg.in/check.v1"
25 )
26
27 type zipstage struct {
28         arv  *arvados.Client
29         ac   *arvadosclient.ArvadosClient
30         kc   *keepclient.KeepClient
31         coll arvados.Collection
32 }
33
34 func (s *IntegrationSuite) zipsetup(c *C, filedata map[string]string) zipstage {
35         arv := arvados.NewClientFromEnv()
36         arv.AuthToken = arvadostest.ActiveToken
37         var coll arvados.Collection
38         err := arv.RequestAndDecode(&coll, "POST", "arvados/v1/collections", nil, map[string]interface{}{"collection": map[string]interface{}{
39                 "name": "keep-web zip test collection",
40                 "properties": map[string]interface{}{
41                         "sailboat": "⛵",
42                 },
43                 "description": "Description of test collection\n",
44         }})
45         c.Assert(err, IsNil)
46         ac, err := arvadosclient.New(arv)
47         c.Assert(err, IsNil)
48         kc, err := keepclient.MakeKeepClient(ac)
49         c.Assert(err, IsNil)
50         fs, err := coll.FileSystem(arv, kc)
51         c.Assert(err, IsNil)
52         for path, data := range filedata {
53                 for i, c := range path {
54                         if c == '/' {
55                                 fs.Mkdir(path[:i], 0777)
56                         }
57                 }
58                 f, err := fs.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
59                 c.Assert(err, IsNil)
60                 _, err = f.Write([]byte(data))
61                 c.Assert(err, IsNil)
62                 err = f.Close()
63                 c.Assert(err, IsNil)
64         }
65         err = fs.Sync()
66         c.Assert(err, IsNil)
67         err = arv.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+coll.UUID, nil, nil)
68         c.Assert(err, IsNil)
69
70         return zipstage{
71                 arv:  arv,
72                 ac:   ac,
73                 kc:   kc,
74                 coll: coll,
75         }
76 }
77
78 func (stage zipstage) teardown(c *C) {
79         if stage.coll.UUID != "" {
80                 err := stage.arv.RequestAndDecode(&stage.coll, "DELETE", "arvados/v1/collections/"+stage.coll.UUID, nil, nil)
81                 c.Check(err, IsNil)
82         }
83 }
84
85 func (s *IntegrationSuite) TestZip_EmptyCollection(c *C) {
86         stage := s.zipsetup(c, nil)
87         defer stage.teardown(c)
88         _, resp := s.do("POST", s.collectionURL(stage.coll.UUID, ""), arvadostest.ActiveTokenV2, http.Header{"Accept": {"application/zip"}}, nil)
89         if !c.Check(resp.StatusCode, Equals, http.StatusOK) {
90                 body, _ := io.ReadAll(resp.Body)
91                 c.Logf("response body: %q", body)
92                 return
93         }
94         zipdata, _ := io.ReadAll(resp.Body)
95         zipr, err := zip.NewReader(bytes.NewReader(zipdata), int64(len(zipdata)))
96         c.Assert(err, IsNil)
97         c.Check(zipr.File, HasLen, 0)
98 }
99
100 func (s *IntegrationSuite) TestZip_Metadata(c *C) {
101         s.testZip(c, testZipOptions{
102                 reqMethod:    "GET",
103                 reqQuery:     "?include_collection_metadata=1",
104                 reqToken:     arvadostest.ActiveTokenV2,
105                 expectStatus: 200,
106                 expectFiles:  []string{"collection.json", "dir1/dir/file1.txt", "dir1/file1.txt", "dir2/file2.txt", "file0.txt"},
107                 expectMetadata: map[string]interface{}{
108                         "name":               "keep-web zip test collection",
109                         "portable_data_hash": "6acf043b102afcf04e3be2443e7ea2ba+223",
110                         "properties": map[string]interface{}{
111                                 "sailboat": "⛵",
112                         },
113                         "uuid":        "{{stage.coll.UUID}}",
114                         "description": "Description of test collection\n",
115                         "created_at":  "{{stage.coll.CreatedAt}}",
116                         "modified_at": "{{stage.coll.ModifiedAt}}",
117                         "modified_by_user": map[string]interface{}{
118                                 "email":     "active-user@arvados.local",
119                                 "full_name": "Active User",
120                                 "username":  "active",
121                                 "uuid":      arvadostest.ActiveUserUUID,
122                         },
123                 },
124                 expectZipComment: `Downloaded from https://collections.example.com/by_id/{{stage.coll.UUID}}/`,
125         })
126 }
127
128 func (s *IntegrationSuite) TestZip_Logging(c *C) {
129         s.testZip(c, testZipOptions{
130                 reqMethod:    "POST",
131                 reqToken:     arvadostest.ActiveTokenV2,
132                 expectStatus: 200,
133                 expectFiles:  []string{"dir1/dir/file1.txt", "dir1/file1.txt", "dir2/file2.txt", "file0.txt"},
134                 expectLogsMatch: []string{
135                         `(?ms).*\smsg="File download".*`,
136                         `(?ms).*\suser_uuid=` + arvadostest.ActiveUserUUID + `\s.*`,
137                         `(?ms).*\suser_full_name="Active User".*`,
138                         `(?ms).*\sportable_data_hash=6acf043b102afcf04e3be2443e7ea2ba\+223\s.*`,
139                         `(?ms).*\scollection_file_path=\s.*`,
140                 },
141         })
142 }
143
144 func (s *IntegrationSuite) TestZip_Logging_OneFile(c *C) {
145         s.testZip(c, testZipOptions{
146                 reqMethod:      "POST",
147                 reqContentType: "application/json",
148                 reqToken:       arvadostest.ActiveTokenV2,
149                 reqBody:        `{"files":["dir1/file1.txt"]}`,
150                 expectStatus:   200,
151                 expectFiles:    []string{"dir1/file1.txt"},
152                 expectLogsMatch: []string{
153                         `(?ms).*\scollection_file_path=dir1/file1.txt\s.*`,
154                 },
155         })
156 }
157
158 func (s *IntegrationSuite) TestZip_EntireCollection_GET(c *C) {
159         s.testZip(c, testZipOptions{
160                 reqMethod:    "GET",
161                 reqToken:     arvadostest.ActiveTokenV2,
162                 expectStatus: 200,
163                 expectFiles:  []string{"dir1/dir/file1.txt", "dir1/file1.txt", "dir2/file2.txt", "file0.txt"},
164         })
165 }
166
167 func (s *IntegrationSuite) TestZip_EntireCollection_JSON(c *C) {
168         s.testZip(c, testZipOptions{
169                 reqMethod:      "GET",
170                 reqContentType: "application/json",
171                 reqToken:       arvadostest.ActiveTokenV2,
172                 reqBody:        `{"files":[]}`,
173                 expectStatus:   200,
174                 expectFiles:    []string{"dir1/dir/file1.txt", "dir1/file1.txt", "dir2/file2.txt", "file0.txt"},
175         })
176 }
177
178 func (s *IntegrationSuite) TestZip_EntireCollection_Slash(c *C) {
179         s.testZip(c, testZipOptions{
180                 reqMethod:      "GET",
181                 reqContentType: "application/json",
182                 reqToken:       arvadostest.ActiveTokenV2,
183                 reqBody:        `{"files":["/"]}`,
184                 expectStatus:   200,
185                 expectFiles:    []string{"dir1/dir/file1.txt", "dir1/file1.txt", "dir2/file2.txt", "file0.txt"},
186         })
187 }
188
189 func (s *IntegrationSuite) TestZip_SelectDirectory_Form(c *C) {
190         s.testZip(c, testZipOptions{
191                 reqMethod:      "POST",
192                 reqContentType: "application/x-www-form-urlencoded",
193                 reqToken:       arvadostest.ActiveTokenV2,
194                 reqBody:        (url.Values{"files": {"dir1"}}).Encode(),
195                 expectStatus:   200,
196                 expectFiles:    []string{"dir1/dir/file1.txt", "dir1/file1.txt"},
197         })
198 }
199
200 func (s *IntegrationSuite) TestZip_SelectDirectory_SpecifyDownloadFilename_Form(c *C) {
201         s.testZip(c, testZipOptions{
202                 reqMethod:         "POST",
203                 reqContentType:    "application/x-www-form-urlencoded",
204                 reqToken:          arvadostest.ActiveTokenV2,
205                 reqBody:           (url.Values{"files": {"dir1"}, "download_filename": {"Foo Bar.zip"}}).Encode(),
206                 expectStatus:      200,
207                 expectFiles:       []string{"dir1/dir/file1.txt", "dir1/file1.txt"},
208                 expectDisposition: `attachment; filename="Foo Bar.zip"`,
209         })
210 }
211
212 func (s *IntegrationSuite) TestZip_SelectDirectory_JSON(c *C) {
213         s.testZip(c, testZipOptions{
214                 reqMethod:         "POST",
215                 reqContentType:    "application/json",
216                 reqToken:          arvadostest.ActiveTokenV2,
217                 reqBody:           `{"files":["dir1"]}`,
218                 expectStatus:      200,
219                 expectFiles:       []string{"dir1/dir/file1.txt", "dir1/file1.txt"},
220                 expectDisposition: `attachment; filename="keep-web zip test collection - 2 files.zip"`,
221         })
222 }
223
224 func (s *IntegrationSuite) TestZip_SelectDirectory_TrailingSlash(c *C) {
225         s.testZip(c, testZipOptions{
226                 reqMethod:         "POST",
227                 reqContentType:    "application/json",
228                 reqToken:          arvadostest.ActiveTokenV2,
229                 reqBody:           `{"files":["dir1/"]}`,
230                 expectStatus:      200,
231                 expectFiles:       []string{"dir1/dir/file1.txt", "dir1/file1.txt"},
232                 expectDisposition: `attachment; filename="keep-web zip test collection - 2 files.zip"`,
233         })
234 }
235
236 func (s *IntegrationSuite) TestZip_SelectDirectory_SpecifyDownloadFilename(c *C) {
237         s.testZip(c, testZipOptions{
238                 reqMethod:         "POST",
239                 reqContentType:    "application/json",
240                 reqToken:          arvadostest.ActiveTokenV2,
241                 reqBody:           `{"files":["dir1/"],"download_filename":"Foo bar ⛵.zip"}`,
242                 expectStatus:      200,
243                 expectFiles:       []string{"dir1/dir/file1.txt", "dir1/file1.txt"},
244                 expectDisposition: `attachment; filename*=utf-8''Foo%20bar%20%E2%9B%B5.zip`,
245         })
246 }
247
248 func (s *IntegrationSuite) TestZip_SelectFile(c *C) {
249         s.testZip(c, testZipOptions{
250                 reqMethod:         "POST",
251                 reqContentType:    "application/json",
252                 reqToken:          arvadostest.ActiveTokenV2,
253                 reqBody:           `{"files":["dir1/file1.txt"]}`,
254                 expectStatus:      200,
255                 expectFiles:       []string{"dir1/file1.txt"},
256                 expectDisposition: `attachment; filename="keep-web zip test collection - file1.txt.zip"`,
257         })
258 }
259
260 func (s *IntegrationSuite) TestZip_SelectFiles_Query(c *C) {
261         s.testZip(c, testZipOptions{
262                 reqMethod:         "POST",
263                 reqQuery:          "?" + (&url.Values{"files": []string{"dir1/file1.txt", "dir2/file2.txt"}}).Encode(),
264                 reqContentType:    "application/json",
265                 reqToken:          arvadostest.ActiveTokenV2,
266                 expectStatus:      200,
267                 expectFiles:       []string{"dir1/file1.txt", "dir2/file2.txt"},
268                 expectDisposition: `attachment; filename="keep-web zip test collection - 2 files.zip"`,
269         })
270 }
271
272 func (s *IntegrationSuite) TestZip_SelectFiles_SpecifyDownloadFilename_Query(c *C) {
273         s.testZip(c, testZipOptions{
274                 reqMethod: "POST",
275                 reqQuery: "?" + (&url.Values{
276                         "files":             []string{"dir1/file1.txt", "dir2/file2.txt"},
277                         "download_filename": []string{"Sue.zip"},
278                 }).Encode(),
279                 reqContentType:    "application/json",
280                 reqToken:          arvadostest.ActiveTokenV2,
281                 expectStatus:      200,
282                 expectFiles:       []string{"dir1/file1.txt", "dir2/file2.txt"},
283                 expectDisposition: `attachment; filename=Sue.zip`,
284         })
285 }
286
287 func (s *IntegrationSuite) TestZip_SpecifyDownloadFilename_NoZipExt(c *C) {
288         s.testZip(c, testZipOptions{
289                 reqMethod: "GET",
290                 reqQuery: "?" + (&url.Values{
291                         "download_filename": []string{"Sue.zap"},
292                 }).Encode(),
293                 reqContentType:    "application/json",
294                 reqToken:          arvadostest.ActiveTokenV2,
295                 expectStatus:      200,
296                 expectFiles:       []string{"dir1/dir/file1.txt", "dir1/file1.txt", "dir2/file2.txt", "file0.txt"},
297                 expectDisposition: `attachment; filename=Sue.zap.zip`,
298         })
299 }
300
301 func (s *IntegrationSuite) TestZip_SelectFile_UseByIDStyle(c *C) {
302         s.testZip(c, testZipOptions{
303                 useByIDStyle:      true,
304                 reqMethod:         "POST",
305                 reqContentType:    "application/json",
306                 reqToken:          arvadostest.ActiveTokenV2,
307                 reqBody:           `{"files":["dir1/file1.txt"]}`,
308                 expectStatus:      200,
309                 expectFiles:       []string{"dir1/file1.txt"},
310                 expectDisposition: `attachment; filename="keep-web zip test collection - file1.txt.zip"`,
311         })
312 }
313
314 func (s *IntegrationSuite) TestZip_SelectFile_UsePathStyle(c *C) {
315         s.testZip(c, testZipOptions{
316                 usePathStyle:      true,
317                 reqMethod:         "POST",
318                 reqContentType:    "application/json",
319                 reqToken:          arvadostest.ActiveTokenV2,
320                 reqBody:           `{"files":["dir1/file1.txt"]}`,
321                 expectStatus:      200,
322                 expectFiles:       []string{"dir1/file1.txt"},
323                 expectDisposition: `attachment; filename="keep-web zip test collection - file1.txt.zip"`,
324         })
325 }
326
327 func (s *IntegrationSuite) TestZip_SelectFile_UsePathStyle_PDH(c *C) {
328         s.testZip(c, testZipOptions{
329                 usePathStyle:      true,
330                 usePDH:            true,
331                 reqMethod:         "POST",
332                 reqQuery:          "?include_collection_metadata=1",
333                 reqContentType:    "application/json",
334                 reqToken:          arvadostest.ActiveTokenV2,
335                 reqBody:           `{"files":["dir1/file1.txt"]}`,
336                 expectStatus:      200,
337                 expectFiles:       []string{"collection.json", "dir1/file1.txt"},
338                 expectDisposition: `attachment; filename="6acf043b102afcf04e3be2443e7ea2ba+223 - file1.txt.zip"`,
339                 expectMetadata: map[string]interface{}{
340                         "portable_data_hash": "6acf043b102afcf04e3be2443e7ea2ba+223",
341                 },
342                 expectZipComment: `Downloaded from http://collections.example.com/by_id/6acf043b102afcf04e3be2443e7ea2ba+223/`,
343         })
344 }
345
346 func (s *IntegrationSuite) TestZip_SelectRedundantFile(c *C) {
347         s.testZip(c, testZipOptions{
348                 reqMethod:      "POST",
349                 reqContentType: "application/json",
350                 reqToken:       arvadostest.ActiveTokenV2,
351                 reqBody:        `{"files":["dir1/dir", "dir1/dir/file1.txt"]}`,
352                 expectStatus:   200,
353                 expectFiles:    []string{"dir1/dir/file1.txt"},
354         })
355 }
356
357 func (s *IntegrationSuite) TestZip_AcceptMediaTypeWithDirective(c *C) {
358         s.testZip(c, testZipOptions{
359                 reqMethod:      "POST",
360                 reqContentType: "application/json",
361                 reqToken:       arvadostest.ActiveTokenV2,
362                 reqBody:        `{"files":["dir1/dir/file1.txt"]}`,
363                 reqAccept:      `application/zip; q=0.9`,
364                 expectStatus:   200,
365                 expectFiles:    []string{"dir1/dir/file1.txt"},
366         })
367 }
368
369 func (s *IntegrationSuite) TestZip_AcceptMediaTypeInQuery(c *C) {
370         s.testZip(c, testZipOptions{
371                 reqMethod:      "POST",
372                 reqContentType: "application/json",
373                 reqToken:       arvadostest.ActiveTokenV2,
374                 reqBody:        `{"files":["dir1/dir/file1.txt"]}`,
375                 reqQuery:       `?accept=application/zip&disposition=attachment`,
376                 reqAccept:      `text/html`,
377                 expectStatus:   200,
378                 expectFiles:    []string{"dir1/dir/file1.txt"},
379         })
380 }
381
382 // disposition=attachment is implied, because usePathStyle causes
383 // testZip to use DownloadURL as the request vhost.
384 func (s *IntegrationSuite) TestZip_AcceptMediaTypeInQuery_ImplicitDisposition(c *C) {
385         s.testZip(c, testZipOptions{
386                 usePathStyle:   true,
387                 reqMethod:      "POST",
388                 reqContentType: "application/json",
389                 reqToken:       arvadostest.ActiveTokenV2,
390                 reqBody:        `{"files":["dir1/dir/file1.txt"]}`,
391                 reqQuery:       `?accept=application/zip`,
392                 reqAccept:      `text/html`,
393                 expectStatus:   200,
394                 expectFiles:    []string{"dir1/dir/file1.txt"},
395         })
396 }
397
398 func (s *IntegrationSuite) TestZip_SelectNonexistentFile(c *C) {
399         s.testZip(c, testZipOptions{
400                 reqMethod:       "POST",
401                 reqContentType:  "application/json",
402                 reqToken:        arvadostest.ActiveTokenV2,
403                 reqBody:         `{"files":["dir1", "file404.txt"]}`,
404                 expectStatus:    404,
405                 expectBodyMatch: `"file404.txt": file does not exist\n`,
406         })
407 }
408
409 func (s *IntegrationSuite) TestZip_SelectBlankFilename(c *C) {
410         s.testZip(c, testZipOptions{
411                 reqMethod:       "POST",
412                 reqContentType:  "application/json",
413                 reqToken:        arvadostest.ActiveTokenV2,
414                 reqBody:         `{"files":[""]}`,
415                 expectStatus:    404,
416                 expectBodyMatch: `"": file does not exist\n`,
417         })
418 }
419
420 func (s *IntegrationSuite) TestZip_JSON_Error(c *C) {
421         s.testZip(c, testZipOptions{
422                 reqMethod:       "POST",
423                 reqContentType:  "application/json",
424                 reqToken:        arvadostest.ActiveTokenV2,
425                 reqBody:         `{"files":["dir1/dir"`,
426                 expectStatus:    http.StatusBadRequest,
427                 expectBodyMatch: `.*unexpected EOF.*\n`,
428         })
429 }
430
431 // Download-via-POST is still allowed if upload permission is turned
432 // off.
433 func (s *IntegrationSuite) TestZip_WebDAVPermission_OK(c *C) {
434         s.handler.Cluster.Collections.WebDAVPermission.User.Upload = false
435         s.testZip(c, testZipOptions{
436                 reqMethod:      "POST",
437                 reqContentType: "application/json",
438                 reqToken:       arvadostest.ActiveTokenV2,
439                 expectFiles:    []string{"dir1/dir/file1.txt", "dir1/file1.txt", "dir2/file2.txt", "file0.txt"},
440                 expectStatus:   http.StatusOK,
441         })
442 }
443
444 func (s *IntegrationSuite) TestZip_WebDAVPermission_Forbidden(c *C) {
445         s.handler.Cluster.Collections.WebDAVPermission.User.Download = false
446         s.testZip(c, testZipOptions{
447                 reqMethod:       "POST",
448                 reqContentType:  "application/json",
449                 reqToken:        arvadostest.ActiveTokenV2,
450                 expectStatus:    http.StatusForbidden,
451                 expectBodyMatch: `Not permitted\n`,
452         })
453 }
454
455 type testZipOptions struct {
456         filedata          map[string]string // if nil, use default set (see testZip)
457         usePDH            bool
458         usePathStyle      bool
459         useByIDStyle      bool
460         reqMethod         string
461         reqQuery          string
462         reqAccept         string
463         reqContentType    string
464         reqToken          string
465         reqBody           string
466         expectStatus      int
467         expectFiles       []string
468         expectBodyMatch   string
469         expectDisposition string
470         expectMetadata    map[string]interface{}
471         expectZipComment  string
472         expectLogsMatch   []string
473 }
474
475 func (s *IntegrationSuite) testZip(c *C, opts testZipOptions) {
476         logbuf := new(bytes.Buffer)
477         logger := logrus.New()
478         logger.Out = io.MultiWriter(logbuf, ctxlog.LogWriter(c.Log))
479         s.ctx = ctxlog.Context(context.Background(), logger)
480         s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host = "collections.example.com"
481
482         if opts.filedata == nil {
483                 opts.filedata = map[string]string{
484                         "dir1/dir/file1.txt": "file1",
485                         "dir1/file1.txt":     "file1",
486                         "dir2/file2.txt":     "file2",
487                         "file0.txt":          "file0",
488                 }
489         }
490         stage := s.zipsetup(c, opts.filedata)
491         defer stage.teardown(c)
492         var collID string
493         if opts.usePDH {
494                 collID = stage.coll.PortableDataHash
495         } else {
496                 collID = stage.coll.UUID
497         }
498         var url string
499         if opts.usePathStyle {
500                 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Scheme = "http"
501                 url = "http://collections.example.com/c=" + collID
502         } else if opts.useByIDStyle {
503                 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Scheme = "http"
504                 url = "http://collections.example.com/by_id/" + collID
505         } else {
506                 url = s.collectionURL(collID, "")
507         }
508         var accept []string
509         if opts.reqAccept != "" {
510                 accept = []string{opts.reqAccept}
511         } else {
512                 accept = []string{"application/zip"}
513         }
514         _, resp := s.do(opts.reqMethod, url+opts.reqQuery, opts.reqToken, http.Header{
515                 "Accept":       accept,
516                 "Content-Type": {opts.reqContentType},
517         }, []byte(opts.reqBody))
518         if !c.Check(resp.StatusCode, Equals, opts.expectStatus) || opts.expectStatus != 200 {
519                 body, _ := io.ReadAll(resp.Body)
520                 c.Logf("response body: %q", body)
521                 if opts.expectBodyMatch != "" {
522                         c.Check(string(body), Matches, opts.expectBodyMatch)
523                 }
524                 return
525         }
526         zipdata, _ := io.ReadAll(resp.Body)
527         zipr, err := zip.NewReader(bytes.NewReader(zipdata), int64(len(zipdata)))
528         c.Assert(err, IsNil)
529         c.Check(zipFileNames(zipr), DeepEquals, opts.expectFiles)
530         if opts.expectDisposition != "" {
531                 c.Check(resp.Header.Get("Content-Disposition"), Equals, opts.expectDisposition)
532         }
533         if opts.expectZipComment != "" {
534                 c.Check(zipr.Comment, Equals, strings.Replace(opts.expectZipComment, "{{stage.coll.UUID}}", stage.coll.UUID, -1))
535         }
536         f, err := zipr.Open("collection.json")
537         c.Check(err == nil, Equals, opts.expectMetadata != nil,
538                 Commentf("collection.json file existence (%v) did not match expectation (%v)", err == nil, opts.expectMetadata != nil))
539         if err == nil {
540                 defer f.Close()
541                 if opts.expectMetadata["uuid"] == "{{stage.coll.UUID}}" {
542                         opts.expectMetadata["uuid"] = stage.coll.UUID
543                 }
544                 if opts.expectMetadata["created_at"] == "{{stage.coll.CreatedAt}}" {
545                         opts.expectMetadata["created_at"] = stage.coll.CreatedAt.Format(rfc3339NanoFixed)
546                 }
547                 if opts.expectMetadata["modified_at"] == "{{stage.coll.ModifiedAt}}" {
548                         opts.expectMetadata["modified_at"] = stage.coll.ModifiedAt.Format(rfc3339NanoFixed)
549                 }
550                 var gotMetadata map[string]interface{}
551                 json.NewDecoder(f).Decode(&gotMetadata)
552                 c.Check(gotMetadata, DeepEquals, opts.expectMetadata)
553         }
554         for _, re := range opts.expectLogsMatch {
555                 c.Check(logbuf.String(), Matches, re)
556         }
557 }
558
559 func zipFileNames(zipr *zip.Reader) []string {
560         var names []string
561         for _, file := range zipr.File {
562                 names = append(names, file.Name)
563         }
564         return names
565 }