github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/rest_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 "mime" 10 "net/http" 11 "net/url" 12 "os" 13 "path/filepath" 14 "runtime" 15 16 jc "github.com/juju/testing/checkers" 17 gc "gopkg.in/check.v1" 18 19 "github.com/juju/juju/apiserver/common" 20 apitesting "github.com/juju/juju/apiserver/testing" 21 "github.com/juju/juju/core/crossmodel" 22 "github.com/juju/juju/rpc/params" 23 "github.com/juju/juju/state" 24 "github.com/juju/juju/testcharms" 25 "github.com/juju/juju/testing/factory" 26 ) 27 28 type restCommonSuite struct { 29 apiserverBaseSuite 30 } 31 32 func (s *restCommonSuite) restURL(modelUUID, path string) *url.URL { 33 return s.URL(fmt.Sprintf("/model/%s/rest/1.0/%s", modelUUID, path), nil) 34 } 35 36 func (s *restCommonSuite) restURI(modelUUID, path string) string { 37 return s.restURL(modelUUID, path).String() 38 } 39 40 func (s *restCommonSuite) assertGetFileResponse(c *gc.C, resp *http.Response, expBody, expContentType string) { 41 body := apitesting.AssertResponse(c, resp, http.StatusOK, expContentType) 42 c.Check(string(body), gc.Equals, expBody) 43 } 44 45 func (s *restCommonSuite) assertErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) { 46 charmResponse := s.assertResponse(c, resp, expCode) 47 c.Check(charmResponse.Error, gc.Matches, expError) 48 } 49 50 func (s *restCommonSuite) assertResponse(c *gc.C, resp *http.Response, expStatus int) params.CharmsResponse { 51 body := apitesting.AssertResponse(c, resp, expStatus, params.ContentTypeJSON) 52 var charmResponse params.CharmsResponse 53 err := json.Unmarshal(body, &charmResponse) 54 c.Assert(err, jc.ErrorIsNil, gc.Commentf("body: %s", body)) 55 return charmResponse 56 } 57 58 type restSuite struct { 59 restCommonSuite 60 } 61 62 var _ = gc.Suite(&restSuite{}) 63 64 func (s *restSuite) SetUpSuite(c *gc.C) { 65 if runtime.GOOS != "linux" { 66 c.Skip("apiservers only run on linux") 67 } 68 s.restCommonSuite.SetUpSuite(c) 69 } 70 71 func (s *restSuite) TestRestServedSecurely(c *gc.C) { 72 url := s.restURL(s.State.ModelUUID(), "") 73 url.Scheme = "http" 74 apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{ 75 Method: "GET", 76 URL: url.String(), 77 ExpectStatus: http.StatusBadRequest, 78 }) 79 } 80 81 func (s *restSuite) TestGETRequiresAuth(c *gc.C) { 82 resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: s.restURI(s.State.ModelUUID(), "entity/name/attribute")}) 83 body := apitesting.AssertResponse(c, resp, http.StatusUnauthorized, "text/plain; charset=utf-8") 84 c.Assert(string(body), gc.Equals, "authentication failed: no credentials provided\n") 85 } 86 87 func (s *restSuite) TestRequiresGET(c *gc.C) { 88 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.restURI(s.State.ModelUUID(), "entity/name/attribute")}) 89 s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "POST"`) 90 } 91 92 func (s *restSuite) TestGetReturnsNotFoundWhenMissing(c *gc.C) { 93 uri := s.restURI(s.State.ModelUUID(), "remote-application/foo/attribute") 94 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri}) 95 s.assertErrorResponse( 96 c, resp, http.StatusNotFound, 97 `cannot retrieve model data: saas application "foo" not found`, 98 ) 99 } 100 101 func (s *restSuite) charmsURI(query string) string { 102 url := s.URL(fmt.Sprintf("/model/%s/charms", s.State.ModelUUID()), nil) 103 url.RawQuery = query 104 return url.String() 105 } 106 107 func (s *restSuite) TestGetRemoteApplicationIcon(c *gc.C) { 108 // Setup the charm and mysql application in the default model. 109 ch := testcharms.Repo.CharmArchive(c.MkDir(), "mysql") 110 111 file, err := os.Open(ch.Path) 112 c.Assert(err, jc.ErrorIsNil) 113 defer file.Close() 114 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ 115 Method: "POST", 116 URL: s.charmsURI("series=quantal"), 117 ContentType: "application/zip", 118 Body: file, 119 }) 120 apitesting.AssertResponse(c, resp, http.StatusOK, "application/json") 121 122 curl := fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()) 123 mysqlCh, err := s.State.Charm(curl) 124 c.Assert(err, jc.ErrorIsNil) 125 _, err = s.State.AddApplication(state.AddApplicationArgs{ 126 Name: "mysql", 127 Charm: mysqlCh, 128 CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{OS: "ubuntu", Channel: "22.04/stable"}}, 129 }) 130 c.Assert(err, jc.ErrorIsNil) 131 132 // Add an offer for the application. 133 offers := state.NewApplicationOffers(s.State) 134 offer, err := offers.AddOffer(crossmodel.AddApplicationOfferArgs{ 135 OfferName: "remote-app-offer", 136 ApplicationName: "mysql", 137 Owner: s.Owner.Id(), 138 }) 139 c.Assert(err, jc.ErrorIsNil) 140 // Set up a charm entry for dummy app with no charm in storage. 141 dummyCh := s.Factory.MakeCharm(c, &factory.CharmParams{ 142 Name: "dummy", 143 }) 144 c.Assert(err, jc.ErrorIsNil) 145 _, err = s.State.AddApplication(state.AddApplicationArgs{ 146 Name: "dummy", 147 Charm: dummyCh, 148 CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{OS: "ubuntu", Channel: "22.04/stable"}}, 149 }) 150 c.Assert(err, jc.ErrorIsNil) 151 offer2, err := offers.AddOffer(crossmodel.AddApplicationOfferArgs{ 152 OfferName: "notfound-remote-app-offer", 153 ApplicationName: "dummy", 154 Owner: s.Owner.Id(), 155 }) 156 c.Assert(err, jc.ErrorIsNil) 157 158 // Add remote applications to other model which we will query below. 159 otherModelState := s.Factory.MakeModel(c, nil) 160 defer otherModelState.Close() 161 _, err = otherModelState.AddRemoteApplication(state.AddRemoteApplicationParams{ 162 Name: "remote-app", 163 SourceModel: s.Model.ModelTag(), 164 OfferUUID: offer.OfferUUID, 165 }) 166 c.Assert(err, jc.ErrorIsNil) 167 _, err = otherModelState.AddRemoteApplication(state.AddRemoteApplicationParams{ 168 Name: "notfound-remote-app", 169 SourceModel: s.Model.ModelTag(), 170 OfferUUID: offer2.OfferUUID, 171 }) 172 c.Assert(err, jc.ErrorIsNil) 173 174 // Prepare the tests. 175 svgMimeType := mime.TypeByExtension(".svg") 176 iconPath := filepath.Join(testcharms.Repo.CharmDirPath("mysql"), "icon.svg") 177 icon, err := os.ReadFile(iconPath) 178 c.Assert(err, jc.ErrorIsNil) 179 tests := []struct { 180 about string 181 query string 182 expectType string 183 expectBody string 184 }{{ 185 about: "icon found", 186 query: "remote-application/remote-app/icon", 187 expectBody: string(icon), 188 }, { 189 about: "icon not found", 190 query: "remote-application/notfound-remote-app/icon", 191 expectBody: common.DefaultCharmIcon, 192 }} 193 194 for i, test := range tests { 195 c.Logf("\ntest %d: %s", i, test.about) 196 uri := s.restURI(otherModelState.ModelUUID(), test.query) 197 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri}) 198 if test.expectType == "" { 199 test.expectType = svgMimeType 200 } 201 s.assertGetFileResponse(c, resp, test.expectBody, test.expectType) 202 } 203 }