~juju-qa/ubuntu/xenial/juju/xenial-2.0-beta3

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/apiserver/gui_test.go

  • Committer: Martin Packman
  • Date: 2016-03-30 19:31:08 UTC
  • mfrom: (1.1.41)
  • Revision ID: martin.packman@canonical.com-20160330193108-h9iz3ak334uk0z5r
Merge new upstream source 2.0~beta3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2016 Canonical Ltd.
 
2
// Licensed under the AGPLv3, see LICENCE file for details.
 
3
 
 
4
package apiserver_test
 
5
 
 
6
import (
 
7
        "archive/tar"
 
8
        "bytes"
 
9
        "crypto/sha256"
 
10
        "encoding/json"
 
11
        "fmt"
 
12
        "io"
 
13
        "io/ioutil"
 
14
        "net/http"
 
15
        "os"
 
16
        "os/exec"
 
17
        "path/filepath"
 
18
        "runtime"
 
19
        "strings"
 
20
 
 
21
        jc "github.com/juju/testing/checkers"
 
22
        gc "gopkg.in/check.v1"
 
23
 
 
24
        agenttools "github.com/juju/juju/agent/tools"
 
25
        "github.com/juju/juju/apiserver"
 
26
        "github.com/juju/juju/apiserver/params"
 
27
        "github.com/juju/juju/state/binarystorage"
 
28
        "github.com/juju/juju/version"
 
29
)
 
30
 
 
31
const (
 
32
        guiConfigPath = "templates/config.js.go"
 
33
        guiIndexPath  = "templates/index.html.go"
 
34
)
 
35
 
 
36
type guiSuite struct {
 
37
        authHttpSuite
 
38
}
 
39
 
 
40
var _ = gc.Suite(&guiSuite{})
 
41
 
 
42
// guiURL returns the complete URL where the Juju GUI can be found, including
 
43
// the given hash and pathAndquery.
 
44
func (s *guiSuite) guiURL(c *gc.C, hash, pathAndquery string) string {
 
45
        u := s.baseURL(c)
 
46
        path := "/gui/" + s.modelUUID
 
47
        if hash != "" {
 
48
                path += "/" + hash
 
49
        }
 
50
        parts := strings.SplitN(pathAndquery, "?", 2)
 
51
        u.Path = path + parts[0]
 
52
        if len(parts) == 2 {
 
53
                u.RawQuery = parts[1]
 
54
        }
 
55
        return u.String()
 
56
}
 
57
 
 
58
type guiSetupFunc func(c *gc.C, baseDir string, storage binarystorage.Storage) string
 
59
 
 
60
var guiHandlerTests = []struct {
 
61
        // about describes the test.
 
62
        about string
 
63
        // setup is optionally used to set up the test.
 
64
        // It receives the Juju GUI base directory and an empty GUI storage.
 
65
        // Optionally it can return a GUI archive hash which is used by the test
 
66
        // to build the URL path for the HTTP request.
 
67
        setup guiSetupFunc
 
68
        // pathAndquery holds the optional path and query for the request, for
 
69
        // instance "/combo?file". If not provided, the "/" path is used.
 
70
        pathAndquery string
 
71
        // expectedStatus holds the expected response HTTP status.
 
72
        // A 200 OK status is used by default.
 
73
        expectedStatus int
 
74
        // expectedContentType holds the expected response content type.
 
75
        // If expectedError is provided this field is ignored.
 
76
        expectedContentType string
 
77
        // expectedBody holds the expected response body, only used if
 
78
        // expectedError is not provided (see below).
 
79
        expectedBody string
 
80
        // expectedError holds the expected error message included in the response.
 
81
        expectedError string
 
82
}{{
 
83
        about:          "metadata not found",
 
84
        expectedStatus: http.StatusNotFound,
 
85
        expectedError:  "Juju GUI not found",
 
86
}, {
 
87
        about: "GUI directory is a file",
 
88
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
89
                err := storage.Add(strings.NewReader(""), binarystorage.Metadata{
 
90
                        SHA256: "fake-hash",
 
91
                })
 
92
                c.Assert(err, jc.ErrorIsNil)
 
93
                err = os.MkdirAll(baseDir, 0755)
 
94
                c.Assert(err, jc.ErrorIsNil)
 
95
                rootDir := filepath.Join(baseDir, "fake-hash")
 
96
                err = ioutil.WriteFile(rootDir, nil, 0644)
 
97
                c.Assert(err, jc.ErrorIsNil)
 
98
                return ""
 
99
        },
 
100
        expectedStatus: http.StatusInternalServerError,
 
101
        expectedError:  "cannot use Juju GUI root directory .*",
 
102
}, {
 
103
        about: "GUI directory is unaccessible",
 
104
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
105
                err := storage.Add(strings.NewReader(""), binarystorage.Metadata{
 
106
                        SHA256: "fake-hash",
 
107
                })
 
108
                c.Assert(err, jc.ErrorIsNil)
 
109
                err = os.MkdirAll(baseDir, 0000)
 
110
                c.Assert(err, jc.ErrorIsNil)
 
111
                return ""
 
112
        },
 
