launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/environs/httpstorage/backend_test.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package httpstorage_test
     5  
     6  import (
     7  	"bytes"
     8  	"crypto/tls"
     9  	"crypto/x509"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"net"
    13  	"net/http"
    14  	"os"
    15  	"path/filepath"
    16  	"strings"
    17  	stdtesting "testing"
    18  
    19  	gc "launchpad.net/gocheck"
    20  
    21  	"launchpad.net/juju-core/environs/filestorage"
    22  	"launchpad.net/juju-core/environs/httpstorage"
    23  	coretesting "launchpad.net/juju-core/testing"
    24  	jc "launchpad.net/juju-core/testing/checkers"
    25  	"launchpad.net/juju-core/testing/testbase"
    26  	"launchpad.net/juju-core/utils"
    27  )
    28  
    29  const testAuthkey = "jabberwocky"
    30  
    31  func TestLocal(t *stdtesting.T) {
    32  	gc.TestingT(t)
    33  }
    34  
    35  type backendSuite struct {
    36  	testbase.LoggingSuite
    37  }
    38  
    39  var _ = gc.Suite(&backendSuite{})
    40  
    41  // startServer starts a new local storage server
    42  // using a temporary directory and returns the listener,
    43  // a base URL for the server and the directory path.
    44  func startServer(c *gc.C) (listener net.Listener, url, dataDir string) {
    45  	dataDir = c.MkDir()
    46  	embedded, err := filestorage.NewFileStorageWriter(dataDir, filestorage.UseDefaultTmpDir)
    47  	c.Assert(err, gc.IsNil)
    48  	listener, err = httpstorage.Serve("localhost:0", embedded)
    49  	c.Assert(err, gc.IsNil)
    50  	return listener, fmt.Sprintf("http://%s/", listener.Addr()), dataDir
    51  }
    52  
    53  // startServerTLS starts a new TLS-based local storage server
    54  // using a temporary directory and returns the listener,
    55  // a base URL for the server and the directory path.
    56  func startServerTLS(c *gc.C) (listener net.Listener, url, dataDir string) {
    57  	dataDir = c.MkDir()
    58  	embedded, err := filestorage.NewFileStorageWriter(dataDir, filestorage.UseDefaultTmpDir)
    59  	c.Assert(err, gc.IsNil)
    60  	hostnames := []string{"127.0.0.1"}
    61  	caCertPEM := []byte(coretesting.CACert)
    62  	caKeyPEM := []byte(coretesting.CAKey)
    63  	listener, err = httpstorage.ServeTLS("127.0.0.1:0", embedded, caCertPEM, caKeyPEM, hostnames, testAuthkey)
    64  	c.Assert(err, gc.IsNil)
    65  	return listener, fmt.Sprintf("http://localhost:%d/", listener.Addr().(*net.TCPAddr).Port), dataDir
    66  }
    67  
    68  type testCase struct {
    69  	name    string
    70  	content string
    71  	found   []string
    72  	status  int
    73  }
    74  
    75  var getTests = []testCase{
    76  	{
    77  		// Get existing file.
    78  		name:    "foo",
    79  		content: "this is file 'foo'",
    80  	},
    81  	{
    82  		// Get existing file.
    83  		name:    "bar",
    84  		content: "this is file 'bar'",
    85  	},
    86  	{
    87  		// Get existing file.
    88  		name:    "baz",
    89  		content: "this is file 'baz'",
    90  	},
    91  	{
    92  		// Get existing file.
    93  		name:    "yadda",
    94  		content: "this is file 'yadda'",
    95  	},
    96  	{
    97  		// Get existing file from nested directory.
    98  		name:    "inner/fooin",
    99  		content: "this is inner file 'fooin'",
   100  	},
   101  	{
   102  		// Get existing file from nested directory.
   103  		name:    "inner/barin",
   104  		content: "this is inner file 'barin'",
   105  	},
   106  	{
   107  		// Get non-existing file.
   108  		name:   "dummy",
   109  		status: 404,
   110  	},
   111  	{
   112  		// Get non-existing file from nested directory.
   113  		name:   "inner/dummy",
   114  		status: 404,
   115  	},
   116  	{
   117  		// Get with a relative path ".." based on the
   118  		// root is passed without invoking the handler
   119  		// function.
   120  		name:   "../dummy",
   121  		status: 404,
   122  	},
   123  	{
   124  		// Get with a relative path ".." based on the
   125  		// root is passed without invoking the handler
   126  		// function.
   127  		name:    "../foo",
   128  		content: "this is file 'foo'",
   129  	},
   130  	{
   131  		// Get on a directory returns a 404 as it is
   132  		// not a file.
   133  		name:   "inner",
   134  		status: 404,
   135  	},
   136  }
   137  
   138  func (s *backendSuite) TestHeadNonAuth(c *gc.C) {
   139  	// HEAD is unsupported for non-authenticating servers.
   140  	listener, url, _ := startServer(c)
   141  	defer listener.Close()
   142  	resp, err := http.Head(url)
   143  	c.Assert(err, gc.IsNil)
   144  	c.Assert(resp.StatusCode, gc.Equals, http.StatusMethodNotAllowed)
   145  }
   146  
   147  func (s *backendSuite) TestHeadAuth(c *gc.C) {
   148  	// HEAD on an authenticating server will return the HTTPS counterpart URL.
   149  	client, url, datadir := s.tlsServerAndClient(c)
   150  	createTestData(c, datadir)
   151  
   152  	resp, err := client.Head(url)
   153  	c.Assert(err, gc.IsNil)
   154  	c.Assert(resp.StatusCode, gc.Equals, http.StatusOK)
   155  	location, err := resp.Location()
   156  	c.Assert(err, gc.IsNil)
   157  	c.Assert(location.String(), gc.Matches, "https://localhost:[0-9]{5}/")
   158  	testGet(c, client, location.String())
   159  }
   160  
   161  func (s *backendSuite) TestHeadCustomHost(c *gc.C) {
   162  	// HEAD with a custom "Host:" header; the server should respond
   163  	// with a Location with the specified Host header.
   164  	client, url, _ := s.tlsServerAndClient(c)
   165  	req, err := http.NewRequest("HEAD", url+"arbitrary", nil)
   166  	c.Assert(err, gc.IsNil)
   167  	req.Host = "notarealhost"
   168  	resp, err := client.Do(req)
   169  	c.Assert(err, gc.IsNil)
   170  	c.Assert(resp.StatusCode, gc.Equals, http.StatusOK)
   171  	location, err := resp.Location()
   172  	c.Assert(err, gc.IsNil)
   173  	c.Assert(location.String(), gc.Matches, "https://notarealhost:[0-9]{5}/arbitrary")
   174  }
   175  
   176  func (s *backendSuite) TestGet(c *gc.C) {
   177  	// Test retrieving a file from a storage.
   178  	listener, url, dataDir := startServer(c)
   179  	defer listener.Close()
   180  	createTestData(c, dataDir)
   181  	testGet(c, http.DefaultClient, url)
   182  }
   183  
   184  func testGet(c *gc.C, client *http.Client, url string) {
   185  	check := func(tc testCase) {
   186  		resp, err := client.Get(url + tc.name)
   187  		c.Assert(err, gc.IsNil)
   188  		if tc.status != 0 {
   189  			c.Assert(resp.StatusCode, gc.Equals, tc.status)
   190  			return
   191  		} else {
   192  			c.Assert(resp.StatusCode, gc.Equals, http.StatusOK)
   193  		}
   194  		defer resp.Body.Close()
   195  		var buf bytes.Buffer
   196  		_, err = buf.ReadFrom(resp.Body)
   197  		c.Assert(err, gc.IsNil)
   198  		c.Assert(buf.String(), gc.Equals, tc.content)
   199  	}
   200  	for _, tc := range getTests {
   201  		check(tc)
   202  	}
   203  }
   204  
   205  var listTests = []testCase{
   206  	{
   207  		// List with a full filename.
   208  		name:  "foo",
   209  		found: []string{"foo"},
   210  	},
   211  	{
   212  		// List with a name matching two files.
   213  		name:  "ba",
   214  		found: []string{"bar", "baz"},
   215  	},
   216  	{
   217  		// List the contents of a directory.
   218  		name:  "inner/",
   219  		found: []string{"inner/barin", "inner/bazin", "inner/fooin"},
   220  	},
   221  	{
   222  		// List with a name matching two files in
   223  		// a directory.
   224  		name:  "inner/ba",
   225  		found: []string{"inner/barin", "inner/bazin"},
   226  	},
   227  	{
   228  		// List with no name also lists the contents of all
   229  		// directories.
   230  		name:  "",
   231  		found: []string{"bar", "baz", "foo", "inner/barin", "inner/bazin", "inner/fooin", "yadda"},
   232  	},
   233  	{
   234  		// List with a non-matching name returns an empty
   235  		// body which is evaluated to a slice with an empty
   236  		// string in the test (simplification).
   237  		name:  "zzz",
   238  		found: []string{""},
   239  	},
   240  	{
   241  		// List with a relative path ".." based on the
   242  		// root is passed without invoking the handler
   243  		// function. So returns the contents of all
   244  		// directories.
   245  		name:  "../",
   246  		found: []string{"bar", "baz", "foo", "inner/barin", "inner/bazin", "inner/fooin", "yadda"},
   247  	},
   248  }
   249  
   250  func (s *backendSuite) TestList(c *gc.C) {
   251  	// Test listing file of a storage.
   252  	listener, url, dataDir := startServer(c)
   253  	defer listener.Close()
   254  	createTestData(c, dataDir)
   255  	testList(c, http.DefaultClient, url)
   256  }
   257  
   258  func testList(c *gc.C, client *http.Client, url string) {
   259  	check := func(tc testCase) {
   260  		resp, err := client.Get(url + tc.name + "*")
   261  		c.Assert(err, gc.IsNil)
   262  		if tc.status != 0 {
   263  			c.Assert(resp.StatusCode, gc.Equals, tc.status)
   264  			return
   265  		}
   266  		defer resp.Body.Close()
   267  		var buf bytes.Buffer
   268  		_, err = buf.ReadFrom(resp.Body)
   269  		c.Assert(err, gc.IsNil)
   270  		names := strings.Split(buf.String(), "\n")
   271  		c.Assert(names, gc.DeepEquals, tc.found)
   272  	}
   273  	for i, tc := range listTests {
   274  		c.Logf("test %d", i)
   275  		check(tc)
   276  	}
   277  }
   278  
   279  var putTests = []testCase{
   280  	{
   281  		// Put a file in the root directory.
   282  		name:    "porterhouse",
   283  		content: "this is the sent file 'porterhouse'",
   284  	},
   285  	{
   286  		// Put a file with a relative path ".." is resolved
   287  		// a redirect 301 by the Go HTTP daemon. The handler
   288  		// isn't aware of it.
   289  		name:   "../no-way",
   290  		status: 301,
   291  	},
   292  	{
   293  		// Put a file in a nested directory.
   294  		name:    "deep/cambridge",
   295  		content: "this is the sent file 'deep/cambridge'",
   296  	},
   297  }
   298  
   299  func (s *backendSuite) TestPut(c *gc.C) {
   300  	// Test sending a file to the storage.
   301  	listener, url, dataDir := startServer(c)
   302  	defer listener.Close()
   303  	createTestData(c, dataDir)
   304  	testPut(c, http.DefaultClient, url, dataDir, true)
   305  }
   306  
   307  func testPut(c *gc.C, client *http.Client, url, dataDir string, authorized bool) {
   308  	check := func(tc testCase) {
   309  		req, err := http.NewRequest("PUT", url+tc.name, bytes.NewBufferString(tc.content))
   310  		c.Assert(err, gc.IsNil)
   311  		req.Header.Set("Content-Type", "application/octet-stream")
   312  		resp, err := client.Do(req)
   313  		c.Assert(err, gc.IsNil)
   314  		if tc.status != 0 {
   315  			c.Assert(resp.StatusCode, gc.Equals, tc.status)
   316  			return
   317  		} else if !authorized {
   318  			c.Assert(resp.StatusCode, gc.Equals, http.StatusUnauthorized)
   319  			return
   320  		}
   321  		c.Assert(resp.StatusCode, gc.Equals, http.StatusCreated)
   322  
   323  		fp := filepath.Join(dataDir, tc.name)
   324  		b, err := ioutil.ReadFile(fp)
   325  		c.Assert(err, gc.IsNil)
   326  		c.Assert(string(b), gc.Equals, tc.content)
   327  	}
   328  	for _, tc := range putTests {
   329  		check(tc)
   330  	}
   331  }
   332  
   333  var removeTests = []testCase{
   334  	{
   335  		// Delete a file in the root directory.
   336  		name:    "fox",
   337  		content: "the quick brown fox jumps over the lazy dog",
   338  	},
   339  	{
   340  		// Delete a file in a nested directory.
   341  		name:    "quick/brown/fox",
   342  		content: "the quick brown fox jumps over the lazy dog",
   343  	},
   344  	{
   345  		// Delete a non-existing file leads to no error.
   346  		name: "dog",
   347  	},
   348  	{
   349  		// Delete a file with a relative path ".." is resolved
   350  		// a redirect 301 by the Go HTTP daemon. The handler
   351  		// doesn't get aware of it.
   352  		name:   "../something",
   353  		status: 301,
   354  	},
   355  }
   356  
   357  func (s *backendSuite) TestRemove(c *gc.C) {
   358  	// Test removing a file in the storage.
   359  	listener, url, dataDir := startServer(c)
   360  	defer listener.Close()
   361  	createTestData(c, dataDir)
   362  	testRemove(c, http.DefaultClient, url, dataDir, true)
   363  }
   364  
   365  func testRemove(c *gc.C, client *http.Client, url, dataDir string, authorized bool) {
   366  	check := func(tc testCase) {
   367  		fp := filepath.Join(dataDir, tc.name)
   368  		dir, _ := filepath.Split(fp)
   369  		err := os.MkdirAll(dir, 0777)
   370  		c.Assert(err, gc.IsNil)
   371  		err = ioutil.WriteFile(fp, []byte(tc.content), 0644)
   372  		c.Assert(err, gc.IsNil)
   373  
   374  		req, err := http.NewRequest("DELETE", url+tc.name, nil)
   375  		c.Assert(err, gc.IsNil)
   376  		resp, err := client.Do(req)
   377  		c.Assert(err, gc.IsNil)
   378  		if tc.status != 0 {
   379  			c.Assert(resp.StatusCode, gc.Equals, tc.status)
   380  			return
   381  		} else if !authorized {
   382  			c.Assert(resp.StatusCode, gc.Equals, http.StatusUnauthorized)
   383  			return
   384  		}
   385  		c.Assert(resp.StatusCode, gc.Equals, http.StatusOK)
   386  
   387  		_, err = os.Stat(fp)
   388  		c.Assert(os.IsNotExist(err), gc.Equals, true)
   389  	}
   390  	for i, tc := range removeTests {
   391  		c.Logf("test %d", i)
   392  		check(tc)
   393  	}
   394  }
   395  
   396  func createTestData(c *gc.C, dataDir string) {
   397  	writeData := func(dir, name, data string) {
   398  		fn := filepath.Join(dir, name)
   399  		c.Logf("writing data to %q", fn)
   400  		err := ioutil.WriteFile(fn, []byte(data), 0644)
   401  		c.Assert(err, gc.IsNil)
   402  	}
   403  
   404  	writeData(dataDir, "foo", "this is file 'foo'")
   405  	writeData(dataDir, "bar", "this is file 'bar'")
   406  	writeData(dataDir, "baz", "this is file 'baz'")
   407  	writeData(dataDir, "yadda", "this is file 'yadda'")
   408  
   409  	innerDir := filepath.Join(dataDir, "inner")
   410  	err := os.MkdirAll(innerDir, 0777)
   411  	c.Assert(err, gc.IsNil)
   412  
   413  	writeData(innerDir, "fooin", "this is inner file 'fooin'")
   414  	writeData(innerDir, "barin", "this is inner file 'barin'")
   415  	writeData(innerDir, "bazin", "this is inner file 'bazin'")
   416  }
   417  
   418  func (b *backendSuite) tlsServerAndClient(c *gc.C) (client *http.Client, url, dataDir string) {
   419  	listener, url, dataDir := startServerTLS(c)
   420  	b.AddCleanup(func(*gc.C) { listener.Close() })
   421  	caCerts := x509.NewCertPool()
   422  	c.Assert(caCerts.AppendCertsFromPEM([]byte(coretesting.CACert)), jc.IsTrue)
   423  	client = &http.Client{
   424  		Transport: utils.NewHttpTLSTransport(&tls.Config{RootCAs: caCerts}),
   425  	}
   426  	return client, url, dataDir
   427  }
   428  
   429  func (b *backendSuite) TestTLSUnauthenticatedGet(c *gc.C) {
   430  	client, url, dataDir := b.tlsServerAndClient(c)
   431  	createTestData(c, dataDir)
   432  	testGet(c, client, url)
   433  }
   434  
   435  func (b *backendSuite) TestTLSUnauthenticatedList(c *gc.C) {
   436  	client, url, dataDir := b.tlsServerAndClient(c)
   437  	createTestData(c, dataDir)
   438  	testList(c, client, url)
   439  }
   440  
   441  func (b *backendSuite) TestTLSUnauthenticatedPut(c *gc.C) {
   442  	client, url, dataDir := b.tlsServerAndClient(c)
   443  	createTestData(c, dataDir)
   444  	testPut(c, client, url, dataDir, false)
   445  }
   446  
   447  func (b *backendSuite) TestTLSUnauthenticatedRemove(c *gc.C) {
   448  	client, url, dataDir := b.tlsServerAndClient(c)
   449  	createTestData(c, dataDir)
   450  	testRemove(c, client, url, dataDir, false)
   451  }