github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/api/http_test.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package api_test 5 6 import ( 7 "net/http" 8 "net/http/httptest" 9 "reflect" 10 11 "github.com/juju/errors" 12 "github.com/juju/httprequest" 13 jc "github.com/juju/testing/checkers" 14 gc "gopkg.in/check.v1" 15 16 "github.com/juju/juju/api" 17 "github.com/juju/juju/apiserver/params" 18 jujutesting "github.com/juju/juju/juju/testing" 19 "github.com/juju/juju/state" 20 "github.com/juju/juju/testing/factory" 21 ) 22 23 type httpSuite struct { 24 jujutesting.JujuConnSuite 25 26 client *httprequest.Client 27 } 28 29 var _ = gc.Suite(&httpSuite{}) 30 31 func (s *httpSuite) SetUpTest(c *gc.C) { 32 s.JujuConnSuite.SetUpTest(c) 33 34 client, err := s.APIState.HTTPClient() 35 c.Assert(err, gc.IsNil) 36 s.client = client 37 } 38 39 var httpClientTests = []struct { 40 about string 41 handler http.HandlerFunc 42 expectResponse interface{} 43 expectError string 44 expectErrorCode string 45 expectErrorInfo *params.ErrorInfo 46 }{{ 47 about: "success", 48 handler: func(w http.ResponseWriter, req *http.Request) { 49 httprequest.WriteJSON(w, http.StatusOK, "hello, world") 50 }, 51 expectResponse: newString("hello, world"), 52 }, { 53 about: "unauthorized status without discharge-required error", 54 handler: func(w http.ResponseWriter, req *http.Request) { 55 httprequest.WriteJSON(w, http.StatusUnauthorized, params.Error{ 56 Message: "something", 57 }) 58 }, 59 expectError: `GET http://.*/: something`, 60 }, { 61 about: "non-JSON error response", 62 handler: http.NotFound, 63 expectError: `GET http://.*/: unexpected content type text/plain; want application/json; content: 404 page not found`, 64 }, { 65 about: "bad error response", 66 handler: func(w http.ResponseWriter, req *http.Request) { 67 type badResponse struct { 68 Message map[string]int 69 } 70 httprequest.WriteJSON(w, http.StatusUnauthorized, badResponse{ 71 Message: make(map[string]int), 72 }) 73 }, 74 expectError: `GET http://.*/: incompatible error response: json: cannot unmarshal object into Go value of type string`, 75 }, { 76 about: "bad charms error response", 77 handler: func(w http.ResponseWriter, req *http.Request) { 78 type badResponse struct { 79 Error string `json:"error"` 80 CharmURL map[string]int `json:"charm-url"` 81 } 82 httprequest.WriteJSON(w, http.StatusUnauthorized, badResponse{ 83 Error: "something", 84 CharmURL: make(map[string]int), 85 }) 86 }, 87 expectError: `GET http://.*/: incompatible error response: json: cannot unmarshal object into Go value of type string`, 88 }, { 89 about: "no message in ErrorResponse", 90 handler: func(w http.ResponseWriter, req *http.Request) { 91 httprequest.WriteJSON(w, http.StatusUnauthorized, params.ErrorResult{ 92 Error: ¶ms.Error{}, 93 }) 94 }, 95 expectError: `GET http://.*/: error response with no message`, 96 }, { 97 about: "no message in Error", 98 handler: func(w http.ResponseWriter, req *http.Request) { 99 httprequest.WriteJSON(w, http.StatusUnauthorized, params.Error{}) 100 }, 101 expectError: `GET http://.*/: error response with no message`, 102 }, { 103 about: "charms error response", 104 handler: func(w http.ResponseWriter, req *http.Request) { 105 httprequest.WriteJSON(w, http.StatusBadRequest, params.CharmsResponse{ 106 Error: "some error", 107 ErrorCode: params.CodeBadRequest, 108 ErrorInfo: ¶ms.ErrorInfo{ 109 MacaroonPath: "foo", 110 }, 111 }) 112 }, 113 expectError: `.*some error$`, 114 expectErrorCode: params.CodeBadRequest, 115 expectErrorInfo: ¶ms.ErrorInfo{ 116 MacaroonPath: "foo", 117 }, 118 }, { 119 about: "discharge-required response with no error info", 120 handler: func(w http.ResponseWriter, req *http.Request) { 121 httprequest.WriteJSON(w, http.StatusUnauthorized, params.Error{ 122 Message: "some error", 123 Code: params.CodeDischargeRequired, 124 }) 125 }, 126 expectError: `GET http://.*/: no error info found in discharge-required response error: some error`, 127 expectErrorCode: params.CodeDischargeRequired, 128 }, { 129 about: "discharge-required response with no macaroon", 130 handler: func(w http.ResponseWriter, req *http.Request) { 131 httprequest.WriteJSON(w, http.StatusUnauthorized, params.Error{ 132 Message: "some error", 133 Code: params.CodeDischargeRequired, 134 Info: ¶ms.ErrorInfo{}, 135 }) 136 }, 137 expectError: `GET http://.*/: no macaroon found in discharge-required response`, 138 }} 139 140 func (s *httpSuite) TestHTTPClient(c *gc.C) { 141 var handler http.HandlerFunc 142 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 143 handler(w, req) 144 })) 145 defer srv.Close() 146 s.client.BaseURL = srv.URL 147 for i, test := range httpClientTests { 148 c.Logf("test %d: %s", i, test.about) 149 handler = test.handler 150 var resp interface{} 151 if test.expectResponse != nil { 152 resp = reflect.New(reflect.TypeOf(test.expectResponse).Elem()).Interface() 153 } 154 err := s.client.Get("/", resp) 155 if test.expectError != "" { 156 c.Check(err, gc.ErrorMatches, test.expectError) 157 c.Check(params.ErrCode(err), gc.Equals, test.expectErrorCode) 158 if err, ok := errors.Cause(err).(*params.Error); ok { 159 c.Check(err.Info, jc.DeepEquals, test.expectErrorInfo) 160 } else if test.expectErrorInfo != nil { 161 c.Fatalf("no error info found in error") 162 } 163 continue 164 } 165 c.Check(err, gc.IsNil) 166 c.Check(resp, jc.DeepEquals, test.expectResponse) 167 } 168 } 169 170 func (s *httpSuite) TestControllerMachineAuthForHostedModel(c *gc.C) { 171 // Create a controller machine & hosted model. 172 const nonce = "gary" 173 m, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ 174 Jobs: []state.MachineJob{state.JobManageModel}, 175 Nonce: nonce, 176 }) 177 hostedState := s.Factory.MakeModel(c, nil) 178 defer hostedState.Close() 179 180 // Connect to the hosted model using the credentials of the 181 // controller machine. 182 apiInfo := s.APIInfo(c) 183 apiInfo.Tag = m.Tag() 184 apiInfo.Password = password 185 apiInfo.ModelTag = hostedState.ModelTag() 186 apiInfo.Nonce = nonce 187 conn, err := api.Open(apiInfo, api.DialOpts{}) 188 c.Assert(err, jc.ErrorIsNil) 189 httpClient, err := conn.HTTPClient() 190 c.Assert(err, jc.ErrorIsNil) 191 192 // Test with a dummy HTTP server returns the auth related headers used. 193 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 194 username, password, ok := req.BasicAuth() 195 if ok { 196 httprequest.WriteJSON(w, http.StatusOK, map[string]string{ 197 "username": username, 198 "password": password, 199 "nonce": req.Header.Get(params.MachineNonceHeader), 200 }) 201 } else { 202 httprequest.WriteJSON(w, http.StatusUnauthorized, params.Error{ 203 Message: "no auth header", 204 }) 205 } 206 })) 207 defer srv.Close() 208 httpClient.BaseURL = srv.URL 209 var out map[string]string 210 c.Assert(httpClient.Get("/", &out), jc.ErrorIsNil) 211 c.Assert(out, gc.DeepEquals, map[string]string{ 212 "username": m.Tag().String(), 213 "password": password, 214 "nonce": nonce, 215 }) 216 } 217 218 // Note: the fact that the code works against the actual API server is 219 // well tested by some of the other API tests. 220 // This suite focuses on less reachable paths by changing 221 // the BaseURL of the httprequest.Client so that 222 // we can use our own custom servers. 223 224 func newString(s string) *string { 225 return &s 226 }