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

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver_test
     5  
     6  import (
     7  	"bytes"
     8  	"crypto/sha256"
     9  	"encoding/hex"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"mime"
    14  	"net/http"
    15  	"net/url"
    16  	"os"
    17  	"path/filepath"
    18  
    19  	"github.com/juju/charm/v12"
    20  	jc "github.com/juju/testing/checkers"
    21  	"github.com/juju/utils/v3"
    22  	gc "gopkg.in/check.v1"
    23  
    24  	"github.com/juju/juju/apiserver/common"
    25  	apitesting "github.com/juju/juju/apiserver/testing"
    26  	jujutesting "github.com/juju/juju/juju/testing"
    27  	"github.com/juju/juju/rpc/params"
    28  	"github.com/juju/juju/state"
    29  	"github.com/juju/juju/state/storage"
    30  	"github.com/juju/juju/testcharms"
    31  	"github.com/juju/juju/testing/factory"
    32  )
    33  
    34  type charmsSuite struct {
    35  	apiserverBaseSuite
    36  }
    37  
    38  var _ = gc.Suite(&charmsSuite{})
    39  
    40  func (s *charmsSuite) charmsURL(query string) *url.URL {
    41  	url := s.URL(fmt.Sprintf("/model/%s/charms", s.State.ModelUUID()), nil)
    42  	url.RawQuery = query
    43  	return url
    44  }
    45  
    46  func (s *charmsSuite) charmsURI(query string) string {
    47  	if query != "" && query[0] == '?' {
    48  		query = query[1:]
    49  	}
    50  	return s.charmsURL(query).String()
    51  }
    52  
    53  func (s *charmsSuite) uploadRequest(c *gc.C, url, contentType string, content io.Reader) *http.Response {
    54  	return s.sendHTTPRequest(c, apitesting.HTTPRequestParams{
    55  		Method:      "POST",
    56  		URL:         url,
    57  		ContentType: contentType,
    58  		Body:        content,
    59  	})
    60  }
    61  
    62  func (s *charmsSuite) assertUploadResponse(c *gc.C, resp *http.Response, expCharmURL string) {
    63  	charmResponse := s.assertResponse(c, resp, http.StatusOK)
    64  	c.Check(charmResponse.Error, gc.Equals, "")
    65  	c.Check(charmResponse.CharmURL, gc.Equals, expCharmURL)
    66  }
    67  
    68  func (s *charmsSuite) assertGetFileResponse(c *gc.C, resp *http.Response, expBody, expContentType string) {
    69  	body := apitesting.AssertResponse(c, resp, http.StatusOK, expContentType)
    70  	c.Check(string(body), gc.Equals, expBody)
    71  }
    72  
    73  func (s *charmsSuite) assertGetFileListResponse(c *gc.C, resp *http.Response, expFiles []string) {
    74  	charmResponse := s.assertResponse(c, resp, http.StatusOK)
    75  	c.Check(charmResponse.Error, gc.Equals, "")
    76  	c.Check(charmResponse.Files, gc.DeepEquals, expFiles)
    77  }
    78  
    79  func (s *charmsSuite) assertErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) {
    80  	charmResponse := s.assertResponse(c, resp, expCode)
    81  	c.Check(charmResponse.Error, gc.Matches, expError)
    82  }
    83  
    84  func (s *charmsSuite) assertResponse(c *gc.C, resp *http.Response, expStatus int) params.CharmsResponse {
    85  	body := apitesting.AssertResponse(c, resp, expStatus, params.ContentTypeJSON)
    86  	var charmResponse params.CharmsResponse
    87  	err := json.Unmarshal(body, &charmResponse)
    88  	c.Assert(err, jc.ErrorIsNil, gc.Commentf("body: %s", body))
    89  	return charmResponse
    90  }
    91  
    92  func (s *charmsSuite) setModelImporting(c *gc.C) {
    93  	model, err := s.State.Model()
    94  	c.Assert(err, jc.ErrorIsNil)
    95  	err = model.SetMigrationMode(state.MigrationModeImporting)
    96  	c.Assert(err, jc.ErrorIsNil)
    97  }
    98  
    99  func (s *charmsSuite) SetUpSuite(c *gc.C) {
   100  	s.apiserverBaseSuite.SetUpSuite(c)
   101  }
   102  
   103  func (s *charmsSuite) TestCharmsServedSecurely(c *gc.C) {
   104  	url := s.charmsURL("")
   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  func (s *charmsSuite) TestPOSTRequiresAuth(c *gc.C) {
   114  	resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.charmsURI("")})
   115  	body := apitesting.AssertResponse(c, resp, http.StatusUnauthorized, "text/plain; charset=utf-8")
   116  	c.Assert(string(body), gc.Equals, "authentication failed: no credentials provided\n")
   117  }
   118  
   119  func (s *charmsSuite) TestGETRequiresAuth(c *gc.C) {
   120  	resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: s.charmsURI("")})
   121  	body := apitesting.AssertResponse(c, resp, http.StatusUnauthorized, "text/plain; charset=utf-8")
   122  	c.Assert(string(body), gc.Equals, "authentication failed: no credentials provided\n")
   123  }
   124  
   125  func (s *charmsSuite) TestRequiresPOSTorGET(c *gc.C) {
   126  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "PUT", URL: s.charmsURI("")})
   127  	body := apitesting.AssertResponse(c, resp, http.StatusMethodNotAllowed, "text/plain; charset=utf-8")
   128  	c.Assert(string(body), gc.Equals, "Method Not Allowed\n")
   129  }
   130  
   131  func (s *charmsSuite) TestPOSTRequiresUserAuth(c *gc.C) {
   132  	// Add a machine and try to login.
   133  	machine, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{
   134  		Nonce: "noncy",
   135  	})
   136  	resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{
   137  		Tag:         machine.Tag().String(),
   138  		Password:    password,
   139  		Method:      "POST",
   140  		URL:         s.charmsURI(""),
   141  		Nonce:       "noncy",
   142  		ContentType: "foo/bar",
   143  	})
   144  	body := apitesting.AssertResponse(c, resp, http.StatusForbidden, "text/plain; charset=utf-8")
   145  	c.Assert(string(body), gc.Equals, "authorization failed: tag kind machine not valid\n")
   146  
   147  	// Now try a user login.
   148  	resp = s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.charmsURI("")})
   149  	s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip.+")
   150  }
   151  
   152  func (s *charmsSuite) TestUploadFailsWithInvalidZip(c *gc.C) {
   153  	var empty bytes.Buffer
   154  
   155  	// Pretend we upload a zip by setting the Content-Type, so we can
   156  	// check the error at extraction time later.
   157  	resp := s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &empty)
   158  	s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*cannot open charm archive: zip: not a valid zip file$")
   159  
   160  	// Now try with the default Content-Type.
   161  	resp = s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/octet-stream", &empty)
   162  	s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip, got: application/octet-stream$")
   163  }
   164  
   165  func (s *charmsSuite) TestUploadBumpsRevision(c *gc.C) {
   166  	// Add the dummy charm with revision 1.
   167  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   168  	curl := fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision())
   169  	info := state.CharmInfo{
   170  		Charm:       ch,
   171  		ID:          curl,
   172  		StoragePath: "dummy-storage-path",
   173  		SHA256:      "dummy-1-sha256",
   174  	}
   175  	_, err := s.State.AddCharm(info)
   176  	c.Assert(err, jc.ErrorIsNil)
   177  
   178  	// Now try uploading the same revision and verify it gets bumped,
   179  	// and the BundleSha256 is calculated.
   180  	f, err := os.Open(ch.Path)
   181  	c.Assert(err, jc.ErrorIsNil)
   182  	defer f.Close()
   183  	resp := s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", f)
   184  	expectedURL := "local:quantal/dummy-2"
   185  	s.assertUploadResponse(c, resp, expectedURL)
   186  	sch, err := s.State.Charm(expectedURL)
   187  	c.Assert(err, jc.ErrorIsNil)
   188  	c.Assert(sch.URL(), gc.Equals, expectedURL)
   189  	c.Assert(sch.Revision(), gc.Equals, 2)
   190  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   191  	// No more checks for the hash here, because it is
   192  	// verified in TestUploadRespectsLocalRevision.
   193  	c.Assert(sch.BundleSha256(), gc.Not(gc.Equals), "")
   194  }
   195  
   196  func (s *charmsSuite) TestUploadVersion(c *gc.C) {
   197  	expectedVersion := "dummy-146-g725cfd3-dirty"
   198  
   199  	// Add the dummy charm with version "juju-2.4-beta3-146-g725cfd3-dirty".
   200  	pathToArchive := testcharms.Repo.CharmArchivePath(c.MkDir(), "dummy")
   201  	err := testcharms.InjectFilesToCharmArchive(pathToArchive, map[string]string{
   202  		"version": expectedVersion,
   203  	})
   204  	c.Assert(err, gc.IsNil)
   205  	ch, err := charm.ReadCharmArchive(pathToArchive)
   206  	c.Assert(err, gc.IsNil)
   207  
   208  	f, err := os.Open(ch.Path)
   209  	c.Assert(err, jc.ErrorIsNil)
   210  	defer f.Close()
   211  	resp := s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", f)
   212  
   213  	inputURL := "local:quantal/dummy-1"
   214  	s.assertUploadResponse(c, resp, inputURL)
   215  	sch, err := s.State.Charm(inputURL)
   216  	c.Assert(err, jc.ErrorIsNil)
   217  
   218  	version := sch.Version()
   219  	c.Assert(version, gc.Equals, expectedVersion)
   220  }
   221  
   222  func (s *charmsSuite) TestUploadRespectsLocalRevision(c *gc.C) {
   223  	// Make a dummy charm dir with revision 123.
   224  	dir := testcharms.Repo.ClonedDir(c.MkDir(), "dummy")
   225  	dir.SetDiskRevision(123)
   226  	// Now bundle the dir.
   227  	var buf bytes.Buffer
   228  	err := dir.ArchiveTo(&buf)
   229  	c.Assert(err, jc.ErrorIsNil)
   230  	hash := sha256.New()
   231  	hash.Write(buf.Bytes())
   232  	expectedSHA256 := hex.EncodeToString(hash.Sum(nil))
   233  
   234  	// Now try uploading it and ensure the revision persists.
   235  	resp := s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &buf)
   236  	expectedURL := "local:quantal/dummy-123"
   237  	s.assertUploadResponse(c, resp, expectedURL)
   238  	sch, err := s.State.Charm(expectedURL)
   239  	c.Assert(err, jc.ErrorIsNil)
   240  	c.Assert(sch.URL(), gc.Equals, expectedURL)
   241  	c.Assert(sch.Revision(), gc.Equals, 123)
   242  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   243  	c.Assert(sch.BundleSha256(), gc.Equals, expectedSHA256)
   244  
   245  	storage := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession())
   246  	reader, _, err := storage.Get(sch.StoragePath())
   247  	c.Assert(err, jc.ErrorIsNil)
   248  	defer reader.Close()
   249  	downloadedSHA256, _, err := utils.ReadSHA256(reader)
   250  	c.Assert(err, jc.ErrorIsNil)
   251  	c.Assert(downloadedSHA256, gc.Equals, expectedSHA256)
   252  }
   253  
   254  func (s *charmsSuite) TestUploadWithMultiSeriesCharm(c *gc.C) {
   255  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   256  	resp := s.uploadRequest(c, s.charmsURL("").String(), "application/zip", &fileReader{path: ch.Path})
   257  	expectedURL := "local:dummy-1"
   258  	s.assertUploadResponse(c, resp, expectedURL)
   259  }
   260  
   261  func (s *charmsSuite) TestUploadAllowsTopLevelPath(c *gc.C) {
   262  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   263  	// Backwards compatibility check, that we can upload charms to
   264  	// https://host:port/charms
   265  	url := s.charmsURL("series=quantal")
   266  	url.Path = "/charms"
   267  	resp := s.uploadRequest(c, url.String(), "application/zip", &fileReader{path: ch.Path})
   268  	expectedURL := "local:quantal/dummy-1"
   269  	s.assertUploadResponse(c, resp, expectedURL)
   270  }
   271  
   272  func (s *charmsSuite) TestUploadAllowsModelUUIDPath(c *gc.C) {
   273  	// Check that we can upload charms to https://host:port/ModelUUID/charms
   274  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   275  	url := s.charmsURL("series=quantal")
   276  	resp := s.uploadRequest(c, url.String(), "application/zip", &fileReader{path: ch.Path})
   277  	expectedURL := "local:quantal/dummy-1"
   278  	s.assertUploadResponse(c, resp, expectedURL)
   279  }
   280  
   281  func (s *charmsSuite) TestUploadAllowsOtherModelUUIDPath(c *gc.C) {
   282  	newSt := s.Factory.MakeModel(c, nil)
   283  	defer newSt.Close()
   284  
   285  	// Check that we can upload charms to https://host:port/ModelUUID/charms
   286  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   287  	url := s.charmsURL("series=quantal")
   288  	url.Path = fmt.Sprintf("/model/%s/charms", newSt.ModelUUID())
   289  	resp := s.uploadRequest(c, url.String(), "application/zip", &fileReader{path: ch.Path})
   290  	expectedURL := "local:quantal/dummy-1"
   291  	s.assertUploadResponse(c, resp, expectedURL)
   292  }
   293  
   294  func (s *charmsSuite) TestUploadRepackagesNestedArchives(c *gc.C) {
   295  	// Make a clone of the dummy charm in a nested directory.
   296  	rootDir := c.MkDir()
   297  	dirPath := filepath.Join(rootDir, "subdir1", "subdir2")
   298  	err := os.MkdirAll(dirPath, 0755)
   299  	c.Assert(err, jc.ErrorIsNil)
   300  	dir := testcharms.Repo.ClonedDir(dirPath, "dummy")
   301  	// Now tweak the path the dir thinks it is in and bundle it.
   302  	dir.Path = rootDir
   303  	var buf bytes.Buffer
   304  	err = dir.ArchiveTo(&buf)
   305  	c.Assert(err, jc.ErrorIsNil)
   306  
   307  	// Try reading it as a bundle - should fail due to nested dirs.
   308  	_, err = charm.ReadCharmArchiveBytes(buf.Bytes())
   309  	c.Assert(err, gc.ErrorMatches, `archive file "metadata.yaml" not found`)
   310  
   311  	// Now try uploading it - should succeed and be repackaged.
   312  	resp := s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &buf)
   313  	expectedURL := "local:quantal/dummy-1"
   314  	s.assertUploadResponse(c, resp, expectedURL)
   315  	sch, err := s.State.Charm(expectedURL)
   316  	c.Assert(err, jc.ErrorIsNil)
   317  	c.Assert(sch.URL(), gc.Equals, expectedURL)
   318  	c.Assert(sch.Revision(), gc.Equals, 1)
   319  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   320  
   321  	// Get it from the storage and try to read it as a bundle - it
   322  	// should succeed, because it was repackaged during upload to
   323  	// strip nested dirs.
   324  	storage := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession())
   325  	reader, _, err := storage.Get(sch.StoragePath())
   326  	c.Assert(err, jc.ErrorIsNil)
   327  	defer reader.Close()
   328  
   329  	data, err := io.ReadAll(reader)
   330  	c.Assert(err, jc.ErrorIsNil)
   331  	downloadedFile, err := os.CreateTemp(c.MkDir(), "downloaded")
   332  	c.Assert(err, jc.ErrorIsNil)
   333  	defer downloadedFile.Close()
   334  	defer os.Remove(downloadedFile.Name())
   335  	err = os.WriteFile(downloadedFile.Name(), data, 0644)
   336  	c.Assert(err, jc.ErrorIsNil)
   337  
   338  	bundle, err := charm.ReadCharmArchive(downloadedFile.Name())
   339  	c.Assert(err, jc.ErrorIsNil)
   340  	c.Assert(bundle.Revision(), jc.DeepEquals, sch.Revision())
   341  	c.Assert(bundle.Meta(), jc.DeepEquals, sch.Meta())
   342  	c.Assert(bundle.Config(), jc.DeepEquals, sch.Config())
   343  }
   344  
   345  func (s *charmsSuite) TestNonLocalCharmUploadFailsIfNotMigrating(c *gc.C) {
   346  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   347  	curl := fmt.Sprintf("ch:quantal/%s-%d", ch.Meta().Name, ch.Revision())
   348  	info := state.CharmInfo{
   349  		Charm:       ch,
   350  		ID:          curl,
   351  		StoragePath: "dummy-storage-path",
   352  		SHA256:      "dummy-1-sha256",
   353  	}
   354  	_, err := s.State.AddCharm(info)
   355  	c.Assert(err, jc.ErrorIsNil)
   356  
   357  	resp := s.uploadRequest(c, s.charmsURI("?schema=ch&series=quantal"), "application/zip", &fileReader{path: ch.Path})
   358  	s.assertErrorResponse(c, resp, 400, ".*charms may only be uploaded during model migration import$")
   359  }
   360  
   361  func (s *charmsSuite) TestNonLocalCharmUpload(c *gc.C) {
   362  	// Check that upload of charms with the "ch:" schema works (for
   363  	// model migrations).
   364  	s.setModelImporting(c)
   365  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   366  
   367  	resp := s.uploadRequest(c, s.charmsURI("?schema=ch&series=quantal"), "application/zip", &fileReader{path: ch.Path})
   368  
   369  	expectedURL := "ch:quantal/dummy-1"
   370  	s.assertUploadResponse(c, resp, expectedURL)
   371  	sch, err := s.State.Charm(expectedURL)
   372  	c.Assert(err, jc.ErrorIsNil)
   373  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   374  	c.Assert(sch.Revision(), gc.Equals, 1)
   375  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   376  }
   377  
   378  func (s *charmsSuite) TestCharmHubCharmUpload(c *gc.C) {
   379  	// Check that upload of charms with the "ch:" schema works (for
   380  	// model migrations).
   381  	s.setModelImporting(c)
   382  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   383  	expectedURL := "ch:s390x/bionic/dummy-15"
   384  	info := state.CharmInfo{
   385  		Charm:       ch,
   386  		ID:          expectedURL,
   387  		StoragePath: "dummy-storage-path",
   388  		SHA256:      "dummy-1-sha256",
   389  	}
   390  	_, err := s.State.AddCharm(info)
   391  	c.Assert(err, jc.ErrorIsNil)
   392  
   393  	resp := s.uploadRequest(c, s.charmsURI("?arch=s390x&revision=15&schema=ch&series=bionic"), "application/zip", &fileReader{path: ch.Path})
   394  
   395  	s.assertUploadResponse(c, resp, expectedURL)
   396  	sch, err := s.State.Charm(expectedURL)
   397  	c.Assert(err, jc.ErrorIsNil)
   398  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   399  	c.Assert(sch.Revision(), gc.Equals, 15)
   400  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   401  }
   402  
   403  func (s *charmsSuite) TestUnsupportedSchema(c *gc.C) {
   404  	s.setModelImporting(c)
   405  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   406  
   407  	resp := s.uploadRequest(c, s.charmsURI("?schema=zz"), "application/zip", &fileReader{path: ch.Path})
   408  	s.assertErrorResponse(
   409  		c, resp, http.StatusBadRequest,
   410  		`cannot upload charm: unsupported schema "zz"`,
   411  	)
   412  }
   413  
   414  func (s *charmsSuite) TestCharmUploadWithUserOverride(c *gc.C) {
   415  	s.setModelImporting(c)
   416  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   417  
   418  	resp := s.uploadRequest(c, s.charmsURI("?schema=ch"), "application/zip", &fileReader{path: ch.Path})
   419  
   420  	expectedURL := "ch:dummy-1"
   421  	s.assertUploadResponse(c, resp, expectedURL)
   422  	sch, err := s.State.Charm(expectedURL)
   423  	c.Assert(err, jc.ErrorIsNil)
   424  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   425  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   426  }
   427  
   428  func (s *charmsSuite) TestNonLocalCharmUploadWithRevisionOverride(c *gc.C) {
   429  	s.setModelImporting(c)
   430  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   431  
   432  	resp := s.uploadRequest(c, s.charmsURI("?schema=ch&&revision=99"), "application/zip", &fileReader{path: ch.Path})
   433  
   434  	expectedURL := "ch:dummy-99"
   435  	s.assertUploadResponse(c, resp, expectedURL)
   436  	sch, err := s.State.Charm(expectedURL)
   437  	c.Assert(err, jc.ErrorIsNil)
   438  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   439  	c.Assert(sch.Revision(), gc.Equals, 99)
   440  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   441  }
   442  
   443  func (s *charmsSuite) TestMigrateCharm(c *gc.C) {
   444  	newSt := s.Factory.MakeModel(c, nil)
   445  	defer newSt.Close()
   446  	importedModel, err := newSt.Model()
   447  	c.Assert(err, jc.ErrorIsNil)
   448  	err = importedModel.SetMigrationMode(state.MigrationModeImporting)
   449  	c.Assert(err, jc.ErrorIsNil)
   450  
   451  	// The default user is just a normal user, not a controller admin
   452  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   453  	url := s.charmsURL("series=quantal")
   454  	url.Path = "/migrate/charms"
   455  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{
   456  		Method:      "POST",
   457  		URL:         url.String(),
   458  		ContentType: "application/zip",
   459  		Body:        &fileReader{path: ch.Path},
   460  		ExtraHeaders: map[string]string{
   461  			params.MigrationModelHTTPHeader: importedModel.UUID(),
   462  		},
   463  	})
   464  	expectedURL := "local:quantal/dummy-1"
   465  	s.assertUploadResponse(c, resp, expectedURL)
   466  
   467  	// The charm was added to the migrated model.
   468  	_, err = newSt.Charm(expectedURL)
   469  	c.Assert(err, jc.ErrorIsNil)
   470  }
   471  
   472  func (s *charmsSuite) TestMigrateCharmName(c *gc.C) {
   473  	newSt := s.Factory.MakeModel(c, nil)
   474  	defer newSt.Close()
   475  	importedModel, err := newSt.Model()
   476  	c.Assert(err, jc.ErrorIsNil)
   477  	err = importedModel.SetMigrationMode(state.MigrationModeImporting)
   478  	c.Assert(err, jc.ErrorIsNil)
   479  
   480  	// The default user is just a normal user, not a controller admin
   481  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   482  	url := s.charmsURL("series=quantal&name=meshuggah")
   483  	url.Path = "/migrate/charms"
   484  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{
   485  		Method:      "POST",
   486  		URL:         url.String(),
   487  		ContentType: "application/zip",
   488  		Body:        &fileReader{path: ch.Path},
   489  		ExtraHeaders: map[string]string{
   490  			params.MigrationModelHTTPHeader: importedModel.UUID(),
   491  		},
   492  	})
   493  	expectedURL := "local:quantal/meshuggah-1"
   494  	s.assertUploadResponse(c, resp, expectedURL)
   495  
   496  	// The charm was added to the migrated model.
   497  	_, err = newSt.Charm(expectedURL)
   498  	c.Assert(err, jc.ErrorIsNil)
   499  }
   500  
   501  func (s *charmsSuite) TestMigrateCharmNotMigrating(c *gc.C) {
   502  	migratedModel := s.Factory.MakeModel(c, nil)
   503  	defer migratedModel.Close()
   504  
   505  	// The default user is just a normal user, not a controller admin
   506  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   507  	url := s.charmsURL("series=quantal")
   508  	url.Path = "/migrate/charms"
   509  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{
   510  		Method:      "POST",
   511  		URL:         url.String(),
   512  		ContentType: "application/zip",
   513  		Body:        &fileReader{path: ch.Path},
   514  		ExtraHeaders: map[string]string{
   515  			params.MigrationModelHTTPHeader: migratedModel.ModelUUID(),
   516  		},
   517  	})
   518  	s.assertErrorResponse(
   519  		c, resp, http.StatusBadRequest,
   520  		`cannot upload charm: model migration mode is "" instead of "importing"`,
   521  	)
   522  }
   523  
   524  func (s *charmsSuite) TestMigrateCharmUnauthorized(c *gc.C) {
   525  	user := s.Factory.MakeUser(c, &factory.UserParams{Password: "hunter2"})
   526  	url := s.charmsURL("series=quantal")
   527  	url.Path = "/migrate/charms"
   528  	resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{
   529  		Method:   "POST",
   530  		URL:      url.String(),
   531  		Tag:      user.Tag().String(),
   532  		Password: "hunter2",
   533  	})
   534  	body := apitesting.AssertResponse(c, resp, http.StatusForbidden, "text/plain; charset=utf-8")
   535  	c.Assert(string(body), gc.Matches, "authorization failed: user .* not a controller admin\n")
   536  }
   537  
   538  func (s *charmsSuite) TestGetRequiresCharmURL(c *gc.C) {
   539  	uri := s.charmsURI("?file=hooks/install")
   540  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri})
   541  	s.assertErrorResponse(
   542  		c, resp, http.StatusBadRequest,
   543  		".*expected url=CharmURL query argument$",
   544  	)
   545  }
   546  
   547  func (s *charmsSuite) TestGetFailsWithInvalidCharmURL(c *gc.C) {
   548  	uri := s.charmsURI("?url=local:precise/no-such")
   549  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri})
   550  	s.assertErrorResponse(
   551  		c, resp, http.StatusNotFound,
   552  		`.*cannot get charm from state: charm "local:precise/no-such" not found$`,
   553  	)
   554  }
   555  
   556  func (s *charmsSuite) TestGetReturnsNotFoundWhenMissing(c *gc.C) {
   557  	// Add the dummy charm.
   558  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   559  	s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path})
   560  
   561  	// Ensure a 404 is returned for files not included in the charm.
   562  	for i, file := range []string{
   563  		"no-such-file", "..", "../../../etc/passwd", "hooks/delete",
   564  	} {
   565  		c.Logf("test %d: %s", i, file)
   566  		uri := s.charmsURI("?url=local:quantal/dummy-1&file=" + file)
   567  		resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri})
   568  		c.Assert(resp.StatusCode, gc.Equals, http.StatusNotFound)
   569  	}
   570  }
   571  
   572  func (s *charmsSuite) TestGetReturnsNotYetAvailableForPendingCharms(c *gc.C) {
   573  	// Add a charm in pending mode.
   574  	chInfo := state.CharmInfo{
   575  		ID:          "ch:focal/dummy-1",
   576  		Charm:       testcharms.Repo.CharmArchive(c.MkDir(), "dummy"),
   577  		StoragePath: "", // indicates that we don't have the data in the blobstore yet.
   578  		SHA256:      "", // indicates that we don't have the data in the blobstore yet.
   579  		Version:     "42",
   580  	}
   581  	_, err := s.State.AddCharmMetadata(chInfo)
   582  	c.Assert(err, jc.ErrorIsNil)
   583  
   584  	// Ensure a 490 is returned if the charm is pending to be downloaded.
   585  	uri := s.charmsURI("?url=ch:focal/dummy-1")
   586  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri})
   587  	c.Assert(resp.StatusCode, gc.Equals, http.StatusConflict, gc.Commentf("expected to get 409 for charm that is pending to be downloaded"))
   588  }
   589  
   590  func (s *charmsSuite) TestGetReturnsForbiddenWithDirectory(c *gc.C) {
   591  	// Add the dummy charm.
   592  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   593  	s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path})
   594  
   595  	// Ensure a 403 is returned if the requested file is a directory.
   596  	uri := s.charmsURI("?url=local:quantal/dummy-1&file=hooks")
   597  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri})
   598  	c.Assert(resp.StatusCode, gc.Equals, http.StatusForbidden)
   599  }
   600  
   601  func (s *charmsSuite) TestGetReturnsFileContents(c *gc.C) {
   602  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   603  	s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path})
   604  
   605  	// Ensure the file contents are properly returned.
   606  	for i, t := range []struct {
   607  		summary  string
   608  		file     string
   609  		response string
   610  	}{{
   611  		summary:  "relative path",
   612  		file:     "revision",
   613  		response: "1",
   614  	}, {
   615  		summary:  "exotic path",
   616  		file:     "./hooks/../revision",
   617  		response: "1",
   618  	}, {
   619  		summary:  "sub-directory path",
   620  		file:     "hooks/install",
   621  		response: "#!/bin/bash\necho \"Done!\"\n",
   622  	},
   623  	} {
   624  		c.Logf("test %d: %s", i, t.summary)
   625  		uri := s.charmsURI("?url=local:quantal/dummy-1&file=" + t.file)
   626  		resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri})
   627  		s.assertGetFileResponse(c, resp, t.response, "text/plain; charset=utf-8")
   628  	}
   629  }
   630  
   631  func (s *charmsSuite) TestGetCharmIcon(c *gc.C) {
   632  	// Upload the local charms.
   633  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "mysql")
   634  	s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path})
   635  	ch = testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   636  	s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path})
   637  
   638  	// Prepare the tests.
   639  	svgMimeType := mime.TypeByExtension(".svg")
   640  	iconPath := filepath.Join(testcharms.Repo.CharmDirPath("mysql"), "icon.svg")
   641  	icon, err := os.ReadFile(iconPath)
   642  	c.Assert(err, jc.ErrorIsNil)
   643  	tests := []struct {
   644  		about      string
   645  		query      string
   646  		expectType string
   647  		expectBody string
   648  	}{{
   649  		about:      "icon found",
   650  		query:      "?url=local:quantal/mysql-1&file=icon.svg",
   651  		expectBody: string(icon),
   652  	}, {
   653  		about: "icon not found",
   654  		query: "?url=local:quantal/dummy-1&file=icon.svg",
   655  	}, {
   656  		about:      "default icon requested: icon found",
   657  		query:      "?url=local:quantal/mysql-1&icon=1",
   658  		expectBody: string(icon),
   659  	}, {
   660  		about:      "default icon requested: icon not found",
   661  		query:      "?url=local:quantal/dummy-1&icon=1",
   662  		expectBody: common.DefaultCharmIcon,
   663  	}, {
   664  		about:      "default icon request ignored",
   665  		query:      "?url=local:quantal/mysql-1&file=revision&icon=1",
   666  		expectType: "text/plain; charset=utf-8",
   667  		expectBody: "1",
   668  	}}
   669  
   670  	for i, test := range tests {
   671  		c.Logf("\ntest %d: %s", i, test.about)
   672  		uri := s.charmsURI(test.query)
   673  		resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri})
   674  		if test.expectBody == "" {
   675  			s.assertErrorResponse(c, resp, http.StatusNotFound, ".*charm file not found$")
   676  			continue
   677  		}
   678  		if test.expectType == "" {
   679  			test.expectType = svgMimeType
   680  		}
   681  		s.assertGetFileResponse(c, resp, test.expectBody, test.expectType)
   682  	}
   683  }
   684  
   685  func (s *charmsSuite) TestGetWorksForControllerMachines(c *gc.C) {
   686  	// Make a controller machine.
   687  	const nonce = "noncey"
   688  	m, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{
   689  		Jobs:  []state.MachineJob{state.JobManageModel},
   690  		Nonce: nonce,
   691  	})
   692  
   693  	// Create a hosted model and upload a charm for it.
   694  	newSt := s.Factory.MakeModel(c, nil)
   695  	defer newSt.Close()
   696  
   697  	curl := charm.MustParseURL("local:quantal/dummy-1")
   698  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   699  	_, err := jujutesting.AddCharm(newSt, curl, ch, false)
   700  	c.Assert(err, jc.ErrorIsNil)
   701  
   702  	// Controller machine should be able to download the charm from
   703  	// the hosted model. This is required for controller workers which
   704  	// are acting on behalf of a particular hosted model.
   705  	url := s.charmsURL("url=" + curl.String() + "&file=revision")
   706  	url.Path = fmt.Sprintf("/model/%s/charms", newSt.ModelUUID())
   707  	params := apitesting.HTTPRequestParams{
   708  		Method:   "GET",
   709  		URL:      url.String(),
   710  		Tag:      m.Tag().String(),
   711  		Password: password,
   712  		Nonce:    nonce,
   713  	}
   714  	resp := apitesting.SendHTTPRequest(c, params)
   715  	s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
   716  }
   717  
   718  func (s *charmsSuite) TestGetStarReturnsArchiveBytes(c *gc.C) {
   719  	// Add the dummy charm.
   720  	ch, err := charm.ReadCharmDir(
   721  		testcharms.RepoWithSeries("quantal").ClonedDirPath(c.MkDir(), "dummy"))
   722  	c.Assert(err, jc.ErrorIsNil)
   723  	// Create an archive from the charm dir.
   724  	tempFile, err := os.CreateTemp(c.MkDir(), "charm")
   725  	c.Assert(err, jc.ErrorIsNil)
   726  	defer tempFile.Close()
   727  	defer os.Remove(tempFile.Name())
   728  	err = ch.ArchiveTo(tempFile)
   729  	c.Assert(err, jc.ErrorIsNil)
   730  	s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: tempFile.Name()})
   731  
   732  	data, err := os.ReadFile(tempFile.Name())
   733  	c.Assert(err, jc.ErrorIsNil)
   734  
   735  	uri := s.charmsURI("?url=local:quantal/dummy-1&file=*")
   736  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri})
   737  	s.assertGetFileResponse(c, resp, string(data), "application/zip")
   738  }
   739  
   740  func (s *charmsSuite) TestGetAllowsTopLevelPath(c *gc.C) {
   741  	// Backwards compatibility check, that we can GET from charms at
   742  	// https://host:port/charms
   743  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   744  	s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path})
   745  	url := s.charmsURL("url=local:quantal/dummy-1&file=revision")
   746  	url.Path = "/charms"
   747  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: url.String()})
   748  	s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
   749  }
   750  
   751  func (s *charmsSuite) TestGetAllowsModelUUIDPath(c *gc.C) {
   752  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   753  	s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path})
   754  	url := s.charmsURL("url=local:quantal/dummy-1&file=revision")
   755  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: url.String()})
   756  	s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
   757  }
   758  
   759  func (s *charmsSuite) TestGetAllowsOtherEnvironment(c *gc.C) {
   760  	newSt := s.Factory.MakeModel(c, nil)
   761  	defer newSt.Close()
   762  
   763  	curl := charm.MustParseURL("local:quantal/dummy-1")
   764  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   765  	_, err := jujutesting.AddCharm(newSt, curl, ch, false)
   766  	c.Assert(err, jc.ErrorIsNil)
   767  
   768  	url := s.charmsURL("url=" + curl.String() + "&file=revision")
   769  	url.Path = fmt.Sprintf("/model/%s/charms", newSt.ModelUUID())
   770  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: url.String()})
   771  	s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
   772  }
   773  
   774  func (s *charmsSuite) TestGetReturnsManifest(c *gc.C) {
   775  	// Add the dummy charm.
   776  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   777  	s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path})
   778  
   779  	// Ensure charm files are properly listed.
   780  	uri := s.charmsURI("?url=local:quantal/dummy-1")
   781  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri})
   782  	manifest, err := ch.ArchiveMembers()
   783  	c.Assert(err, jc.ErrorIsNil)
   784  	expectedFiles := manifest.SortedValues()
   785  	s.assertGetFileListResponse(c, resp, expectedFiles)
   786  	ctype := resp.Header.Get("content-type")
   787  	c.Assert(ctype, gc.Equals, params.ContentTypeJSON)
   788  }
   789  
   790  func (s *charmsSuite) TestNoTempFilesLeftBehind(c *gc.C) {
   791  	// Add the dummy charm.
   792  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   793  	s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path})
   794  
   795  	// Download it.
   796  	uri := s.charmsURI("?url=local:quantal/dummy-1&file=*")
   797  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri})
   798  	apitesting.AssertResponse(c, resp, http.StatusOK, "application/zip")
   799  
   800  	// Ensure the tmp directory exists but nothing is in it.
   801  	files, err := os.ReadDir(filepath.Join(s.config.DataDir, "charm-get-tmp"))
   802  	c.Assert(err, jc.ErrorIsNil)
   803  	c.Check(files, gc.HasLen, 0)
   804  }
   805  
   806  type fileReader struct {
   807  	path string
   808  	r    io.Reader
   809  }
   810  
   811  func (r *fileReader) Read(out []byte) (int, error) {
   812  	if r.r == nil {
   813  		content, err := os.ReadFile(r.path)
   814  		if err != nil {
   815  			return 0, err
   816  		}
   817  		r.r = bytes.NewReader(content)
   818  	}
   819  	return r.r.Read(out)
   820  }