gitee.com/mysnapcore/mysnapd@v0.1.0/daemon/api_themes_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 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  	"bytes"
    24  	"context"
    25  	"errors"
    26  	"fmt"
    27  	"net/http/httptest"
    28  
    29  	. "gopkg.in/check.v1"
    30  
    31  	"gitee.com/mysnapcore/mysnapd/daemon"
    32  	"gitee.com/mysnapcore/mysnapd/overlord/auth"
    33  	"gitee.com/mysnapcore/mysnapd/overlord/hookstate"
    34  	"gitee.com/mysnapcore/mysnapd/overlord/ifacestate"
    35  	"gitee.com/mysnapcore/mysnapd/overlord/snapstate"
    36  	"gitee.com/mysnapcore/mysnapd/overlord/state"
    37  	"gitee.com/mysnapcore/mysnapd/snap"
    38  	"gitee.com/mysnapcore/mysnapd/snap/channel"
    39  	"gitee.com/mysnapcore/mysnapd/snap/naming"
    40  	"gitee.com/mysnapcore/mysnapd/store"
    41  )
    42  
    43  var _ = Suite(&themesSuite{})
    44  
    45  type themesSuite struct {
    46  	apiBaseSuite
    47  
    48  	available map[string]*snap.Info
    49  }
    50  
    51  func (s *themesSuite) SetUpTest(c *C) {
    52  	s.apiBaseSuite.SetUpTest(c)
    53  
    54  	s.available = make(map[string]*snap.Info)
    55  	s.err = store.ErrSnapNotFound
    56  }
    57  
    58  func (s *themesSuite) SnapExists(ctx context.Context, spec store.SnapSpec, user *auth.UserState) (naming.SnapRef, *channel.Channel, error) {
    59  	s.pokeStateLock()
    60  	if info := s.available[spec.Name]; info != nil {
    61  		ch, err := channel.Parse(info.Channel, "")
    62  		if err != nil {
    63  			panic(fmt.Sprintf("bad Info Channel: %v", err))
    64  		}
    65  		return info, &ch, nil
    66  	}
    67  	return nil, nil, s.err
    68  }
    69  
    70  func (s *themesSuite) daemon(c *C) *daemon.Daemon {
    71  	return s.apiBaseSuite.daemonWithStore(c, s)
    72  }
    73  
    74  func (s *themesSuite) expectThemesAccess() {
    75  	s.expectReadAccess(daemon.ThemesOpenAccess{})
    76  	s.expectWriteAccess(daemon.ThemesAuthenticatedAccess{Polkit: "io.snapcraft.snapd.manage"})
    77  }
    78  
    79  func (s *themesSuite) TestInstalledThemes(c *C) {
    80  	d := s.daemon(c)
    81  	s.mockSnap(c, `name: snap1
    82  version: 42
    83  slots:
    84    gtk-3-themes:
    85      interface: content
    86      content: gtk-3-themes
    87      source:
    88        read:
    89         - $SNAP/share/themes/Foo-gtk
    90         - $SNAP/share/themes/Foo-gtk-dark
    91    icon-themes:
    92      interface: content
    93      content: icon-themes
    94      source:
    95        read:
    96         - $SNAP/share/icons/Foo-icons
    97    sound-themes:
    98      interface: content
    99      content: sound-themes
   100      source:
   101        read:
   102         - $SNAP/share/sounds/Foo-sounds`)
   103  	s.mockSnap(c, `name: snap2
   104  version: 42
   105  slots:
   106    gtk-3-themes:
   107      interface: content
   108      content: gtk-3-themes
   109      source:
   110        read:
   111         - $SNAP/share/themes/Bar-gtk
   112    icon-themes:
   113      interface: content
   114      content: icon-themes
   115      source:
   116        read:
   117         - $SNAP/share/icons/Bar-icons
   118    sound-themes:
   119      interface: content
   120      content: sound-themes
   121      source:
   122        read:
   123         - $SNAP/share/sounds/Bar-sounds`)
   124  	s.mockSnap(c, `name: not-a-theme
   125  version: 42
   126  slots:
   127    foo:
   128      interface: content
   129      content: foo
   130      read: $SNAP/foo`)
   131  
   132  	gtkThemes, iconThemes, soundThemes, err := daemon.InstalledThemes(d.Overlord())
   133  	c.Check(err, IsNil)
   134  	c.Check(gtkThemes, DeepEquals, []string{"Bar-gtk", "Foo-gtk", "Foo-gtk-dark"})
   135  	c.Check(iconThemes, DeepEquals, []string{"Bar-icons", "Foo-icons"})
   136  	c.Check(soundThemes, DeepEquals, []string{"Bar-sounds", "Foo-sounds"})
   137  }
   138  
   139  func (s *themesSuite) TestThemePackageCandidates(c *C) {
   140  	// The package name includes the passed in prefix
   141  	c.Check(daemon.ThemePackageCandidates("gtk-theme-", "Yaru"), DeepEquals, []string{"gtk-theme-yaru"})
   142  	c.Check(daemon.ThemePackageCandidates("icon-theme-", "Yaru"), DeepEquals, []string{"icon-theme-yaru"})
   143  	c.Check(daemon.ThemePackageCandidates("sound-theme-", "Yaru"), DeepEquals, []string{"sound-theme-yaru"})
   144  
   145  	// If a theme name includes multiple dash separated
   146  	// components, multiple possible package names are returned,
   147  	// from most specific to least.
   148  	c.Check(daemon.ThemePackageCandidates("gtk-theme-", "Yaru-dark"), DeepEquals, []string{"gtk-theme-yaru-dark", "gtk-theme-yaru"})
   149  	c.Check(daemon.ThemePackageCandidates("gtk-theme-", "Matcha-dark-azul"), DeepEquals, []string{"gtk-theme-matcha-dark-azul", "gtk-theme-matcha-dark", "gtk-theme-matcha"})
   150  
   151  	// Digits are accepted in package names
   152  	c.Check(daemon.ThemePackageCandidates("gtk-theme-", "abc123xyz"), DeepEquals, []string{"gtk-theme-abc123xyz"})
   153  
   154  	// In addition to case folding, bad characters are converted to dashes
   155  	c.Check(daemon.ThemePackageCandidates("icon-theme-", "Breeze_Snow"), DeepEquals, []string{"icon-theme-breeze-snow", "icon-theme-breeze"})
   156  
   157  	// Groups of bad characters are collapsed to a single dash,
   158  	// with leading and trailing dashes removed
   159  	c.Check(daemon.ThemePackageCandidates("gtk-theme-", "+foo_"), DeepEquals, []string{"gtk-theme-foo"})
   160  	c.Check(daemon.ThemePackageCandidates("gtk-theme-", "foo-_--bar+-"), DeepEquals, []string{"gtk-theme-foo-bar", "gtk-theme-foo"})
   161  }
   162  
   163  func (s *themesSuite) TestThemeStatusForPrefix(c *C) {
   164  	s.daemon(c)
   165  
   166  	s.available = map[string]*snap.Info{
   167  		"gtk-theme-available": {
   168  			SuggestedName: "gtk-theme-available",
   169  			SideInfo: snap.SideInfo{
   170  				Channel: "stable",
   171  			},
   172  		},
   173  		"gtk-theme-installed": {
   174  			SuggestedName: "gtk-theme-installed",
   175  			SideInfo: snap.SideInfo{
   176  				Channel: "stable",
   177  			},
   178  		},
   179  	}
   180  
   181  	ctx := context.Background()
   182  	status := make(map[string]daemon.ThemeStatus)
   183  	toInstall := make(map[string]bool)
   184  
   185  	err := daemon.CollectThemeStatusForPrefix(ctx, s, nil, "gtk-theme-", []string{"Installed", "Installed", "Available", "Unavailable"}, []string{"Installed"}, status, toInstall)
   186  	c.Check(err, IsNil)
   187  	c.Check(status, DeepEquals, map[string]daemon.ThemeStatus{
   188  		"Installed":   daemon.ThemeInstalled,
   189  		"Available":   daemon.ThemeAvailable,
   190  		"Unavailable": daemon.ThemeUnavailable,
   191  	})
   192  	c.Check(toInstall, HasLen, 1)
   193  	c.Check(toInstall["gtk-theme-available"], NotNil)
   194  }
   195  
   196  func (s *themesSuite) TestThemeStatusForPrefixStripsSuffixes(c *C) {
   197  	s.daemon(c)
   198  
   199  	s.available = map[string]*snap.Info{
   200  		"gtk-theme-yaru": {
   201  			SuggestedName: "gtk-theme-yaru",
   202  			SideInfo: snap.SideInfo{
   203  				Channel: "stable",
   204  			},
   205  		},
   206  	}
   207  
   208  	ctx := context.Background()
   209  	status := make(map[string]daemon.ThemeStatus)
   210  	toInstall := make(map[string]bool)
   211  
   212  	err := daemon.CollectThemeStatusForPrefix(ctx, s, nil, "gtk-theme-", []string{"Yaru-dark"}, nil, status, toInstall)
   213  	c.Check(err, IsNil)
   214  	c.Check(status, DeepEquals, map[string]daemon.ThemeStatus{
   215  		"Yaru-dark": daemon.ThemeAvailable,
   216  	})
   217  	c.Check(toInstall, HasLen, 1)
   218  	c.Check(toInstall["gtk-theme-yaru"], NotNil)
   219  }
   220  
   221  func (s *themesSuite) TestThemeStatusForPrefixIgnoresUnstable(c *C) {
   222  	s.daemon(c)
   223  
   224  	s.available = map[string]*snap.Info{
   225  		"gtk-theme-yaru": {
   226  			SuggestedName: "gtk-theme-yaru",
   227  			SideInfo: snap.SideInfo{
   228  				Channel: "edge",
   229  			},
   230  		},
   231  	}
   232  
   233  	ctx := context.Background()
   234  	status := make(map[string]daemon.ThemeStatus)
   235  	toInstall := make(map[string]bool)
   236  
   237  	err := daemon.CollectThemeStatusForPrefix(ctx, s, nil, "gtk-theme-", []string{"Yaru"}, nil, status, toInstall)
   238  	c.Check(err, IsNil)
   239  	c.Check(status, DeepEquals, map[string]daemon.ThemeStatus{
   240  		"Yaru": daemon.ThemeUnavailable,
   241  	})
   242  	c.Check(toInstall, HasLen, 0)
   243  }
   244  
   245  func (s *themesSuite) TestThemeStatusForPrefixReturnsErrors(c *C) {
   246  	s.daemon(c)
   247  
   248  	s.err = errors.New("store error")
   249  
   250  	ctx := context.Background()
   251  	status := make(map[string]daemon.ThemeStatus)
   252  	toInstall := make(map[string]bool)
   253  
   254  	err := daemon.CollectThemeStatusForPrefix(ctx, s, nil, "gtk-theme-", []string{"Theme"}, nil, status, toInstall)
   255  	c.Check(err, Equals, s.err)
   256  	c.Check(status, DeepEquals, map[string]daemon.ThemeStatus{
   257  		"Theme": daemon.ThemeUnavailable,
   258  	})
   259  	c.Check(toInstall, HasLen, 0)
   260  }
   261  
   262  func (s *themesSuite) TestThemeStatusAndCandidateSnaps(c *C) {
   263  	s.daemon(c)
   264  	s.mockSnap(c, `name: snap1
   265  version: 42
   266  slots:
   267    gtk-3-themes:
   268      interface: content
   269      content: gtk-3-themes
   270      source:
   271        read:
   272         - $SNAP/share/themes/Foo-gtk
   273    icon-themes:
   274      interface: content
   275      content: icon-themes
   276      source:
   277        read:
   278         - $SNAP/share/icons/Foo-icons
   279    sound-themes:
   280      interface: content
   281      content: sound-themes
   282      source:
   283        read:
   284         - $SNAP/share/sounds/Foo-sounds`)
   285  	s.available = map[string]*snap.Info{
   286  		"gtk-theme-bar": {
   287  			SuggestedName: "gtk-theme-bar",
   288  			SideInfo: snap.SideInfo{
   289  				Channel: "stable",
   290  			},
   291  		},
   292  		"icon-theme-bar": {
   293  			SuggestedName: "icon-theme-bar",
   294  			SideInfo: snap.SideInfo{
   295  				Channel: "stable",
   296  			},
   297  		},
   298  		"sound-theme-bar": {
   299  			SuggestedName: "sound-theme-bar",
   300  			SideInfo: snap.SideInfo{
   301  				Channel: "stable",
   302  			},
   303  		},
   304  	}
   305  
   306  	ctx := context.Background()
   307  	status, candidateSnaps, err := daemon.ThemeStatusAndCandidateSnaps(ctx, s.d, nil, []string{"Foo-gtk", "Bar-gtk", "Baz-gtk"}, []string{"Foo-icons", "Bar-icons", "Baz-icons"}, []string{"Foo-sounds", "Bar-sounds", "Baz-sounds"})
   308  	c.Check(err, IsNil)
   309  	c.Check(status.GtkThemes, DeepEquals, map[string]daemon.ThemeStatus{
   310  		"Foo-gtk": daemon.ThemeInstalled,
   311  		"Bar-gtk": daemon.ThemeAvailable,
   312  		"Baz-gtk": daemon.ThemeUnavailable,
   313  	})
   314  	c.Check(status.IconThemes, DeepEquals, map[string]daemon.ThemeStatus{
   315  		"Foo-icons": daemon.ThemeInstalled,
   316  		"Bar-icons": daemon.ThemeAvailable,
   317  		"Baz-icons": daemon.ThemeUnavailable,
   318  	})
   319  	c.Check(status.SoundThemes, DeepEquals, map[string]daemon.ThemeStatus{
   320  		"Foo-sounds": daemon.ThemeInstalled,
   321  		"Bar-sounds": daemon.ThemeAvailable,
   322  		"Baz-sounds": daemon.ThemeUnavailable,
   323  	})
   324  	c.Check(candidateSnaps, DeepEquals, map[string]bool{
   325  		"gtk-theme-bar":   true,
   326  		"icon-theme-bar":  true,
   327  		"sound-theme-bar": true,
   328  	})
   329  }
   330  
   331  func (s *themesSuite) TestThemesCmdGet(c *C) {
   332  	s.expectThemesAccess()
   333  	s.daemon(c)
   334  	s.available = map[string]*snap.Info{
   335  		"gtk-theme-foo": {
   336  			SuggestedName: "gtk-theme-foo",
   337  			SideInfo: snap.SideInfo{
   338  				Channel: "stable",
   339  			},
   340  		},
   341  		"icon-theme-foo": {
   342  			SuggestedName: "icon-theme-foo",
   343  			SideInfo: snap.SideInfo{
   344  				Channel: "stable",
   345  			},
   346  		},
   347  		"sound-theme-foo": {
   348  			SuggestedName: "sound-theme-foo",
   349  			SideInfo: snap.SideInfo{
   350  				Channel: "stable",
   351  			},
   352  		},
   353  	}
   354  
   355  	req := httptest.NewRequest("GET", "/v2/accessories/themes?gtk-theme=Foo-gtk&gtk-theme=Bar&icon-theme=Foo-icons&sound-theme=Foo-sounds", nil)
   356  	rsp := s.syncReq(c, req, nil)
   357  
   358  	c.Check(rsp.Type, Equals, daemon.ResponseTypeSync)
   359  	c.Check(rsp.Status, Equals, 200)
   360  	c.Check(rsp.Result, DeepEquals, daemon.ThemeStatusResponse{
   361  		GtkThemes: map[string]daemon.ThemeStatus{
   362  			"Foo-gtk": daemon.ThemeAvailable,
   363  			"Bar":     daemon.ThemeUnavailable,
   364  		},
   365  		IconThemes: map[string]daemon.ThemeStatus{
   366  			"Foo-icons": daemon.ThemeAvailable,
   367  		},
   368  		SoundThemes: map[string]daemon.ThemeStatus{
   369  			"Foo-sounds": daemon.ThemeAvailable,
   370  		},
   371  	})
   372  }
   373  
   374  func (s *themesSuite) daemonWithIfaceMgr(c *C) *daemon.Daemon {
   375  	d := s.apiBaseSuite.daemonWithOverlordMock()
   376  
   377  	overlord := d.Overlord()
   378  	st := overlord.State()
   379  	runner := overlord.TaskRunner()
   380  	hookMgr, err := hookstate.Manager(st, runner)
   381  	c.Assert(err, IsNil)
   382  	overlord.AddManager(hookMgr)
   383  	ifaceMgr, err := ifacestate.Manager(st, hookMgr, runner, nil, nil)
   384  	c.Assert(err, IsNil)
   385  	overlord.AddManager(ifaceMgr)
   386  	overlord.AddManager(runner)
   387  	c.Assert(overlord.StartUp(), IsNil)
   388  
   389  	st.Lock()
   390  	defer st.Unlock()
   391  	snapstate.ReplaceStore(st, s)
   392  	return d
   393  }
   394  
   395  func (s *themesSuite) TestThemesCmdPost(c *C) {
   396  	s.expectThemesAccess()
   397  	s.daemonWithIfaceMgr(c)
   398  
   399  	s.available = map[string]*snap.Info{
   400  		"gtk-theme-foo": {
   401  			SuggestedName: "gtk-theme-foo",
   402  			SideInfo: snap.SideInfo{
   403  				Channel: "stable",
   404  			},
   405  		},
   406  		"icon-theme-foo": {
   407  			SuggestedName: "icon-theme-foo",
   408  			SideInfo: snap.SideInfo{
   409  				Channel: "stable",
   410  			},
   411  		},
   412  		"sound-theme-foo": {
   413  			SuggestedName: "sound-theme-foo",
   414  			SideInfo: snap.SideInfo{
   415  				Channel: "stable",
   416  			},
   417  		},
   418  	}
   419  	restore := daemon.MockSnapstateInstallMany(func(s *state.State, names []string, _ []*snapstate.RevisionOptions, _ int, _ *snapstate.Flags) ([]string, []*state.TaskSet, error) {
   420  		t := s.NewTask("fake-theme-install", "Theme install")
   421  		return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
   422  	})
   423  	defer restore()
   424  
   425  	buf := bytes.NewBufferString(`{"gtk-themes":["Foo-gtk"],"icon-themes":["Foo-icons"],"sound-themes":["Foo-sounds"]}`)
   426  	req := httptest.NewRequest("POST", "/v2/accessories/themes", buf)
   427  	rsp := s.asyncReq(c, req, nil)
   428  	c.Check(rsp.Status, Equals, 202)
   429  
   430  	st := s.d.Overlord().State()
   431  	st.Lock()
   432  	defer st.Unlock()
   433  	chg := st.Change(rsp.Change)
   434  	c.Check(chg.Kind(), Equals, "install-themes")
   435  	c.Check(chg.Summary(), Equals, `Install snaps "gtk-theme-foo", "icon-theme-foo", "sound-theme-foo"`)
   436  	var names []string
   437  	err := chg.Get("snap-names", &names)
   438  	c.Assert(err, IsNil)
   439  	c.Check(names, DeepEquals, []string{"gtk-theme-foo", "icon-theme-foo", "sound-theme-foo"})
   440  }