113
        expectedStatus: http.StatusInternalServerError,
 
114
        expectedError:  "cannot stat Juju GUI root directory: .*",
 
115
}, {
 
116
        about: "invalid GUI archive",
 
117
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
118
                err := storage.Add(strings.NewReader(""), binarystorage.Metadata{
 
119
                        SHA256: "fake-hash",
 
120
                })
 
121
                c.Assert(err, jc.ErrorIsNil)
 
122
                return ""
 
123
        },
 
124
        expectedStatus: http.StatusInternalServerError,
 
125
        expectedError:  "cannot uncompress Juju GUI archive: cannot parse archive: .*",
 
126
}, {
 
127
        about: "index: sprite file not found",
 
128
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
129
                setupGUIArchive(c, storage, "2.0.42", nil)
 
130
                return ""
 
131
        },
 
132
        expectedStatus: http.StatusInternalServerError,
 
133
        expectedError:  "cannot read sprite file: .*",
 
134
}, {
 
135
        about: "index: template not found",
 
136
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
137
                setupGUIArchive(c, storage, "2.0.42", map[string]string{
 
138
                        apiserver.SpritePath: "",
 
139
                })
 
140
                return ""
 
141
        },
 
142
        expectedStatus: http.StatusInternalServerError,
 
143
        expectedError:  "cannot parse template: .*: no such file or directory",
 
144
}, {
 
145
        about: "index: invalid template",
 
146
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
147
                setupGUIArchive(c, storage, "2.0.47", map[string]string{
 
148
                        guiIndexPath:         "{{.BadWolf.47}}",
 
149
                        apiserver.SpritePath: "",
 
150
                })
 
151
                return ""
 
152
        },
 
153
        expectedStatus: http.StatusInternalServerError,
 
154
        expectedError:  `cannot parse template: template: index.html.go:1: unexpected ".47" .*`,
 
155
}, {
 
156
        about: "index: invalid template and context",
 
157
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
158
                setupGUIArchive(c, storage, "2.0.47", map[string]string{
 
159
                        guiIndexPath:         "{{range .debug}}{{end}}",
 
160
                        apiserver.SpritePath: "",
 
161
                })
 
162
                return ""
 
163
        },
 
164
        expectedStatus: http.StatusInternalServerError,
 
165
        expectedError:  `cannot render template: template: .*: range can't iterate over .*`,
 
166
}, {
 
167
        about: "config: template not found",
 
168
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
169
                return setupGUIArchive(c, storage, "2.0.42", nil)
 
170
        },
 
171
        pathAndquery:   "/config.js",
 
172
        expectedStatus: http.StatusInternalServerError,
 
173
        expectedError:  "cannot parse template: .*: no such file or directory",
 
174
}, {
 
175
        about: "config: invalid template",
 
176
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
177
                return setupGUIArchive(c, storage, "2.0.47", map[string]string{
 
178
                        guiConfigPath: "{{.BadWolf.47}}",
 
179
                })
 
180
        },
 
181
        pathAndquery:   "/config.js",
 
182
        expectedStatus: http.StatusInternalServerError,
 
