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