launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/provider/azure/storage_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package azure 5 6 import ( 7 "encoding/base64" 8 "fmt" 9 "io/ioutil" 10 "net/http" 11 "net/url" 12 "strings" 13 14 gc "launchpad.net/gocheck" 15 "launchpad.net/gwacl" 16 17 "launchpad.net/juju-core/environs/storage" 18 "launchpad.net/juju-core/errors" 19 jc "launchpad.net/juju-core/testing/checkers" 20 ) 21 22 type storageSuite struct { 23 providerSuite 24 } 25 26 var _ = gc.Suite(&storageSuite{}) 27 28 func makeResponse(content string, status int) *http.Response { 29 return &http.Response{ 30 Status: fmt.Sprintf("%d", status), 31 StatusCode: status, 32 Body: ioutil.NopCloser(strings.NewReader(content)), 33 } 34 } 35 36 // MockingTransportExchange is a recording of a request and a response over 37 // HTTP. 38 type MockingTransportExchange struct { 39 Request *http.Request 40 Response *http.Response 41 Error error 42 } 43 44 // MockingTransport is used as an http.Client.Transport for testing. It 45 // records the sequence of requests, and returns a predetermined sequence of 46 // Responses and errors. 47 type MockingTransport struct { 48 Exchanges []*MockingTransportExchange 49 ExchangeCount int 50 } 51 52 // MockingTransport implements the http.RoundTripper interface. 53 var _ http.RoundTripper = &MockingTransport{} 54 55 func (t *MockingTransport) AddExchange(response *http.Response, err error) { 56 exchange := MockingTransportExchange{Response: response, Error: err} 57 t.Exchanges = append(t.Exchanges, &exchange) 58 } 59 60 func (t *MockingTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { 61 exchange := t.Exchanges[t.ExchangeCount] 62 t.ExchangeCount++ 63 exchange.Request = req 64 return exchange.Response, exchange.Error 65 } 66 67 // testStorageContext is a struct implementing the storageContext interface 68 // used in test. It will return, via getContainer() and getStorageContext() 69 // the objects used at creation time. 70 type testStorageContext struct { 71 container string 72 storageContext *gwacl.StorageContext 73 } 74 75 func (context *testStorageContext) getContainer() string { 76 return context.container 77 } 78 79 func (context *testStorageContext) getStorageContext() (*gwacl.StorageContext, error) { 80 return context.storageContext, nil 81 } 82 83 // makeFakeStorage creates a test azureStorage object that will talk to a 84 // fake HTTP server set up to always return preconfigured http.Response objects. 85 // The MockingTransport object can be used to check that the expected query has 86 // been issued to the test server. 87 func makeFakeStorage(container, account, key string) (*azureStorage, *MockingTransport) { 88 transport := &MockingTransport{} 89 client := &http.Client{Transport: transport} 90 storageContext := gwacl.NewTestStorageContext(client) 91 storageContext.Account = account 92 storageContext.Key = key 93 context := &testStorageContext{container: container, storageContext: storageContext} 94 azStorage := &azureStorage{storageContext: context} 95 return azStorage, transport 96 } 97 98 // setStorageEndpoint sets a given Azure API endpoint on a given azureStorage. 99 func setStorageEndpoint(azStorage *azureStorage, endpoint gwacl.APIEndpoint) { 100 // Ugly, because of the confusingly similar layers of nesting. 101 testContext := azStorage.storageContext.(*testStorageContext) 102 var gwaclContext *gwacl.StorageContext = testContext.storageContext 103 gwaclContext.AzureEndpoint = endpoint 104 } 105 106 var blobListResponse = ` 107 <?xml version="1.0" encoding="utf-8"?> 108 <EnumerationResults ContainerName="http://myaccount.blob.core.windows.net/mycontainer"> 109 <Prefix>prefix</Prefix> 110 <Marker>marker</Marker> 111 <MaxResults>maxresults</MaxResults> 112 <Delimiter>delimiter</Delimiter> 113 <Blobs> 114 <Blob> 115 <Name>prefix-1</Name> 116 <Url>blob-url1</Url> 117 </Blob> 118 <Blob> 119 <Name>prefix-2</Name> 120 <Url>blob-url2</Url> 121 </Blob> 122 </Blobs> 123 <NextMarker /> 124 </EnumerationResults>` 125 126 func (*storageSuite) TestList(c *gc.C) { 127 container := "container" 128 response := makeResponse(blobListResponse, http.StatusOK) 129 azStorage, transport := makeFakeStorage(container, "account", "") 130 transport.AddExchange(response, nil) 131 132 prefix := "prefix" 133 names, err := storage.List(azStorage, prefix) 134 c.Assert(err, gc.IsNil) 135 c.Assert(transport.ExchangeCount, gc.Equals, 1) 136 // The prefix has been passed down as a query parameter. 137 c.Check(transport.Exchanges[0].Request.URL.Query()["prefix"], gc.DeepEquals, []string{prefix}) 138 // The container name is used in the requested URL. 139 c.Check(transport.Exchanges[0].Request.URL.String(), gc.Matches, ".*"+container+".*") 140 c.Check(names, gc.DeepEquals, []string{"prefix-1", "prefix-2"}) 141 } 142 143 func (*storageSuite) TestListWithNonexistentContainerReturnsNoFiles(c *gc.C) { 144 // If Azure returns a 404 it means the container doesn't exist. In this 145 // case the provider should interpret this as "no files" and return nil. 146 container := "container" 147 response := makeResponse("", http.StatusNotFound) 148 azStorage, transport := makeFakeStorage(container, "account", "") 149 transport.AddExchange(response, nil) 150 151 names, err := storage.List(azStorage, "prefix") 152 c.Assert(err, gc.IsNil) 153 c.Assert(names, gc.IsNil) 154 } 155 156 func (*storageSuite) TestGet(c *gc.C) { 157 blobContent := "test blob" 158 container := "container" 159 filename := "blobname" 160 response := makeResponse(blobContent, http.StatusOK) 161 azStorage, transport := makeFakeStorage(container, "account", "") 162 transport.AddExchange(response, nil) 163 164 reader, err := storage.Get(azStorage, filename) 165 c.Assert(err, gc.IsNil) 166 c.Assert(reader, gc.NotNil) 167 defer reader.Close() 168 169 context, err := azStorage.getStorageContext() 170 c.Assert(err, gc.IsNil) 171 c.Assert(transport.ExchangeCount, gc.Equals, 1) 172 c.Check(transport.Exchanges[0].Request.URL.String(), gc.Matches, context.GetFileURL(container, filename)+"?.*") 173 data, err := ioutil.ReadAll(reader) 174 c.Assert(err, gc.IsNil) 175 c.Check(string(data), gc.Equals, blobContent) 176 } 177 178 func (*storageSuite) TestGetReturnsNotFoundIf404(c *gc.C) { 179 container := "container" 180 filename := "blobname" 181 response := makeResponse("not found", http.StatusNotFound) 182 azStorage, transport := makeFakeStorage(container, "account", "") 183 transport.AddExchange(response, nil) 184 _, err := storage.Get(azStorage, filename) 185 c.Assert(err, gc.NotNil) 186 c.Check(err, jc.Satisfies, errors.IsNotFoundError) 187 } 188 189 func (*storageSuite) TestPut(c *gc.C) { 190 blobContent := "test blob" 191 container := "container" 192 filename := "blobname" 193 azStorage, transport := makeFakeStorage(container, "account", "") 194 // The create container call makes two exchanges. 195 transport.AddExchange(makeResponse("", http.StatusNotFound), nil) 196 transport.AddExchange(makeResponse("", http.StatusCreated), nil) 197 putResponse := makeResponse("", http.StatusCreated) 198 transport.AddExchange(putResponse, nil) 199 transport.AddExchange(putResponse, nil) 200 err := azStorage.Put(filename, strings.NewReader(blobContent), int64(len(blobContent))) 201 c.Assert(err, gc.IsNil) 202 203 context, err := azStorage.getStorageContext() 204 c.Assert(err, gc.IsNil) 205 c.Assert(transport.ExchangeCount, gc.Equals, 4) 206 c.Check(transport.Exchanges[2].Request.URL.String(), gc.Matches, context.GetFileURL(container, filename)+"?.*") 207 } 208 209 func (*storageSuite) TestRemove(c *gc.C) { 210 container := "container" 211 filename := "blobname" 212 response := makeResponse("", http.StatusAccepted) 213 azStorage, transport := makeFakeStorage(container, "account", "") 214 transport.AddExchange(response, nil) 215 err := azStorage.Remove(filename) 216 c.Assert(err, gc.IsNil) 217 218 context, err := azStorage.getStorageContext() 219 c.Assert(err, gc.IsNil) 220 c.Assert(transport.ExchangeCount, gc.Equals, 1) 221 c.Check(transport.Exchanges[0].Request.URL.String(), gc.Matches, context.GetFileURL(container, filename)+"?.*") 222 c.Check(transport.Exchanges[0].Request.Method, gc.Equals, "DELETE") 223 } 224 225 func (*storageSuite) TestRemoveErrors(c *gc.C) { 226 container := "container" 227 filename := "blobname" 228 response := makeResponse("", http.StatusForbidden) 229 azStorage, transport := makeFakeStorage(container, "account", "") 230 transport.AddExchange(response, nil) 231 err := azStorage.Remove(filename) 232 c.Assert(err, gc.NotNil) 233 } 234 235 func (*storageSuite) TestRemoveAll(c *gc.C) { 236 // When we ask gwacl to remove all blobs, it calls DeleteContainer. 237 response := makeResponse("", http.StatusAccepted) 238 storage, transport := makeFakeStorage("cntnr", "account", "") 239 transport.AddExchange(response, nil) 240 241 err := storage.RemoveAll() 242 c.Assert(err, gc.IsNil) 243 244 _, err = storage.getStorageContext() 245 c.Assert(err, gc.IsNil) 246 // Without going too far into gwacl's innards, this is roughly what 247 // it needs to do in order to delete a container. 248 c.Assert(transport.ExchangeCount, gc.Equals, 1) 249 c.Check(transport.Exchanges[0].Request.URL.String(), gc.Matches, "http.*/cntnr?.*restype=container.*") 250 c.Check(transport.Exchanges[0].Request.Method, gc.Equals, "DELETE") 251 } 252 253 func (*storageSuite) TestRemoveNonExistentBlobSucceeds(c *gc.C) { 254 container := "container" 255 filename := "blobname" 256 response := makeResponse("", http.StatusNotFound) 257 azStorage, transport := makeFakeStorage(container, "account", "") 258 transport.AddExchange(response, nil) 259 err := azStorage.Remove(filename) 260 c.Assert(err, gc.IsNil) 261 } 262 263 func (*storageSuite) TestURL(c *gc.C) { 264 container := "container" 265 filename := "blobname" 266 account := "account" 267 key := "bWFkZXlvdWxvb2sK" 268 azStorage, _ := makeFakeStorage(container, account, key) 269 // Use a realistic service endpoint for this test, so that we can see 270 // that we're really getting the expected kind of URL. 271 setStorageEndpoint(azStorage, gwacl.GetEndpoint("West US")) 272 URL, err := azStorage.URL(filename) 273 c.Assert(err, gc.IsNil) 274 parsedURL, err := url.Parse(URL) 275 c.Assert(err, gc.IsNil) 276 c.Check(parsedURL.Host, gc.Matches, fmt.Sprintf("%s.blob.core.windows.net", account)) 277 c.Check(parsedURL.Path, gc.Matches, fmt.Sprintf("/%s/%s", container, filename)) 278 values, err := url.ParseQuery(parsedURL.RawQuery) 279 c.Assert(err, gc.IsNil) 280 signature := values.Get("sig") 281 // The query string contains a non-empty signature. 282 c.Check(signature, gc.Not(gc.HasLen), 0) 283 // The signature is base64-encoded. 284 _, err = base64.StdEncoding.DecodeString(signature) 285 c.Assert(err, gc.IsNil) 286 // If Key is empty, query string does not contain a signature. 287 key = "" 288 azStorage, _ = makeFakeStorage(container, account, key) 289 URL, err = azStorage.URL(filename) 290 c.Assert(err, gc.IsNil) 291 parsedURL, err = url.Parse(URL) 292 c.Assert(err, gc.IsNil) 293 values, err = url.ParseQuery(parsedURL.RawQuery) 294 c.Assert(err, gc.IsNil) 295 c.Check(values.Get("sig"), gc.HasLen, 0) 296 } 297 298 func (*storageSuite) TestCreateContainerCreatesContainerIfDoesNotExist(c *gc.C) { 299 azStorage, transport := makeFakeStorage("", "account", "") 300 transport.AddExchange(makeResponse("", http.StatusNotFound), nil) 301 transport.AddExchange(makeResponse("", http.StatusCreated), nil) 302 303 err := azStorage.createContainer("cntnr") 304 305 c.Assert(err, gc.IsNil) 306 c.Assert(transport.ExchangeCount, gc.Equals, 2) 307 // Without going too far into gwacl's innards, this is roughly what 308 // it needs to do in order to call GetContainerProperties. 309 c.Check(transport.Exchanges[0].Request.URL.String(), gc.Matches, "http.*/cntnr?.*restype=container.*") 310 c.Check(transport.Exchanges[0].Request.Method, gc.Equals, "GET") 311 312 // ... and for CreateContainer. 313 c.Check(transport.Exchanges[1].Request.URL.String(), gc.Matches, "http.*/cntnr?.*restype=container.*") 314 c.Check(transport.Exchanges[1].Request.Method, gc.Equals, "PUT") 315 } 316 317 func (*storageSuite) TestCreateContainerIsDoneIfContainerAlreadyExists(c *gc.C) { 318 container := "" 319 azStorage, transport := makeFakeStorage(container, "account", "") 320 header := make(http.Header) 321 header.Add("Last-Modified", "last-modified") 322 header.Add("ETag", "etag") 323 header.Add("X-Ms-Lease-Status", "status") 324 header.Add("X-Ms-Lease-State", "state") 325 header.Add("X-Ms-Lease-Duration", "duration") 326 response := makeResponse("", http.StatusOK) 327 response.Header = header 328 transport.AddExchange(response, nil) 329 330 err := azStorage.createContainer("cntnr") 331 332 c.Assert(err, gc.IsNil) 333 c.Assert(transport.ExchangeCount, gc.Equals, 1) 334 // Without going too far into gwacl's innards, this is roughly what 335 // it needs to do in order to call GetContainerProperties. 336 c.Check(transport.Exchanges[0].Request.URL.String(), gc.Matches, "http.*/cntnr?.*restype=container.*") 337 c.Check(transport.Exchanges[0].Request.Method, gc.Equals, "GET") 338 } 339 340 func (*storageSuite) TestCreateContainerFailsIfContainerInaccessible(c *gc.C) { 341 azStorage, transport := makeFakeStorage("", "account", "") 342 transport.AddExchange(makeResponse("", http.StatusInternalServerError), nil) 343 344 err := azStorage.createContainer("cntnr") 345 c.Assert(err, gc.NotNil) 346 347 // createContainer got an error when trying to query for an existing 348 // container of the right name. But it does not mistake that error for 349 // "this container does not exist yet so go ahead and create it." 350 // The proper response to the situation is to report the failure. 351 c.Assert(err, gc.ErrorMatches, ".*Internal Server Error.*") 352 } 353 354 func (*storageSuite) TestDeleteContainer(c *gc.C) { 355 azStorage, transport := makeFakeStorage("", "account", "") 356 transport.AddExchange(makeResponse("", http.StatusAccepted), nil) 357 358 err := azStorage.deleteContainer("cntnr") 359 360 c.Assert(err, gc.IsNil) 361 c.Assert(transport.ExchangeCount, gc.Equals, 1) 362 // Without going too far into gwacl's innards, this is roughly what 363 // it needs to do in order to call GetContainerProperties. 364 c.Check(transport.Exchanges[0].Request.URL.String(), gc.Matches, "http.*/cntnr?.*restype=container.*") 365 c.Check(transport.Exchanges[0].Request.Method, gc.Equals, "DELETE") 366 }