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>k-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 }