183
        expectedError:  `cannot parse template: template: config.js.go:1: unexpected ".47" .*`,
 
184
}, {
 
185
        about: "config: invalid hash",
 
186
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
187
                setupGUIArchive(c, storage, "2.0.47", nil)
 
188
                return "invalid"
 
189
        },
 
190
        pathAndquery:   "/config.js",
 
191
        expectedStatus: http.StatusNotFound,
 
192
        expectedError:  `resource with "invalid" hash not found`,
 
193
}, {
 
194
        about: "combo: invalid file name",
 
195
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
196
                return setupGUIArchive(c, storage, "1.0.0", nil)
 
197
        },
 
198
        pathAndquery:   "/combo?foo&%%",
 
199
        expectedStatus: http.StatusBadRequest,
 
200
        expectedError:  `cannot combine files: invalid file name "%": invalid URL escape "%%"`,
 
201
}, {
 
202
        about: "combo: invalid file path",
 
203
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
204
                return setupGUIArchive(c, storage, "1.0.0", nil)
 
205
        },
 
206
        pathAndquery:   "/combo?../../../../../../etc/passwd",
 
207
        expectedStatus: http.StatusBadRequest,
 
208
        expectedError:  `cannot combine files: forbidden file path "../../../../../../etc/passwd"`,
 
209
}, {
 
210
        about: "combo: invalid hash",
 
211
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
212
                setupGUIArchive(c, storage, "2.0.47", nil)
 
213
                return "invalid"
 
214
        },
 
215
        pathAndquery:   "/combo?foo",
 
216
        expectedStatus: http.StatusNotFound,
 
217
        expectedError:  `resource with "invalid" hash not found`,
 
218
}, {
 
219
        about: "combo: success",
 
220
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
221
                return setupGUIArchive(c, storage, "1.0.0", map[string]string{
 
222
                        "static/gui/build/tng/picard.js":  "enterprise",
 
223
                        "static/gui/build/ds9/sisko.js":   "deep space nine",
 
224
                        "static/gui/build/voy/janeway.js": "voyager",
 
225
                        "static/gui/build/borg.js":        "cube",
 
226
                })
 
227
        },
 
228
        pathAndquery:        "/combo?voy/janeway.js&tng/picard.js&borg.js&ds9/sisko.js",
 
229
        expectedStatus:      http.StatusOK,
 
230
        expectedContentType: apiserver.JSMimeType,
 
231
        expectedBody: `voyager
 
232
/* janeway.js */
 
233
enterprise
 
234
/* picard.js */
 
235
cube
 
236
/* borg.js */
 
237
deep space nine
 
238
/* sisko.js */
 
239
`,
 
240
}, {
 
241
        about: "combo: non-existing files ignored + different content types",
 
242
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
243
                return setupGUIArchive(c, storage, "1.0.0", map[string]string{
 
244
                        "static/gui/build/foo.css": "my-style",
 
245
                })
 
246
        },
 
247
        pathAndquery:        "/combo?no-such.css&foo.css&bad-wolf.css",
 
248
        expectedStatus:      http.StatusOK,
 
249
        expectedContentType: "text/css; charset=utf-8",
 
250
        expectedBody: `my-style
 
251
/* foo.css */
 
252
`,
 
253
}, {
 
254
        about: "static files",
 
255
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
256
                return setupGUIArchive(c, storage, "1.0.0", map[string]string{
 
257
                        "static/file.js": "static file content",
 
258
                })
 
259
        },
 
260
        pathAndquery:        "/static/file.js",
 
261
        expectedStatus:      http.StatusOK,
 
262
        expectedContentType: apiserver.JSMimeType,
 
263
        expectedBody:        "static file content",
 
264
}, {
 
265
        about: "static files: invalid hash",
 
266
        setup: func(c *gc.C, baseDir string, storage binarystorage.Storage) string {
 
267
                setupGUIArchive(c, storage, "2.0.47", nil)
 
268
                return "bad-wolf"
 
269
        },
 
270
        pathAndquery:   "/static/file.js",
 
271
        expectedStatus: http.StatusNotFound,
 
272
        expectedError:  `resource with "bad-wolf" hash not found`,
 
273
}}
 
