github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/api/client/resources/client_upload_test.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package resources_test 5 6 import ( 7 "context" 8 "fmt" 9 "io" 10 "net/http" 11 "reflect" 12 "strings" 13 "time" 14 15 charmresource "github.com/juju/charm/v12/resource" 16 "github.com/juju/errors" 17 jc "github.com/juju/testing/checkers" 18 "github.com/juju/utils/v3" 19 "github.com/kr/pretty" 20 "go.uber.org/mock/gomock" 21 gc "gopkg.in/check.v1" 22 23 "github.com/juju/juju/api/base/mocks" 24 "github.com/juju/juju/api/client/resources" 25 apicharm "github.com/juju/juju/api/common/charm" 26 httpmocks "github.com/juju/juju/api/http/mocks" 27 corebase "github.com/juju/juju/core/base" 28 coreresources "github.com/juju/juju/core/resources" 29 resourcetesting "github.com/juju/juju/core/resources/testing" 30 "github.com/juju/juju/rpc/params" 31 ) 32 33 var _ = gc.Suite(&UploadSuite{}) 34 35 type UploadSuite struct { 36 mockHTTPClient *httpmocks.MockHTTPDoer 37 mockAPICaller *mocks.MockAPICallCloser 38 mockFacadeCaller *mocks.MockFacadeCaller 39 client *resources.Client 40 } 41 42 func (s *UploadSuite) setup(c *gc.C) *gomock.Controller { 43 ctrl := gomock.NewController(c) 44 45 s.mockHTTPClient = httpmocks.NewMockHTTPDoer(ctrl) 46 s.mockAPICaller = mocks.NewMockAPICallCloser(ctrl) 47 s.mockAPICaller.EXPECT().BestFacadeVersion(gomock.Any()).Return(3).AnyTimes() 48 49 s.mockFacadeCaller = mocks.NewMockFacadeCaller(ctrl) 50 s.mockFacadeCaller.EXPECT().RawAPICaller().Return(s.mockAPICaller).AnyTimes() 51 s.mockFacadeCaller.EXPECT().BestAPIVersion().Return(2).AnyTimes() 52 s.client = resources.NewClientForTest(s.mockFacadeCaller, s.mockHTTPClient) 53 return ctrl 54 } 55 56 func (s *UploadSuite) TestUpload(c *gc.C) { 57 defer s.setup(c).Finish() 58 59 ctx := context.TODO() 60 s.mockAPICaller.EXPECT().Context().Return(ctx) 61 62 data := "<data>" 63 fp, err := charmresource.GenerateFingerprint(strings.NewReader(data)) 64 c.Assert(err, jc.ErrorIsNil) 65 req, err := http.NewRequest("PUT", "/applications/a-application/resources/spam", strings.NewReader(data)) 66 c.Assert(err, jc.ErrorIsNil) 67 req.Header.Set("Content-Type", "application/octet-stream") 68 req.Header.Set("Content-SHA384", fp.String()) 69 req.Header.Set("Content-Length", fmt.Sprint(len(data))) 70 req.Header.Set("Content-Disposition", "form-data; filename=foo.zip") 71 req.ContentLength = int64(len(data)) 72 73 s.mockHTTPClient.EXPECT().Do(ctx, reqMatcher{c, req}, gomock.Any()) 74 75 err = s.client.Upload("a-application", "spam", "foo.zip", "", strings.NewReader(data)) 76 c.Assert(err, jc.ErrorIsNil) 77 } 78 79 type reqMatcher struct { 80 c *gc.C 81 req *http.Request 82 } 83 84 func (m reqMatcher) Matches(x interface{}) bool { 85 obtained, ok := x.(*http.Request) 86 if !ok { 87 return false 88 } 89 obtainedCopy := *obtained 90 obtainedBody, err := io.ReadAll(obtainedCopy.Body) 91 m.c.Assert(err, jc.ErrorIsNil) 92 obtainedCopy.Body = nil 93 obtainedCopy.GetBody = nil 94 95 reqCopy := *m.req 96 reqBody, err := io.ReadAll(reqCopy.Body) 97 m.c.Assert(err, jc.ErrorIsNil) 98 reqCopy.Body = nil 99 reqCopy.GetBody = nil 100 if string(obtainedBody) != string(reqBody) { 101 return false 102 } 103 return reflect.DeepEqual(reqCopy, obtainedCopy) 104 } 105 106 func (m reqMatcher) String() string { 107 return pretty.Sprint(m.req) 108 } 109 110 func (s *UploadSuite) TestUploadBadApplication(c *gc.C) { 111 defer s.setup(c).Finish() 112 113 err := s.client.Upload("???", "spam", "file.zip", "", nil) 114 c.Check(err, gc.ErrorMatches, `.*invalid application.*`) 115 } 116 117 func (s *UploadSuite) TestUploadFailed(c *gc.C) { 118 defer s.setup(c).Finish() 119 120 data := "<data>" 121 fp, err := charmresource.GenerateFingerprint(strings.NewReader(data)) 122 c.Assert(err, jc.ErrorIsNil) 123 req, err := http.NewRequest("PUT", "/applications/a-application/resources/spam", strings.NewReader(data)) 124 c.Assert(err, jc.ErrorIsNil) 125 req.Header.Set("Content-Type", "application/octet-stream") 126 req.Header.Set("Content-SHA384", fp.String()) 127 req.Header.Set("Content-Length", fmt.Sprint(len(data))) 128 req.Header.Set("Content-Disposition", "form-data; filename=foo.zip") 129 req.ContentLength = int64(len(data)) 130 131 ctx := context.TODO() 132 s.mockAPICaller.EXPECT().Context().Return(ctx) 133 s.mockHTTPClient.EXPECT().Do(ctx, reqMatcher{c, req}, gomock.Any()).Return(errors.New("boom")) 134 err = s.client.Upload("a-application", "spam", "foo.zip", "", strings.NewReader(data)) 135 c.Assert(err, gc.ErrorMatches, "boom") 136 } 137 138 func (s *UploadSuite) TestAddPendingResources(c *gc.C) { 139 defer s.setup(c).Finish() 140 141 res, apiResult := newResourceResult(c, "spam") 142 args := params.AddPendingResourcesArgsV2{ 143 Entity: params.Entity{Tag: "application-a-application"}, 144 URL: "ch:spam", 145 CharmOrigin: params.CharmOrigin{ 146 Source: "charm-hub", 147 ID: "id", 148 Risk: "stable", 149 Base: params.Base{Name: "ubuntu", Channel: "22.04/stable"}, 150 Architecture: "arm64", 151 }, 152 Resources: []params.CharmResource{apiResult.Resources[0].CharmResource}, 153 } 154 uuid, err := utils.NewUUID() 155 c.Assert(err, jc.ErrorIsNil) 156 expected := []string{uuid.String()} 157 result := new(params.AddPendingResourcesResult) 158 results := params.AddPendingResourcesResult{ 159 PendingIDs: expected, 160 } 161 s.mockFacadeCaller.EXPECT().FacadeCall("AddPendingResources", &args, result).SetArg(2, results).Return(nil) 162 163 cURL := "ch:spam" 164 pendingIDs, err := s.client.AddPendingResources(resources.AddPendingResourcesArgs{ 165 ApplicationID: "a-application", 166 CharmID: resources.CharmID{ 167 URL: cURL, 168 Origin: apicharm.Origin{ 169 Source: apicharm.OriginCharmHub, 170 ID: "id", 171 Risk: "stable", 172 Base: corebase.MakeDefaultBase("ubuntu", "22.04"), 173 Architecture: "arm64", 174 }, 175 }, 176 Resources: []charmresource.Resource{res[0].Resource}, 177 }) 178 c.Assert(err, jc.ErrorIsNil) 179 c.Assert(pendingIDs, jc.DeepEquals, expected) 180 } 181 182 func (s *UploadSuite) TestUploadPendingResource(c *gc.C) { 183 defer s.setup(c).Finish() 184 185 res, apiResult := newResourceResult(c, "spam") 186 args := params.AddPendingResourcesArgsV2{ 187 Entity: params.Entity{Tag: "application-a-application"}, 188 Resources: []params.CharmResource{apiResult.Resources[0].CharmResource}, 189 } 190 uuid, err := utils.NewUUID() 191 c.Assert(err, jc.ErrorIsNil) 192 expected := uuid.String() 193 results := params.AddPendingResourcesResult{ 194 PendingIDs: []string{expected}, 195 } 196 data := "<data>" 197 fp, err := charmresource.GenerateFingerprint(strings.NewReader(data)) 198 c.Assert(err, jc.ErrorIsNil) 199 200 url := fmt.Sprintf("/applications/a-application/resources/spam?pendingid=%v", expected) 201 req, err := http.NewRequest("PUT", url, strings.NewReader(data)) 202 c.Assert(err, jc.ErrorIsNil) 203 req.Header.Set("Content-Type", "application/octet-stream") 204 req.Header.Set("Content-SHA384", fp.String()) 205 req.Header.Set("Content-Length", fmt.Sprint(len(data))) 206 req.ContentLength = int64(len(data)) 207 req.Header.Set("Content-Disposition", "form-data; filename=file.zip") 208 209 ctx := context.TODO() 210 s.mockAPICaller.EXPECT().Context().Return(ctx) 211 s.mockFacadeCaller.EXPECT().FacadeCall("AddPendingResources", &args, gomock.Any()).SetArg(2, results).Return(nil) 212 s.mockHTTPClient.EXPECT().Do(ctx, reqMatcher{c, req}, gomock.Any()) 213 214 uploadID, err := s.client.UploadPendingResource("a-application", res[0].Resource, "file.zip", strings.NewReader(data)) 215 c.Assert(err, jc.ErrorIsNil) 216 c.Assert(uploadID, gc.Equals, expected) 217 } 218 219 func (s *UploadSuite) TestUploadPendingResourceNoFile(c *gc.C) { 220 defer s.setup(c).Finish() 221 222 res, apiResult := newResourceResult(c, "spam") 223 args := params.AddPendingResourcesArgsV2{ 224 Entity: params.Entity{Tag: "application-a-application"}, 225 Resources: []params.CharmResource{apiResult.Resources[0].CharmResource}, 226 } 227 uuid, err := utils.NewUUID() 228 c.Assert(err, jc.ErrorIsNil) 229 expected := uuid.String() 230 results := params.AddPendingResourcesResult{ 231 PendingIDs: []string{expected}, 232 } 233 s.mockFacadeCaller.EXPECT().FacadeCall("AddPendingResources", &args, gomock.Any()).SetArg(2, results).Return(nil) 234 235 uploadID, err := s.client.UploadPendingResource("a-application", res[0].Resource, "file.zip", nil) 236 c.Assert(err, jc.ErrorIsNil) 237 c.Assert(uploadID, gc.Equals, expected) 238 } 239 240 func (s *UploadSuite) TestUploadPendingResourceBadApplication(c *gc.C) { 241 ctrl := gomock.NewController(c) 242 defer ctrl.Finish() 243 244 res, _ := newResourceResult(c, "spam") 245 _, err := s.client.UploadPendingResource("???", res[0].Resource, "file.zip", nil) 246 c.Assert(err, gc.ErrorMatches, `.*invalid application.*`) 247 } 248 249 func (s *UploadSuite) TestUploadPendingResourceFailed(c *gc.C) { 250 defer s.setup(c).Finish() 251 252 res, apiResult := newResourceResult(c, "spam") 253 args := params.AddPendingResourcesArgsV2{ 254 Entity: params.Entity{Tag: "application-a-application"}, 255 Resources: []params.CharmResource{apiResult.Resources[0].CharmResource}, 256 } 257 uuid, err := utils.NewUUID() 258 c.Assert(err, jc.ErrorIsNil) 259 expected := uuid.String() 260 results := params.AddPendingResourcesResult{ 261 PendingIDs: []string{expected}, 262 } 263 data := "<data>" 264 fp, err := charmresource.GenerateFingerprint(strings.NewReader(data)) 265 c.Assert(err, jc.ErrorIsNil) 266 url := fmt.Sprintf("/applications/a-application/resources/spam?pendingid=%v", expected) 267 req, err := http.NewRequest("PUT", url, strings.NewReader(data)) 268 c.Assert(err, jc.ErrorIsNil) 269 req.Header.Set("Content-Type", "application/octet-stream") 270 req.Header.Set("Content-SHA384", fp.String()) 271 req.Header.Set("Content-Length", fmt.Sprint(len(data))) 272 req.ContentLength = int64(len(data)) 273 req.Header.Set("Content-Disposition", "form-data; filename=file.zip") 274 275 ctx := context.TODO() 276 s.mockAPICaller.EXPECT().Context().Return(ctx) 277 s.mockFacadeCaller.EXPECT().FacadeCall("AddPendingResources", &args, gomock.Any()).SetArg(2, results).Return(nil) 278 s.mockHTTPClient.EXPECT().Do(ctx, reqMatcher{c, req}, gomock.Any()).Return(errors.New("boom")) 279 280 _, err = s.client.UploadPendingResource("a-application", res[0].Resource, "file.zip", strings.NewReader(data)) 281 c.Assert(err, gc.ErrorMatches, "boom") 282 } 283 284 func newResourceResult(c *gc.C, names ...string) ([]coreresources.Resource, params.ResourcesResult) { 285 var res []coreresources.Resource 286 var apiResult params.ResourcesResult 287 for _, name := range names { 288 data := name + "...spamspamspam" 289 newRes, apiRes := newResource(c, name, "a-user", data) 290 res = append(res, newRes) 291 apiResult.Resources = append(apiResult.Resources, apiRes) 292 } 293 return res, apiResult 294 } 295 296 func newResource(c *gc.C, name, username, data string) (coreresources.Resource, params.Resource) { 297 opened := resourcetesting.NewResource(c, nil, name, "a-application", data) 298 res := opened.Resource 299 res.Revision = 1 300 res.Username = username 301 if username == "" { 302 // Note that resourcetesting.NewResource() returns a resources 303 // with a username and timestamp set. So if the username was 304 // "un-set" then we have to also unset the timestamp. 305 res.Timestamp = time.Time{} 306 } 307 308 apiRes := params.Resource{ 309 CharmResource: params.CharmResource{ 310 Name: name, 311 Description: name + " description", 312 Type: "file", 313 Path: res.Path, 314 Origin: "upload", 315 Revision: 1, 316 Fingerprint: res.Fingerprint.Bytes(), 317 Size: res.Size, 318 }, 319 ID: res.ID, 320 ApplicationID: res.ApplicationID, 321 Username: username, 322 Timestamp: res.Timestamp, 323 } 324 325 return res, apiRes 326 }