github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/daemon/api_base_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2020 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 daemon_test
    21  
    22  import (
    23  	"context"
    24  	"crypto"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"net/http"
    28  	"os"
    29  	"path/filepath"
    30  	"time"
    31  
    32  	"github.com/gorilla/mux"
    33  	"golang.org/x/crypto/sha3"
    34  	"gopkg.in/check.v1"
    35  	"gopkg.in/tomb.v2"
    36  
    37  	"github.com/snapcore/snapd/asserts"
    38  	"github.com/snapcore/snapd/asserts/assertstest"
    39  	"github.com/snapcore/snapd/asserts/sysdb"
    40  	"github.com/snapcore/snapd/daemon"
    41  	"github.com/snapcore/snapd/dirs"
    42  	"github.com/snapcore/snapd/osutil"
    43  	"github.com/snapcore/snapd/overlord"
    44  	"github.com/snapcore/snapd/overlord/assertstate"
    45  	"github.com/snapcore/snapd/overlord/assertstate/assertstatetest"
    46  	"github.com/snapcore/snapd/overlord/auth"
    47  	"github.com/snapcore/snapd/overlord/devicestate"
    48  	"github.com/snapcore/snapd/overlord/devicestate/devicestatetest"
    49  	"github.com/snapcore/snapd/overlord/ifacestate"
    50  	"github.com/snapcore/snapd/overlord/snapstate"
    51  	"github.com/snapcore/snapd/overlord/state"
    52  	"github.com/snapcore/snapd/sandbox"
    53  	"github.com/snapcore/snapd/snap"
    54  	"github.com/snapcore/snapd/snap/snaptest"
    55  	"github.com/snapcore/snapd/store"
    56  	"github.com/snapcore/snapd/store/storetest"
    57  	"github.com/snapcore/snapd/systemd"
    58  	"github.com/snapcore/snapd/testutil"
    59  )
    60  
    61  // TODO: as we split api_test.go and move more tests to live in daemon_test
    62  // instead of daemon, split out functionality from APIBaseSuite
    63  // to only the relevant suite when possible
    64  
    65  type apiBaseSuite struct {
    66  	testutil.BaseTest
    67  
    68  	storetest.Store
    69  
    70  	rsnaps            []*snap.Info
    71  	err               error
    72  	vars              map[string]string
    73  	storeSearch       store.Search
    74  	suggestedCurrency string
    75  	d                 *daemon.Daemon
    76  	user              *auth.UserState
    77  	ctx               context.Context
    78  	currentSnaps      []*store.CurrentSnap
    79  	actions           []*store.SnapAction
    80  
    81  	restoreRelease func()
    82  
    83  	StoreSigning *assertstest.StoreStack
    84  	Brands       *assertstest.SigningAccounts
    85  
    86  	systemctlRestorer func()
    87  	SysctlBufs        [][]byte
    88  
    89  	connectivityResult     map[string]bool
    90  	loginUserStoreMacaroon string
    91  	loginUserDischarge     string
    92  
    93  	restoreSanitize func()
    94  	restoreMuxVars  func()
    95  }
    96  
    97  func (s *apiBaseSuite) pokeStateLock() {
    98  	// the store should be called without the state lock held. Try
    99  	// to acquire it.
   100  	st := s.d.Overlord().State()
   101  	st.Lock()
   102  	st.Unlock()
   103  }
   104  
   105  func (s *apiBaseSuite) SnapInfo(ctx context.Context, spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) {
   106  	s.pokeStateLock()
   107  	s.user = user
   108  	s.ctx = ctx
   109  	if len(s.rsnaps) > 0 {
   110  		return s.rsnaps[0], s.err
   111  	}
   112  	return nil, s.err
   113  }
   114  
   115  func (s *apiBaseSuite) Find(ctx context.Context, search *store.Search, user *auth.UserState) ([]*snap.Info, error) {
   116  	s.pokeStateLock()
   117  
   118  	s.storeSearch = *search
   119  	s.user = user
   120  	s.ctx = ctx
   121  
   122  	return s.rsnaps, s.err
   123  }
   124  
   125  func (s *apiBaseSuite) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) {
   126  	s.pokeStateLock()
   127  	if assertQuery != nil {
   128  		toResolve, err := assertQuery.ToResolve()
   129  		if err != nil {
   130  			return nil, nil, err
   131  		}
   132  		if len(toResolve) != 0 {
   133  			panic("no assertion query support")
   134  		}
   135  	}
   136  
   137  	if ctx == nil {
   138  		panic("context required")
   139  	}
   140  	s.currentSnaps = currentSnaps
   141  	s.actions = actions
   142  	s.user = user
   143  
   144  	sars := make([]store.SnapActionResult, len(s.rsnaps))
   145  	for i, rsnap := range s.rsnaps {
   146  		sars[i] = store.SnapActionResult{Info: rsnap}
   147  	}
   148  	return sars, nil, s.err
   149  }
   150  
   151  func (s *apiBaseSuite) SuggestedCurrency() string {
   152  	s.pokeStateLock()
   153  
   154  	return s.suggestedCurrency
   155  }
   156  
   157  func (s *apiBaseSuite) ConnectivityCheck() (map[string]bool, error) {
   158  	s.pokeStateLock()
   159  
   160  	return s.connectivityResult, s.err
   161  }
   162  
   163  func (s *apiBaseSuite) LoginUser(username, password, otp string) (string, string, error) {
   164  	s.pokeStateLock()
   165  
   166  	return s.loginUserStoreMacaroon, s.loginUserDischarge, s.err
   167  }
   168  
   169  func (s *apiBaseSuite) muxVars(*http.Request) map[string]string {
   170  	return s.vars
   171  }
   172  
   173  func (s *apiBaseSuite) SetUpSuite(c *check.C) {
   174  	s.restoreMuxVars = daemon.MockMuxVars(s.muxVars)
   175  	s.restoreRelease = sandbox.MockForceDevMode(false)
   176  	s.systemctlRestorer = systemd.MockSystemctl(s.systemctl)
   177  	s.restoreSanitize = snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})
   178  }
   179  
   180  func (s *apiBaseSuite) TearDownSuite(c *check.C) {
   181  	s.restoreMuxVars()
   182  	s.restoreRelease()
   183  	s.systemctlRestorer()
   184  	s.restoreSanitize()
   185  }
   186  
   187  func (s *apiBaseSuite) systemctl(args ...string) (buf []byte, err error) {
   188  	if len(s.SysctlBufs) > 0 {
   189  		buf, s.SysctlBufs = s.SysctlBufs[0], s.SysctlBufs[1:]
   190  	}
   191  	return buf, err
   192  }
   193  
   194  var (
   195  	brandPrivKey, _ = assertstest.GenerateKey(752)
   196  )
   197  
   198  func (s *apiBaseSuite) SetUpTest(c *check.C) {
   199  	s.BaseTest.SetUpTest(c)
   200  
   201  	ctlcmds := testutil.MockCommand(c, "systemctl", "").Also("journalctl", "")
   202  	s.AddCleanup(ctlcmds.Restore)
   203  
   204  	s.SysctlBufs = nil
   205  
   206  	dirs.SetRootDir(c.MkDir())
   207  	err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755)
   208  	restore := osutil.MockMountInfo("")
   209  	s.AddCleanup(restore)
   210  
   211  	c.Assert(err, check.IsNil)
   212  	c.Assert(os.MkdirAll(dirs.SnapMountDir, 0755), check.IsNil)
   213  	c.Assert(os.MkdirAll(dirs.SnapBlobDir, 0755), check.IsNil)
   214  
   215  	s.rsnaps = nil
   216  	s.suggestedCurrency = ""
   217  	s.storeSearch = store.Search{}
   218  	s.err = nil
   219  	s.vars = nil
   220  	s.user = nil
   221  	s.d = nil
   222  	s.currentSnaps = nil
   223  	s.actions = nil
   224  	// Disable real security backends for all API tests
   225  	s.AddCleanup(ifacestate.MockSecurityBackends(nil))
   226  
   227  	s.StoreSigning = assertstest.NewStoreStack("can0nical", nil)
   228  	s.AddCleanup(sysdb.InjectTrusted(s.StoreSigning.Trusted))
   229  
   230  	s.Brands = assertstest.NewSigningAccounts(s.StoreSigning)
   231  	s.Brands.Register("my-brand", brandPrivKey, nil)
   232  }
   233  
   234  func (s *apiBaseSuite) TearDownTest(c *check.C) {
   235  	s.d = nil
   236  	s.ctx = nil
   237  
   238  	dirs.SetRootDir("")
   239  	s.BaseTest.TearDownTest(c)
   240  }
   241  
   242  func (s *apiBaseSuite) mockModel(c *check.C, st *state.State, model *asserts.Model) {
   243  	// realistic model setup
   244  	if model == nil {
   245  		model = s.Brands.Model("can0nical", "pc", map[string]interface{}{
   246  			"architecture": "amd64",
   247  			"gadget":       "gadget",
   248  			"kernel":       "kernel",
   249  		})
   250  	}
   251  
   252  	snapstate.DeviceCtx = devicestate.DeviceCtx
   253  
   254  	assertstatetest.AddMany(st, model)
   255  
   256  	devicestatetest.SetDevice(st, &auth.DeviceState{
   257  		Brand:  model.BrandID(),
   258  		Model:  model.Model(),
   259  		Serial: "serialserial",
   260  	})
   261  }
   262  
   263  func (s *apiBaseSuite) daemonWithStore(c *check.C, sto snapstate.StoreService) *daemon.Daemon {
   264  	if s.d != nil {
   265  		panic("called Daemon*() twice")
   266  	}
   267  	d, err := daemon.NewAndAddRoutes()
   268  	c.Assert(err, check.IsNil)
   269  
   270  	c.Assert(d.Overlord().StartUp(), check.IsNil)
   271  
   272  	st := d.Overlord().State()
   273  	st.Lock()
   274  	defer st.Unlock()
   275  	snapstate.ReplaceStore(st, sto)
   276  	// mark as already seeded
   277  	st.Set("seeded", true)
   278  	// registered
   279  	s.mockModel(c, st, nil)
   280  
   281  	// don't actually try to talk to the store on snapstate.Ensure
   282  	// needs doing after the call to devicestate.Manager (which
   283  	// happens in daemon.New via overlord.New)
   284  	snapstate.CanAutoRefresh = nil
   285  
   286  	s.d = d
   287  	return d
   288  }
   289  
   290  func (s *apiBaseSuite) resetDaemon() {
   291  	s.d = nil
   292  }
   293  
   294  func (s *apiBaseSuite) daemon(c *check.C) *daemon.Daemon {
   295  	return s.daemonWithStore(c, s)
   296  }
   297  
   298  func (s *apiBaseSuite) daemonWithOverlordMock(c *check.C) *daemon.Daemon {
   299  	if s.d != nil {
   300  		panic("called Daemon*() twice")
   301  	}
   302  
   303  	o := overlord.Mock()
   304  	s.d = daemon.NewWithOverlord(o)
   305  	return s.d
   306  }
   307  
   308  func (s *apiBaseSuite) daemonWithOverlordMockAndStore(c *check.C) *daemon.Daemon {
   309  	if s.d != nil {
   310  		panic("called Daemon*() twice")
   311  	}
   312  
   313  	o := overlord.Mock()
   314  	d := daemon.NewWithOverlord(o)
   315  
   316  	st := d.Overlord().State()
   317  	// adds an assertion db
   318  	assertstate.Manager(st, o.TaskRunner())
   319  	st.Lock()
   320  	defer st.Unlock()
   321  	snapstate.ReplaceStore(st, s)
   322  
   323  	s.d = d
   324  	return d
   325  }
   326  
   327  type fakeSnapManager struct{}
   328  
   329  func newFakeSnapManager(st *state.State, runner *state.TaskRunner) *fakeSnapManager {
   330  	runner.AddHandler("fake-install-snap", func(t *state.Task, _ *tomb.Tomb) error {
   331  		return nil
   332  	}, nil)
   333  	runner.AddHandler("fake-install-snap-error", func(t *state.Task, _ *tomb.Tomb) error {
   334  		return fmt.Errorf("fake-install-snap-error errored")
   335  	}, nil)
   336  
   337  	return &fakeSnapManager{}
   338  }
   339  
   340  func (m *fakeSnapManager) Ensure() error {
   341  	return nil
   342  }
   343  
   344  // sanity
   345  var _ overlord.StateManager = (*fakeSnapManager)(nil)
   346  
   347  func (s *apiBaseSuite) daemonWithFakeSnapManager(c *check.C) *daemon.Daemon {
   348  	d := s.daemonWithOverlordMockAndStore(c)
   349  	st := d.Overlord().State()
   350  	runner := d.Overlord().TaskRunner()
   351  	d.Overlord().AddManager(newFakeSnapManager(st, runner))
   352  	d.Overlord().AddManager(runner)
   353  	c.Assert(d.Overlord().StartUp(), check.IsNil)
   354  	return d
   355  }
   356  
   357  func (s *apiBaseSuite) waitTrivialChange(c *check.C, chg *state.Change) {
   358  	err := s.d.Overlord().Settle(5 * time.Second)
   359  	c.Assert(err, check.IsNil)
   360  	c.Assert(chg.IsReady(), check.Equals, true)
   361  }
   362  
   363  func (s *apiBaseSuite) mkInstalledDesktopFile(c *check.C, name, content string) string {
   364  	df := filepath.Join(dirs.SnapDesktopFilesDir, name)
   365  	err := os.MkdirAll(filepath.Dir(df), 0755)
   366  	c.Assert(err, check.IsNil)
   367  	err = ioutil.WriteFile(df, []byte(content), 0644)
   368  	c.Assert(err, check.IsNil)
   369  	return df
   370  }
   371  
   372  func (s *apiBaseSuite) mockSnap(c *check.C, yamlText string) *snap.Info {
   373  	if s.d == nil {
   374  		panic("call s.Daemon(c) etc in your test first")
   375  	}
   376  
   377  	snapInfo := snaptest.MockSnap(c, yamlText, &snap.SideInfo{Revision: snap.R(1)})
   378  
   379  	st := s.d.Overlord().State()
   380  
   381  	st.Lock()
   382  	defer st.Unlock()
   383  
   384  	// Put a side info into the state
   385  	snapstate.Set(st, snapInfo.InstanceName(), &snapstate.SnapState{
   386  		Active: true,
   387  		Sequence: []*snap.SideInfo{
   388  			{
   389  				RealName: snapInfo.SnapName(),
   390  				Revision: snapInfo.Revision,
   391  				SnapID:   "ididid",
   392  			},
   393  		},
   394  		Current:  snapInfo.Revision,
   395  		SnapType: string(snapInfo.Type()),
   396  	})
   397  
   398  	// Put the snap into the interface repository
   399  	repo := s.d.Overlord().InterfaceManager().Repository()
   400  	err := repo.AddSnap(snapInfo)
   401  	c.Assert(err, check.IsNil)
   402  	return snapInfo
   403  }
   404  
   405  func (s *apiBaseSuite) mkInstalledInState(c *check.C, d *daemon.Daemon, instanceName, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info {
   406  	snapName, instanceKey := snap.SplitInstanceName(instanceName)
   407  
   408  	if revision.Local() && developer != "" {
   409  		panic("not supported")
   410  	}
   411  
   412  	var snapID string
   413  	if revision.Store() {
   414  		snapID = snapName + "-id"
   415  	}
   416  	// Collect arguments into a snap.SideInfo structure
   417  	sideInfo := &snap.SideInfo{
   418  		SnapID:   snapID,
   419  		RealName: snapName,
   420  		Revision: revision,
   421  		Channel:  "stable",
   422  	}
   423  
   424  	// Collect other arguments into a yaml string
   425  	yamlText := fmt.Sprintf(`
   426  name: %s
   427  version: %s
   428  %s`, snapName, version, extraYaml)
   429  
   430  	// Mock the snap on disk
   431  	snapInfo := snaptest.MockSnapInstance(c, instanceName, yamlText, sideInfo)
   432  	if active {
   433  		dir, rev := filepath.Split(snapInfo.MountDir())
   434  		c.Assert(os.Symlink(rev, dir+"current"), check.IsNil)
   435  	}
   436  	c.Assert(snapInfo.InstanceName(), check.Equals, instanceName)
   437  
   438  	c.Assert(os.MkdirAll(snapInfo.DataDir(), 0755), check.IsNil)
   439  	metadir := filepath.Join(snapInfo.MountDir(), "meta")
   440  	guidir := filepath.Join(metadir, "gui")
   441  	c.Assert(os.MkdirAll(guidir, 0755), check.IsNil)
   442  	c.Check(ioutil.WriteFile(filepath.Join(guidir, "icon.svg"), []byte("yadda icon"), 0644), check.IsNil)
   443  
   444  	if d == nil {
   445  		return snapInfo
   446  	}
   447  	st := d.Overlord().State()
   448  	st.Lock()
   449  	defer st.Unlock()
   450  
   451  	var snapst snapstate.SnapState
   452  	snapstate.Get(st, instanceName, &snapst)
   453  	snapst.Active = active
   454  	snapst.Sequence = append(snapst.Sequence, &snapInfo.SideInfo)
   455  	snapst.Current = snapInfo.SideInfo.Revision
   456  	snapst.TrackingChannel = "stable"
   457  	snapst.InstanceKey = instanceKey
   458  
   459  	snapstate.Set(st, instanceName, &snapst)
   460  
   461  	if developer == "" {
   462  		return snapInfo
   463  	}
   464  
   465  	devAcct := assertstest.NewAccount(s.StoreSigning, developer, map[string]interface{}{
   466  		"account-id": developer + "-id",
   467  	}, "")
   468  
   469  	snapInfo.Publisher = snap.StoreAccount{
   470  		ID:          devAcct.AccountID(),
   471  		Username:    devAcct.Username(),
   472  		DisplayName: devAcct.DisplayName(),
   473  		Validation:  devAcct.Validation(),
   474  	}
   475  
   476  	snapDecl, err := s.StoreSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
   477  		"series":       "16",
   478  		"snap-id":      snapID,
   479  		"snap-name":    snapName,
   480  		"publisher-id": devAcct.AccountID(),
   481  		"timestamp":    time.Now().Format(time.RFC3339),
   482  	}, nil, "")
   483  	c.Assert(err, check.IsNil)
   484  
   485  	content, err := ioutil.ReadFile(snapInfo.MountFile())
   486  	c.Assert(err, check.IsNil)
   487  	h := sha3.Sum384(content)
   488  	dgst, err := asserts.EncodeDigest(crypto.SHA3_384, h[:])
   489  	c.Assert(err, check.IsNil)
   490  	snapRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
   491  		"snap-sha3-384": string(dgst),
   492  		"snap-size":     "999",
   493  		"snap-id":       snapID,
   494  		"snap-revision": fmt.Sprintf("%s", revision),
   495  		"developer-id":  devAcct.AccountID(),
   496  		"timestamp":     time.Now().Format(time.RFC3339),
   497  	}, nil, "")
   498  	c.Assert(err, check.IsNil)
   499  
   500  	assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey(""), devAcct, snapDecl, snapRev)
   501  
   502  	return snapInfo
   503  }
   504  
   505  func handlerCommand(c *check.C, d *daemon.Daemon, req *http.Request) (cmd *daemon.Command, vars map[string]string) {
   506  	m := &mux.RouteMatch{}
   507  	if !d.RouterMatch(req, m) {
   508  		c.Fatalf("no command for URL %q", req.URL)
   509  	}
   510  	cmd, ok := m.Handler.(*daemon.Command)
   511  	if !ok {
   512  		c.Fatalf("no command for URL %q", req.URL)
   513  	}
   514  	return cmd, m.Vars
   515  }
   516  
   517  func (s *apiBaseSuite) checkGetOnly(c *check.C, req *http.Request) {
   518  	if s.d == nil {
   519  		panic("call s.Daemon(c) etc in your test first")
   520  	}
   521  
   522  	cmd, _ := handlerCommand(c, s.d, req)
   523  	c.Check(cmd.POST, check.IsNil)
   524  	c.Check(cmd.PUT, check.IsNil)
   525  	c.Check(cmd.GET, check.NotNil)
   526  }
   527  
   528  func (s *apiBaseSuite) req(c *check.C, req *http.Request, u *auth.UserState) daemon.Response {
   529  	if s.d == nil {
   530  		panic("call s.Daemon(c) etc in your test first")
   531  	}
   532  
   533  	cmd, vars := handlerCommand(c, s.d, req)
   534  	s.vars = vars
   535  	var f daemon.ResponseFunc
   536  	switch req.Method {
   537  	case "GET":
   538  		f = cmd.GET
   539  	case "POST":
   540  		f = cmd.POST
   541  	case "PUT":
   542  		f = cmd.PUT
   543  	default:
   544  		c.Fatalf("unsupported HTTP method %q", req.Method)
   545  	}
   546  	if f == nil {
   547  		c.Fatalf("no support for %q for %q", req.Method, req.URL)
   548  	}
   549  	return f(cmd, req, u)
   550  }
   551  
   552  func (s *apiBaseSuite) serveHTTP(c *check.C, w http.ResponseWriter, req *http.Request) {
   553  	if s.d == nil {
   554  		panic("call s.Daemon(c) etc in your test first")
   555  	}
   556  
   557  	cmd, vars := handlerCommand(c, s.d, req)
   558  	s.vars = vars
   559  
   560  	cmd.ServeHTTP(w, req)
   561  }
   562  
   563  func (s *apiBaseSuite) simulateConflict(name string) {
   564  	if s.d == nil {
   565  		panic("call s.Daemon(c) etc in your test first")
   566  	}
   567  
   568  	o := s.d.Overlord()
   569  	st := o.State()
   570  	st.Lock()
   571  	defer st.Unlock()
   572  	t := st.NewTask("link-snap", "...")
   573  	snapsup := &snapstate.SnapSetup{SideInfo: &snap.SideInfo{
   574  		RealName: name,
   575  	}}
   576  	t.Set("snap-setup", snapsup)
   577  	chg := st.NewChange("manip", "...")
   578  	chg.AddTask(t)
   579  }