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