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  }