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(&registrationSuite{})
    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(&registrationPost)
   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  }