Files
snapd/daemon/api_themes_test.go
Oliver Calder 5a9ca3b23e daemon: allow multiple interfaces in interface{Open,Authenticated}Access
Some endpoints needs to be accessible to snaps via more than one
interface. This change turns the `Interface string` field of
`interface{Open,Authenticated}Access` into `Interfaces []string`.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
2024-04-02 14:32:49 -05:00

441 lines
13 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2020-2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package daemon_test
import (
"bytes"
"context"
"errors"
"fmt"
"net/http/httptest"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/daemon"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/ifacestate"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/channel"
"github.com/snapcore/snapd/snap/naming"
"github.com/snapcore/snapd/store"
)
var _ = Suite(&themesSuite{})
type themesSuite struct {
apiBaseSuite
available map[string]*snap.Info
}
func (s *themesSuite) SetUpTest(c *C) {
s.apiBaseSuite.SetUpTest(c)
s.available = make(map[string]*snap.Info)
s.err = store.ErrSnapNotFound
}
func (s *themesSuite) SnapExists(ctx context.Context, spec store.SnapSpec, user *auth.UserState) (naming.SnapRef, *channel.Channel, error) {
s.pokeStateLock()
if info := s.available[spec.Name]; info != nil {
ch, err := channel.Parse(info.Channel, "")
if err != nil {
panic(fmt.Sprintf("bad Info Channel: %v", err))
}
return info, &ch, nil
}
return nil, nil, s.err
}
func (s *themesSuite) daemon(c *C) *daemon.Daemon {
return s.apiBaseSuite.daemonWithStore(c, s)
}
func (s *themesSuite) expectThemesAccess() {
s.expectReadAccess(daemon.InterfaceOpenAccess{Interfaces: []string{"snap-themes-control"}})
s.expectWriteAccess(daemon.InterfaceAuthenticatedAccess{Interfaces: []string{"snap-themes-control"}, Polkit: "io.snapcraft.snapd.manage"})
}
func (s *themesSuite) TestInstalledThemes(c *C) {
d := s.daemon(c)
s.mockSnap(c, `name: snap1
version: 42
slots:
gtk-3-themes:
interface: content
content: gtk-3-themes
source:
read:
- $SNAP/share/themes/Foo-gtk
- $SNAP/share/themes/Foo-gtk-dark
icon-themes:
interface: content
content: icon-themes
source:
read:
- $SNAP/share/icons/Foo-icons
sound-themes:
interface: content
content: sound-themes
source:
read:
- $SNAP/share/sounds/Foo-sounds`)
s.mockSnap(c, `name: snap2
version: 42
slots:
gtk-3-themes:
interface: content
content: gtk-3-themes
source:
read:
- $SNAP/share/themes/Bar-gtk
icon-themes:
interface: content
content: icon-themes
source:
read:
- $SNAP/share/icons/Bar-icons
sound-themes:
interface: content
content: sound-themes
source:
read:
- $SNAP/share/sounds/Bar-sounds`)
s.mockSnap(c, `name: not-a-theme
version: 42
slots:
foo:
interface: content
content: foo
read: $SNAP/foo`)
gtkThemes, iconThemes, soundThemes, err := daemon.InstalledThemes(d.Overlord())
c.Check(err, IsNil)
c.Check(gtkThemes, DeepEquals, []string{"Bar-gtk", "Foo-gtk", "Foo-gtk-dark"})
c.Check(iconThemes, DeepEquals, []string{"Bar-icons", "Foo-icons"})
c.Check(soundThemes, DeepEquals, []string{"Bar-sounds", "Foo-sounds"})
}
func (s *themesSuite) TestThemePackageCandidates(c *C) {
// The package name includes the passed in prefix
c.Check(daemon.ThemePackageCandidates("gtk-theme-", "Yaru"), DeepEquals, []string{"gtk-theme-yaru"})
c.Check(daemon.ThemePackageCandidates("icon-theme-", "Yaru"), DeepEquals, []string{"icon-theme-yaru"})
c.Check(daemon.ThemePackageCandidates("sound-theme-", "Yaru"), DeepEquals, []string{"sound-theme-yaru"})
// If a theme name includes multiple dash separated
// components, multiple possible package names are returned,
// from most specific to least.
c.Check(daemon.ThemePackageCandidates("gtk-theme-", "Yaru-dark"), DeepEquals, []string{"gtk-theme-yaru-dark", "gtk-theme-yaru"})
c.Check(daemon.ThemePackageCandidates("gtk-theme-", "Matcha-dark-azul"), DeepEquals, []string{"gtk-theme-matcha-dark-azul", "gtk-theme-matcha-dark", "gtk-theme-matcha"})
// Digits are accepted in package names
c.Check(daemon.ThemePackageCandidates("gtk-theme-", "abc123xyz"), DeepEquals, []string{"gtk-theme-abc123xyz"})
// In addition to case folding, bad characters are converted to dashes
c.Check(daemon.ThemePackageCandidates("icon-theme-", "Breeze_Snow"), DeepEquals, []string{"icon-theme-breeze-snow", "icon-theme-breeze"})
// Groups of bad characters are collapsed to a single dash,
// with leading and trailing dashes removed
c.Check(daemon.ThemePackageCandidates("gtk-theme-", "+foo_"), DeepEquals, []string{"gtk-theme-foo"})
c.Check(daemon.ThemePackageCandidates("gtk-theme-", "foo-_--bar+-"), DeepEquals, []string{"gtk-theme-foo-bar", "gtk-theme-foo"})
}
func (s *themesSuite) TestThemeStatusForPrefix(c *C) {
s.daemon(c)
s.available = map[string]*snap.Info{
"gtk-theme-available": {
SuggestedName: "gtk-theme-available",
SideInfo: snap.SideInfo{
Channel: "stable",
},
},
"gtk-theme-installed": {
SuggestedName: "gtk-theme-installed",
SideInfo: snap.SideInfo{
Channel: "stable",
},
},
}
ctx := context.Background()
status := make(map[string]daemon.ThemeStatus)
toInstall := make(map[string]bool)
err := daemon.CollectThemeStatusForPrefix(ctx, s, nil, "gtk-theme-", []string{"Installed", "Installed", "Available", "Unavailable"}, []string{"Installed"}, status, toInstall)
c.Check(err, IsNil)
c.Check(status, DeepEquals, map[string]daemon.ThemeStatus{
"Installed": daemon.ThemeInstalled,
"Available": daemon.ThemeAvailable,
"Unavailable": daemon.ThemeUnavailable,
})
c.Check(toInstall, HasLen, 1)
c.Check(toInstall["gtk-theme-available"], NotNil)
}
func (s *themesSuite) TestThemeStatusForPrefixStripsSuffixes(c *C) {
s.daemon(c)
s.available = map[string]*snap.Info{
"gtk-theme-yaru": {
SuggestedName: "gtk-theme-yaru",
SideInfo: snap.SideInfo{
Channel: "stable",
},
},
}
ctx := context.Background()
status := make(map[string]daemon.ThemeStatus)
toInstall := make(map[string]bool)
err := daemon.CollectThemeStatusForPrefix(ctx, s, nil, "gtk-theme-", []string{"Yaru-dark"}, nil, status, toInstall)
c.Check(err, IsNil)
c.Check(status, DeepEquals, map[string]daemon.ThemeStatus{
"Yaru-dark": daemon.ThemeAvailable,
})
c.Check(toInstall, HasLen, 1)
c.Check(toInstall["gtk-theme-yaru"], NotNil)
}
func (s *themesSuite) TestThemeStatusForPrefixIgnoresUnstable(c *C) {
s.daemon(c)
s.available = map[string]*snap.Info{
"gtk-theme-yaru": {
SuggestedName: "gtk-theme-yaru",
SideInfo: snap.SideInfo{
Channel: "edge",
},
},
}
ctx := context.Background()
status := make(map[string]daemon.ThemeStatus)
toInstall := make(map[string]bool)
err := daemon.CollectThemeStatusForPrefix(ctx, s, nil, "gtk-theme-", []string{"Yaru"}, nil, status, toInstall)
c.Check(err, IsNil)
c.Check(status, DeepEquals, map[string]daemon.ThemeStatus{
"Yaru": daemon.ThemeUnavailable,
})
c.Check(toInstall, HasLen, 0)
}
func (s *themesSuite) TestThemeStatusForPrefixReturnsErrors(c *C) {
s.daemon(c)
s.err = errors.New("store error")
ctx := context.Background()
status := make(map[string]daemon.ThemeStatus)
toInstall := make(map[string]bool)
err := daemon.CollectThemeStatusForPrefix(ctx, s, nil, "gtk-theme-", []string{"Theme"}, nil, status, toInstall)
c.Check(err, Equals, s.err)
c.Check(status, DeepEquals, map[string]daemon.ThemeStatus{
"Theme": daemon.ThemeUnavailable,
})
c.Check(toInstall, HasLen, 0)
}
func (s *themesSuite) TestThemeStatusAndCandidateSnaps(c *C) {
s.daemon(c)
s.mockSnap(c, `name: snap1
version: 42
slots:
gtk-3-themes:
interface: content
content: gtk-3-themes
source:
read:
- $SNAP/share/themes/Foo-gtk
icon-themes:
interface: content
content: icon-themes
source:
read:
- $SNAP/share/icons/Foo-icons
sound-themes:
interface: content
content: sound-themes
source:
read:
- $SNAP/share/sounds/Foo-sounds`)
s.available = map[string]*snap.Info{
"gtk-theme-bar": {
SuggestedName: "gtk-theme-bar",
SideInfo: snap.SideInfo{
Channel: "stable",
},
},
"icon-theme-bar": {
SuggestedName: "icon-theme-bar",
SideInfo: snap.SideInfo{
Channel: "stable",
},
},
"sound-theme-bar": {
SuggestedName: "sound-theme-bar",
SideInfo: snap.SideInfo{
Channel: "stable",
},
},
}
ctx := context.Background()
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"})
c.Check(err, IsNil)
c.Check(status.GtkThemes, DeepEquals, map[string]daemon.ThemeStatus{
"Foo-gtk": daemon.ThemeInstalled,
"Bar-gtk": daemon.ThemeAvailable,
"Baz-gtk": daemon.ThemeUnavailable,
})
c.Check(status.IconThemes, DeepEquals, map[string]daemon.ThemeStatus{
"Foo-icons": daemon.ThemeInstalled,
"Bar-icons": daemon.ThemeAvailable,
"Baz-icons": daemon.ThemeUnavailable,
})
c.Check(status.SoundThemes, DeepEquals, map[string]daemon.ThemeStatus{
"Foo-sounds": daemon.ThemeInstalled,
"Bar-sounds": daemon.ThemeAvailable,
"Baz-sounds": daemon.ThemeUnavailable,
})
c.Check(candidateSnaps, DeepEquals, map[string]bool{
"gtk-theme-bar": true,
"icon-theme-bar": true,
"sound-theme-bar": true,
})
}
func (s *themesSuite) TestThemesCmdGet(c *C) {
s.expectThemesAccess()
s.daemon(c)
s.available = map[string]*snap.Info{
"gtk-theme-foo": {
SuggestedName: "gtk-theme-foo",
SideInfo: snap.SideInfo{
Channel: "stable",
},
},
"icon-theme-foo": {
SuggestedName: "icon-theme-foo",
SideInfo: snap.SideInfo{
Channel: "stable",
},
},
"sound-theme-foo": {
SuggestedName: "sound-theme-foo",
SideInfo: snap.SideInfo{
Channel: "stable",
},
},
}
req := httptest.NewRequest("GET", "/v2/accessories/themes?gtk-theme=Foo-gtk&gtk-theme=Bar&icon-theme=Foo-icons&sound-theme=Foo-sounds", nil)
rsp := s.syncReq(c, req, nil)
c.Check(rsp.Type, Equals, daemon.ResponseTypeSync)
c.Check(rsp.Status, Equals, 200)
c.Check(rsp.Result, DeepEquals, daemon.ThemeStatusResponse{
GtkThemes: map[string]daemon.ThemeStatus{
"Foo-gtk": daemon.ThemeAvailable,
"Bar": daemon.ThemeUnavailable,
},
IconThemes: map[string]daemon.ThemeStatus{
"Foo-icons": daemon.ThemeAvailable,
},
SoundThemes: map[string]daemon.ThemeStatus{
"Foo-sounds": daemon.ThemeAvailable,
},
})
}
func (s *themesSuite) daemonWithIfaceMgr(c *C) *daemon.Daemon {
d := s.apiBaseSuite.daemonWithOverlordMock()
overlord := d.Overlord()
st := overlord.State()
runner := overlord.TaskRunner()
hookMgr, err := hookstate.Manager(st, runner)
c.Assert(err, IsNil)
overlord.AddManager(hookMgr)
ifaceMgr, err := ifacestate.Manager(st, hookMgr, runner, nil, nil)
c.Assert(err, IsNil)
overlord.AddManager(ifaceMgr)
overlord.AddManager(runner)
c.Assert(overlord.StartUp(), IsNil)
st.Lock()
defer st.Unlock()
snapstate.ReplaceStore(st, s)
return d
}
func (s *themesSuite) TestThemesCmdPost(c *C) {
s.expectThemesAccess()
s.daemonWithIfaceMgr(c)
s.available = map[string]*snap.Info{
"gtk-theme-foo": {
SuggestedName: "gtk-theme-foo",
SideInfo: snap.SideInfo{
Channel: "stable",
},
},
"icon-theme-foo": {
SuggestedName: "icon-theme-foo",
SideInfo: snap.SideInfo{
Channel: "stable",
},
},
"sound-theme-foo": {
SuggestedName: "sound-theme-foo",
SideInfo: snap.SideInfo{
Channel: "stable",
},
},
}
restore := daemon.MockSnapstateInstallMany(func(s *state.State, names []string, _ []*snapstate.RevisionOptions, _ int, _ *snapstate.Flags) ([]string, []*state.TaskSet, error) {
t := s.NewTask("fake-theme-install", "Theme install")
return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
})
defer restore()
buf := bytes.NewBufferString(`{"gtk-themes":["Foo-gtk"],"icon-themes":["Foo-icons"],"sound-themes":["Foo-sounds"]}`)
req := httptest.NewRequest("POST", "/v2/accessories/themes", buf)
rsp := s.asyncReq(c, req, nil)
c.Check(rsp.Status, Equals, 202)
st := s.d.Overlord().State()
st.Lock()
defer st.Unlock()
chg := st.Change(rsp.Change)
c.Check(chg.Kind(), Equals, "install-themes")
c.Check(chg.Summary(), Equals, `Install snaps "gtk-theme-foo", "icon-theme-foo", "sound-theme-foo"`)
var names []string
err := chg.Get("snap-names", &names)
c.Assert(err, IsNil)
c.Check(names, DeepEquals, []string{"gtk-theme-foo", "icon-theme-foo", "sound-theme-foo"})
}