github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/apiserver/backup_test.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver_test 5 6 import ( 7 "bytes" 8 "encoding/base64" 9 "encoding/json" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "mime/multipart" 14 "net/http" 15 "net/textproto" 16 17 "github.com/juju/errors" 18 jc "github.com/juju/testing/checkers" 19 "github.com/juju/utils" 20 gc "gopkg.in/check.v1" 21 "gopkg.in/macaroon-bakery.v1/httpbakery" 22 23 "github.com/juju/juju/apiserver" 24 apiserverbackups "github.com/juju/juju/apiserver/backups" 25 "github.com/juju/juju/apiserver/params" 26 "github.com/juju/juju/state" 27 "github.com/juju/juju/state/backups" 28 backupstesting "github.com/juju/juju/state/backups/testing" 29 ) 30 31 type backupsCommonSuite struct { 32 authHTTPSuite 33 fake *backupstesting.FakeBackups 34 } 35 36 func (s *backupsCommonSuite) SetUpTest(c *gc.C) { 37 s.authHTTPSuite.SetUpTest(c) 38 39 s.fake = &backupstesting.FakeBackups{} 40 s.PatchValue(apiserver.NewBackups, 41 func(st *state.State) (backups.Backups, io.Closer) { 42 return s.fake, ioutil.NopCloser(nil) 43 }, 44 ) 45 } 46 47 func (s *backupsCommonSuite) backupURL(c *gc.C) string { 48 environ, err := s.State.Model() 49 c.Assert(err, jc.ErrorIsNil) 50 uri := s.baseURL(c) 51 uri.Path = fmt.Sprintf("/model/%s/backups", environ.UUID()) 52 return uri.String() 53 } 54 55 func (s *backupsCommonSuite) assertErrorResponse(c *gc.C, resp *http.Response, statusCode int, msg string) *params.Error { 56 body, err := ioutil.ReadAll(resp.Body) 57 c.Assert(err, jc.ErrorIsNil) 58 59 c.Assert(resp.StatusCode, gc.Equals, statusCode, gc.Commentf("body: %s", body)) 60 c.Assert(resp.Header.Get("Content-Type"), gc.Equals, params.ContentTypeJSON, gc.Commentf("body: %q", body)) 61 62 var failure params.Error 63 err = json.Unmarshal(body, &failure) 64 c.Assert(err, jc.ErrorIsNil) 65 c.Assert(&failure, gc.ErrorMatches, msg, gc.Commentf("body: %s", body)) 66 return &failure 67 } 68 69 type backupsSuite struct { 70 backupsCommonSuite 71 } 72 73 var _ = gc.Suite(&backupsSuite{}) 74 75 func (s *backupsSuite) TestRequiresAuth(c *gc.C) { 76 resp := s.sendRequest(c, httpRequestParams{method: "GET", url: s.backupURL(c)}) 77 s.assertErrorResponse(c, resp, http.StatusUnauthorized, "no credentials provided") 78 } 79 80 func (s *backupsSuite) checkInvalidMethod(c *gc.C, method, url string) { 81 resp := s.authRequest(c, httpRequestParams{method: method, url: url}) 82 s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "`+method+`"`) 83 } 84 85 func (s *backupsSuite) TestInvalidHTTPMethods(c *gc.C) { 86 url := s.backupURL(c) 87 for _, method := range []string{"POST", "DELETE", "OPTIONS"} { 88 c.Log("testing HTTP method: " + method) 89 s.checkInvalidMethod(c, method, url) 90 } 91 } 92 93 func (s *backupsSuite) TestAuthRequiresClientNotMachine(c *gc.C) { 94 // Add a machine and try to login. 95 machine, err := s.State.AddMachine("quantal", state.JobHostUnits) 96 c.Assert(err, jc.ErrorIsNil) 97 err = machine.SetProvisioned("foo", "fake_nonce", nil) 98 c.Assert(err, jc.ErrorIsNil) 99 password, err := utils.RandomPassword() 100 c.Assert(err, jc.ErrorIsNil) 101 err = machine.SetPassword(password) 102 c.Assert(err, jc.ErrorIsNil) 103 104 resp := s.sendRequest(c, httpRequestParams{ 105 tag: machine.Tag().String(), 106 password: password, 107 method: "GET", 108 url: s.backupURL(c), 109 nonce: "fake_nonce", 110 }) 111 s.assertErrorResponse(c, resp, http.StatusInternalServerError, "tag kind machine not valid") 112 113 // Now try a user login. 114 resp = s.authRequest(c, httpRequestParams{method: "POST", url: s.backupURL(c)}) 115 s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "POST"`) 116 } 117 118 type backupsWithMacaroonsSuite struct { 119 backupsCommonSuite 120 } 121 122 var _ = gc.Suite(&backupsWithMacaroonsSuite{}) 123 124 func (s *backupsWithMacaroonsSuite) SetUpTest(c *gc.C) { 125 s.macaroonAuthEnabled = true 126 s.backupsCommonSuite.SetUpTest(c) 127 } 128 129 func (s *backupsWithMacaroonsSuite) TestWithNoBasicAuthReturnsDischargeRequiredError(c *gc.C) { 130 resp := s.sendRequest(c, httpRequestParams{ 131 method: "GET", 132 jsonBody: ¶ms.BackupsDownloadArgs{"bad-id"}, 133 url: s.backupURL(c), 134 }) 135 136 errResp := s.assertErrorResponse(c, resp, http.StatusUnauthorized, "verification failed: no macaroons") 137 c.Assert(errResp.Code, gc.Equals, params.CodeDischargeRequired) 138 c.Assert(errResp.Info, gc.NotNil) 139 c.Assert(errResp.Info.Macaroon, gc.NotNil) 140 } 141 142 func (s *backupsWithMacaroonsSuite) TestCanGetWithDischargedMacaroon(c *gc.C) { 143 checkCount := 0 144 s.DischargerLogin = func() string { 145 checkCount++ 146 return s.userTag.Id() 147 } 148 s.fake.Error = errors.New("failed!") 149 resp := s.sendRequest(c, httpRequestParams{ 150 do: s.doer(), 151 method: "GET", 152 jsonBody: ¶ms.BackupsDownloadArgs{"bad-id"}, 153 url: s.backupURL(c), 154 }) 155 s.assertErrorResponse(c, resp, http.StatusInternalServerError, "failed!") 156 c.Assert(checkCount, gc.Equals, 1) 157 } 158 159 // doer returns a Do function that can make a bakery request 160 // appropriate for a backups endpoint. 161 func (s *backupsWithMacaroonsSuite) doer() func(*http.Request) (*http.Response, error) { 162 return bakeryDo(nil, backupsBakeryGetError) 163 } 164 165 // backupsBakeryGetError implements a getError function 166 // appropriate for passing to httpbakery.Client.DoWithBodyAndCustomError 167 // for the backups endpoint. 168 func backupsBakeryGetError(resp *http.Response) error { 169 if resp.StatusCode != http.StatusUnauthorized { 170 return nil 171 } 172 data, err := ioutil.ReadAll(resp.Body) 173 if err != nil { 174 return errors.Annotatef(err, "cannot read body") 175 } 176 var errResp params.Error 177 if err := json.Unmarshal(data, &errResp); err != nil { 178 return errors.Annotatef(err, "cannot unmarshal body") 179 } 180 if errResp.Code != params.CodeDischargeRequired { 181 return &errResp 182 } 183 if errResp.Info == nil { 184 return errors.Annotatef(err, "no error info found in discharge-required response error") 185 } 186 // It's a discharge-required error, so make an appropriate httpbakery 187 // error from it. 188 return &httpbakery.Error{ 189 Message: errResp.Message, 190 Code: httpbakery.ErrDischargeRequired, 191 Info: &httpbakery.ErrorInfo{ 192 Macaroon: errResp.Info.Macaroon, 193 MacaroonPath: errResp.Info.MacaroonPath, 194 }, 195 } 196 } 197 198 type backupsDownloadSuite struct { 199 backupsCommonSuite 200 } 201 202 var _ = gc.Suite(&backupsDownloadSuite{}) 203 204 // sendValid sends a valid GET request to the backups endpoint 205 // and returns the response and the expected contents of the 206 // archive if the request succeeds. 207 func (s *backupsDownloadSuite) sendValidGet(c *gc.C) (resp *http.Response, archiveBytes []byte) { 208 meta := backupstesting.NewMetadata() 209 archive, err := backupstesting.NewArchiveBasic(meta) 210 c.Assert(err, jc.ErrorIsNil) 211 archiveBytes = archive.Bytes() 212 s.fake.Meta = meta 213 s.fake.Archive = ioutil.NopCloser(archive) 214 215 return s.authRequest(c, httpRequestParams{ 216 method: "GET", 217 url: s.backupURL(c), 218 contentType: params.ContentTypeJSON, 219 jsonBody: params.BackupsDownloadArgs{ 220 ID: meta.ID(), 221 }, 222 }), archiveBytes 223 } 224 225 func (s *backupsDownloadSuite) TestCalls(c *gc.C) { 226 resp, _ := s.sendValidGet(c) 227 defer resp.Body.Close() 228 229 c.Check(s.fake.Calls, gc.DeepEquals, []string{"Get"}) 230 c.Check(s.fake.IDArg, gc.Equals, s.fake.Meta.ID()) 231 } 232 233 func (s *backupsDownloadSuite) TestResponse(c *gc.C) { 234 resp, _ := s.sendValidGet(c) 235 defer resp.Body.Close() 236 meta := s.fake.Meta 237 238 c.Check(resp.StatusCode, gc.Equals, http.StatusOK) 239 expectedChecksum := base64.StdEncoding.EncodeToString([]byte(meta.Checksum())) 240 c.Check(resp.Header.Get("Digest"), gc.Equals, string(params.DigestSHA256)+"="+expectedChecksum) 241 c.Check(resp.Header.Get("Content-Type"), gc.Equals, params.ContentTypeRaw) 242 } 243 244 func (s *backupsDownloadSuite) TestBody(c *gc.C) { 245 resp, archiveBytes := s.sendValidGet(c) 246 defer resp.Body.Close() 247 248 body, err := ioutil.ReadAll(resp.Body) 249 c.Assert(err, jc.ErrorIsNil) 250 c.Check(body, jc.DeepEquals, archiveBytes) 251 } 252 253 func (s *backupsDownloadSuite) TestErrorWhenGetFails(c *gc.C) { 254 s.fake.Error = errors.New("failed!") 255 resp, _ := s.sendValidGet(c) 256 defer resp.Body.Close() 257 258 s.assertErrorResponse(c, resp, http.StatusInternalServerError, "failed!") 259 } 260 261 type backupsUploadSuite struct { 262 backupsCommonSuite 263 meta *backups.Metadata 264 } 265 266 var _ = gc.Suite(&backupsUploadSuite{}) 267 268 func (s *backupsUploadSuite) sendValid(c *gc.C, id string) *http.Response { 269 s.fake.Meta = backups.NewMetadata() 270 s.fake.Meta.SetID("<a new backup ID>") 271 272 var parts bytes.Buffer 273 writer := multipart.NewWriter(&parts) 274 275 // Set the metadata part. 276 s.meta = backups.NewMetadata() 277 metaResult := apiserverbackups.ResultFromMetadata(s.meta) 278 header := make(textproto.MIMEHeader) 279 header.Set("Content-Disposition", `form-data; name="metadata"`) 280 header.Set("Content-Type", params.ContentTypeJSON) 281 part, err := writer.CreatePart(header) 282 c.Assert(err, jc.ErrorIsNil) 283 err = json.NewEncoder(part).Encode(metaResult) 284 c.Assert(err, jc.ErrorIsNil) 285 286 // Set the attached part. 287 archive := bytes.NewBufferString("<compressed data>") 288 part, err = writer.CreateFormFile("attached", "juju-backup.tar.gz") 289 c.Assert(err, jc.ErrorIsNil) 290 _, err = io.Copy(part, archive) 291 c.Assert(err, jc.ErrorIsNil) 292 293 // Send the request. 294 ctype := writer.FormDataContentType() 295 return s.authRequest(c, httpRequestParams{method: "PUT", url: s.backupURL(c), contentType: ctype, body: &parts}) 296 } 297 298 func (s *backupsUploadSuite) TestCalls(c *gc.C) { 299 resp := s.sendValid(c, "<a new backup ID>") 300 defer resp.Body.Close() 301 302 c.Check(s.fake.Calls, gc.DeepEquals, []string{"Add"}) 303 c.Check(s.fake.ArchiveArg, gc.NotNil) 304 c.Check(s.fake.MetaArg, jc.DeepEquals, s.meta) 305 } 306 307 func (s *backupsUploadSuite) TestResponse(c *gc.C) { 308 resp := s.sendValid(c, "<a new backup ID>") 309 defer resp.Body.Close() 310 311 c.Check(resp.StatusCode, gc.Equals, http.StatusOK) 312 c.Check(resp.Header.Get("Content-Type"), gc.Equals, params.ContentTypeJSON) 313 } 314 315 func (s *backupsUploadSuite) TestBody(c *gc.C) { 316 resp := s.sendValid(c, "<a new backup ID>") 317 defer resp.Body.Close() 318 body, err := ioutil.ReadAll(resp.Body) 319 c.Assert(err, jc.ErrorIsNil) 320 var result params.BackupsUploadResult 321 err = json.Unmarshal(body, &result) 322 c.Assert(err, jc.ErrorIsNil) 323 324 c.Check(result.ID, gc.Equals, "<a new backup ID>") 325 } 326 327 func (s *backupsUploadSuite) TestErrorWhenGetFails(c *gc.C) { 328 s.fake.Error = errors.New("failed!") 329 resp := s.sendValid(c, "<a new backup ID>") 330 defer resp.Body.Close() 331 332 s.assertErrorResponse(c, resp, http.StatusInternalServerError, "failed!") 333 }