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

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package store_test
     5  
     6  import (
     7  	"encoding/json"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"net/url"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"labix.org/v2/mgo/bson"
    17  	gc "launchpad.net/gocheck"
    18  
    19  	"launchpad.net/juju-core/charm"
    20  	"launchpad.net/juju-core/store"
    21  )
    22  
    23  func (s *StoreSuite) prepareServer(c *gc.C) (*store.Server, *charm.URL) {
    24  	curl := charm.MustParseURL("cs:oneiric/wordpress")
    25  	pub, err := s.store.CharmPublisher([]*charm.URL{curl}, "some-digest")
    26  	c.Assert(err, gc.IsNil)
    27  	err = pub.Publish(&FakeCharmDir{})
    28  	c.Assert(err, gc.IsNil)
    29  
    30  	server, err := store.NewServer(s.store)
    31  	c.Assert(err, gc.IsNil)
    32  	return server, curl
    33  }
    34  
    35  func (s *StoreSuite) TestServerCharmInfo(c *gc.C) {
    36  	server, curl := s.prepareServer(c)
    37  	req, err := http.NewRequest("GET", "/charm-info", nil)
    38  	c.Assert(err, gc.IsNil)
    39  
    40  	var tests = []struct{ url, sha, digest, err string }{
    41  		{curl.String(), fakeRevZeroSha, "some-digest", ""},
    42  		{"cs:oneiric/non-existent", "", "", "entry not found"},
    43  		{"cs:bad", "", "", `charm URL without series: "cs:bad"`},
    44  	}
    45  
    46  	for _, t := range tests {
    47  		req.Form = url.Values{"charms": []string{t.url}}
    48  		rec := httptest.NewRecorder()
    49  		server.ServeHTTP(rec, req)
    50  
    51  		expected := make(map[string]interface{})
    52  		if t.sha != "" {
    53  			expected[t.url] = map[string]interface{}{
    54  				"revision": float64(0),
    55  				"sha256":   t.sha,
    56  				"digest":   t.digest,
    57  			}
    58  		} else {
    59  			expected[t.url] = map[string]interface{}{
    60  				"revision": float64(0),
    61  				"errors":   []interface{}{t.err},
    62  			}
    63  		}
    64  		obtained := map[string]interface{}{}
    65  		err = json.NewDecoder(rec.Body).Decode(&obtained)
    66  		c.Assert(err, gc.IsNil)
    67  		c.Assert(obtained, gc.DeepEquals, expected)
    68  		c.Assert(rec.Header().Get("Content-Type"), gc.Equals, "application/json")
    69  	}
    70  
    71  	s.checkCounterSum(c, []string{"charm-info", curl.Series, curl.Name}, false, 1)
    72  	s.checkCounterSum(c, []string{"charm-missing", "oneiric", "non-existent"}, false, 1)
    73  }
    74  
    75  func (s *StoreSuite) TestServerCharmEvent(c *gc.C) {
    76  	server, _ := s.prepareServer(c)
    77  	req, err := http.NewRequest("GET", "/charm-event", nil)
    78  	c.Assert(err, gc.IsNil)
    79  
    80  	url1 := charm.MustParseURL("cs:oneiric/wordpress")
    81  	url2 := charm.MustParseURL("cs:oneiric/mysql")
    82  	urls := []*charm.URL{url1, url2}
    83  
    84  	event1 := &store.CharmEvent{
    85  		Kind:     store.EventPublished,
    86  		Revision: 42,
    87  		Digest:   "revKey1",
    88  		URLs:     urls,
    89  		Warnings: []string{"A warning."},
    90  		Time:     time.Unix(1, 0),
    91  	}
    92  	event2 := &store.CharmEvent{
    93  		Kind:     store.EventPublished,
    94  		Revision: 43,
    95  		Digest:   "revKey2",
    96  		URLs:     urls,
    97  		Time:     time.Unix(2, 0),
    98  	}
    99  	event3 := &store.CharmEvent{
   100  		Kind:   store.EventPublishError,
   101  		Digest: "revKey3",
   102  		Errors: []string{"An error."},
   103  		URLs:   urls[:1],
   104  		Time:   time.Unix(3, 0),
   105  	}
   106  
   107  	for _, event := range []*store.CharmEvent{event1, event2, event3} {
   108  		err := s.store.LogCharmEvent(event)
   109  		c.Assert(err, gc.IsNil)
   110  	}
   111  
   112  	var tests = []struct {
   113  		query        string
   114  		kind, digest string
   115  		err, warn    string
   116  		time         string
   117  		revision     int
   118  	}{
   119  		{
   120  			query:  url1.String(),
   121  			digest: "revKey3",
   122  			kind:   "publish-error",
   123  			err:    "An error.",
   124  			time:   "1970-01-01T00:00:03Z",
   125  		}, {
   126  			query:    url2.String(),
   127  			digest:   "revKey2",
   128  			kind:     "published",
   129  			revision: 43,
   130  			time:     "1970-01-01T00:00:02Z",
   131  		}, {
   132  			query:    url1.String() + "@revKey1",
   133  			digest:   "revKey1",
   134  			kind:     "published",
   135  			revision: 42,
   136  			warn:     "A warning.",
   137  			time:     "1970-01-01T00:00:01Z",
   138  		}, {
   139  			query:    "cs:non/existent",
   140  			revision: 0,
   141  			err:      "entry not found",
   142  		},
   143  	}
   144  
   145  	for _, t := range tests {
   146  		req.Form = url.Values{"charms": []string{t.query}}
   147  		rec := httptest.NewRecorder()
   148  		server.ServeHTTP(rec, req)
   149  
   150  		url := t.query
   151  		if i := strings.Index(url, "@"); i >= 0 {
   152  			url = url[:i]
   153  		}
   154  		info := map[string]interface{}{
   155  			"kind":     "",
   156  			"revision": float64(0),
   157  		}
   158  		if t.kind != "" {
   159  			info["kind"] = t.kind
   160  			info["revision"] = float64(t.revision)
   161  			info["digest"] = t.digest
   162  			info["time"] = t.time
   163  		}
   164  		if t.err != "" {
   165  			info["errors"] = []interface{}{t.err}
   166  		}
   167  		if t.warn != "" {
   168  			info["warnings"] = []interface{}{t.warn}
   169  		}
   170  		expected := map[string]interface{}{url: info}
   171  		obtained := map[string]interface{}{}
   172  		err = json.NewDecoder(rec.Body).Decode(&obtained)
   173  		c.Assert(err, gc.IsNil)
   174  		c.Assert(obtained, gc.DeepEquals, expected)
   175  		c.Assert(rec.Header().Get("Content-Type"), gc.Equals, "application/json")
   176  	}
   177  
   178  	s.checkCounterSum(c, []string{"charm-event", "oneiric", "wordpress"}, false, 2)
   179  	s.checkCounterSum(c, []string{"charm-event", "oneiric", "mysql"}, false, 1)
   180  }
   181  
   182  // checkCounterSum checks that statistics are properly collected.
   183  // It retries a few times as they are generally collected in background.
   184  func (s *StoreSuite) checkCounterSum(c *gc.C, key []string, prefix bool, expected int64) {
   185  	var sum int64
   186  	for retry := 0; retry < 10; retry++ {
   187  		time.Sleep(1e8)
   188  		req := store.CounterRequest{Key: key, Prefix: prefix}
   189  		cs, err := s.store.Counters(&req)
   190  		c.Assert(err, gc.IsNil)
   191  		if sum = cs[0].Count; sum == expected {
   192  			if expected == 0 && retry < 2 {
   193  				continue // Wait a bit to make sure.
   194  			}
   195  			return
   196  		}
   197  	}
   198  	c.Errorf("counter sum for %#v is %d, want %d", key, sum, expected)
   199  }
   200  
   201  func (s *StoreSuite) TestCharmStreaming(c *gc.C) {
   202  	server, curl := s.prepareServer(c)
   203  
   204  	req, err := http.NewRequest("GET", "/charm/"+curl.String()[3:], nil)
   205  	c.Assert(err, gc.IsNil)
   206  	rec := httptest.NewRecorder()
   207  	server.ServeHTTP(rec, req)
   208  
   209  	data, err := ioutil.ReadAll(rec.Body)
   210  	c.Assert(string(data), gc.Equals, "charm-revision-0")
   211  
   212  	c.Assert(rec.Header().Get("Connection"), gc.Equals, "close")
   213  	c.Assert(rec.Header().Get("Content-Type"), gc.Equals, "application/octet-stream")
   214  	c.Assert(rec.Header().Get("Content-Length"), gc.Equals, "16")
   215  
   216  	// Check that it was accounted for in statistics.
   217  	s.checkCounterSum(c, []string{"charm-bundle", curl.Series, curl.Name}, false, 1)
   218  }
   219  
   220  func (s *StoreSuite) TestDisableStats(c *gc.C) {
   221  	server, curl := s.prepareServer(c)
   222  
   223  	req, err := http.NewRequest("GET", "/charm-info", nil)
   224  	c.Assert(err, gc.IsNil)
   225  	req.Form = url.Values{"charms": []string{curl.String()}, "stats": []string{"0"}}
   226  	rec := httptest.NewRecorder()
   227  	server.ServeHTTP(rec, req)
   228  	c.Assert(rec.Code, gc.Equals, 200)
   229  
   230  	req, err = http.NewRequest("GET", "/charm/"+curl.String()[3:], nil)
   231  	c.Assert(err, gc.IsNil)
   232  	req.Form = url.Values{"stats": []string{"0"}}
   233  	rec = httptest.NewRecorder()
   234  	server.ServeHTTP(rec, req)
   235  	c.Assert(rec.Code, gc.Equals, 200)
   236  
   237  	// No statistics should have been collected given the use of stats=0.
   238  	for _, prefix := range []string{"charm-info", "charm-bundle", "charm-missing"} {
   239  		s.checkCounterSum(c, []string{prefix}, true, 0)
   240  	}
   241  }
   242  
   243  func (s *StoreSuite) TestServerStatus(c *gc.C) {
   244  	server, err := store.NewServer(s.store)
   245  	c.Assert(err, gc.IsNil)
   246  	tests := []struct {
   247  		path string
   248  		code int
   249  	}{
   250  		{"/charm-info/any", 404},
   251  		{"/charm/bad-url", 404},
   252  		{"/charm/bad-series/wordpress", 404},
   253  		{"/stats/counter/", 403},
   254  		{"/stats/counter/*", 403},
   255  		{"/stats/counter/any/", 404},
   256  		{"/stats/", 404},
   257  		{"/stats/any", 404},
   258  	}
   259  	for _, test := range tests {
   260  		req, err := http.NewRequest("GET", test.path, nil)
   261  		c.Assert(err, gc.IsNil)
   262  		rec := httptest.NewRecorder()
   263  		server.ServeHTTP(rec, req)
   264  		c.Assert(rec.Code, gc.Equals, test.code, gc.Commentf("Path: %s", test.path))
   265  	}
   266  }
   267  
   268  func (s *StoreSuite) TestRootRedirect(c *gc.C) {
   269  	server, err := store.NewServer(s.store)
   270  	c.Assert(err, gc.IsNil)
   271  	req, err := http.NewRequest("GET", "/", nil)
   272  	c.Assert(err, gc.IsNil)
   273  	rec := httptest.NewRecorder()
   274  	server.ServeHTTP(rec, req)
   275  	c.Assert(rec.Code, gc.Equals, 303)
   276  	c.Assert(rec.Header().Get("Location"), gc.Equals, "https://juju.ubuntu.com")
   277  }
   278  
   279  func (s *StoreSuite) TestStatsCounter(c *gc.C) {
   280  	for _, key := range [][]string{{"a", "b"}, {"a", "b"}, {"a", "c"}, {"a"}} {
   281  		err := s.store.IncCounter(key)
   282  		c.Assert(err, gc.IsNil)
   283  	}
   284  
   285  	server, _ := s.prepareServer(c)
   286  
   287  	expected := map[string]string{
   288  		"a:b":   "2",
   289  		"a:b:*": "0",
   290  		"a:*":   "3",
   291  		"a":     "1",
   292  		"a:b:c": "0",
   293  	}
   294  
   295  	for counter, n := range expected {
   296  		req, err := http.NewRequest("GET", "/stats/counter/"+counter, nil)
   297  		c.Assert(err, gc.IsNil)
   298  		rec := httptest.NewRecorder()
   299  		server.ServeHTTP(rec, req)
   300  
   301  		data, err := ioutil.ReadAll(rec.Body)
   302  		c.Assert(string(data), gc.Equals, n)
   303  
   304  		c.Assert(rec.Header().Get("Content-Type"), gc.Equals, "text/plain")
   305  		c.Assert(rec.Header().Get("Content-Length"), gc.Equals, strconv.Itoa(len(n)))
   306  	}
   307  }
   308  
   309  func (s *StoreSuite) TestStatsCounterList(c *gc.C) {
   310  	incs := [][]string{
   311  		{"a"},
   312  		{"a", "b"},
   313  		{"a", "b", "c"},
   314  		{"a", "b", "c"},
   315  		{"a", "b", "d"},
   316  		{"a", "b", "e"},
   317  		{"a", "f", "g"},
   318  		{"a", "f", "h"},
   319  		{"a", "i"},
   320  		{"j", "k"},
   321  	}
   322  	for _, key := range incs {
   323  		err := s.store.IncCounter(key)
   324  		c.Assert(err, gc.IsNil)
   325  	}
   326  
   327  	server, _ := s.prepareServer(c)
   328  
   329  	tests := []struct {
   330  		key, format, result string
   331  	}{
   332  		{"a", "", "a  1\n"},
   333  		{"a:*", "", "a:b:*  4\na:f:*  2\na:b    1\na:i    1\n"},
   334  		{"a:b:*", "", "a:b:c  2\na:b:d  1\na:b:e  1\n"},
   335  		{"a:*", "csv", "a:b:*,4\na:f:*,2\na:b,1\na:i,1\n"},
   336  		{"a:*", "json", `[["a:b:*",4],["a:f:*",2],["a:b",1],["a:i",1]]`},
   337  	}
   338  
   339  	for _, test := range tests {
   340  		req, err := http.NewRequest("GET", "/stats/counter/"+test.key, nil)
   341  		c.Assert(err, gc.IsNil)
   342  		req.Form = url.Values{"list": []string{"1"}}
   343  		if test.format != "" {
   344  			req.Form.Set("format", test.format)
   345  		}
   346  		rec := httptest.NewRecorder()
   347  		server.ServeHTTP(rec, req)
   348  
   349  		data, err := ioutil.ReadAll(rec.Body)
   350  		c.Assert(string(data), gc.Equals, test.result)
   351  
   352  		c.Assert(rec.Header().Get("Content-Type"), gc.Equals, "text/plain")
   353  		c.Assert(rec.Header().Get("Content-Length"), gc.Equals, strconv.Itoa(len(test.result)))
   354  	}
   355  }
   356  
   357  func (s *StoreSuite) TestStatsCounterBy(c *gc.C) {
   358  	incs := []struct {
   359  		key []string
   360  		day int
   361  	}{
   362  		{[]string{"a"}, 1},
   363  		{[]string{"a"}, 1},
   364  		{[]string{"b"}, 1},
   365  		{[]string{"a", "b"}, 1},
   366  		{[]string{"a", "c"}, 1},
   367  		{[]string{"a"}, 3},
   368  		{[]string{"a", "b"}, 3},
   369  		{[]string{"b"}, 9},
   370  		{[]string{"b"}, 9},
   371  		{[]string{"a", "c", "d"}, 9},
   372  		{[]string{"a", "c", "e"}, 9},
   373  		{[]string{"a", "c", "f"}, 9},
   374  	}
   375  
   376  	day := func(i int) time.Time {
   377  		return time.Date(2012, time.May, i, 0, 0, 0, 0, time.UTC)
   378  	}
   379  
   380  	server, _ := s.prepareServer(c)
   381  
   382  	counters := s.Session.DB("juju").C("stat.counters")
   383  	for i, inc := range incs {
   384  		err := s.store.IncCounter(inc.key)
   385  		c.Assert(err, gc.IsNil)
   386  
   387  		// Hack time so counters are assigned to 2012-05-<day>
   388  		filter := bson.M{"t": bson.M{"$gt": store.TimeToStamp(time.Date(2013, time.January, 1, 0, 0, 0, 0, time.UTC))}}
   389  		stamp := store.TimeToStamp(day(inc.day))
   390  		stamp += int32(i) * 60 // Make every entry unique.
   391  		err = counters.Update(filter, bson.D{{"$set", bson.D{{"t", stamp}}}})
   392  		c.Check(err, gc.IsNil)
   393  	}
   394  
   395  	tests := []struct {
   396  		request store.CounterRequest
   397  		format  string
   398  		result  string
   399  	}{
   400  		{
   401  			store.CounterRequest{
   402  				Key:    []string{"a"},
   403  				Prefix: false,
   404  				List:   false,
   405  				By:     store.ByDay,
   406  			},
   407  			"",
   408  			"2012-05-01  2\n2012-05-03  1\n",
   409  		}, {
   410  			store.CounterRequest{
   411  				Key:    []string{"a"},
   412  				Prefix: false,
   413  				List:   false,
   414  				By:     store.ByDay,
   415  			},
   416  			"csv",
   417  			"2012-05-01,2\n2012-05-03,1\n",
   418  		}, {
   419  			store.CounterRequest{
   420  				Key:    []string{"a"},
   421  				Prefix: false,
   422  				List:   false,
   423  				By:     store.ByDay,
   424  			},
   425  			"json",
   426  			`[["2012-05-01",2],["2012-05-03",1]]`,
   427  		}, {
   428  			store.CounterRequest{
   429  				Key:    []string{"a"},
   430  				Prefix: true,
   431  				List:   false,
   432  				By:     store.ByDay,
   433  			},
   434  			"",
   435  			"2012-05-01  2\n2012-05-03  1\n2012-05-09  3\n",
   436  		}, {
   437  			store.CounterRequest{
   438  				Key:    []string{"a"},
   439  				Prefix: true,
   440  				List:   false,
   441  				By:     store.ByDay,
   442  				Start:  time.Date(2012, 5, 2, 0, 0, 0, 0, time.UTC),
   443  			},
   444  			"",
   445  			"2012-05-03  1\n2012-05-09  3\n",
   446  		}, {
   447  			store.CounterRequest{
   448  				Key:    []string{"a"},
   449  				Prefix: true,
   450  				List:   false,
   451  				By:     store.ByDay,
   452  				Stop:   time.Date(2012, 5, 4, 0, 0, 0, 0, time.UTC),
   453  			},
   454  			"",
   455  			"2012-05-01  2\n2012-05-03  1\n",
   456  		}, {
   457  			store.CounterRequest{
   458  				Key:    []string{"a"},
   459  				Prefix: true,
   460  				List:   false,
   461  				By:     store.ByDay,
   462  				Start:  time.Date(2012, 5, 3, 0, 0, 0, 0, time.UTC),
   463  				Stop:   time.Date(2012, 5, 3, 0, 0, 0, 0, time.UTC),
   464  			},
   465  			"",
   466  			"2012-05-03  1\n",
   467  		}, {
   468  			store.CounterRequest{
   469  				Key:    []string{"a"},
   470  				Prefix: true,
   471  				List:   true,
   472  				By:     store.ByDay,
   473  			},
   474  			"",
   475  			"a:b    2012-05-01  1\na:c    2012-05-01  1\na:b    2012-05-03  1\na:c:*  2012-05-09  3\n",
   476  		}, {
   477  			store.CounterRequest{
   478  				Key:    []string{"a"},
   479  				Prefix: true,
   480  				List:   false,
   481  				By:     store.ByWeek,
   482  			},
   483  			"",
   484  			"2012-05-06  3\n2012-05-13  3\n",
   485  		}, {
   486  			store.CounterRequest{
   487  				Key:    []string{"a"},
   488  				Prefix: true,
   489  				List:   true,
   490  				By:     store.ByWeek,
   491  			},
   492  			"",
   493  			"a:b    2012-05-06  2\na:c    2012-05-06  1\na:c:*  2012-05-13  3\n",
   494  		}, {
   495  			store.CounterRequest{
   496  				Key:    []string{"a"},
   497  				Prefix: true,
   498  				List:   true,
   499  				By:     store.ByWeek,
   500  			},
   501  			"csv",
   502  			"a:b,2012-05-06,2\na:c,2012-05-06,1\na:c:*,2012-05-13,3\n",
   503  		}, {
   504  			store.CounterRequest{
   505  				Key:    []string{"a"},
   506  				Prefix: true,
   507  				List:   true,
   508  				By:     store.ByWeek,
   509  			},
   510  			"json",
   511  			`[["a:b","2012-05-06",2],["a:c","2012-05-06",1],["a:c:*","2012-05-13",3]]`,
   512  		},
   513  	}
   514  
   515  	for _, test := range tests {
   516  		path := "/stats/counter/" + strings.Join(test.request.Key, ":")
   517  		if test.request.Prefix {
   518  			path += ":*"
   519  		}
   520  		req, err := http.NewRequest("GET", path, nil)
   521  		req.Form = url.Values{}
   522  		c.Assert(err, gc.IsNil)
   523  		if test.request.List {
   524  			req.Form.Set("list", "1")
   525  		}
   526  		if test.format != "" {
   527  			req.Form.Set("format", test.format)
   528  		}
   529  		if !test.request.Start.IsZero() {
   530  			req.Form.Set("start", test.request.Start.Format("2006-01-02"))
   531  		}
   532  		if !test.request.Stop.IsZero() {
   533  			req.Form.Set("stop", test.request.Stop.Format("2006-01-02"))
   534  		}
   535  		switch test.request.By {
   536  		case store.ByDay:
   537  			req.Form.Set("by", "day")
   538  		case store.ByWeek:
   539  			req.Form.Set("by", "week")
   540  		}
   541  		rec := httptest.NewRecorder()
   542  		server.ServeHTTP(rec, req)
   543  
   544  		data, err := ioutil.ReadAll(rec.Body)
   545  		c.Assert(string(data), gc.Equals, test.result)
   546  
   547  		c.Assert(rec.Header().Get("Content-Type"), gc.Equals, "text/plain")
   548  		c.Assert(rec.Header().Get("Content-Length"), gc.Equals, strconv.Itoa(len(test.result)))
   549  	}
   550  }
   551  
   552  func (s *StoreSuite) TestBlitzKey(c *gc.C) {
   553  	server, _ := s.prepareServer(c)
   554  
   555  	// This is just a validation key to allow blitz.io to run
   556  	// performance tests against the site.
   557  	req, err := http.NewRequest("GET", "/mu-35700a31-6bf320ca-a800b670-05f845ee", nil)
   558  	c.Assert(err, gc.IsNil)
   559  	rec := httptest.NewRecorder()
   560  	server.ServeHTTP(rec, req)
   561  
   562  	data, err := ioutil.ReadAll(rec.Body)
   563  	c.Assert(string(data), gc.Equals, "42")
   564  
   565  	c.Assert(rec.Header().Get("Connection"), gc.Equals, "close")
   566  	c.Assert(rec.Header().Get("Content-Type"), gc.Equals, "text/plain")
   567  	c.Assert(rec.Header().Get("Content-Length"), gc.Equals, "2")
   568  }