github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/daemon/api_find_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-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 "errors" 24 "fmt" 25 "net" 26 "net/http" 27 "net/url" 28 29 "gopkg.in/check.v1" 30 31 "github.com/snapcore/snapd/client" 32 "github.com/snapcore/snapd/httputil" 33 "github.com/snapcore/snapd/overlord/auth" 34 "github.com/snapcore/snapd/overlord/snapstate" 35 "github.com/snapcore/snapd/snap" 36 "github.com/snapcore/snapd/store" 37 ) 38 39 var _ = check.Suite(&findSuite{}) 40 41 type findSuite struct { 42 apiBaseSuite 43 } 44 45 func (s *findSuite) TestFind(c *check.C) { 46 s.daemon(c) 47 48 s.suggestedCurrency = "EUR" 49 50 s.rsnaps = []*snap.Info{{ 51 SideInfo: snap.SideInfo{ 52 RealName: "store", 53 }, 54 Publisher: snap.StoreAccount{ 55 ID: "foo-id", 56 Username: "foo", 57 DisplayName: "Foo", 58 Validation: "unproven", 59 }, 60 }} 61 62 req, err := http.NewRequest("GET", "/v2/find?q=hi", nil) 63 c.Assert(err, check.IsNil) 64 65 rsp := s.syncReq(c, req, nil) 66 67 snaps := snapList(rsp.Result) 68 c.Assert(snaps, check.HasLen, 1) 69 c.Assert(snaps[0]["name"], check.Equals, "store") 70 c.Check(snaps[0]["prices"], check.IsNil) 71 c.Check(snaps[0]["channels"], check.IsNil) 72 73 c.Check(rsp.SuggestedCurrency, check.Equals, "EUR") 74 75 c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "hi"}) 76 c.Check(s.currentSnaps, check.HasLen, 0) 77 c.Check(s.actions, check.HasLen, 0) 78 } 79 80 func (s *findSuite) TestFindRefreshes(c *check.C) { 81 s.daemon(c) 82 83 s.rsnaps = []*snap.Info{{ 84 SideInfo: snap.SideInfo{ 85 RealName: "store", 86 }, 87 Publisher: snap.StoreAccount{ 88 ID: "foo-id", 89 Username: "foo", 90 DisplayName: "Foo", 91 Validation: "unproven", 92 }, 93 }} 94 s.mockSnap(c, "name: store\nversion: 1.0") 95 96 req, err := http.NewRequest("GET", "/v2/find?select=refresh", nil) 97 c.Assert(err, check.IsNil) 98 99 rsp := s.syncReq(c, req, nil) 100 101 snaps := snapList(rsp.Result) 102 c.Assert(snaps, check.HasLen, 1) 103 c.Assert(snaps[0]["name"], check.Equals, "store") 104 c.Check(s.currentSnaps, check.HasLen, 1) 105 c.Check(s.actions, check.HasLen, 1) 106 } 107 108 func (s *findSuite) TestFindRefreshSideloaded(c *check.C) { 109 d := s.daemon(c) 110 111 s.rsnaps = []*snap.Info{{ 112 SideInfo: snap.SideInfo{ 113 RealName: "store", 114 }, 115 Publisher: snap.StoreAccount{ 116 ID: "foo-id", 117 Username: "foo", 118 DisplayName: "Foo", 119 Validation: "unproven", 120 }, 121 }} 122 123 s.mockSnap(c, "name: store\nversion: 1.0") 124 125 var snapst snapstate.SnapState 126 st := d.Overlord().State() 127 st.Lock() 128 err := snapstate.Get(st, "store", &snapst) 129 st.Unlock() 130 c.Assert(err, check.IsNil) 131 c.Assert(snapst.Sequence, check.HasLen, 1) 132 133 // clear the snapid 134 snapst.Sequence[0].SnapID = "" 135 st.Lock() 136 snapstate.Set(st, "store", &snapst) 137 st.Unlock() 138 139 req, err := http.NewRequest("GET", "/v2/find?select=refresh", nil) 140 c.Assert(err, check.IsNil) 141 142 rsp := s.syncReq(c, req, nil) 143 144 snaps := snapList(rsp.Result) 145 c.Assert(snaps, check.HasLen, 0) 146 c.Check(s.currentSnaps, check.HasLen, 0) 147 c.Check(s.actions, check.HasLen, 0) 148 } 149 150 func (s *findSuite) TestFindPrivate(c *check.C) { 151 s.daemon(c) 152 153 s.rsnaps = []*snap.Info{} 154 155 req, err := http.NewRequest("GET", "/v2/find?q=foo&select=private", nil) 156 c.Assert(err, check.IsNil) 157 158 _ = s.syncReq(c, req, nil) 159 160 c.Check(s.storeSearch, check.DeepEquals, store.Search{ 161 Query: "foo", 162 Private: true, 163 }) 164 } 165 166 func (s *findSuite) TestFindUserAgentContextCreated(c *check.C) { 167 s.daemon(c) 168 169 req, err := http.NewRequest("GET", "/v2/find", nil) 170 c.Assert(err, check.IsNil) 171 req.Header.Add("User-Agent", "some-agent/1.0") 172 173 _ = s.syncReq(c, req, nil) 174 175 c.Check(store.ClientUserAgent(s.ctx), check.Equals, "some-agent/1.0") 176 } 177 178 func (s *findSuite) TestFindOneUserAgentContextCreated(c *check.C) { 179 s.daemon(c) 180 181 s.rsnaps = []*snap.Info{{ 182 SnapType: snap.TypeApp, 183 Version: "v2", 184 SideInfo: snap.SideInfo{ 185 RealName: "banana", 186 }, 187 Publisher: snap.StoreAccount{ 188 ID: "foo-id", 189 Username: "foo", 190 DisplayName: "Foo", 191 Validation: "unproven", 192 }, 193 }} 194 req, err := http.NewRequest("GET", "/v2/find?name=foo", nil) 195 c.Assert(err, check.IsNil) 196 req.Header.Add("User-Agent", "some-agent/1.0") 197 198 _ = s.syncReq(c, req, nil) 199 200 c.Check(store.ClientUserAgent(s.ctx), check.Equals, "some-agent/1.0") 201 } 202 203 func (s *findSuite) TestFindPrefix(c *check.C) { 204 s.daemon(c) 205 206 s.rsnaps = []*snap.Info{} 207 208 req, err := http.NewRequest("GET", "/v2/find?name=foo*", nil) 209 c.Assert(err, check.IsNil) 210 211 _ = s.syncReq(c, req, nil) 212 213 c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "foo", Prefix: true}) 214 } 215 216 func (s *findSuite) TestFindSection(c *check.C) { 217 s.daemon(c) 218 219 s.rsnaps = []*snap.Info{} 220 221 req, err := http.NewRequest("GET", "/v2/find?q=foo§ion=bar", nil) 222 c.Assert(err, check.IsNil) 223 224 _ = s.syncReq(c, req, nil) 225 226 c.Check(s.storeSearch, check.DeepEquals, store.Search{ 227 Query: "foo", 228 Category: "bar", 229 }) 230 } 231 232 func (s *findSuite) TestFindScope(c *check.C) { 233 s.daemon(c) 234 235 s.rsnaps = []*snap.Info{} 236 237 req, err := http.NewRequest("GET", "/v2/find?q=foo&scope=creep", nil) 238 c.Assert(err, check.IsNil) 239 240 _ = s.syncReq(c, req, nil) 241 242 c.Check(s.storeSearch, check.DeepEquals, store.Search{ 243 Query: "foo", 244 Scope: "creep", 245 }) 246 } 247 248 func (s *findSuite) TestFindCommonID(c *check.C) { 249 s.daemon(c) 250 251 s.rsnaps = []*snap.Info{{ 252 SideInfo: snap.SideInfo{ 253 RealName: "store", 254 }, 255 Publisher: snap.StoreAccount{ 256 ID: "foo-id", 257 Username: "foo", 258 DisplayName: "Foo", 259 Validation: "unproven", 260 }, 261 CommonIDs: []string{"org.foo"}, 262 }} 263 s.mockSnap(c, "name: store\nversion: 1.0") 264 265 req, err := http.NewRequest("GET", "/v2/find?name=foo", nil) 266 c.Assert(err, check.IsNil) 267 268 rsp := s.syncReq(c, req, nil) 269 270 snaps := snapList(rsp.Result) 271 c.Assert(snaps, check.HasLen, 1) 272 c.Check(snaps[0]["common-ids"], check.DeepEquals, []interface{}{"org.foo"}) 273 } 274 275 func (s *findSuite) TestFindByCommonID(c *check.C) { 276 s.daemon(c) 277 278 s.rsnaps = []*snap.Info{{ 279 SideInfo: snap.SideInfo{ 280 RealName: "store", 281 }, 282 Publisher: snap.StoreAccount{ 283 ID: "foo-id", 284 Username: "foo", 285 DisplayName: "Foo", 286 Validation: "unproven", 287 }, 288 CommonIDs: []string{"org.foo"}, 289 }} 290 s.mockSnap(c, "name: store\nversion: 1.0") 291 292 req, err := http.NewRequest("GET", "/v2/find?common-id=org.foo", nil) 293 c.Assert(err, check.IsNil) 294 295 rsp := s.syncReq(c, req, nil) 296 297 snaps := snapList(rsp.Result) 298 c.Assert(snaps, check.HasLen, 1) 299 c.Check(s.storeSearch, check.DeepEquals, store.Search{CommonID: "org.foo"}) 300 } 301 302 func (s *findSuite) TestFindOne(c *check.C) { 303 s.daemon(c) 304 305 s.rsnaps = []*snap.Info{{ 306 SideInfo: snap.SideInfo{ 307 RealName: "store", 308 }, 309 Base: "base0", 310 Publisher: snap.StoreAccount{ 311 ID: "foo-id", 312 Username: "foo", 313 DisplayName: "Foo", 314 Validation: "verified", 315 }, 316 Channels: map[string]*snap.ChannelSnapInfo{ 317 "stable": { 318 Revision: snap.R(42), 319 }, 320 }, 321 }} 322 s.mockSnap(c, "name: store\nversion: 1.0") 323 324 req, err := http.NewRequest("GET", "/v2/find?name=foo", nil) 325 c.Assert(err, check.IsNil) 326 327 rsp := s.syncReq(c, req, nil) 328 329 c.Check(s.storeSearch, check.DeepEquals, store.Search{}) 330 331 snaps := snapList(rsp.Result) 332 c.Assert(snaps, check.HasLen, 1) 333 c.Check(snaps[0]["name"], check.Equals, "store") 334 c.Check(snaps[0]["base"], check.Equals, "base0") 335 c.Check(snaps[0]["publisher"], check.DeepEquals, map[string]interface{}{ 336 "id": "foo-id", 337 "username": "foo", 338 "display-name": "Foo", 339 "validation": "verified", 340 }) 341 m := snaps[0]["channels"].(map[string]interface{})["stable"].(map[string]interface{}) 342 343 c.Check(m["revision"], check.Equals, "42") 344 } 345 346 func (s *findSuite) TestFindOneNotFound(c *check.C) { 347 s.daemon(c) 348 349 s.err = store.ErrSnapNotFound 350 s.mockSnap(c, "name: store\nversion: 1.0") 351 352 req, err := http.NewRequest("GET", "/v2/find?name=foo", nil) 353 c.Assert(err, check.IsNil) 354 355 rsp := s.errorReq(c, req, nil) 356 357 c.Check(s.storeSearch, check.DeepEquals, store.Search{}) 358 c.Check(rsp.Status, check.Equals, 404) 359 } 360 361 func (s *findSuite) TestFindOneWithAuth(c *check.C) { 362 d := s.daemon(c) 363 364 state := d.Overlord().State() 365 state.Lock() 366 user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) 367 state.Unlock() 368 c.Check(err, check.IsNil) 369 370 req, err := http.NewRequest("GET", "/v2/find?q=name:gfoo", nil) 371 c.Assert(err, check.IsNil) 372 373 c.Assert(s.user, check.IsNil) 374 375 _ = s.syncReq(c, req, user) 376 377 // ensure user was set 378 c.Assert(s.user, check.DeepEquals, user) 379 } 380 381 func (s *findSuite) TestFindRefreshNotOther(c *check.C) { 382 s.daemon(c) 383 384 for _, other := range []string{"name", "q", "common-id"} { 385 req, err := http.NewRequest("GET", "/v2/find?select=refresh&"+other+"=foo*", nil) 386 c.Assert(err, check.IsNil) 387 388 rspe := s.errorReq(c, req, nil) 389 c.Check(rspe.Status, check.Equals, 400) 390 c.Check(rspe.Message, check.Equals, "cannot use '"+other+"' with 'select=refresh'") 391 } 392 } 393 394 func (s *findSuite) TestFindNotTogether(c *check.C) { 395 s.daemon(c) 396 397 queries := map[string]string{"q": "foo", "name": "foo*", "common-id": "foo"} 398 for ki, vi := range queries { 399 for kj, vj := range queries { 400 if ki == kj { 401 continue 402 } 403 404 req, err := http.NewRequest("GET", fmt.Sprintf("/v2/find?%s=%s&%s=%s", ki, vi, kj, vj), nil) 405 c.Assert(err, check.IsNil) 406 407 rspe := s.errorReq(c, req, nil) 408 c.Check(rspe.Status, check.Equals, 400) 409 exp1 := "cannot use '" + ki + "' and '" + kj + "' together" 410 exp2 := "cannot use '" + kj + "' and '" + ki + "' together" 411 c.Check(rspe.Message, check.Matches, exp1+"|"+exp2) 412 } 413 } 414 } 415 416 func (s *findSuite) TestFindBadQueryReturnsCorrectErrorKind(c *check.C) { 417 s.daemon(c) 418 419 s.err = store.ErrBadQuery 420 req, err := http.NewRequest("GET", "/v2/find?q=return-bad-query-please", nil) 421 c.Assert(err, check.IsNil) 422 423 rspe := s.errorReq(c, req, nil) 424 c.Check(rspe.Status, check.Equals, 400) 425 c.Check(rspe.Message, check.Matches, "bad query") 426 c.Check(rspe.Kind, check.Equals, client.ErrorKindBadQuery) 427 } 428 429 func (s *findSuite) TestFindNetworkErrorsReturnCorrectErrorKind(c *check.C) { 430 s.daemon(c) 431 432 req, err := http.NewRequest("GET", "/v2/find?q=query", nil) 433 c.Assert(err, check.IsNil) 434 435 pne := &httputil.PersistentNetworkError{Err: errors.New("problem")} 436 neTout := fakeNetError{message: "net problem", timeout: true} 437 dnse := &net.DNSError{Name: "store", IsTemporary: true} 438 uDNSe := &url.Error{ 439 Op: "Get", 440 URL: "http://...", 441 Err: &net.OpError{ 442 Op: "dial", 443 Net: "tcp", 444 Err: dnse, 445 }, 446 } 447 448 tests := []struct { 449 err error 450 kind client.ErrorKind 451 expected string 452 }{ 453 {pne, client.ErrorKindDNSFailure, "persistent network error: problem"}, 454 {neTout, client.ErrorKindNetworkTimeout, "net problem"}, 455 {uDNSe, client.ErrorKindDNSFailure, dnse.Error()}, 456 } 457 458 for _, t := range tests { 459 s.err = t.err 460 461 rspe := s.errorReq(c, req, nil) 462 c.Check(rspe.Status, check.Equals, 400) 463 c.Check(rspe.Message, check.Equals, t.expected) 464 c.Check(rspe.Kind, check.Equals, t.kind) 465 } 466 } 467 468 func (s *findSuite) TestFindPriced(c *check.C) { 469 s.daemon(c) 470 471 s.suggestedCurrency = "GBP" 472 473 s.rsnaps = []*snap.Info{{ 474 SnapType: snap.TypeApp, 475 Version: "v2", 476 Prices: map[string]float64{ 477 "GBP": 1.23, 478 "EUR": 2.34, 479 }, 480 MustBuy: true, 481 SideInfo: snap.SideInfo{ 482 RealName: "banana", 483 }, 484 Publisher: snap.StoreAccount{ 485 ID: "foo-id", 486 Username: "foo", 487 DisplayName: "Foo", 488 Validation: "unproven", 489 }, 490 }} 491 492 req, err := http.NewRequest("GET", "/v2/find?q=banana&channel=stable", nil) 493 c.Assert(err, check.IsNil) 494 rsp := s.syncReq(c, req, nil) 495 496 snaps := snapList(rsp.Result) 497 c.Assert(snaps, check.HasLen, 1) 498 499 snap := snaps[0] 500 c.Check(snap["name"], check.Equals, "banana") 501 c.Check(snap["prices"], check.DeepEquals, map[string]interface{}{ 502 "EUR": 2.34, 503 "GBP": 1.23, 504 }) 505 c.Check(snap["status"], check.Equals, "priced") 506 507 c.Check(rsp.SuggestedCurrency, check.Equals, "GBP") 508 } 509 510 func (s *findSuite) TestFindScreenshotted(c *check.C) { 511 s.daemon(c) 512 513 s.rsnaps = []*snap.Info{{ 514 SnapType: snap.TypeApp, 515 Version: "v2", 516 Media: []snap.MediaInfo{ 517 { 518 Type: "screenshot", 519 URL: "http://example.com/screenshot.png", 520 Width: 800, 521 Height: 1280, 522 }, 523 { 524 Type: "screenshot", 525 URL: "http://example.com/screenshot2.png", 526 }, 527 }, 528 MustBuy: true, 529 SideInfo: snap.SideInfo{ 530 RealName: "test-screenshot", 531 }, 532 Publisher: snap.StoreAccount{ 533 ID: "foo-id", 534 Username: "foo", 535 DisplayName: "Foo", 536 Validation: "unproven", 537 }, 538 }} 539 540 req, err := http.NewRequest("GET", "/v2/find?q=test-screenshot", nil) 541 c.Assert(err, check.IsNil) 542 rsp := s.syncReq(c, req, nil) 543 544 snaps := snapList(rsp.Result) 545 c.Assert(snaps, check.HasLen, 1) 546 547 c.Check(snaps[0]["name"], check.Equals, "test-screenshot") 548 c.Check(snaps[0]["media"], check.DeepEquals, []interface{}{ 549 map[string]interface{}{ 550 "type": "screenshot", 551 "url": "http://example.com/screenshot.png", 552 "width": float64(800), 553 "height": float64(1280), 554 }, 555 map[string]interface{}{ 556 "type": "screenshot", 557 "url": "http://example.com/screenshot2.png", 558 }, 559 }) 560 } 561 562 func (s *findSuite) TestSnapsStoreConfinement(c *check.C) { 563 s.daemon(c) 564 565 s.rsnaps = []*snap.Info{ 566 { 567 // no explicit confinement in this one 568 SideInfo: snap.SideInfo{ 569 RealName: "foo", 570 }, 571 }, 572 { 573 Confinement: snap.StrictConfinement, 574 SideInfo: snap.SideInfo{ 575 RealName: "bar", 576 }, 577 }, 578 { 579 Confinement: snap.DevModeConfinement, 580 SideInfo: snap.SideInfo{ 581 RealName: "baz", 582 }, 583 }, 584 } 585 586 req, err := http.NewRequest("GET", "/v2/find", nil) 587 c.Assert(err, check.IsNil) 588 589 rsp := s.syncReq(c, req, nil) 590 591 snaps := snapList(rsp.Result) 592 c.Assert(snaps, check.HasLen, 3) 593 594 for i, ss := range [][2]string{ 595 {"foo", string(snap.StrictConfinement)}, 596 {"bar", string(snap.StrictConfinement)}, 597 {"baz", string(snap.DevModeConfinement)}, 598 } { 599 name, mode := ss[0], ss[1] 600 c.Check(snaps[i]["name"], check.Equals, name, check.Commentf(name)) 601 c.Check(snaps[i]["confinement"], check.Equals, mode, check.Commentf(name)) 602 } 603 }