github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/resources_test.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver_test
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/http/httptest"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	charmresource "github.com/juju/charm/v12/resource"
    17  	"github.com/juju/collections/set"
    18  	"github.com/juju/errors"
    19  	"github.com/juju/names/v5"
    20  	"github.com/juju/testing"
    21  	jc "github.com/juju/testing/checkers"
    22  	gc "gopkg.in/check.v1"
    23  
    24  	api "github.com/juju/juju/api/client/resources"
    25  	"github.com/juju/juju/apiserver"
    26  	apiservererrors "github.com/juju/juju/apiserver/errors"
    27  	apiservertesting "github.com/juju/juju/apiserver/testing"
    28  	"github.com/juju/juju/core/resources"
    29  	resourcetesting "github.com/juju/juju/core/resources/testing"
    30  	"github.com/juju/juju/rpc/params"
    31  	"github.com/juju/juju/state"
    32  )
    33  
    34  type ResourcesHandlerSuite struct {
    35  	testing.IsolationSuite
    36  
    37  	stateAuthErr error
    38  	backend      *fakeBackend
    39  	username     string
    40  	req          *http.Request
    41  	recorder     *httptest.ResponseRecorder
    42  	handler      *apiserver.ResourcesHandler
    43  }
    44  
    45  var _ = gc.Suite(&ResourcesHandlerSuite{})
    46  
    47  func (s *ResourcesHandlerSuite) SetUpTest(c *gc.C) {
    48  	s.IsolationSuite.SetUpTest(c)
    49  
    50  	s.stateAuthErr = nil
    51  	s.backend = new(fakeBackend)
    52  	s.username = "youknowwho"
    53  
    54  	method := "..."
    55  	urlStr := "..."
    56  	body := strings.NewReader("...")
    57  	req, err := http.NewRequest(method, urlStr, body)
    58  	c.Assert(err, jc.ErrorIsNil)
    59  	s.req = req
    60  	s.recorder = httptest.NewRecorder()
    61  	s.handler = &apiserver.ResourcesHandler{
    62  		StateAuthFunc:     s.authState,
    63  		ChangeAllowedFunc: func(*http.Request) error { return nil },
    64  	}
    65  }
    66  
    67  func (s *ResourcesHandlerSuite) authState(req *http.Request, tagKinds ...string) (
    68  	apiserver.ResourcesBackend, state.PoolHelper, names.Tag, error,
    69  ) {
    70  	if s.stateAuthErr != nil {
    71  		return nil, nil, nil, errors.Trace(s.stateAuthErr)
    72  	}
    73  
    74  	ph := apiservertesting.StubPoolHelper{StubRelease: func() bool { return false }}
    75  	tag := names.NewUserTag(s.username)
    76  	return s.backend, ph, tag, nil
    77  }
    78  
    79  func (s *ResourcesHandlerSuite) TestExpectedAuthTags(c *gc.C) {
    80  	expectedTags := set.NewStrings(names.UserTagKind, names.MachineTagKind, names.ControllerAgentTagKind, names.ApplicationTagKind)
    81  
    82  	s.handler.StateAuthFunc = func(req *http.Request, tagKinds ...string) (apiserver.ResourcesBackend, state.PoolHelper, names.Tag, error) {
    83  		gotTags := set.NewStrings(tagKinds...)
    84  		if gotTags.Difference(expectedTags).Size() != 0 || expectedTags.Difference(gotTags).Size() != 0 {
    85  			c.Fatalf("unexpected tag kinds %v", tagKinds)
    86  			return nil, nil, nil, errors.NotValidf("tag kinds %v", tagKinds)
    87  		}
    88  		ph := apiservertesting.StubPoolHelper{StubRelease: func() bool { return false }}
    89  		tag := names.NewUserTag(s.username)
    90  		return s.backend, ph, tag, nil
    91  	}
    92  	s.req.Method = "GET"
    93  	s.handler.ServeHTTP(s.recorder, s.req)
    94  	s.checkResp(c, http.StatusOK, "application/octet-stream", resourceBody)
    95  }
    96  
    97  func (s *ResourcesHandlerSuite) TestStateAuthFailure(c *gc.C) {
    98  	failure, expected := apiFailure("<failure>", "")
    99  	s.stateAuthErr = failure
   100  
   101  	s.handler.ServeHTTP(s.recorder, s.req)
   102  
   103  	s.checkResp(c, http.StatusInternalServerError, "application/json", expected)
   104  }
   105  
   106  func (s *ResourcesHandlerSuite) TestUnsupportedMethod(c *gc.C) {
   107  	s.req.Method = "POST"
   108  
   109  	s.handler.ServeHTTP(s.recorder, s.req)
   110  
   111  	_, expected := apiFailure(`unsupported method: "POST"`, params.CodeMethodNotAllowed)
   112  	s.checkResp(c, http.StatusMethodNotAllowed, "application/json", expected)
   113  }
   114  
   115  func (s *ResourcesHandlerSuite) TestGetSuccess(c *gc.C) {
   116  	s.req.Method = "GET"
   117  	s.handler.ServeHTTP(s.recorder, s.req)
   118  	s.checkResp(c, http.StatusOK, "application/octet-stream", resourceBody)
   119  }
   120  
   121  func (s *ResourcesHandlerSuite) TestPutSuccess(c *gc.C) {
   122  	uploadContent := "<some data>"
   123  	res, _ := newResource(c, "spam", "a-user", content)
   124  	stored, _ := newResource(c, "spam", "", "")
   125  	s.backend.ReturnGetResource = stored
   126  	s.backend.ReturnSetResource = res
   127  
   128  	req, _ := newUploadRequest(c, "spam", "a-application", uploadContent)
   129  	s.handler.ServeHTTP(s.recorder, req)
   130  
   131  	expected := mustMarshalJSON(&params.UploadResult{
   132  		Resource: api.Resource2API(res),
   133  	})
   134  	s.checkResp(c, http.StatusOK, "application/json", string(expected))
   135  }
   136  
   137  func (s *ResourcesHandlerSuite) TestPutChangeBlocked(c *gc.C) {
   138  	uploadContent := "<some data>"
   139  	res, _ := newResource(c, "spam", "a-user", content)
   140  	stored, _ := newResource(c, "spam", "", "")
   141  	s.backend.ReturnGetResource = stored
   142  	s.backend.ReturnSetResource = res
   143  
   144  	expectedError := apiservererrors.OperationBlockedError("test block")
   145  	s.handler.ChangeAllowedFunc = func(*http.Request) error {
   146  		return expectedError
   147  	}
   148  
   149  	req, _ := newUploadRequest(c, "spam", "a-application", uploadContent)
   150  	s.handler.ServeHTTP(s.recorder, req)
   151  
   152  	expected := mustMarshalJSON(&params.ErrorResult{apiservererrors.ServerError(expectedError)})
   153  	s.checkResp(c, http.StatusBadRequest, "application/json", string(expected))
   154  }
   155  
   156  func (s *ResourcesHandlerSuite) TestPutSuccessDockerResource(c *gc.C) {
   157  	uploadContent := "<some data>"
   158  	res := newDockerResource(c, "spam", "a-user", content)
   159  	stored := newDockerResource(c, "spam", "", "")
   160  	s.backend.ReturnGetResource = stored
   161  	s.backend.ReturnSetResource = res
   162  
   163  	req, _ := newUploadRequest(c, "spam", "a-application", uploadContent)
   164  	s.handler.ServeHTTP(s.recorder, req)
   165  
   166  	expected := mustMarshalJSON(&params.UploadResult{
   167  		Resource: api.Resource2API(res),
   168  	})
   169  	s.checkResp(c, http.StatusOK, "application/json", string(expected))
   170  }
   171  
   172  func (s *ResourcesHandlerSuite) TestPutExtensionMismatch(c *gc.C) {
   173  	content := "<some data>"
   174  
   175  	// newResource returns a resource with a Path = name + ".tgz"
   176  	res, _ := newResource(c, "spam", "a-user", content)
   177  	stored, _ := newResource(c, "spam", "", "")
   178  	s.backend.ReturnGetResource = stored
   179  	s.backend.ReturnSetResource = res
   180  
   181  	req, _ := newUploadRequest(c, "spam", "a-application", content)
   182  	req.Header.Set("Content-Disposition", "form-data; filename=different.ext")
   183  	s.handler.ServeHTTP(s.recorder, req)
   184  
   185  	_, expected := apiFailure(`incorrect extension on resource upload "different.ext", expected ".tgz"`,
   186  		"")
   187  	s.checkResp(c, http.StatusInternalServerError, "application/json", expected)
   188  }
   189  
   190  func (s *ResourcesHandlerSuite) TestPutWithPending(c *gc.C) {
   191  	uploadContent := "<some data>"
   192  	res, _ := newResource(c, "spam", "a-user", uploadContent)
   193  	res.PendingID = "some-unique-id"
   194  	stored, _ := newResource(c, "spam", "", "")
   195  	stored.PendingID = "some-unique-id"
   196  	s.backend.ReturnGetPendingResource = stored
   197  	s.backend.ReturnUpdatePendingResource = res
   198  
   199  	req, _ := newUploadRequest(c, "spam", "a-application", content)
   200  	req.URL.RawQuery += "&pendingid=some-unique-id"
   201  	s.handler.ServeHTTP(s.recorder, req)
   202  
   203  	expected := mustMarshalJSON(&params.UploadResult{
   204  		Resource: api.Resource2API(res),
   205  	})
   206  	s.checkResp(c, http.StatusOK, "application/json", string(expected))
   207  }
   208  
   209  func (s *ResourcesHandlerSuite) TestPutSetResourceFailure(c *gc.C) {
   210  	content := "<some data>"
   211  	stored, _ := newResource(c, "spam", "", "")
   212  	s.backend.ReturnGetResource = stored
   213  	failure, expected := apiFailure("boom", "")
   214  	s.backend.SetResourceErr = failure
   215  
   216  	req, _ := newUploadRequest(c, "spam", "a-application", content)
   217  	s.handler.ServeHTTP(s.recorder, req)
   218  	s.checkResp(c, http.StatusInternalServerError, "application/json", expected)
   219  }
   220  
   221  func (s *ResourcesHandlerSuite) checkResp(c *gc.C, status int, ctype, body string) {
   222  	checkHTTPResp(c, s.recorder, status, ctype, body)
   223  }
   224  
   225  func checkHTTPResp(c *gc.C, recorder *httptest.ResponseRecorder, status int, ctype, body string) {
   226  	c.Assert(recorder.Code, gc.Equals, status)
   227  	hdr := recorder.Header()
   228  	c.Check(hdr.Get("Content-Type"), gc.Equals, ctype)
   229  	c.Check(hdr.Get("Content-Length"), gc.Equals, strconv.Itoa(len(body)))
   230  
   231  	actualBody, err := io.ReadAll(recorder.Body)
   232  	c.Assert(err, jc.ErrorIsNil)
   233  	c.Check(string(actualBody), gc.Equals, body)
   234  }
   235  
   236  type fakeBackend struct {
   237  	ReturnGetResource           resources.Resource
   238  	ReturnGetPendingResource    resources.Resource
   239  	ReturnSetResource           resources.Resource
   240  	SetResourceErr              error
   241  	ReturnUpdatePendingResource resources.Resource
   242  }
   243  
   244  const resourceBody = "body"
   245  
   246  func (s *fakeBackend) OpenResource(application, name string) (resources.Resource, io.ReadCloser, error) {
   247  	res := resources.Resource{}
   248  	res.Size = int64(len(resourceBody))
   249  	reader := io.NopCloser(strings.NewReader(resourceBody))
   250  	return res, reader, nil
   251  }
   252  
   253  func (s *fakeBackend) GetResource(service, name string) (resources.Resource, error) {
   254  	return s.ReturnGetResource, nil
   255  }
   256  
   257  func (s *fakeBackend) GetPendingResource(service, name, pendingID string) (resources.Resource, error) {
   258  	return s.ReturnGetPendingResource, nil
   259  }
   260  
   261  func (s *fakeBackend) SetResource(
   262  	applicationID, userID string,
   263  	res charmresource.Resource, r io.Reader,
   264  	incrementCharmModifiedVersion state.IncrementCharmModifiedVersionType,
   265  ) (resources.Resource, error) {
   266  	if s.SetResourceErr != nil {
   267  		return resources.Resource{}, s.SetResourceErr
   268  	}
   269  	return s.ReturnSetResource, nil
   270  }
   271  
   272  func (s *fakeBackend) UpdatePendingResource(applicationID, pendingID, userID string, res charmresource.Resource, r io.Reader) (resources.Resource, error) {
   273  	return s.ReturnUpdatePendingResource, nil
   274  }
   275  
   276  func newDockerResource(c *gc.C, name, username, data string) resources.Resource {
   277  	opened := resourcetesting.NewDockerResource(c, nil, name, "a-application", data)
   278  	res := opened.Resource
   279  	res.Username = username
   280  	if username == "" {
   281  		res.Timestamp = time.Time{}
   282  	}
   283  	return res
   284  }
   285  
   286  func newResource(c *gc.C, name, username, data string) (resources.Resource, params.Resource) {
   287  	opened := resourcetesting.NewResource(c, nil, name, "a-application", data)
   288  	res := opened.Resource
   289  	res.Username = username
   290  	if username == "" {
   291  		res.Timestamp = time.Time{}
   292  	}
   293  
   294  	apiRes := params.Resource{
   295  		CharmResource: params.CharmResource{
   296  			Name:        name,
   297  			Description: name + " description",
   298  			Type:        "file",
   299  			Path:        res.Path,
   300  			Origin:      "upload",
   301  			Revision:    0,
   302  			Fingerprint: res.Fingerprint.Bytes(),
   303  			Size:        res.Size,
   304  		},
   305  		ID:            res.ID,
   306  		ApplicationID: res.ApplicationID,
   307  		Username:      username,
   308  		Timestamp:     res.Timestamp,
   309  	}
   310  
   311  	return res, apiRes
   312  }
   313  
   314  func newUploadRequest(c *gc.C, name, service, content string) (*http.Request, io.Reader) {
   315  	fp, err := charmresource.GenerateFingerprint(strings.NewReader(content))
   316  	c.Assert(err, jc.ErrorIsNil)
   317  
   318  	method := "PUT"
   319  	urlStr := "https://api:17017/applications/%s/resources/%s"
   320  	urlStr += "?:application=%s&:resource=%s" // ...added by the mux.
   321  	urlStr = fmt.Sprintf(urlStr, service, name, service, name)
   322  	body := strings.NewReader(content)
   323  	req, err := http.NewRequest(method, urlStr, body)
   324  	c.Assert(err, jc.ErrorIsNil)
   325  
   326  	req.Header.Set("Content-Type", "application/octet-stream")
   327  	req.Header.Set("Content-Length", fmt.Sprint(len(content)))
   328  	req.Header.Set("Content-SHA384", fp.String())
   329  	req.Header.Set("Content-Disposition", "form-data; filename="+name+".tgz")
   330  
   331  	return req, body
   332  }
   333  
   334  func apiFailure(msg, code string) (error, string) {
   335  	failure := errors.New(msg)
   336  	data := mustMarshalJSON(params.ErrorResult{
   337  		Error: &params.Error{
   338  			Message: msg,
   339  			Code:    code,
   340  		},
   341  	})
   342  	return failure, string(data)
   343  }
   344  
   345  func mustMarshalJSON(v interface{}) []byte {
   346  	data, err := json.Marshal(v)
   347  	if err != nil {
   348  		panic(err)
   349  	}
   350  	return data
   351  }