mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
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>
441 lines
13 KiB
Go
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>k-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"})
|
|
}
|