gitee.com/mysnapcore/mysnapd@v0.1.0/store/tooling/tooling_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2022 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 tooling_test 21 22 import ( 23 "context" 24 "encoding/base64" 25 "fmt" 26 "io/ioutil" 27 "net/http" 28 "net/url" 29 "os" 30 "path/filepath" 31 "runtime" 32 "strings" 33 "testing" 34 35 . "gopkg.in/check.v1" 36 37 "gitee.com/mysnapcore/mysnapd/asserts" 38 "gitee.com/mysnapcore/mysnapd/asserts/assertstest" 39 "gitee.com/mysnapcore/mysnapd/logger" 40 "gitee.com/mysnapcore/mysnapd/osutil" 41 "gitee.com/mysnapcore/mysnapd/overlord/auth" 42 "gitee.com/mysnapcore/mysnapd/progress" 43 "gitee.com/mysnapcore/mysnapd/seed/seedtest" 44 "gitee.com/mysnapcore/mysnapd/snap" 45 "gitee.com/mysnapcore/mysnapd/store" 46 "gitee.com/mysnapcore/mysnapd/store/tooling" 47 "gitee.com/mysnapcore/mysnapd/testutil" 48 ) 49 50 func Test(t *testing.T) { TestingT(t) } 51 52 type toolingSuite struct { 53 testutil.BaseTest 54 root string 55 56 storeActionsBunchSizes []int 57 storeActions []*store.SnapAction 58 curSnaps [][]*store.CurrentSnap 59 60 tsto *tooling.ToolingStore 61 62 // SeedSnaps helps creating and making available seed snaps 63 // (it provides MakeAssertedSnap etc.) for the tests. 64 *seedtest.SeedSnaps 65 } 66 67 var _ = Suite(&toolingSuite{}) 68 69 var ( 70 brandPrivKey, _ = assertstest.GenerateKey(752) 71 ) 72 73 const packageCore = ` 74 name: core 75 version: 16.04 76 type: os 77 ` 78 79 func (s *toolingSuite) SetUpTest(c *C) { 80 s.root = c.MkDir() 81 82 s.BaseTest.SetUpTest(c) 83 s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) 84 85 s.tsto = tooling.MockToolingStore(s) 86 87 s.SeedSnaps = &seedtest.SeedSnaps{} 88 s.SetupAssertSigning("canonical") 89 s.Brands.Register("my-brand", brandPrivKey, map[string]interface{}{ 90 "verification": "verified", 91 }) 92 assertstest.AddMany(s.StoreSigning, s.Brands.AccountsAndKeys("my-brand")...) 93 94 otherAcct := assertstest.NewAccount(s.StoreSigning, "other", map[string]interface{}{ 95 "account-id": "other", 96 }, "") 97 s.StoreSigning.Add(otherAcct) 98 99 // mock the mount cmds (for the extract kernel assets stuff) 100 c1 := testutil.MockCommand(c, "mount", "") 101 s.AddCleanup(c1.Restore) 102 c2 := testutil.MockCommand(c, "umount", "") 103 s.AddCleanup(c2.Restore) 104 } 105 106 func (s *toolingSuite) MakeAssertedSnap(c *C, snapYaml string, files [][]string, revision snap.Revision, developerID string) { 107 s.SeedSnaps.MakeAssertedSnap(c, snapYaml, files, revision, developerID, s.StoreSigning.Database) 108 } 109 110 func (s *toolingSuite) setupSnaps(c *C, publishers map[string]string, defaultsYaml string) { 111 s.MakeAssertedSnap(c, packageCore, nil, snap.R(3), "canonical") 112 } 113 114 func (s *toolingSuite) TestNewToolingStore(c *C) { 115 // default 116 u, err := url.Parse("https://api.snapcraft.io/") 117 c.Assert(err, IsNil) 118 119 tsto, err := tooling.NewToolingStore() 120 c.Assert(err, IsNil) 121 122 c.Check(tsto.StoreURL(), DeepEquals, u) 123 } 124 125 func (s *toolingSuite) TestNewToolingStoreUbuntuStoreURL(c *C) { 126 u, err := url.Parse("https://api.other") 127 c.Assert(err, IsNil) 128 129 os.Setenv("UBUNTU_STORE_URL", "https://api.other") 130 defer os.Unsetenv("UBUNTU_STORE_URL") 131 132 tsto, err := tooling.NewToolingStore() 133 c.Assert(err, IsNil) 134 135 c.Check(tsto.StoreURL(), DeepEquals, u) 136 } 137 138 func (s *toolingSuite) TestNewToolingStoreInvalidUbuntuStoreURL(c *C) { 139 os.Setenv("UBUNTU_STORE_URL", ":/what") 140 defer os.Unsetenv("UBUNTU_STORE_URL") 141 142 _, err := tooling.NewToolingStore() 143 c.Assert(err, ErrorMatches, `invalid UBUNTU_STORE_URL: .*`) 144 } 145 146 func (s *toolingSuite) TestNewToolingStoreWithAuthFile(c *C) { 147 tmpdir := c.MkDir() 148 authFn := filepath.Join(tmpdir, "auth.json") 149 err := ioutil.WriteFile(authFn, []byte(`{ 150 "macaroon": "MACAROON", 151 "discharges": ["DISCHARGE"] 152 }`), 0600) 153 c.Assert(err, IsNil) 154 155 os.Setenv("UBUNTU_STORE_AUTH_DATA_FILENAME", authFn) 156 defer os.Unsetenv("UBUNTU_STORE_AUTH_DATA_FILENAME") 157 158 tsto, err := tooling.NewToolingStore() 159 c.Assert(err, IsNil) 160 creds := tsto.Creds() 161 u1creds, ok := creds.(*tooling.UbuntuOneCreds) 162 c.Assert(ok, Equals, true) 163 c.Check(u1creds.User.StoreMacaroon, Equals, "MACAROON") 164 c.Check(u1creds.User.StoreDischarges, DeepEquals, []string{"DISCHARGE"}) 165 } 166 167 func (s *toolingSuite) TestNewToolingStoreWithBase64AuthFile(c *C) { 168 tmpdir := c.MkDir() 169 authFn := filepath.Join(tmpdir, "auth7a") 170 authObj := []byte(`{ 171 "r": "MACAROON", 172 "d": "DISCHARGE" 173 }`) 174 enc := []byte(base64.StdEncoding.EncodeToString(authObj)) 175 err := ioutil.WriteFile(authFn, enc, 0600) 176 c.Assert(err, IsNil) 177 178 os.Setenv("UBUNTU_STORE_AUTH_DATA_FILENAME", authFn) 179 defer os.Unsetenv("UBUNTU_STORE_AUTH_DATA_FILENAME") 180 181 tsto, err := tooling.NewToolingStore() 182 c.Assert(err, IsNil) 183 creds := tsto.Creds() 184 u1creds, ok := creds.(*tooling.UbuntuOneCreds) 185 c.Assert(ok, Equals, true) 186 c.Check(u1creds.User.StoreMacaroon, Equals, "MACAROON") 187 c.Check(u1creds.User.StoreDischarges, DeepEquals, []string{"DISCHARGE"}) 188 } 189 190 func (s *toolingSuite) TestNewToolingStoreWithAuthFileErrors(c *C) { 191 tmpdir := c.MkDir() 192 authFn := filepath.Join(tmpdir, "creds") 193 194 os.Setenv("UBUNTU_STORE_AUTH_DATA_FILENAME", authFn) 195 defer os.Unsetenv("UBUNTU_STORE_AUTH_DATA_FILENAME") 196 197 tests := []struct { 198 data string 199 err string 200 }{ 201 {"", `invalid auth file ".*/creds": empty`}, 202 {" {}", `invalid auth file ".*/creds": missing fields`}, 203 {" [...", `invalid snapcraft login file ".*/creds": No section: login.ubuntu.com`}, 204 {`[login.ubuntu.com] 205 macaroon = 206 unbound_discharge = 207 `, `invalid snapcraft login file ".*/creds": empty fields`}, 208 {"=", `invalid auth file ".*/creds": not a recognizable format`}, 209 } 210 211 for _, t := range tests { 212 err := ioutil.WriteFile(authFn, []byte(t.data), 0600) 213 c.Assert(err, IsNil) 214 215 _, err = tooling.NewToolingStore() 216 c.Check(err, ErrorMatches, t.err) 217 } 218 } 219 220 func (s *toolingSuite) TestNewToolingStoreWithAuthFromSnapcraftLoginFile(c *C) { 221 tmpdir := c.MkDir() 222 authFn := filepath.Join(tmpdir, "auth.json") 223 err := ioutil.WriteFile(authFn, []byte(`[login.ubuntu.com] 224 macaroon = MACAROON 225 unbound_discharge = DISCHARGE 226 227 `), 0600) 228 c.Assert(err, IsNil) 229 230 os.Setenv("UBUNTU_STORE_AUTH_DATA_FILENAME", authFn) 231 defer os.Unsetenv("UBUNTU_STORE_AUTH_DATA_FILENAME") 232 233 tsto, err := tooling.NewToolingStore() 234 c.Assert(err, IsNil) 235 creds := tsto.Creds() 236 u1creds, ok := creds.(*tooling.UbuntuOneCreds) 237 c.Assert(ok, Equals, true) 238 c.Check(u1creds.User.StoreMacaroon, Equals, "MACAROON") 239 c.Check(u1creds.User.StoreDischarges, DeepEquals, []string{"DISCHARGE"}) 240 } 241 func (s *toolingSuite) TestNewToolingStoreWithAuthFromEnv(c *C) { 242 tests := []struct { 243 dat string 244 a store.Authorizer 245 err string 246 }{ 247 {dat: `{ 248 "r": "MACAROON", 249 "d": "DISCHARGE" 250 }`, a: &tooling.UbuntuOneCreds{User: auth.UserState{ 251 StoreMacaroon: "MACAROON", 252 StoreDischarges: []string{"DISCHARGE"}, 253 }}}, {dat: `{ "t": "u1-macaroon", 254 "v": { 255 "r": "MACAROON", 256 "d": "DISCHARGE" 257 }}`, a: &tooling.UbuntuOneCreds{User: auth.UserState{ 258 StoreMacaroon: "MACAROON", 259 StoreDischarges: []string{"DISCHARGE"}, 260 }}}, {dat: `{`, err: `cannot unmarshal base64-decoded auth credentials from UBUNTU_STORE_AUTH: unexpected end of JSON input`}, {dat: `{}`, err: `cannot recognize unmarshalled base64-decoded auth credentials from UBUNTU_STORE_AUTH: no known field combination set`}, {dat: `{ "t": "macaroon", 261 "v": "MACAROON0" 262 }`, a: &tooling.SimpleCreds{ 263 Scheme: "Macaroon", 264 Value: "MACAROON0", 265 }}, {dat: `{ "t": "bearer", 266 "v": "tok" 267 }`, a: &tooling.SimpleCreds{ 268 Scheme: "Bearer", 269 Value: "tok", 270 }}, {dat: `{"t": "u1-macaroon"}`, 271 err: `cannot recognize unmarshalled base64-decoded auth credentials from UBUNTU_STORE_AUTH: no known field combination set`, 272 }, {dat: `{"t": "macaroon"}`, 273 err: `cannot recognize unmarshalled base64-decoded auth credentials from UBUNTU_STORE_AUTH: no known field combination set`, 274 }, {dat: `{"t": 1}`, 275 err: `cannot recognize unmarshalled base64-decoded auth credentials from UBUNTU_STORE_AUTH: no known field combination set`, 276 }, {dat: `{"t": "macaroon", "v": []}`, 277 err: `cannot recognize unmarshalled base64-decoded auth credentials from UBUNTU_STORE_AUTH: no known field combination set`, 278 }} 279 defer os.Unsetenv("UBUNTU_STORE_AUTH") 280 281 for _, t := range tests { 282 os.Setenv("UBUNTU_STORE_AUTH", base64.StdEncoding.EncodeToString([]byte(t.dat))) 283 tsto, err := tooling.NewToolingStore() 284 if t.err == "" { 285 c.Assert(err, IsNil) 286 creds := tsto.Creds() 287 c.Check(creds, DeepEquals, t.a) 288 } else { 289 c.Check(err, ErrorMatches, t.err) 290 } 291 } 292 } 293 294 func (s *toolingSuite) TestDownloadpOptionsString(c *C) { 295 tests := []struct { 296 opts tooling.DownloadSnapOptions 297 str string 298 }{ 299 {tooling.DownloadSnapOptions{LeavePartialOnError: true}, ""}, 300 {tooling.DownloadSnapOptions{}, ""}, 301 {tooling.DownloadSnapOptions{TargetDir: "/foo"}, `in "/foo"`}, 302 {tooling.DownloadSnapOptions{Basename: "foo"}, `to "foo.snap"`}, 303 {tooling.DownloadSnapOptions{Channel: "foo"}, `from channel "foo"`}, 304 {tooling.DownloadSnapOptions{Revision: snap.R(42)}, `(42)`}, 305 {tooling.DownloadSnapOptions{ 306 CohortKey: "AbCdEfGhIjKlMnOpQrStUvWxYz", 307 }, `from cohort "…rStUvWxYz"`}, 308 {tooling.DownloadSnapOptions{ 309 TargetDir: "/foo", 310 Basename: "bar", 311 Channel: "baz", 312 Revision: snap.R(13), 313 CohortKey: "MSBIc3dwOW9PemozYjRtdzhnY0MwMFh0eFduS0g5UWlDUSAxNTU1NDExNDE1IDBjYzJhNTc1ZjNjOTQ3ZDEwMWE1NTNjZWFkNmFmZDE3ZWJhYTYyNjM4ZWQ3ZGMzNjI5YmU4YjQ3NzAwMjdlMDk=", 314 }, `(13) from channel "baz" from cohort "…wMjdlMDk=" to "bar.snap" in "/foo"`}, // note this one is not 'valid' so it's ok if the string is a bit wonky 315 316 } 317 318 for _, t := range tests { 319 c.Check(t.opts.String(), Equals, t.str) 320 } 321 } 322 323 func (s *toolingSuite) TestDownloadSnapOptionsValid(c *C) { 324 tests := []struct { 325 opts tooling.DownloadSnapOptions 326 err error 327 }{ 328 {tooling.DownloadSnapOptions{}, nil}, // might want to error if no targetdir 329 {tooling.DownloadSnapOptions{TargetDir: "foo"}, nil}, 330 {tooling.DownloadSnapOptions{Channel: "foo"}, nil}, 331 {tooling.DownloadSnapOptions{Revision: snap.R(42)}, nil}, 332 {tooling.DownloadSnapOptions{ 333 CohortKey: "AbCdEfGhIjKlMnOpQrStUvWxYz", 334 }, nil}, 335 {tooling.DownloadSnapOptions{ 336 Channel: "foo", 337 Revision: snap.R(42), 338 }, nil}, 339 {tooling.DownloadSnapOptions{ 340 Channel: "foo", 341 CohortKey: "bar", 342 }, nil}, 343 {tooling.DownloadSnapOptions{ 344 Revision: snap.R(1), 345 CohortKey: "bar", 346 }, tooling.ErrRevisionAndCohort}, 347 {tooling.DownloadSnapOptions{ 348 Basename: "/foo", 349 }, tooling.ErrPathInBase}, 350 } 351 352 for _, t := range tests { 353 t.opts.LeavePartialOnError = true 354 c.Check(t.opts.Validate(), Equals, t.err) 355 t.opts.LeavePartialOnError = false 356 c.Check(t.opts.Validate(), Equals, t.err) 357 } 358 } 359 360 func (s *toolingSuite) TestDownloadSnap(c *C) { 361 // TODO: maybe expand on this (test coverage of DownloadSnap is really bad) 362 363 // env shenanigans 364 runtime.LockOSThread() 365 defer runtime.UnlockOSThread() 366 367 debug, hadDebug := os.LookupEnv("SNAPD_DEBUG") 368 os.Setenv("SNAPD_DEBUG", "1") 369 if hadDebug { 370 defer os.Setenv("SNAPD_DEBUG", debug) 371 } else { 372 defer os.Unsetenv("SNAPD_DEBUG") 373 } 374 logbuf, restore := logger.MockLogger() 375 defer restore() 376 377 s.setupSnaps(c, map[string]string{ 378 "core": "canonical", 379 }, "") 380 381 dlDir := c.MkDir() 382 opts := tooling.DownloadSnapOptions{ 383 TargetDir: dlDir, 384 } 385 dlSnap, err := s.tsto.DownloadSnap("core", opts) 386 c.Assert(err, IsNil) 387 c.Check(dlSnap.Path, Matches, filepath.Join(dlDir, `core_\d+.snap`)) 388 c.Check(dlSnap.Info.SnapName(), Equals, "core") 389 c.Check(dlSnap.RedirectChannel, Equals, "") 390 391 c.Check(logbuf.String(), Matches, `.* DEBUG: Going to download snap "core" `+opts.String()+".\n") 392 } 393 394 // interface for the store 395 func (s *toolingSuite) SnapAction(_ context.Context, curSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, _ *auth.UserState, _ *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) { 396 if assertQuery != nil { 397 return nil, nil, fmt.Errorf("unexpected assertion query") 398 } 399 400 s.storeActionsBunchSizes = append(s.storeActionsBunchSizes, len(actions)) 401 s.curSnaps = append(s.curSnaps, curSnaps) 402 sars := make([]store.SnapActionResult, 0, len(actions)) 403 for _, a := range actions { 404 if a.Action != "download" { 405 return nil, nil, fmt.Errorf("unexpected action %q", a.Action) 406 } 407 408 if _, instanceKey := snap.SplitInstanceName(a.InstanceName); instanceKey != "" { 409 return nil, nil, fmt.Errorf("unexpected instance key in %q", a.InstanceName) 410 } 411 // record 412 s.storeActions = append(s.storeActions, a) 413 414 info := s.AssertedSnapInfo(a.InstanceName) 415 if info == nil { 416 return nil, nil, fmt.Errorf("no %q in the fake store", a.InstanceName) 417 } 418 info1 := *info 419 channel := a.Channel 420 redirectChannel := "" 421 if strings.HasPrefix(a.InstanceName, "default-track-") { 422 channel = "default-track/stable" 423 redirectChannel = channel 424 } 425 info1.Channel = channel 426 sars = append(sars, store.SnapActionResult{ 427 Info: &info1, 428 RedirectChannel: redirectChannel, 429 }) 430 } 431 432 return sars, nil, nil 433 } 434 435 func (s *toolingSuite) Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *store.DownloadOptions) error { 436 return osutil.CopyFile(s.AssertedSnap(name), targetFn, 0) 437 } 438 439 func (s *toolingSuite) Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) { 440 ref := &asserts.Ref{Type: assertType, PrimaryKey: primaryKey} 441 return ref.Resolve(s.StoreSigning.Find) 442 } 443 444 func (s *toolingSuite) TestUpdateUserAuth(c *C) { 445 u := auth.UserState{ 446 StoreMacaroon: "macaroon", 447 StoreDischarges: []string{"discharge1"}, 448 } 449 creds := &tooling.UbuntuOneCreds{ 450 User: u, 451 } 452 453 u1, err := creds.UpdateUserAuth(&u, []string{"discharge2"}) 454 c.Assert(err, IsNil) 455 c.Check(u1, Equals, &u) 456 c.Check(u1.StoreDischarges, DeepEquals, []string{"discharge2"}) 457 } 458 459 func (s *toolingSuite) TestSimpleCreds(c *C) { 460 creds := &tooling.SimpleCreds{ 461 Scheme: "Auth-Scheme", 462 Value: "auth-value", 463 } 464 c.Check(creds.CanAuthorizeForUser(nil), Equals, true) 465 r, err := http.NewRequest("POST", "http://svc", nil) 466 c.Assert(err, IsNil) 467 c.Assert(creds.Authorize(r, nil, nil, nil), IsNil) 468 auth := r.Header.Get("Authorization") 469 c.Check(auth, Equals, `Auth-Scheme auth-value`) 470 }