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

     1  // Copyright 2023 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver_test
     5  
     6  import (
     7  	"crypto/sha256"
     8  	"encoding/hex"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"net/http"
    13  	"net/url"
    14  	"os"
    15  	"strings"
    16  
    17  	jc "github.com/juju/testing/checkers"
    18  	gc "gopkg.in/check.v1"
    19  
    20  	apitesting "github.com/juju/juju/apiserver/testing"
    21  	"github.com/juju/juju/rpc/params"
    22  	"github.com/juju/juju/state"
    23  	"github.com/juju/juju/testcharms"
    24  )
    25  
    26  type objectsSuite struct {
    27  	apiserverBaseSuite
    28  	method      string
    29  	contentType string
    30  }
    31  
    32  func (s *objectsSuite) SetUpSuite(c *gc.C) {
    33  	s.apiserverBaseSuite.SetUpSuite(c)
    34  }
    35  
    36  func (s *objectsSuite) objectsCharmsURL(charmRef string) *url.URL {
    37  	return s.URL(fmt.Sprintf("/model-%s/charms/%s", s.State.ModelUUID(), charmRef), nil)
    38  }
    39  
    40  func (s *objectsSuite) objectsCharmsURI(charmRef string) string {
    41  	return s.objectsCharmsURL(charmRef).String()
    42  }
    43  
    44  func (s *objectsSuite) assertResponse(c *gc.C, resp *http.Response, expStatus int) params.CharmsResponse {
    45  	body := apitesting.AssertResponse(c, resp, expStatus, params.ContentTypeJSON)
    46  	var charmResponse params.CharmsResponse
    47  	err := json.Unmarshal(body, &charmResponse)
    48  	c.Assert(err, jc.ErrorIsNil, gc.Commentf("body: %s", body))
    49  	return charmResponse
    50  }
    51  
    52  func (s *objectsSuite) assertErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) {
    53  	charmResponse := s.assertResponse(c, resp, expCode)
    54  	c.Check(charmResponse.Error, gc.Matches, expError)
    55  }
    56  
    57  func (s *objectsSuite) TestRequiresAuth(c *gc.C) {
    58  	resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{Method: s.method, URL: s.objectsCharmsURI("somecharm-abcd0123")})
    59  	body := apitesting.AssertResponse(c, resp, http.StatusUnauthorized, "text/plain; charset=utf-8")
    60  	c.Assert(string(body), gc.Equals, "authentication failed: no credentials provided\n")
    61  }
    62  
    63  func (s *objectsSuite) TestFailsWithInvalidObjectSha256(c *gc.C) {
    64  	uri := s.objectsCharmsURI("invalidsha256")
    65  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: s.method, ContentType: s.contentType, URL: uri})
    66  	s.assertErrorResponse(
    67  		c, resp, http.StatusBadRequest,
    68  		`.*"invalidsha256" is not a valid charm object path$`,
    69  	)
    70  }
    71  
    72  func (s *objectsSuite) TestInvalidBucket(c *gc.C) {
    73  	wrongURL := s.URL("modelwrongbucket/charms/somecharm-abcd0123", nil)
    74  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: s.method, URL: wrongURL.String()})
    75  	body := apitesting.AssertResponse(c, resp, http.StatusNotFound, "text/plain; charset=utf-8")
    76  	c.Assert(string(body), gc.Equals, "404 page not found\n")
    77  }
    78  
    79  func (s *objectsSuite) TestInvalidModel(c *gc.C) {
    80  	wrongURL := s.URL("model-wrongbucket/charms/somecharm-abcd0123", nil)
    81  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: s.method, URL: wrongURL.String()})
    82  	body := apitesting.AssertResponse(c, resp, http.StatusBadRequest, "text/plain; charset=utf-8")
    83  	c.Assert(string(body), gc.Equals, "invalid model UUID \"wrongbucket\"\n")
    84  }
    85  
    86  func (s *objectsSuite) TestInvalidObject(c *gc.C) {
    87  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: s.method, ContentType: s.contentType, URL: s.objectsCharmsURI("invalidcharm")})
    88  	body := apitesting.AssertResponse(c, resp, http.StatusBadRequest, "application/json")
    89  	c.Assert(string(body), gc.Matches, `{"error":".*\\"invalidcharm\\" is not a valid charm object path","error-code":"bad request"}`)
    90  }
    91  
    92  type getObjectsSuite struct {
    93  	objectsSuite
    94  }
    95  
    96  var _ = gc.Suite(&getObjectsSuite{})
    97  
    98  func (s *getObjectsSuite) SetUpSuite(c *gc.C) {
    99  	s.objectsSuite.SetUpSuite(c)
   100  	s.objectsSuite.method = "GET"
   101  }
   102  
   103  func (s *getObjectsSuite) TestObjectsCharmsServedSecurely(c *gc.C) {
   104  	url := s.objectsCharmsURL("")
   105  	url.Scheme = "http"
   106  	apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{
   107  		Method:       "GET",
   108  		URL:          url.String(),
   109  		ExpectStatus: http.StatusBadRequest,
   110  	})
   111  }
   112  
   113  type putObjectsSuite struct {
   114  	objectsSuite
   115  }
   116  
   117  var _ = gc.Suite(&putObjectsSuite{})
   118  
   119  func (s *putObjectsSuite) SetUpSuite(c *gc.C) {
   120  	s.objectsSuite.SetUpSuite(c)
   121  	s.objectsSuite.method = "PUT"
   122  	s.objectsSuite.contentType = "application/zip"
   123  }
   124  
   125  func (s *putObjectsSuite) uploadRequest(c *gc.C, url, contentType, curl string, content io.Reader) *http.Response {
   126  	return s.sendHTTPRequest(c, apitesting.HTTPRequestParams{
   127  		Method:      "PUT",
   128  		URL:         url,
   129  		ContentType: contentType,
   130  		Body:        content,
   131  		ExtraHeaders: map[string]string{
   132  			"Juju-Curl": curl,
   133  		},
   134  	})
   135  }
   136  
   137  func (s *putObjectsSuite) assertUploadResponse(c *gc.C, resp *http.Response, expCharmURL string) {
   138  	charmResponse := s.assertResponse(c, resp, http.StatusOK)
   139  	c.Check(charmResponse.Error, gc.Equals, "")
   140  	c.Check(charmResponse.CharmURL, gc.Equals, expCharmURL)
   141  }
   142  
   143  func (s *putObjectsSuite) TestUploadFailsWithInvalidZip(c *gc.C) {
   144  	empty := strings.NewReader("")
   145  
   146  	// Pretend we upload a zip by setting the Content-Type, so we can
   147  	// check the error at extraction time later.
   148  	resp := s.uploadRequest(c, s.objectsCharmsURI("somecharm-"+getCharmHash(c, empty)), "application/zip", "local:somecharm", empty)
   149  	s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*zip: not a valid zip file$")
   150  
   151  	// Now try with the default Content-Type.
   152  	resp = s.uploadRequest(c, s.objectsCharmsURI("somecharm-"+getCharmHash(c, empty)), "application/octet-stream", "local:somecharm", empty)
   153  	s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip, got: application/octet-stream$")
   154  }
   155  
   156  func (s *putObjectsSuite) TestCannotUploadCharmhubCharm(c *gc.C) {
   157  	// We should run verifications like this before processing the charm.
   158  	empty := strings.NewReader("")
   159  	resp := s.uploadRequest(c, s.objectsCharmsURI("somecharm-"+getCharmHash(c, empty)), "application/zip", "ch:somecharm", empty)
   160  	s.assertErrorResponse(c, resp, http.StatusBadRequest, `.*non-local charms may only be uploaded during model migration import`)
   161  }
   162  
   163  func (s *putObjectsSuite) TestUploadBumpsRevision(c *gc.C) {
   164  	// Add the dummy charm with revision 1.
   165  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   166  	curl := fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision())
   167  	info := state.CharmInfo{
   168  		Charm:       ch,
   169  		ID:          curl,
   170  		StoragePath: "dummy-storage-path",
   171  		SHA256:      "dummy-1-sha256",
   172  	}
   173  	_, err := s.State.AddCharm(info)
   174  	c.Assert(err, jc.ErrorIsNil)
   175  
   176  	// Now try uploading the same revision and verify it gets bumped,
   177  	// and the BundleSha256 is calculated.
   178  	f, err := os.Open(ch.Path)
   179  	c.Assert(err, jc.ErrorIsNil)
   180  	defer f.Close()
   181  	resp := s.uploadRequest(c, s.objectsCharmsURI("dummy-"+getCharmHash(c, f)), "application/zip", "local:quantal/dummy", f)
   182  	expectedURL := "local:quantal/dummy-2"
   183  	s.assertUploadResponse(c, resp, expectedURL)
   184  	sch, err := s.State.Charm(expectedURL)
   185  	c.Assert(err, jc.ErrorIsNil)
   186  	c.Assert(sch.URL(), gc.Equals, expectedURL)
   187  	c.Assert(sch.Revision(), gc.Equals, 2)
   188  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   189  	// No more checks for the hash here, because it is
   190  	// verified in TestUploadRespectsLocalRevision.
   191  	c.Assert(sch.BundleSha256(), gc.Not(gc.Equals), "")
   192  }
   193  
   194  func getCharmHash(c *gc.C, stream io.ReadSeeker) string {
   195  	hash := sha256.New()
   196  	_, err := io.Copy(hash, stream)
   197  	c.Assert(err, jc.ErrorIsNil)
   198  	_, err = stream.Seek(0, os.SEEK_SET)
   199  	c.Assert(err, jc.ErrorIsNil)
   200  	return hex.EncodeToString(hash.Sum(nil))[0:7]
   201  }