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 }