274
 
 
275
func (s *guiSuite) TestGUIHandler(c *gc.C) {
 
276
        if runtime.GOOS == "windows" {
 
277
                // Skipping the tests on Windows is not a problem as the Juju GUI is
 
278
                // only served from Linux machines.
 
279
                c.Skip("bzip2 command not available")
 
280
        }
 
281
        sendRequest := func(setup guiSetupFunc, pathAndquery string) *http.Response {
 
282
                // Set up the GUI base directory.
 
283
                datadir := filepath.ToSlash(s.DataDir())
 
284
                baseDir := filepath.FromSlash(agenttools.SharedGUIDir(datadir))
 
285
                defer func() {
 
286
                        os.Chmod(baseDir, 0755)
 
287
                        os.Remove(baseDir)
 
288
                }()
 
289
 
 
290
                // Run specific test set up.
 
291
                var hash string
 
292
                if setup != nil {
 
293
                        storage, err := s.State.GUIStorage()
 
294
                        c.Assert(err, jc.ErrorIsNil)
 
295
                        defer storage.Close()
 
296
 
 
297
                        // Ensure the GUI storage is empty.
 
298
                        allMeta, err := storage.AllMetadata()
 
299
                        c.Assert(err, jc.ErrorIsNil)
 
300
                        c.Assert(allMeta, gc.HasLen, 0)
 
301
 
 
302
                        hash = setup(c, baseDir, storage)
 
303
                }
 
304
 
 
305
                // Send a request to the test path.
 
306
                if pathAndquery == "" {
 
307
                        pathAndquery = "/"
 
308
                }
 
309
                return s.sendRequest(c, httpRequestParams{
 
310
                        url: s.guiURL(c, hash, pathAndquery),
 
311
                })
 
312
        }
 
313
 
 
314
        for i, test := range guiHandlerTests {
 
315
                c.Logf("\n%d: %s", i, test.about)
 
316
 
 
317
                // Reset the db so that the GUI storage is empty in each test.
 
318
                s.Reset(c)
 
319
 
 
320
                // Perform the request.
 
321
                resp := sendRequest(test.setup, test.pathAndquery)
 
322
 
 
323
                // Check the response.
 
324
                if test.expectedStatus == 0 {
 
325
                        test.expectedStatus = http.StatusOK
 
326
                }
 
327
                if test.expectedError != "" {
 
328
                        test.expectedContentType = "application/json"
 
329
                }
 
330
                body := assertResponse(c, resp, test.expectedStatus, test.expectedContentType)
 
331
                if test.expectedError == "" {
 
332
                        c.Assert(string(body), gc.Equals, test.expectedBody)
 
333
                } else {
 
334
                        var jsonResp params.ErrorResult
 
335
                        err := json.Unmarshal(body, &jsonResp)
 
336
                        c.Assert(err, jc.ErrorIsNil, gc.Commentf("body: %s", body))
 
337
                        c.Assert(jsonResp.Error.Message, gc.Matches, test.expectedError)
 
338
                }
 
339
        }
 
340
}
 
341
 
 
342
func (s *guiSuite) TestGUIIndex(c *gc.C) {
 
343
        storage, err := s.State.GUIStorage()
 
344
        c.Assert(err, jc.ErrorIsNil)
 
345
        defer storage.Close()
 
346
 
 
347
        // Create a Juju GUI archive and save it into the storage.
 
348
        indexContent := `
 
349
<!DOCTYPE html>
 
350
<html>
 
351
<body>
 
352
    comboURL: {{.comboURL}}
 
353
    configURL: {{.configURL}}
 
354
    debug: {{.debug}}
 
355
    spriteContent: {{.spriteContent}}
 
356
</body>
 
357
</html>`
 
358
        hash := setupGUIArchive(c, storage, "2.0.0", map[string]string{
 
359
                guiIndexPath:         indexContent,
 
360
                apiserver.SpritePath: "sprite content",
 
361
        })
 
362
        expectedIndexContent := fmt.Sprintf(`
 
363
<!DOCTYPE html>
 
364
<html>
 
365
<body>
 
366
    comboURL: /gui/%[1]s/%[2]s/combo
 
367
    configURL: /gui/%[1]s/%[2]s/config.js
 
368
    debug: false
 
369
    spriteContent: sprite content
 
370
</body>
 
371
</html>`, s.modelUUID, hash)
 
372
        // Make a request for the Juju GUI index.
 
373
        resp := s.sendRequest(c, httpRequestParams{
 
374
                url: s.guiURL(c, "", "/"),
 
375
        })
 
376
        body := assertResponse(c, resp, http.StatusOK, "text/html; charset=utf-8")
 
377
        c.Assert(string(body), gc.Equals, expectedIndexContent)
 
378
 
 
379
        // Non-handled paths are served by the index handler.
 
380
        resp = s.sendRequest(c, httpRequestParams{
 
381
                url: s.guiURL(c, "", "/no-such-path/"),
 
382
        })
 
383
        body = assertResponse(c, resp, http.StatusOK, "text/html; charset=utf-8")
 
384
        c.Assert(string(body), gc.Equals, expectedIndexContent)
 
385
}
 
