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  }