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 }