github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/cmd/juju/service/register_test.go (about) 1 // Copyright 2015 Canonical Ltd. All rights reserved. 2 3 package service 4 5 import ( 6 "encoding/json" 7 "fmt" 8 "net/http" 9 "net/http/httptest" 10 11 "github.com/juju/cmd" 12 "github.com/juju/errors" 13 "github.com/juju/testing" 14 jc "github.com/juju/testing/checkers" 15 gc "gopkg.in/check.v1" 16 "gopkg.in/juju/charm.v6-unstable" 17 "gopkg.in/macaroon-bakery.v1/httpbakery" 18 19 "github.com/juju/juju/api" 20 "github.com/juju/juju/apiserver/params" 21 "github.com/juju/juju/charmstore" 22 coretesting "github.com/juju/juju/testing" 23 ) 24 25 var _ = gc.Suite(®istrationSuite{}) 26 27 type registrationSuite struct { 28 testing.CleanupSuite 29 stub *testing.Stub 30 handler *testMetricsRegistrationHandler 31 server *httptest.Server 32 register DeployStep 33 ctx *cmd.Context 34 } 35 36 func (s *registrationSuite) SetUpTest(c *gc.C) { 37 s.CleanupSuite.SetUpTest(c) 38 s.stub = &testing.Stub{} 39 s.handler = &testMetricsRegistrationHandler{Stub: s.stub} 40 s.server = httptest.NewServer(s.handler) 41 s.register = &RegisterMeteredCharm{ 42 Plan: "someplan", 43 RegisterURL: s.server.URL, 44 AllocationSpec: "personal:100", 45 } 46 s.ctx = coretesting.Context(c) 47 } 48 49 func (s *registrationSuite) TearDownTest(c *gc.C) { 50 s.CleanupSuite.TearDownTest(c) 51 s.server.Close() 52 } 53 54 func (s *registrationSuite) TestMeteredCharm(c *gc.C) { 55 client := httpbakery.NewClient() 56 d := DeploymentInfo{ 57 CharmID: charmstore.CharmID{ 58 URL: charm.MustParseURL("cs:quantal/metered-1"), 59 }, 60 ServiceName: "service name", 61 ModelUUID: "model uuid", 62 } 63 err := s.register.RunPre(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d) 64 c.Assert(err, jc.ErrorIsNil) 65 err = s.register.RunPost(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d, nil) 66 c.Assert(err, jc.ErrorIsNil) 67 authorization, err := json.Marshal([]byte("hello registration")) 68 authorization = append(authorization, byte(0xa)) 69 c.Assert(err, jc.ErrorIsNil) 70 s.stub.CheckCalls(c, []testing.StubCall{{ 71 "APICall", []interface{}{"Charms", "IsMetered", params.CharmInfo{CharmURL: "cs:quantal/metered-1"}}, 72 }, { 73 "Authorize", []interface{}{metricRegistrationPost{ 74 ModelUUID: "model uuid", 75 CharmURL: "cs:quantal/metered-1", 76 ServiceName: "service name", 77 PlanURL: "someplan", 78 Budget: "personal", 79 Limit: "100", 80 }}, 81 }, { 82 "APICall", []interface{}{"Service", "SetMetricCredentials", params.ServiceMetricCredentials{ 83 Creds: []params.ServiceMetricCredential{params.ServiceMetricCredential{ 84 ServiceName: "service name", 85 MetricCredentials: authorization, 86 }}, 87 }}, 88 }}) 89 } 90 91 func (s *registrationSuite) TestMeteredCharmAPIError(c *gc.C) { 92 s.stub.SetErrors(nil, errors.New("something failed")) 93 client := httpbakery.NewClient() 94 d := DeploymentInfo{ 95 CharmID: charmstore.CharmID{ 96 URL: charm.MustParseURL("cs:quantal/metered-1"), 97 }, 98 ServiceName: "service name", 99 ModelUUID: "model uuid", 100 } 101 err := s.register.RunPre(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d) 102 c.Assert(err, gc.ErrorMatches, `authorization failed: something failed`) 103 s.stub.CheckCalls(c, []testing.StubCall{{ 104 "APICall", []interface{}{"Charms", "IsMetered", params.CharmInfo{CharmURL: "cs:quantal/metered-1"}}, 105 }, { 106 "Authorize", []interface{}{metricRegistrationPost{ 107 ModelUUID: "model uuid", 108 CharmURL: "cs:quantal/metered-1", 109 ServiceName: "service name", 110 PlanURL: "someplan", 111 Budget: "personal", 112 Limit: "100", 113 }}, 114 }}) 115 } 116 117 func (s *registrationSuite) TestMeteredCharmInvalidAllocation(c *gc.C) { 118 client := httpbakery.NewClient() 119 d := DeploymentInfo{ 120 CharmID: charmstore.CharmID{ 121 URL: charm.MustParseURL("cs:quantal/metered-1"), 122 }, 123 ServiceName: "service name", 124 ModelUUID: "model uuid", 125 } 126 s.register = &RegisterMeteredCharm{ 127 Plan: "someplan", 128 RegisterURL: s.server.URL, 129 AllocationSpec: "invalid allocation", 130 } 131 132 err := s.register.RunPre(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d) 133 c.Assert(err, gc.ErrorMatches, `invalid allocation, expecting <budget>:<limit>`) 134 s.stub.CheckNoCalls(c) 135 } 136 137 func (s *registrationSuite) TestMeteredCharmDeployError(c *gc.C) { 138 client := httpbakery.NewClient() 139 d := DeploymentInfo{ 140 CharmID: charmstore.CharmID{ 141 URL: charm.MustParseURL("cs:quantal/metered-1"), 142 }, 143 ServiceName: "service name", 144 ModelUUID: "model uuid", 145 } 146 err := s.register.RunPre(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d) 147 c.Assert(err, jc.ErrorIsNil) 148 deployError := errors.New("deployment failed") 149 err = s.register.RunPost(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d, deployError) 150 c.Assert(err, jc.ErrorIsNil) 151 authorization, err := json.Marshal([]byte("hello registration")) 152 authorization = append(authorization, byte(0xa)) 153 c.Assert(err, jc.ErrorIsNil) 154 s.stub.CheckCalls(c, []testing.StubCall{{ 155 "APICall", []interface{}{"Charms", "IsMetered", params.CharmInfo{CharmURL: "cs:quantal/metered-1"}}, 156 }, { 157 "Authorize", []interface{}{metricRegistrationPost{ 158 ModelUUID: "model uuid", 159 CharmURL: "cs:quantal/metered-1", 160 ServiceName: "service name", 161 PlanURL: "someplan", 162 Budget: "personal", 163 Limit: "100", 164 }}, 165 }}) 166 } 167 168 func (s *registrationSuite) TestMeteredLocalCharmWithPlan(c *gc.C) { 169 client := httpbakery.NewClient() 170 d := DeploymentInfo{ 171 CharmID: charmstore.CharmID{ 172 URL: charm.MustParseURL("local:quantal/metered-1"), 173 }, 174 ServiceName: "service name", 175 ModelUUID: "model uuid", 176 } 177 err := s.register.RunPre(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d) 178 c.Assert(err, jc.ErrorIsNil) 179 err = s.register.RunPost(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d, nil) 180 c.Assert(err, jc.ErrorIsNil) 181 authorization, err := json.Marshal([]byte("hello registration")) 182 authorization = append(authorization, byte(0xa)) 183 s.stub.CheckCalls(c, []testing.StubCall{{ 184 "APICall", []interface{}{"Charms", "IsMetered", params.CharmInfo{CharmURL: "local:quantal/metered-1"}}, 185 }, { 186 "Authorize", []interface{}{metricRegistrationPost{ 187 ModelUUID: "model uuid", 188 CharmURL: "local:quantal/metered-1", 189 ServiceName: "service name", 190 PlanURL: "someplan", 191 Budget: "personal", 192 Limit: "100", 193 }}, 194 }, { 195 "APICall", []interface{}{"Service", "SetMetricCredentials", params.ServiceMetricCredentials{ 196 Creds: []params.ServiceMetricCredential{params.ServiceMetricCredential{ 197 ServiceName: "service name", 198 MetricCredentials: authorization, 199 }}, 200 }}, 201 }}) 202 } 203 204 func (s *registrationSuite) TestMeteredLocalCharmNoPlan(c *gc.C) { 205 s.register = &RegisterMeteredCharm{ 206 RegisterURL: s.server.URL, 207 QueryURL: s.server.URL, 208 AllocationSpec: "personal:100", 209 } 210 client := httpbakery.NewClient() 211 d := DeploymentInfo{ 212 CharmID: charmstore.CharmID{ 213 URL: charm.MustParseURL("local:quantal/metered-1"), 214 }, 215 ServiceName: "service name", 216 ModelUUID: "model uuid", 217 } 218 err := s.register.RunPre(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d) 219 c.Assert(err, jc.ErrorIsNil) 220 err = s.register.RunPost(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d, nil) 221 c.Assert(err, jc.ErrorIsNil) 222 authorization, err := json.Marshal([]byte("hello registration")) 223 authorization = append(authorization, byte(0xa)) 224 s.stub.CheckCalls(c, []testing.StubCall{{ 225 "APICall", []interface{}{"Charms", "IsMetered", params.CharmInfo{CharmURL: "local:quantal/metered-1"}}, 226 }, { 227 "Authorize", []interface{}{metricRegistrationPost{ 228 ModelUUID: "model uuid", 229 CharmURL: "local:quantal/metered-1", 230 ServiceName: "service name", 231 PlanURL: "", 232 Budget: "personal", 233 Limit: "100", 234 }}, 235 }, { 236 "APICall", []interface{}{"Service", "SetMetricCredentials", params.ServiceMetricCredentials{ 237 Creds: []params.ServiceMetricCredential{params.ServiceMetricCredential{ 238 ServiceName: "service name", 239 MetricCredentials: authorization, 240 }}, 241 }}, 242 }}) 243 } 244 245 func (s *registrationSuite) TestMeteredCharmNoPlanSet(c *gc.C) { 246 s.register = &RegisterMeteredCharm{ 247 AllocationSpec: "personal:100", 248 RegisterURL: s.server.URL, 249 QueryURL: s.server.URL} 250 client := httpbakery.NewClient() 251 d := DeploymentInfo{ 252 CharmID: charmstore.CharmID{ 253 URL: charm.MustParseURL("cs:quantal/metered-1"), 254 }, 255 ServiceName: "service name", 256 ModelUUID: "model uuid", 257 } 258 err := s.register.RunPre(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d) 259 c.Assert(err, jc.ErrorIsNil) 260 err = s.register.RunPost(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d, nil) 261 c.Assert(err, jc.ErrorIsNil) 262 authorization, err := json.Marshal([]byte("hello registration")) 263 authorization = append(authorization, byte(0xa)) 264 c.Assert(err, jc.ErrorIsNil) 265 s.stub.CheckCalls(c, []testing.StubCall{{ 266 "APICall", []interface{}{"Charms", "IsMetered", params.CharmInfo{CharmURL: "cs:quantal/metered-1"}}, 267 }, { 268 "DefaultPlan", []interface{}{"cs:quantal/metered-1"}, 269 }, { 270 "Authorize", []interface{}{metricRegistrationPost{ 271 ModelUUID: "model uuid", 272 CharmURL: "cs:quantal/metered-1", 273 ServiceName: "service name", 274 PlanURL: "thisplan", 275 Budget: "personal", 276 Limit: "100", 277 }}, 278 }, { 279 "APICall", []interface{}{"Service", "SetMetricCredentials", params.ServiceMetricCredentials{ 280 Creds: []params.ServiceMetricCredential{params.ServiceMetricCredential{ 281 ServiceName: "service name", 282 MetricCredentials: authorization, 283 }}, 284 }}, 285 }}) 286 } 287 288 func (s *registrationSuite) TestMeteredCharmNoDefaultPlan(c *gc.C) { 289 s.stub.SetErrors(nil, errors.NotFoundf("default plan")) 290 s.register = &RegisterMeteredCharm{ 291 AllocationSpec: "personal:100", 292 RegisterURL: s.server.URL, 293 QueryURL: s.server.URL} 294 client := httpbakery.NewClient() 295 d := DeploymentInfo{ 296 CharmID: charmstore.CharmID{ 297 URL: charm.MustParseURL("cs:quantal/metered-1"), 298 }, 299 ServiceName: "service name", 300 ModelUUID: "model uuid", 301 } 302 err := s.register.RunPre(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d) 303 c.Assert(err, gc.ErrorMatches, `cs:quantal/metered-1 has no default plan. Try "juju deploy --plan <plan-name> with one of thisplan, thisotherplan"`) 304 s.stub.CheckCalls(c, []testing.StubCall{{ 305 "APICall", []interface{}{"Charms", "IsMetered", params.CharmInfo{CharmURL: "cs:quantal/metered-1"}}, 306 }, { 307 "DefaultPlan", []interface{}{"cs:quantal/metered-1"}, 308 }, { 309 "ListPlans", []interface{}{"cs:quantal/metered-1"}, 310 }}) 311 } 312 313 func (s *registrationSuite) TestMeteredCharmFailToQueryDefaultCharm(c *gc.C) { 314 s.stub.SetErrors(nil, errors.New("something failed")) 315 s.register = &RegisterMeteredCharm{ 316 AllocationSpec: "personal:100", 317 RegisterURL: s.server.URL, 318 QueryURL: s.server.URL} 319 client := httpbakery.NewClient() 320 d := DeploymentInfo{ 321 CharmID: charmstore.CharmID{ 322 URL: charm.MustParseURL("cs:quantal/metered-1"), 323 }, 324 ServiceName: "service name", 325 ModelUUID: "model uuid", 326 } 327 err := s.register.RunPre(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d) 328 c.Assert(err, gc.ErrorMatches, `failed to query default plan:.*`) 329 s.stub.CheckCalls(c, []testing.StubCall{{ 330 "APICall", []interface{}{"Charms", "IsMetered", params.CharmInfo{CharmURL: "cs:quantal/metered-1"}}, 331 }, { 332 "DefaultPlan", []interface{}{"cs:quantal/metered-1"}, 333 }}) 334 } 335 336 func (s *registrationSuite) TestUnmeteredCharm(c *gc.C) { 337 client := httpbakery.NewClient() 338 d := DeploymentInfo{ 339 CharmID: charmstore.CharmID{ 340 URL: charm.MustParseURL("cs:quantal/unmetered-1"), 341 }, 342 ServiceName: "service name", 343 ModelUUID: "model uuid", 344 } 345 err := s.register.RunPre(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d) 346 c.Assert(err, jc.ErrorIsNil) 347 s.stub.CheckCalls(c, []testing.StubCall{{ 348 "APICall", []interface{}{"Charms", "IsMetered", params.CharmInfo{CharmURL: "cs:quantal/unmetered-1"}}, 349 }}) 350 s.stub.ResetCalls() 351 err = s.register.RunPost(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d, nil) 352 c.Assert(err, jc.ErrorIsNil) 353 s.stub.CheckCalls(c, []testing.StubCall{}) 354 } 355 356 func (s *registrationSuite) TestFailedAuth(c *gc.C) { 357 s.stub.SetErrors(nil, fmt.Errorf("could not authorize")) 358 client := httpbakery.NewClient() 359 d := DeploymentInfo{ 360 CharmID: charmstore.CharmID{ 361 URL: charm.MustParseURL("cs:quantal/metered-1"), 362 }, 363 ServiceName: "service name", 364 ModelUUID: "model uuid", 365 } 366 err := s.register.RunPre(&mockAPIConnection{Stub: s.stub}, client, s.ctx, d) 367 c.Assert(err, gc.ErrorMatches, `authorization failed:.*`) 368 authorization, err := json.Marshal([]byte("hello registration")) 369 authorization = append(authorization, byte(0xa)) 370 c.Assert(err, jc.ErrorIsNil) 371 s.stub.CheckCalls(c, []testing.StubCall{{ 372 "APICall", []interface{}{"Charms", "IsMetered", params.CharmInfo{CharmURL: "cs:quantal/metered-1"}}, 373 }, { 374 "Authorize", []interface{}{metricRegistrationPost{ 375 ModelUUID: "model uuid", 376 CharmURL: "cs:quantal/metered-1", 377 ServiceName: "service name", 378 PlanURL: "someplan", 379 Budget: "personal", 380 Limit: "100", 381 }}, 382 }}) 383 } 384 385 type testMetricsRegistrationHandler struct { 386 *testing.Stub 387 } 388 389 type respErr struct { 390 Error string `json:"error"` 391 } 392 393 // ServeHTTP implements http.Handler. 394 func (c *testMetricsRegistrationHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 395 if req.Method == "POST" { 396 var registrationPost metricRegistrationPost 397 decoder := json.NewDecoder(req.Body) 398 err := decoder.Decode(®istrationPost) 399 if err != nil { 400 http.Error(w, "bad request", http.StatusBadRequest) 401 return 402 } 403 c.AddCall("Authorize", registrationPost) 404 rErr := c.NextErr() 405 if rErr != nil { 406 w.WriteHeader(http.StatusInternalServerError) 407 err = json.NewEncoder(w).Encode(respErr{Error: rErr.Error()}) 408 if err != nil { 409 panic(err) 410 } 411 return 412 } 413 err = json.NewEncoder(w).Encode([]byte("hello registration")) 414 if err != nil { 415 panic(err) 416 } 417 } else if req.Method == "GET" { 418 if req.URL.Path == "/default" { 419 cURL := req.URL.Query().Get("charm-url") 420 c.AddCall("DefaultPlan", cURL) 421 rErr := c.NextErr() 422 if rErr != nil { 423 if errors.IsNotFound(rErr) { 424 http.Error(w, rErr.Error(), http.StatusNotFound) 425 return 426 } 427 http.Error(w, rErr.Error(), http.StatusInternalServerError) 428 return 429 } 430 result := struct { 431 URL string `json:"url"` 432 }{"thisplan"} 433 err := json.NewEncoder(w).Encode(result) 434 if err != nil { 435 panic(err) 436 } 437 return 438 } 439 cURL := req.URL.Query().Get("charm-url") 440 c.AddCall("ListPlans", cURL) 441 rErr := c.NextErr() 442 if rErr != nil { 443 http.Error(w, rErr.Error(), http.StatusInternalServerError) 444 return 445 } 446 result := []struct { 447 URL string `json:"url"` 448 }{ 449 {"thisplan"}, 450 {"thisotherplan"}, 451 } 452 err := json.NewEncoder(w).Encode(result) 453 if err != nil { 454 panic(err) 455 } 456 } else { 457 http.Error(w, "method not allowed", http.StatusMethodNotAllowed) 458 return 459 } 460 } 461 462 type mockAPIConnection struct { 463 api.Connection 464 *testing.Stub 465 } 466 467 func (*mockAPIConnection) BestFacadeVersion(facade string) int { 468 return 42 469 } 470 471 func (*mockAPIConnection) Close() error { 472 return nil 473 } 474 475 func (m *mockAPIConnection) APICall(objType string, version int, id, request string, parameters, response interface{}) error { 476 m.MethodCall(m, "APICall", objType, request, parameters) 477 478 switch request { 479 case "IsMetered": 480 parameters := parameters.(params.CharmInfo) 481 response := response.(*params.IsMeteredResult) 482 if parameters.CharmURL == "cs:quantal/metered-1" || parameters.CharmURL == "local:quantal/metered-1" { 483 response.Metered = true 484 } 485 case "SetMetricCredentials": 486 response := response.(*params.ErrorResults) 487 response.Results = append(response.Results, params.ErrorResult{Error: nil}) 488 } 489 return m.NextErr() 490 }