1
// Copyright 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
14
"github.com/juju/errors"
15
"github.com/juju/gomaasapi"
16
jc "github.com/juju/testing/checkers"
17
gc "gopkg.in/check.v1"
19
"github.com/juju/juju/environs/storage"
22
var _ storage.Storage = (*maas1Storage)(nil)
24
type storageSuite struct {
28
var _ = gc.Suite(&storageSuite{})
30
// makeStorage creates a MAAS storage object for the running test.
31
func (s *storageSuite) makeStorage(name string) *maas1Storage {
32
maasobj := s.testMAASObject.MAASObject
33
env := s.makeEnviron()
35
env.maasClientUnlocked = &maasobj
36
return NewStorage(env).(*maas1Storage)
39
// makeRandomBytes returns an array of arbitrary byte values.
40
func makeRandomBytes(length int) []byte {
41
data := make([]byte, length)
42
for index := range data {
43
data[index] = byte(rand.Intn(256))
48
// fakeStoredFile creates a file directly in the (simulated) MAAS file store.
49
// It will contain an arbitrary amount of random data. The contents are also
52
// If you want properly random data here, initialize the randomizer first.
53
// Or don't, if you want consistent (and debuggable) results.
54
func (s *storageSuite) fakeStoredFile(stor storage.Storage, name string) gomaasapi.MAASObject {
55
data := makeRandomBytes(rand.Intn(10))
56
// The filename must be prefixed with the private namespace as we're
57
// bypassing the Put() method that would normally do that.
58
prefixFilename := stor.(*maas1Storage).prefixWithPrivateNamespace("") + name
59
return s.testMAASObject.TestServer.NewFile(prefixFilename, data)
62
func (s *storageSuite) TestGetRetrievesFile(c *gc.C) {
63
const filename = "stored-data"
64
stor := s.makeStorage("get-retrieves-file")
65
file := s.fakeStoredFile(stor, filename)
66
base64Content, err := file.GetField("content")
67
c.Assert(err, jc.ErrorIsNil)
68
content, err := base64.StdEncoding.DecodeString(base64Content)
69
c.Assert(err, jc.ErrorIsNil)
71
reader, err := storage.Get(stor, filename)
72
c.Assert(err, jc.ErrorIsNil)
75
buf, err := ioutil.ReadAll(reader)
76
c.Assert(err, jc.ErrorIsNil)
77
c.Check(len(buf), gc.Equals, len(content))
78
c.Check(buf, gc.DeepEquals, content)
81
func (s *storageSuite) TestRetrieveFileObjectReturnsFileObject(c *gc.C) {
82
const filename = "myfile"
83
stor := s.makeStorage("rfo-test")
84
file := s.fakeStoredFile(stor, filename)
85
fileURI, err := file.GetField("anon_resource_uri")
86
c.Assert(err, jc.ErrorIsNil)
87
fileContent, err := file.GetField("content")
88
c.Assert(err, jc.ErrorIsNil)
90
prefixFilename := stor.prefixWithPrivateNamespace(filename)
91
obj, err := stor.retrieveFileObject(prefixFilename)
92
c.Assert(err, jc.ErrorIsNil)
94
uri, err := obj.GetField("anon_resource_uri")
95
c.Assert(err, jc.ErrorIsNil)
96
c.Check(uri, gc.Equals, fileURI)
97
content, err := obj.GetField("content")
98
c.Check(content, gc.Equals, fileContent)
101
func (s *storageSuite) TestRetrieveFileObjectReturnsNotFoundForMissingFile(c *gc.C) {
102
stor := s.makeStorage("rfo-test")
103
_, err := stor.retrieveFileObject("nonexistent-file")
104
c.Assert(err, gc.NotNil)
105
c.Check(err, jc.Satisfies, errors.IsNotFound)
108
func (s *storageSuite) TestRetrieveFileObjectEscapesName(c *gc.C) {
109
const filename = "#a?b c&d%e!"
110
data := []byte("File contents here")
111
stor := s.makeStorage("rfo-test")
112
err := stor.Put(filename, bytes.NewReader(data), int64(len(data)))
113
c.Assert(err, jc.ErrorIsNil)
115
prefixFilename := stor.prefixWithPrivateNamespace(filename)
116
obj, err := stor.retrieveFileObject(prefixFilename)
117
c.Assert(err, jc.ErrorIsNil)
119
base64Content, err := obj.GetField("content")
120
c.Assert(err, jc.ErrorIsNil)
121
content, err := base64.StdEncoding.DecodeString(base64Content)
122
c.Assert(err, jc.ErrorIsNil)
123
c.Check(content, gc.DeepEquals, data)
126
func (s *storageSuite) TestFileContentsAreBinary(c *gc.C) {
127
const filename = "myfile.bin"
128
data := []byte{0, 1, 255, 2, 254, 3}
129
stor := s.makeStorage("binary-test")
131
err := stor.Put(filename, bytes.NewReader(data), int64(len(data)))
132
c.Assert(err, jc.ErrorIsNil)
133
file, err := storage.Get(stor, filename)
134
c.Assert(err, jc.ErrorIsNil)
135
content, err := ioutil.ReadAll(file)
136
c.Assert(err, jc.ErrorIsNil)
138
c.Check(content, gc.DeepEquals, data)
141
func (s *storageSuite) TestGetReturnsNotFoundErrorIfNotFound(c *gc.C) {
142
const filename = "lost-data"
143
stor := NewStorage(s.makeEnviron())
144
_, err := storage.Get(stor, filename)
145
c.Assert(err, jc.Satisfies, errors.IsNotFound)
148
func (s *storageSuite) TestListReturnsEmptyIfNoFilesStored(c *gc.C) {
149
stor := NewStorage(s.makeEnviron())
150
listing, err := storage.List(stor, "")
151
c.Assert(err, jc.ErrorIsNil)
152
c.Check(listing, gc.DeepEquals, []string{})
155
func (s *storageSuite) TestListReturnsAllFilesIfPrefixEmpty(c *gc.C) {
156
stor := NewStorage(s.makeEnviron())
157
files := []string{"1a", "2b", "3c"}
158
for _, name := range files {
159
s.fakeStoredFile(stor, name)
162
listing, err := storage.List(stor, "")
163
c.Assert(err, jc.ErrorIsNil)
164
c.Check(listing, gc.DeepEquals, files)
167
func (s *storageSuite) TestListSortsResults(c *gc.C) {
168
stor := NewStorage(s.makeEnviron())
169
files := []string{"4d", "1a", "3c", "2b"}
170
for _, name := range files {
171
s.fakeStoredFile(stor, name)
174
listing, err := storage.List(stor, "")
175
c.Assert(err, jc.ErrorIsNil)
176
c.Check(listing, gc.DeepEquals, []string{"1a", "2b", "3c", "4d"})
179
func (s *storageSuite) TestListReturnsNoFilesIfNoFilesMatchPrefix(c *gc.C) {
180
stor := NewStorage(s.makeEnviron())
181
s.fakeStoredFile(stor, "foo")
183
listing, err := storage.List(stor, "bar")
184
c.Assert(err, jc.ErrorIsNil)
185
c.Check(listing, gc.DeepEquals, []string{})
188
func (s *storageSuite) TestListReturnsOnlyFilesWithMatchingPrefix(c *gc.C) {
189
stor := NewStorage(s.makeEnviron())
190
s.fakeStoredFile(stor, "abc")
191
s.fakeStoredFile(stor, "xyz")
193
listing, err := storage.List(stor, "x")
194
c.Assert(err, jc.ErrorIsNil)
195
c.Check(listing, gc.DeepEquals, []string{"xyz"})
198
func (s *storageSuite) TestListMatchesPrefixOnly(c *gc.C) {
199
stor := NewStorage(s.makeEnviron())
200
s.fakeStoredFile(stor, "abc")
201
s.fakeStoredFile(stor, "xabc")
203
listing, err := storage.List(stor, "a")
204
c.Assert(err, jc.ErrorIsNil)
205
c.Check(listing, gc.DeepEquals, []string{"abc"})
208
func (s *storageSuite) TestListOperatesOnFlatNamespace(c *gc.C) {
209
stor := NewStorage(s.makeEnviron())
210
s.fakeStoredFile(stor, "a/b/c/d")
212
listing, err := storage.List(stor, "a/b")
213
c.Assert(err, jc.ErrorIsNil)
214
c.Check(listing, gc.DeepEquals, []string{"a/b/c/d"})
217
// getFileAtURL requests, and returns, the file at the given URL.
218
func getFileAtURL(fileURL string) ([]byte, error) {
219
response, err := http.Get(fileURL)
223
body, err := ioutil.ReadAll(response.Body)
230
func (s *storageSuite) TestURLReturnsURLCorrespondingToFile(c *gc.C) {
231
const filename = "my-file.txt"
232
stor := NewStorage(s.makeEnviron()).(*maas1Storage)
233
file := s.fakeStoredFile(stor, filename)
234
// The file contains an anon_resource_uri, which lacks a network part
235
// (but will probably contain a query part). anonURL will be the
237
anonURI, err := file.GetField("anon_resource_uri")
238
c.Assert(err, jc.ErrorIsNil)
239
parsedURI, err := url.Parse(anonURI)
240
c.Assert(err, jc.ErrorIsNil)
241
anonURL := stor.maasClient.URL().ResolveReference(parsedURI)
242
c.Assert(err, jc.ErrorIsNil)
244
fileURL, err := stor.URL(filename)
245
c.Assert(err, jc.ErrorIsNil)
247
c.Check(fileURL, gc.NotNil)
248
c.Check(fileURL, gc.Equals, anonURL.String())
251
func (s *storageSuite) TestPutStoresRetrievableFile(c *gc.C) {
252
const filename = "broken-toaster.jpg"
253
contents := []byte("Contents here")
254
length := int64(len(contents))
255
stor := NewStorage(s.makeEnviron())
257
err := stor.Put(filename, bytes.NewReader(contents), length)
259
reader, err := storage.Get(stor, filename)
260
c.Assert(err, jc.ErrorIsNil)
263
buf, err := ioutil.ReadAll(reader)
264
c.Assert(err, jc.ErrorIsNil)
265
c.Check(buf, gc.DeepEquals, contents)
268
func (s *storageSuite) TestPutOverwritesFile(c *gc.C) {
269
const filename = "foo.bar"
270
stor := NewStorage(s.makeEnviron())
271
s.fakeStoredFile(stor, filename)
272
newContents := []byte("Overwritten")
274
err := stor.Put(filename, bytes.NewReader(newContents), int64(len(newContents)))
275
c.Assert(err, jc.ErrorIsNil)
277
reader, err := storage.Get(stor, filename)
278
c.Assert(err, jc.ErrorIsNil)
281
buf, err := ioutil.ReadAll(reader)
282
c.Assert(err, jc.ErrorIsNil)
283
c.Check(len(buf), gc.Equals, len(newContents))
284
c.Check(buf, gc.DeepEquals, newContents)
287
func (s *storageSuite) TestPutStopsAtGivenLength(c *gc.C) {
288
const filename = "xyzzyz.2.xls"
290
contents := []byte("abcdefghijklmnopqrstuvwxyz")
291
stor := NewStorage(s.makeEnviron())
293
err := stor.Put(filename, bytes.NewReader(contents), length)
294
c.Assert(err, jc.ErrorIsNil)
296
reader, err := storage.Get(stor, filename)
297
c.Assert(err, jc.ErrorIsNil)
300
buf, err := ioutil.ReadAll(reader)
301
c.Assert(err, jc.ErrorIsNil)
302
c.Check(len(buf), gc.Equals, length)
305
func (s *storageSuite) TestPutToExistingFileTruncatesAtGivenLength(c *gc.C) {
306
const filename = "a-file-which-is-mine"
307
oldContents := []byte("abcdefghijklmnopqrstuvwxyz")
308
newContents := []byte("xyz")
309
stor := NewStorage(s.makeEnviron())
310
err := stor.Put(filename, bytes.NewReader(oldContents), int64(len(oldContents)))
311
c.Assert(err, jc.ErrorIsNil)
313
err = stor.Put(filename, bytes.NewReader(newContents), int64(len(newContents)))
314
c.Assert(err, jc.ErrorIsNil)
316
reader, err := storage.Get(stor, filename)
317
c.Assert(err, jc.ErrorIsNil)
320
buf, err := ioutil.ReadAll(reader)
321
c.Assert(err, jc.ErrorIsNil)
322
c.Check(len(buf), gc.Equals, len(newContents))
323
c.Check(buf, gc.DeepEquals, newContents)
326
func (s *storageSuite) TestRemoveDeletesFile(c *gc.C) {
327
const filename = "doomed.txt"
328
stor := NewStorage(s.makeEnviron())
329
s.fakeStoredFile(stor, filename)
331
err := stor.Remove(filename)
332
c.Assert(err, jc.ErrorIsNil)
334
_, err = storage.Get(stor, filename)
335
c.Assert(err, jc.Satisfies, errors.IsNotFound)
337
listing, err := storage.List(stor, filename)
338
c.Assert(err, jc.ErrorIsNil)
339
c.Assert(listing, gc.DeepEquals, []string{})
342
func (s *storageSuite) TestRemoveIsIdempotent(c *gc.C) {
343
const filename = "half-a-file"
344
stor := NewStorage(s.makeEnviron())
345
s.fakeStoredFile(stor, filename)
347
err := stor.Remove(filename)
348
c.Assert(err, jc.ErrorIsNil)
350
err = stor.Remove(filename)
351
c.Assert(err, jc.ErrorIsNil)
354
func (s *storageSuite) TestNamesMayHaveSlashes(c *gc.C) {
355
const filename = "name/with/slashes"
356
content := []byte("File contents")
357
stor := NewStorage(s.makeEnviron())
359
err := stor.Put(filename, bytes.NewReader(content), int64(len(content)))
360
c.Assert(err, jc.ErrorIsNil)
362
// There's not much we can say about the anonymous URL, except that
364
anonURL, err := stor.URL(filename)
365
c.Assert(err, jc.ErrorIsNil)
366
c.Check(anonURL, gc.Matches, "http[s]*://.*")
368
reader, err := storage.Get(stor, filename)
369
c.Assert(err, jc.ErrorIsNil)
371
data, err := ioutil.ReadAll(reader)
372
c.Assert(err, jc.ErrorIsNil)
373
c.Check(data, gc.DeepEquals, content)
376
func (s *storageSuite) TestRemoveAllDeletesAllFiles(c *gc.C) {
377
stor := s.makeStorage("get-retrieves-file")
378
const filename1 = "stored-data1"
379
s.fakeStoredFile(stor, filename1)
380
const filename2 = "stored-data2"
381
s.fakeStoredFile(stor, filename2)
383
err := stor.RemoveAll()
384
c.Assert(err, jc.ErrorIsNil)
385
listing, err := storage.List(stor, "")
386
c.Assert(err, jc.ErrorIsNil)
387
c.Assert(listing, gc.DeepEquals, []string{})
390
func (s *storageSuite) TestprefixWithPrivateNamespacePrefixesWithAgentName(c *gc.C) {
391
env := s.makeEnviron()
392
sstor := NewStorage(env)
393
stor := sstor.(*maas1Storage)
394
expectedPrefix := env.Config().UUID() + "-"
395
const name = "myname"
396
expectedResult := expectedPrefix + name
397
c.Assert(stor.prefixWithPrivateNamespace(name), gc.Equals, expectedResult)