386
 
 
387
func (s *guiSuite) TestGUIConfig(c *gc.C) {
 
388
        storage, err := s.State.GUIStorage()
 
389
        c.Assert(err, jc.ErrorIsNil)
 
390
        defer storage.Close()
 
391
 
 
392
        // Create a Juju GUI archive and save it into the storage.
 
393
        configContent := `
 
394
var config = {
 
395
    // This is just an example and does not reflect the real Juju GUI config.
 
396
    base: '{{.base}}',
 
397
    host: '{{.host}}',
 
398
    socket: '{{.socket}}',
 
399
    uuid: '{{.uuid}}',
 
400
    version: '{{.version}}'
 
401
};`
 
402
        hash := setupGUIArchive(c, storage, "2.0.0", map[string]string{
 
403
                guiConfigPath: configContent,
 
404
        })
 
405
        expectedConfigContent := fmt.Sprintf(`
 
406
var config = {
 
407
    // This is just an example and does not reflect the real Juju GUI config.
 
408
    base: '/gui/%s/',
 
409
    host: '%s',
 
410
    socket: '/model/$uuid/api',
 
411
    uuid: '%s',
 
412
    version: '%s'
 
413
};`, s.modelUUID, s.baseURL(c).Host, s.modelUUID, version.Current)
 
414
 
 
415
        // Make a request for the Juju GUI config.
 
416
        resp := s.sendRequest(c, httpRequestParams{
 
417
                url: s.guiURL(c, hash, "/config.js"),
 
418
        })
 
419
        body := assertResponse(c, resp, http.StatusOK, apiserver.JSMimeType)
 
420
        c.Assert(string(body), gc.Equals, expectedConfigContent)
 
421
}
 
422
 
 
423
func (s *guiSuite) TestGUIDirectory(c *gc.C) {
 
424
        storage, err := s.State.GUIStorage()
 
425
        c.Assert(err, jc.ErrorIsNil)
 
426
        defer storage.Close()
 
427
 
 
428
        // Create a Juju GUI archive and save it into the storage.
 
429
        indexContent := "<!DOCTYPE html><html><body>Exterminate!</body></html>"
 
430
        hash := setupGUIArchive(c, storage, "2.0.0", map[string]string{
 
431
                guiIndexPath:         indexContent,
 
432
                apiserver.SpritePath: "",
 
433
        })
 
434
 
 
435
        // Initially the GUI directory on the server is empty.
 
436
        baseDir := agenttools.SharedGUIDir(s.DataDir())
 
437
        c.Assert(baseDir, jc.DoesNotExist)
 
438
 
 
439
        // Make a request for the Juju GUI.
 
440
        resp := s.sendRequest(c, httpRequestParams{
 
441
                url: s.guiURL(c, "", "/"),
 
442
        })
 
443
        body := assertResponse(c, resp, http.StatusOK, "text/html; charset=utf-8")
 
444
        c.Assert(string(body), gc.Equals, indexContent)
 
445
 
 
446
        // Now the GUI is stored on disk, in a directory corresponding to its
 
447
        // archive SHA256 hash.
 
448
        indexPath := filepath.Join(baseDir, hash, guiIndexPath)
 
449
        c.Assert(indexPath, jc.IsNonEmptyFile)
 
450
        b, err := ioutil.ReadFile(indexPath)
 
451
        c.Assert(err, jc.ErrorIsNil)
 
452
        c.Assert(string(b), gc.Equals, indexContent)
 
453
}
 
