github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/store/details_v2_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2018 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 // not using store_test as this is a very low level test 21 package store 22 23 import ( 24 "encoding/json" 25 "reflect" 26 "strings" 27 28 . "gopkg.in/check.v1" 29 30 "github.com/snapcore/snapd/jsonutil/safejson" 31 "github.com/snapcore/snapd/snap" 32 "github.com/snapcore/snapd/testutil" 33 ) 34 35 type detailsV2Suite struct { 36 testutil.BaseTest 37 } 38 39 var _ = Suite(&detailsV2Suite{}) 40 41 const ( 42 coreStoreJSON = `{ 43 "architectures": [ 44 "amd64" 45 ], 46 "base": null, 47 "confinement": "strict", 48 "contact": "mailto:snappy-canonical-storeaccount@canonical.com", 49 "created-at": "2018-01-22T07:49:19.440720+00:00", 50 "description": "The core runtime environment for snapd", 51 "download": { 52 "sha3-384": "b691f6dde3d8022e4db563840f0ef82320cb824b6292ffd027dbc838535214dac31c3512c619beaf73f1aeaf35ac62d5", 53 "size": 85291008, 54 "url": "https://api.snapcraft.io/api/v1/snaps/download/99T7MUlRhtI3U0QFgl5mXXESAiSwt776_3887.snap", 55 "deltas": [] 56 }, 57 "epoch": { 58 "read": [0], 59 "write": [0] 60 }, 61 "license": null, 62 "name": "core", 63 "prices": {}, 64 "private": false, 65 "publisher": { 66 "id": "canonical", 67 "username": "canonical", 68 "display-name": "Canonical", 69 "validation": "verified" 70 }, 71 "revision": 3887, 72 "snap-id": "99T7MUlRhtI3U0QFgl5mXXESAiSwt776", 73 "store-url": "https://snapcraft.io/core", 74 "summary": "snapd runtime environment", 75 "title": "core", 76 "type": "os", 77 "version": "16-2.30", 78 "website": "http://example.com/core", 79 "media": [] 80 }` 81 82 thingyStoreJSON = `{ 83 "architectures": [ 84 "amd64" 85 ], 86 "base": "base-18", 87 "confinement": "strict", 88 "contact": "https://thingy.com", 89 "common-ids": ["org.thingy"], 90 "created-at": "2018-01-26T11:38:35.536410+00:00", 91 "description": "Useful thingy for thinging", 92 "download": { 93 "sha3-384": "a29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4d", 94 "size": 10000021, 95 "url": "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_21.snap", 96 "deltas": [ 97 { 98 "format": "xdelta3", 99 "source": 19, 100 "target": 21, 101 "url": "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_19_21_xdelta3.delta", 102 "size": 9999, 103 "sha3-384": "29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4df" 104 } 105 ] 106 }, 107 "epoch": { 108 "read": [0,1], 109 "write": [1] 110 }, 111 "license": "Proprietary", 112 "name": "thingy", 113 "prices": {"USD": "9.99"}, 114 "private": false, 115 "publisher": { 116 "id": "ZvtzsxbsHivZLdvzrt0iqW529riGLfXJ", 117 "username": "thingyinc", 118 "display-name": "Thingy Inc.", 119 "validation": "unproven" 120 }, 121 "revision": 21, 122 "snap-id": "XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV", 123 "snap-yaml": "name: test-snapd-content-plug\nversion: 1.0\napps:\n content-plug:\n command: bin/content-plug\n plugs: [shared-content-plug]\nplugs:\n shared-content-plug:\n interface: content\n target: import\n content: mylib\n default-provider: test-snapd-content-slot\nslots:\n shared-content-slot:\n interface: content\n content: mylib\n read:\n - /\n", 124 "store-url": "https://snapcraft.io/thingy", 125 "summary": "useful thingy", 126 "title": "This Is The Most Fantastical Snap of Thingy", 127 "type": "app", 128 "version": "9.50", 129 "website": "http://example.com/thingy", 130 "media": [ 131 {"type": "icon", "url": "https://dashboard.snapcraft.io/site_media/appmedia/2017/12/Thingy.png"}, 132 {"type": "screenshot", "url": "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_01.png"}, 133 {"type": "screenshot", "url": "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_02.png", "width": 600, "height": 200} 134 ] 135 }` 136 ) 137 138 func (s *detailsV2Suite) SetUpTest(c *C) { 139 s.BaseTest.SetUpTest(c) 140 s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) 141 } 142 143 func (s *detailsV2Suite) TearDownTest(c *C) { 144 s.BaseTest.TearDownTest(c) 145 } 146 147 func (s *detailsV2Suite) TestInfoFromStoreSnapSimple(c *C) { 148 var snp storeSnap 149 err := json.Unmarshal([]byte(coreStoreJSON), &snp) 150 c.Assert(err, IsNil) 151 152 info, err := infoFromStoreSnap(&snp) 153 c.Assert(err, IsNil) 154 c.Check(snap.Validate(info), IsNil) 155 156 c.Check(info, DeepEquals, &snap.Info{ 157 Architectures: []string{"amd64"}, 158 SideInfo: snap.SideInfo{ 159 RealName: "core", 160 SnapID: "99T7MUlRhtI3U0QFgl5mXXESAiSwt776", 161 Revision: snap.R(3887), 162 EditedContact: "mailto:snappy-canonical-storeaccount@canonical.com", 163 EditedTitle: "core", 164 EditedSummary: "snapd runtime environment", 165 EditedDescription: "The core runtime environment for snapd", 166 Private: false, 167 Paid: false, 168 }, 169 Epoch: snap.E("0"), 170 SnapType: snap.TypeOS, 171 Version: "16-2.30", 172 Confinement: snap.StrictConfinement, 173 Publisher: snap.StoreAccount{ 174 ID: "canonical", 175 Username: "canonical", 176 DisplayName: "Canonical", 177 Validation: "verified", 178 }, 179 DownloadInfo: snap.DownloadInfo{ 180 DownloadURL: "https://api.snapcraft.io/api/v1/snaps/download/99T7MUlRhtI3U0QFgl5mXXESAiSwt776_3887.snap", 181 Sha3_384: "b691f6dde3d8022e4db563840f0ef82320cb824b6292ffd027dbc838535214dac31c3512c619beaf73f1aeaf35ac62d5", 182 Size: 85291008, 183 }, 184 Plugs: make(map[string]*snap.PlugInfo), 185 Slots: make(map[string]*snap.SlotInfo), 186 Website: "http://example.com/core", 187 StoreURL: "https://snapcraft.io/core", 188 }) 189 } 190 191 func (s *detailsV2Suite) TestInfoFromStoreSnap(c *C) { 192 var snp storeSnap 193 // base, prices, media 194 err := json.Unmarshal([]byte(thingyStoreJSON), &snp) 195 c.Assert(err, IsNil) 196 197 info, err := infoFromStoreSnap(&snp) 198 c.Assert(err, IsNil) 199 c.Check(snap.Validate(info), IsNil) 200 201 info2 := *info 202 // clear recursive bits 203 info2.Plugs = nil 204 info2.Slots = nil 205 c.Check(&info2, DeepEquals, &snap.Info{ 206 Architectures: []string{"amd64"}, 207 Base: "base-18", 208 SideInfo: snap.SideInfo{ 209 RealName: "thingy", 210 SnapID: "XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV", 211 Revision: snap.R(21), 212 EditedContact: "https://thingy.com", 213 EditedTitle: "This Is The Most Fantastical Snap of Th…", 214 EditedSummary: "useful thingy", 215 EditedDescription: "Useful thingy for thinging", 216 Private: false, 217 Paid: true, 218 }, 219 Epoch: snap.Epoch{ 220 Read: []uint32{0, 1}, 221 Write: []uint32{1}, 222 }, 223 SnapType: snap.TypeApp, 224 Version: "9.50", 225 Confinement: snap.StrictConfinement, 226 License: "Proprietary", 227 Publisher: snap.StoreAccount{ 228 ID: "ZvtzsxbsHivZLdvzrt0iqW529riGLfXJ", 229 Username: "thingyinc", 230 DisplayName: "Thingy Inc.", 231 Validation: "unproven", 232 }, 233 DownloadInfo: snap.DownloadInfo{ 234 DownloadURL: "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_21.snap", 235 Sha3_384: "a29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4d", 236 Size: 10000021, 237 Deltas: []snap.DeltaInfo{ 238 { 239 Format: "xdelta3", 240 FromRevision: 19, 241 ToRevision: 21, 242 DownloadURL: "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_19_21_xdelta3.delta", 243 Size: 9999, 244 Sha3_384: "29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4df", 245 }, 246 }, 247 }, 248 Prices: map[string]float64{ 249 "USD": 9.99, 250 }, 251 Media: []snap.MediaInfo{ 252 {Type: "icon", URL: "https://dashboard.snapcraft.io/site_media/appmedia/2017/12/Thingy.png"}, 253 {Type: "screenshot", URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_01.png"}, 254 {Type: "screenshot", URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_02.png", Width: 600, Height: 200}, 255 }, 256 CommonIDs: []string{"org.thingy"}, 257 Website: "http://example.com/thingy", 258 StoreURL: "https://snapcraft.io/thingy", 259 }) 260 261 // validate the plugs/slots 262 c.Assert(info.Plugs, HasLen, 1) 263 plug := info.Plugs["shared-content-plug"] 264 c.Check(plug.Name, Equals, "shared-content-plug") 265 c.Check(plug.Snap, Equals, info) 266 c.Check(plug.Apps, HasLen, 1) 267 c.Check(plug.Apps["content-plug"].Command, Equals, "bin/content-plug") 268 269 c.Assert(info.Slots, HasLen, 1) 270 slot := info.Slots["shared-content-slot"] 271 c.Check(slot.Name, Equals, "shared-content-slot") 272 c.Check(slot.Snap, Equals, info) 273 c.Check(slot.Apps, HasLen, 1) 274 c.Check(slot.Apps["content-plug"].Command, Equals, "bin/content-plug") 275 276 // private 277 err = json.Unmarshal([]byte(strings.Replace(thingyStoreJSON, `"private": false`, `"private": true`, 1)), &snp) 278 c.Assert(err, IsNil) 279 280 info, err = infoFromStoreSnap(&snp) 281 c.Assert(err, IsNil) 282 c.Check(snap.Validate(info), IsNil) 283 284 c.Check(info.Private, Equals, true) 285 286 // check that up to few exceptions info is filled 287 expectedZeroFields := []string{ 288 "SuggestedName", 289 "InstanceKey", 290 "Assumes", 291 "OriginalTitle", 292 "OriginalSummary", 293 "OriginalDescription", 294 "Environment", 295 "LicenseAgreement", // XXX go away? 296 "LicenseVersion", // XXX go away? 297 "Apps", 298 "LegacyAliases", 299 "Hooks", 300 "BadInterfaces", 301 "Broken", 302 "MustBuy", 303 "Channels", // handled at a different level (see TestInfo) 304 "Tracks", // handled at a different level (see TestInfo) 305 "Layout", 306 "SideInfo.Channel", 307 "DownloadInfo.AnonDownloadURL", // TODO: going away at some point 308 "SystemUsernames", 309 } 310 var checker func(string, reflect.Value) 311 checker = func(pfx string, x reflect.Value) { 312 t := x.Type() 313 for i := 0; i < x.NumField(); i++ { 314 f := t.Field(i) 315 if f.PkgPath != "" { 316 // not exported, ignore 317 continue 318 } 319 v := x.Field(i) 320 if f.Anonymous { 321 checker(pfx+f.Name+".", v) 322 continue 323 } 324 if reflect.DeepEqual(v.Interface(), reflect.Zero(f.Type).Interface()) { 325 name := pfx + f.Name 326 c.Check(expectedZeroFields, testutil.Contains, name, Commentf("%s not set", name)) 327 } 328 } 329 } 330 x := reflect.ValueOf(info).Elem() 331 checker("", x) 332 } 333 334 // arg must be a pointer to a struct 335 func fillStruct(a interface{}, c *C) { 336 if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct { 337 k := t.Kind() 338 if k == reflect.Ptr { 339 k = t.Elem().Kind() 340 } 341 c.Fatalf("first argument must be expected a pointer to a struct, not %s", k) 342 } 343 va := reflect.ValueOf(a) 344 n := va.Elem().NumField() 345 for i := 0; i < n; i++ { 346 field := va.Elem().Field(i) 347 var x interface{} 348 switch field.Interface().(type) { 349 case string: 350 x = "foo" 351 case []string: 352 x = []string{"foo"} 353 case safejson.String: 354 var s safejson.String 355 c.Assert(json.Unmarshal([]byte(`"foo"`), &s), IsNil) 356 x = s 357 case safejson.Paragraph: 358 var p safejson.Paragraph 359 c.Assert(json.Unmarshal([]byte(`"foo"`), &p), IsNil) 360 x = p 361 case storeSnapDownload: 362 x = storeSnapDownload{ 363 URL: "http://example.com/foo", 364 Size: 42, 365 Sha3_384: "foo", 366 } 367 case snap.Epoch: 368 x = snap.E("1") 369 case map[string]string: 370 x = map[string]string{"foo": "bar"} 371 case bool: 372 x = true 373 case snap.StoreAccount: 374 x = snap.StoreAccount{ 375 ID: "foo-id", 376 Username: "foo", 377 DisplayName: "Foo Bar", 378 Validation: "VALIDATION", 379 } 380 case int: 381 x = 42 382 case snap.Type: 383 x = snap.Type("invalid") 384 case []storeSnapMedia: 385 x = []storeSnapMedia{{ 386 Type: "potato", 387 URL: "http://example.com/foo.pot", 388 }} 389 default: 390 c.Fatalf("unhandled field type %T", field.Interface()) 391 } 392 field.Set(reflect.ValueOf(x)) 393 } 394 } 395 396 func (s *detailsV2Suite) TestCopyNonZero(c *C) { 397 // a is a storeSnap with everything non-zero 398 a := storeSnap{} 399 fillStruct(&a, c) 400 // b is all zeros 401 b := storeSnap{} 402 403 aCopy := a 404 bCopy := b 405 406 // sanity check 407 c.Check(a, DeepEquals, aCopy) 408 c.Check(b, DeepEquals, bCopy) 409 c.Check(a, Not(DeepEquals), b) 410 411 // copying from b to a does nothing: 412 copyNonZeroFrom(&b, &a) 413 c.Check(a, DeepEquals, aCopy) 414 c.Check(b, DeepEquals, bCopy) 415 416 // copying from a to b does its thing: 417 copyNonZeroFrom(&a, &b) 418 c.Check(a, DeepEquals, b) 419 c.Check(b, Not(DeepEquals), bCopy) 420 }