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