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