1
// Copyright 2014 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
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"
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"
31
type backupsCommonSuite struct {
33
fake *backupstesting.FakeBackups
36
func (s *backupsCommonSuite) SetUpTest(c *gc.C) {
37
s.authHttpSuite.SetUpTest(c)
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)
47
func (s *backupsCommonSuite) backupURL(c *gc.C) string {
48
environ, err := s.State.Model()
49
c.Assert(err, jc.ErrorIsNil)
51
uri.Path = fmt.Sprintf("/model/%s/backups", environ.UUID())
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)
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))
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))
69
type backupsSuite struct {
73
var _ = gc.Suite(&backupsSuite{})
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")
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+`"`)
85
func (s *backupsSuite) TestInvalidHTTPMethods(c *gc.C) {
87
for _, method := range []string{"POST", "DELETE", "OPTIONS"} {
88
c.Log("testing HTTP method: " + method)
89
s.checkInvalidMethod(c, method, url)
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)
104
resp := s.sendRequest(c, httpRequestParams{
105
tag: machine.Tag().String(),
111
s.assertErrorResponse(c, resp, http.StatusInternalServerError, "tag kind machine not valid")
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"`)
118
type backupsWithMacaroonsSuite struct {
122
var _ = gc.Suite(&backupsWithMacaroonsSuite{})
124
func (s *backupsWithMacaroonsSuite) SetUpTest(c *gc.C) {
125
s.macaroonAuthEnabled = true
126
s.backupsCommonSuite.SetUpTest(c)
129
func (s *backupsWithMacaroonsSuite) TestWithNoBasicAuthReturnsDischargeRequiredError(c *gc.C) {
130
resp := s.sendRequest(c, httpRequestParams{
132
jsonBody: ¶ms.BackupsDownloadArgs{"bad-id"},
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)
142
func (s *backupsWithMacaroonsSuite) TestCanGetWithDischargedMacaroon(c *gc.C) {
144
s.DischargerLogin = func() string {
146
return s.userTag.Id()
148
s.fake.Error = errors.New("failed!")
149
resp := s.sendRequest(c, httpRequestParams{
152
jsonBody: ¶ms.BackupsDownloadArgs{"bad-id"},
155
s.assertErrorResponse(c, resp, http.StatusInternalServerError, "failed!")
156
c.Assert(checkCount, gc.Equals, 1)
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)
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 {
172
data, err := ioutil.ReadAll(resp.Body)
174
return errors.Annotatef(err, "cannot read body")
176
var errResp params.Error
177
if err := json.Unmarshal(data, &errResp); err != nil {
178
return errors.Annotatef(err, "cannot unmarshal body")
180
if errResp.Code != params.CodeDischargeRequired {
183
if errResp.Info == nil {
184
return errors.Annotatef(err, "no error info found in discharge-required response error")
186
// It's a discharge-required error, so make an appropriate httpbakery
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,
198
type backupsDownloadSuite struct {
202
var _ = gc.Suite(&backupsDownloadSuite{})
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()
213
s.fake.Archive = ioutil.NopCloser(archive)
215
return s.authRequest(c, httpRequestParams{
218
contentType: params.ContentTypeJSON,
219
jsonBody: params.BackupsDownloadArgs{
225
func (s *backupsDownloadSuite) TestCalls(c *gc.C) {
226
resp, _ := s.sendValidGet(c)
227
defer resp.Body.Close()
229
c.Check(s.fake.Calls, gc.DeepEquals, []string{"Get"})
230
c.Check(s.fake.IDArg, gc.Equals, s.fake.Meta.ID())
233
func (s *backupsDownloadSuite) TestResponse(c *gc.C) {
234
resp, _ := s.sendValidGet(c)
235
defer resp.Body.Close()
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)
244
func (s *backupsDownloadSuite) TestBody(c *gc.C) {
245
resp, archiveBytes := s.sendValidGet(c)
246
defer resp.Body.Close()
248
body, err := ioutil.ReadAll(resp.Body)
249
c.Assert(err, jc.ErrorIsNil)
250
c.Check(body, jc.DeepEquals, archiveBytes)
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()
258
s.assertErrorResponse(c, resp, http.StatusInternalServerError, "failed!")
261
type backupsUploadSuite struct {
263
meta *backups.Metadata
266
var _ = gc.Suite(&backupsUploadSuite{})
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>")
272
var parts bytes.Buffer
273
writer := multipart.NewWriter(&parts)
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)
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)
294
ctype := writer.FormDataContentType()
295
return s.authRequest(c, httpRequestParams{method: "PUT", url: s.backupURL(c), contentType: ctype, body: &parts})
298
func (s *backupsUploadSuite) TestCalls(c *gc.C) {
299
resp := s.sendValid(c, "<a new backup ID>")
300
defer resp.Body.Close()
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)
307
func (s *backupsUploadSuite) TestResponse(c *gc.C) {
308
resp := s.sendValid(c, "<a new backup ID>")
309
defer resp.Body.Close()
311
c.Check(resp.StatusCode, gc.Equals, http.StatusOK)
312
c.Check(resp.Header.Get("Content-Type"), gc.Equals, params.ContentTypeJSON)
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)
324
c.Check(result.ID, gc.Equals, "<a new backup ID>")
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()
332
s.assertErrorResponse(c, resp, http.StatusInternalServerError, "failed!")