454
 
 
455
// makeGUIArchive creates a Juju GUI tar.bz2 archive with the given files.
 
456
// The files parameter maps file names (relative to the internal "jujugui"
 
457
// directory) to their contents. This function returns a reader for the
 
458
// archive, its hash and size.
 
459
func makeGUIArchive(c *gc.C, vers string, files map[string]string) (r io.Reader, hash string, size int64) {
 
460
        if runtime.GOOS == "windows" {
 
461
                // Skipping the tests on Windows is not a problem as the Juju GUI is
 
462
                // only served from Linux machines.
 
463
                c.Skip("bzip2 command not available")
 
464
        }
 
465
        cmd := exec.Command("bzip2", "--compress", "--stdout", "--fast")
 
466
 
 
467
        stdin, err := cmd.StdinPipe()
 
468
        c.Assert(err, jc.ErrorIsNil)
 
469
        stdout, err := cmd.StdoutPipe()
 
470
        c.Assert(err, jc.ErrorIsNil)
 
471
 
 
472
        err = cmd.Start()
 
473
        c.Assert(err, jc.ErrorIsNil)
 
474
 
 
475
        tw := tar.NewWriter(stdin)
 
476
        baseDir := filepath.Join("jujugui-"+vers, "jujugui")
 
477
        err = tw.WriteHeader(&tar.Header{
 
478
                Name:     baseDir,
 
479
                Mode:     0700,
 
480
                Typeflag: tar.TypeDir,
 
481
        })
 
482
        c.Assert(err, jc.ErrorIsNil)
 
483
        for path, content := range files {
 
484
                name := filepath.Join(baseDir, path)
 
485
                err = tw.WriteHeader(&tar.Header{
 
486
                        Name:     filepath.Dir(name),
 
487
                        Mode:     0700,
 
488
                        Typeflag: tar.TypeDir,
 
489
                })
 
490
                c.Assert(err, jc.ErrorIsNil)
 
491
                err = tw.WriteHeader(&tar.Header{
 
492
                        Name: name,
 
493
                        Mode: 0600,
 
494
                        Size: int64(len(content)),
 
495
                })
 
496
                c.Assert(err, jc.ErrorIsNil)
 
497
                _, err = io.WriteString(tw, content)
 
498
                c.Assert(err, jc.ErrorIsNil)
 
499
        }
 
500
        err = tw.Close()
 
501
        c.Assert(err, jc.ErrorIsNil)
 
502
        err = stdin.Close()
 
503
        c.Assert(err, jc.ErrorIsNil)
 
504
 
 
505
        h := sha256.New()
 
506
        r = io.TeeReader(stdout, h)
 
507
        b, err := ioutil.ReadAll(r)
 
508
        c.Assert(err, jc.ErrorIsNil)
 
509
 
 
510
        err = cmd.Wait()
 
511
        c.Assert(err, jc.ErrorIsNil)
 
512
 
 
513
        return bytes.NewReader(b), fmt.Sprintf("%x", h.Sum(nil)), int64(len(b))
 
514
}
 
515
 
 
516
// setupGUIArchive creates a Juju GUI tar.bz2 archive with the given version
 
517
// and files and saves it into the given storage. The Juju GUI archive SHA256
 
518
// hash is returned.
 
519
func setupGUIArchive(c *gc.C, storage binarystorage.Storage, vers string, files map[string]string) (hash string) {
 
520
        r, hash, size := makeGUIArchive(c, vers, files)
 
521
        err := storage.Add(r, binarystorage.Metadata{
 
522
                Version: vers,
 
523
                Size:    size,
 
524
                SHA256:  hash,
 
525
        })
 
526
        c.Assert(err, jc.ErrorIsNil)
 
527
        return hash
 
528
}