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  }