gitee.com/mysnapcore/mysnapd@v0.1.0/store/tooling/tooling_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2022 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package tooling_test
    21  
    22  import (
    23  	"context"
    24  	"encoding/base64"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"net/http"
    28  	"net/url"
    29  	"os"
    30  	"path/filepath"
    31  	"runtime"
    32  	"strings"
    33  	"testing"
    34  
    35  	. "gopkg.in/check.v1"
    36  
    37  	"gitee.com/mysnapcore/mysnapd/asserts"
    38  	"gitee.com/mysnapcore/mysnapd/asserts/assertstest"
    39  	"gitee.com/mysnapcore/mysnapd/logger"
    40  	"gitee.com/mysnapcore/mysnapd/osutil"
    41  	"gitee.com/mysnapcore/mysnapd/overlord/auth"
    42  	"gitee.com/mysnapcore/mysnapd/progress"
    43  	"gitee.com/mysnapcore/mysnapd/seed/seedtest"
    44  	"gitee.com/mysnapcore/mysnapd/snap"
    45  	"gitee.com/mysnapcore/mysnapd/store"
    46  	"gitee.com/mysnapcore/mysnapd/store/tooling"
    47  	"gitee.com/mysnapcore/mysnapd/testutil"
    48  )
    49  
    50  func Test(t *testing.T) { TestingT(t) }
    51  
    52  type toolingSuite struct {
    53  	testutil.BaseTest
    54  	root string
    55  
    56  	storeActionsBunchSizes []int
    57  	storeActions           []*store.SnapAction
    58  	curSnaps               [][]*store.CurrentSnap
    59  
    60  	tsto *tooling.ToolingStore
    61  
    62  	// SeedSnaps helps creating and making available seed snaps
    63  	// (it provides MakeAssertedSnap etc.) for the tests.
    64  	*seedtest.SeedSnaps
    65  }
    66  
    67  var _ = Suite(&toolingSuite{})
    68  
    69  var (
    70  	brandPrivKey, _ = assertstest.GenerateKey(752)
    71  )
    72  
    73  const packageCore = `
    74  name: core
    75  version: 16.04
    76  type: os
    77  `
    78  
    79  func (s *toolingSuite) SetUpTest(c *C) {
    80  	s.root = c.MkDir()
    81  
    82  	s.BaseTest.SetUpTest(c)
    83  	s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
    84  
    85  	s.tsto = tooling.MockToolingStore(s)
    86  
    87  	s.SeedSnaps = &seedtest.SeedSnaps{}
    88  	s.SetupAssertSigning("canonical")
    89  	s.Brands.Register("my-brand", brandPrivKey, map[string]interface{}{
    90  		"verification": "verified",
    91  	})
    92  	assertstest.AddMany(s.StoreSigning, s.Brands.AccountsAndKeys("my-brand")...)
    93  
    94  	otherAcct := assertstest.NewAccount(s.StoreSigning, "other", map[string]interface{}{
    95  		"account-id": "other",
    96  	}, "")
    97  	s.StoreSigning.Add(otherAcct)
    98  
    99  	// mock the mount cmds (for the extract kernel assets stuff)
   100  	c1 := testutil.MockCommand(c, "mount", "")
   101  	s.AddCleanup(c1.Restore)
   102  	c2 := testutil.MockCommand(c, "umount", "")
   103  	s.AddCleanup(c2.Restore)
   104  }
   105  
   106  func (s *toolingSuite) MakeAssertedSnap(c *C, snapYaml string, files [][]string, revision snap.Revision, developerID string) {
   107  	s.SeedSnaps.MakeAssertedSnap(c, snapYaml, files, revision, developerID, s.StoreSigning.Database)
   108  }
   109  
   110  func (s *toolingSuite) setupSnaps(c *C, publishers map[string]string, defaultsYaml string) {
   111  	s.MakeAssertedSnap(c, packageCore, nil, snap.R(3), "canonical")
   112  }
   113  
   114  func (s *toolingSuite) TestNewToolingStore(c *C) {
   115  	// default
   116  	u, err := url.Parse("https://api.snapcraft.io/")
   117  	c.Assert(err, IsNil)
   118  
   119  	tsto, err := tooling.NewToolingStore()
   120  	c.Assert(err, IsNil)
   121  
   122  	c.Check(tsto.StoreURL(), DeepEquals, u)
   123  }
   124  
   125  func (s *toolingSuite) TestNewToolingStoreUbuntuStoreURL(c *C) {
   126  	u, err := url.Parse("https://api.other")
   127  	c.Assert(err, IsNil)
   128  
   129  	os.Setenv("UBUNTU_STORE_URL", "https://api.other")
   130  	defer os.Unsetenv("UBUNTU_STORE_URL")
   131  
   132  	tsto, err := tooling.NewToolingStore()
   133  	c.Assert(err, IsNil)
   134  
   135  	c.Check(tsto.StoreURL(), DeepEquals, u)
   136  }
   137  
   138  func (s *toolingSuite) TestNewToolingStoreInvalidUbuntuStoreURL(c *C) {
   139  	os.Setenv("UBUNTU_STORE_URL", ":/what")
   140  	defer os.Unsetenv("UBUNTU_STORE_URL")
   141  
   142  	_, err := tooling.NewToolingStore()
   143  	c.Assert(err, ErrorMatches, `invalid UBUNTU_STORE_URL: .*`)
   144  }
   145  
   146  func (s *toolingSuite) TestNewToolingStoreWithAuthFile(c *C) {
   147  	tmpdir := c.MkDir()
   148  	authFn := filepath.Join(tmpdir, "auth.json")
   149  	err := ioutil.WriteFile(authFn, []byte(`{
   150  "macaroon": "MACAROON",
   151  "discharges": ["DISCHARGE"]
   152  }`), 0600)
   153  	c.Assert(err, IsNil)
   154  
   155  	os.Setenv("UBUNTU_STORE_AUTH_DATA_FILENAME", authFn)
   156  	defer os.Unsetenv("UBUNTU_STORE_AUTH_DATA_FILENAME")
   157  
   158  	tsto, err := tooling.NewToolingStore()
   159  	c.Assert(err, IsNil)
   160  	creds := tsto.Creds()
   161  	u1creds, ok := creds.(*tooling.UbuntuOneCreds)
   162  	c.Assert(ok, Equals, true)
   163  	c.Check(u1creds.User.StoreMacaroon, Equals, "MACAROON")
   164  	c.Check(u1creds.User.StoreDischarges, DeepEquals, []string{"DISCHARGE"})
   165  }
   166  
   167  func (s *toolingSuite) TestNewToolingStoreWithBase64AuthFile(c *C) {
   168  	tmpdir := c.MkDir()
   169  	authFn := filepath.Join(tmpdir, "auth7a")
   170  	authObj := []byte(`{
   171  "r": "MACAROON",
   172  "d": "DISCHARGE"
   173  }`)
   174  	enc := []byte(base64.StdEncoding.EncodeToString(authObj))
   175  	err := ioutil.WriteFile(authFn, enc, 0600)
   176  	c.Assert(err, IsNil)
   177  
   178  	os.Setenv("UBUNTU_STORE_AUTH_DATA_FILENAME", authFn)
   179  	defer os.Unsetenv("UBUNTU_STORE_AUTH_DATA_FILENAME")
   180  
   181  	tsto, err := tooling.NewToolingStore()
   182  	c.Assert(err, IsNil)
   183  	creds := tsto.Creds()
   184  	u1creds, ok := creds.(*tooling.UbuntuOneCreds)
   185  	c.Assert(ok, Equals, true)
   186  	c.Check(u1creds.User.StoreMacaroon, Equals, "MACAROON")
   187  	c.Check(u1creds.User.StoreDischarges, DeepEquals, []string{"DISCHARGE"})
   188  }
   189  
   190  func (s *toolingSuite) TestNewToolingStoreWithAuthFileErrors(c *C) {
   191  	tmpdir := c.MkDir()
   192  	authFn := filepath.Join(tmpdir, "creds")
   193  
   194  	os.Setenv("UBUNTU_STORE_AUTH_DATA_FILENAME", authFn)
   195  	defer os.Unsetenv("UBUNTU_STORE_AUTH_DATA_FILENAME")
   196  
   197  	tests := []struct {
   198  		data string
   199  		err  string
   200  	}{
   201  		{"", `invalid auth file ".*/creds": empty`},
   202  		{" {}", `invalid auth file ".*/creds": missing fields`},
   203  		{" [...", `invalid snapcraft login file ".*/creds": No section: login.ubuntu.com`},
   204  		{`[login.ubuntu.com]
   205  macaroon =
   206  unbound_discharge =
   207  `, `invalid snapcraft login file ".*/creds": empty fields`},
   208  		{"=", `invalid auth file ".*/creds": not a recognizable format`},
   209  	}
   210  
   211  	for _, t := range tests {
   212  		err := ioutil.WriteFile(authFn, []byte(t.data), 0600)
   213  		c.Assert(err, IsNil)
   214  
   215  		_, err = tooling.NewToolingStore()
   216  		c.Check(err, ErrorMatches, t.err)
   217  	}
   218  }
   219  
   220  func (s *toolingSuite) TestNewToolingStoreWithAuthFromSnapcraftLoginFile(c *C) {
   221  	tmpdir := c.MkDir()
   222  	authFn := filepath.Join(tmpdir, "auth.json")
   223  	err := ioutil.WriteFile(authFn, []byte(`[login.ubuntu.com]
   224  macaroon = MACAROON
   225  unbound_discharge = DISCHARGE
   226  
   227  `), 0600)
   228  	c.Assert(err, IsNil)
   229  
   230  	os.Setenv("UBUNTU_STORE_AUTH_DATA_FILENAME", authFn)
   231  	defer os.Unsetenv("UBUNTU_STORE_AUTH_DATA_FILENAME")
   232  
   233  	tsto, err := tooling.NewToolingStore()
   234  	c.Assert(err, IsNil)
   235  	creds := tsto.Creds()
   236  	u1creds, ok := creds.(*tooling.UbuntuOneCreds)
   237  	c.Assert(ok, Equals, true)
   238  	c.Check(u1creds.User.StoreMacaroon, Equals, "MACAROON")
   239  	c.Check(u1creds.User.StoreDischarges, DeepEquals, []string{"DISCHARGE"})
   240  }
   241  func (s *toolingSuite) TestNewToolingStoreWithAuthFromEnv(c *C) {
   242  	tests := []struct {
   243  		dat string
   244  		a   store.Authorizer
   245  		err string
   246  	}{
   247  		{dat: `{
   248  "r": "MACAROON",
   249  "d": "DISCHARGE"
   250  }`, a: &tooling.UbuntuOneCreds{User: auth.UserState{
   251  			StoreMacaroon:   "MACAROON",
   252  			StoreDischarges: []string{"DISCHARGE"},
   253  		}}}, {dat: `{ "t": "u1-macaroon",
   254  "v": {
   255    "r": "MACAROON",
   256    "d": "DISCHARGE"
   257  }}`, a: &tooling.UbuntuOneCreds{User: auth.UserState{
   258  			StoreMacaroon:   "MACAROON",
   259  			StoreDischarges: []string{"DISCHARGE"},
   260  		}}}, {dat: `{`, err: `cannot unmarshal base64-decoded auth credentials from UBUNTU_STORE_AUTH: unexpected end of JSON input`}, {dat: `{}`, err: `cannot recognize unmarshalled base64-decoded auth credentials from UBUNTU_STORE_AUTH: no known field combination set`}, {dat: `{ "t": "macaroon",
   261  "v": "MACAROON0"
   262  }`, a: &tooling.SimpleCreds{
   263  			Scheme: "Macaroon",
   264  			Value:  "MACAROON0",
   265  		}}, {dat: `{ "t": "bearer",
   266  "v": "tok"
   267  }`, a: &tooling.SimpleCreds{
   268  			Scheme: "Bearer",
   269  			Value:  "tok",
   270  		}}, {dat: `{"t": "u1-macaroon"}`,
   271  			err: `cannot recognize unmarshalled base64-decoded auth credentials from UBUNTU_STORE_AUTH: no known field combination set`,
   272  		}, {dat: `{"t": "macaroon"}`,
   273  			err: `cannot recognize unmarshalled base64-decoded auth credentials from UBUNTU_STORE_AUTH: no known field combination set`,
   274  		}, {dat: `{"t": 1}`,
   275  			err: `cannot recognize unmarshalled base64-decoded auth credentials from UBUNTU_STORE_AUTH: no known field combination set`,
   276  		}, {dat: `{"t": "macaroon", "v": []}`,
   277  			err: `cannot recognize unmarshalled base64-decoded auth credentials from UBUNTU_STORE_AUTH: no known field combination set`,
   278  		}}
   279  	defer os.Unsetenv("UBUNTU_STORE_AUTH")
   280  
   281  	for _, t := range tests {
   282  		os.Setenv("UBUNTU_STORE_AUTH", base64.StdEncoding.EncodeToString([]byte(t.dat)))
   283  		tsto, err := tooling.NewToolingStore()
   284  		if t.err == "" {
   285  			c.Assert(err, IsNil)
   286  			creds := tsto.Creds()
   287  			c.Check(creds, DeepEquals, t.a)
   288  		} else {
   289  			c.Check(err, ErrorMatches, t.err)
   290  		}
   291  	}
   292  }
   293  
   294  func (s *toolingSuite) TestDownloadpOptionsString(c *C) {
   295  	tests := []struct {
   296  		opts tooling.DownloadSnapOptions
   297  		str  string
   298  	}{
   299  		{tooling.DownloadSnapOptions{LeavePartialOnError: true}, ""},
   300  		{tooling.DownloadSnapOptions{}, ""},
   301  		{tooling.DownloadSnapOptions{TargetDir: "/foo"}, `in "/foo"`},
   302  		{tooling.DownloadSnapOptions{Basename: "foo"}, `to "foo.snap"`},
   303  		{tooling.DownloadSnapOptions{Channel: "foo"}, `from channel "foo"`},
   304  		{tooling.DownloadSnapOptions{Revision: snap.R(42)}, `(42)`},
   305  		{tooling.DownloadSnapOptions{
   306  			CohortKey: "AbCdEfGhIjKlMnOpQrStUvWxYz",
   307  		}, `from cohort "…rStUvWxYz"`},
   308  		{tooling.DownloadSnapOptions{
   309  			TargetDir: "/foo",
   310  			Basename:  "bar",
   311  			Channel:   "baz",
   312  			Revision:  snap.R(13),
   313  			CohortKey: "MSBIc3dwOW9PemozYjRtdzhnY0MwMFh0eFduS0g5UWlDUSAxNTU1NDExNDE1IDBjYzJhNTc1ZjNjOTQ3ZDEwMWE1NTNjZWFkNmFmZDE3ZWJhYTYyNjM4ZWQ3ZGMzNjI5YmU4YjQ3NzAwMjdlMDk=",
   314  		}, `(13) from channel "baz" from cohort "…wMjdlMDk=" to "bar.snap" in "/foo"`}, // note this one is not 'valid' so it's ok if the string is a bit wonky
   315  
   316  	}
   317  
   318  	for _, t := range tests {
   319  		c.Check(t.opts.String(), Equals, t.str)
   320  	}
   321  }
   322  
   323  func (s *toolingSuite) TestDownloadSnapOptionsValid(c *C) {
   324  	tests := []struct {
   325  		opts tooling.DownloadSnapOptions
   326  		err  error
   327  	}{
   328  		{tooling.DownloadSnapOptions{}, nil}, // might want to error if no targetdir
   329  		{tooling.DownloadSnapOptions{TargetDir: "foo"}, nil},
   330  		{tooling.DownloadSnapOptions{Channel: "foo"}, nil},
   331  		{tooling.DownloadSnapOptions{Revision: snap.R(42)}, nil},
   332  		{tooling.DownloadSnapOptions{
   333  			CohortKey: "AbCdEfGhIjKlMnOpQrStUvWxYz",
   334  		}, nil},
   335  		{tooling.DownloadSnapOptions{
   336  			Channel:  "foo",
   337  			Revision: snap.R(42),
   338  		}, nil},
   339  		{tooling.DownloadSnapOptions{
   340  			Channel:   "foo",
   341  			CohortKey: "bar",
   342  		}, nil},
   343  		{tooling.DownloadSnapOptions{
   344  			Revision:  snap.R(1),
   345  			CohortKey: "bar",
   346  		}, tooling.ErrRevisionAndCohort},
   347  		{tooling.DownloadSnapOptions{
   348  			Basename: "/foo",
   349  		}, tooling.ErrPathInBase},
   350  	}
   351  
   352  	for _, t := range tests {
   353  		t.opts.LeavePartialOnError = true
   354  		c.Check(t.opts.Validate(), Equals, t.err)
   355  		t.opts.LeavePartialOnError = false
   356  		c.Check(t.opts.Validate(), Equals, t.err)
   357  	}
   358  }
   359  
   360  func (s *toolingSuite) TestDownloadSnap(c *C) {
   361  	// TODO: maybe expand on this (test coverage of DownloadSnap is really bad)
   362  
   363  	// env shenanigans
   364  	runtime.LockOSThread()
   365  	defer runtime.UnlockOSThread()
   366  
   367  	debug, hadDebug := os.LookupEnv("SNAPD_DEBUG")
   368  	os.Setenv("SNAPD_DEBUG", "1")
   369  	if hadDebug {
   370  		defer os.Setenv("SNAPD_DEBUG", debug)
   371  	} else {
   372  		defer os.Unsetenv("SNAPD_DEBUG")
   373  	}
   374  	logbuf, restore := logger.MockLogger()
   375  	defer restore()
   376  
   377  	s.setupSnaps(c, map[string]string{
   378  		"core": "canonical",
   379  	}, "")
   380  
   381  	dlDir := c.MkDir()
   382  	opts := tooling.DownloadSnapOptions{
   383  		TargetDir: dlDir,
   384  	}
   385  	dlSnap, err := s.tsto.DownloadSnap("core", opts)
   386  	c.Assert(err, IsNil)
   387  	c.Check(dlSnap.Path, Matches, filepath.Join(dlDir, `core_\d+.snap`))
   388  	c.Check(dlSnap.Info.SnapName(), Equals, "core")
   389  	c.Check(dlSnap.RedirectChannel, Equals, "")
   390  
   391  	c.Check(logbuf.String(), Matches, `.* DEBUG: Going to download snap "core" `+opts.String()+".\n")
   392  }
   393  
   394  // interface for the store
   395  func (s *toolingSuite) SnapAction(_ context.Context, curSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, _ *auth.UserState, _ *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) {
   396  	if assertQuery != nil {
   397  		return nil, nil, fmt.Errorf("unexpected assertion query")
   398  	}
   399  
   400  	s.storeActionsBunchSizes = append(s.storeActionsBunchSizes, len(actions))
   401  	s.curSnaps = append(s.curSnaps, curSnaps)
   402  	sars := make([]store.SnapActionResult, 0, len(actions))
   403  	for _, a := range actions {
   404  		if a.Action != "download" {
   405  			return nil, nil, fmt.Errorf("unexpected action %q", a.Action)
   406  		}
   407  
   408  		if _, instanceKey := snap.SplitInstanceName(a.InstanceName); instanceKey != "" {
   409  			return nil, nil, fmt.Errorf("unexpected instance key in %q", a.InstanceName)
   410  		}
   411  		// record
   412  		s.storeActions = append(s.storeActions, a)
   413  
   414  		info := s.AssertedSnapInfo(a.InstanceName)
   415  		if info == nil {
   416  			return nil, nil, fmt.Errorf("no %q in the fake store", a.InstanceName)
   417  		}
   418  		info1 := *info
   419  		channel := a.Channel
   420  		redirectChannel := ""
   421  		if strings.HasPrefix(a.InstanceName, "default-track-") {
   422  			channel = "default-track/stable"
   423  			redirectChannel = channel
   424  		}
   425  		info1.Channel = channel
   426  		sars = append(sars, store.SnapActionResult{
   427  			Info:            &info1,
   428  			RedirectChannel: redirectChannel,
   429  		})
   430  	}
   431  
   432  	return sars, nil, nil
   433  }
   434  
   435  func (s *toolingSuite) Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *store.DownloadOptions) error {
   436  	return osutil.CopyFile(s.AssertedSnap(name), targetFn, 0)
   437  }
   438  
   439  func (s *toolingSuite) Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) {
   440  	ref := &asserts.Ref{Type: assertType, PrimaryKey: primaryKey}
   441  	return ref.Resolve(s.StoreSigning.Find)
   442  }
   443  
   444  func (s *toolingSuite) TestUpdateUserAuth(c *C) {
   445  	u := auth.UserState{
   446  		StoreMacaroon:   "macaroon",
   447  		StoreDischarges: []string{"discharge1"},
   448  	}
   449  	creds := &tooling.UbuntuOneCreds{
   450  		User: u,
   451  	}
   452  
   453  	u1, err := creds.UpdateUserAuth(&u, []string{"discharge2"})
   454  	c.Assert(err, IsNil)
   455  	c.Check(u1, Equals, &u)
   456  	c.Check(u1.StoreDischarges, DeepEquals, []string{"discharge2"})
   457  }
   458  
   459  func (s *toolingSuite) TestSimpleCreds(c *C) {
   460  	creds := &tooling.SimpleCreds{
   461  		Scheme: "Auth-Scheme",
   462  		Value:  "auth-value",
   463  	}
   464  	c.Check(creds.CanAuthorizeForUser(nil), Equals, true)
   465  	r, err := http.NewRequest("POST", "http://svc", nil)
   466  	c.Assert(err, IsNil)
   467  	c.Assert(creds.Authorize(r, nil, nil, nil), IsNil)
   468  	auth := r.Header.Get("Authorization")
   469  	c.Check(auth, Equals, `Auth-Scheme auth-value`)
   470  }