github.com/ubuntu-core/snappy@v0.0.0-20210827154228-9e584df982bb/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\nassumes: [snapd2.49]\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 Assumes: []string{"snapd2.49"}, 208 Base: "base-18", 209 SideInfo: snap.SideInfo{ 210 RealName: "thingy", 211 SnapID: "XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV", 212 Revision: snap.R(21), 213 EditedContact: "https://thingy.com", 214 EditedTitle: "This Is The Most Fantastical Snap of Th…", 215 EditedSummary: "useful thingy", 216 EditedDescription: "Useful thingy for thinging", 217 Private: false, 218 Paid: true, 219 }, 220 Epoch: snap.Epoch{ 221 Read: []uint32{0, 1}, 222 Write: []uint32{1}, 223 }, 224 SnapType: snap.TypeApp, 225 Version: "9.50", 226 Confinement: snap.StrictConfinement, 227 License: "Proprietary", 228 Publisher: snap.StoreAccount{ 229 ID: "ZvtzsxbsHivZLdvzrt0iqW529riGLfXJ", 230 Username: "thingyinc", 231 DisplayName: "Thingy Inc.", 232 Validation: "unproven", 233 }, 234 DownloadInfo: snap.DownloadInfo{ 235 DownloadURL: "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_21.snap", 236 Sha3_384: "a29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4d", 237 Size: 10000021, 238 Deltas: []snap.DeltaInfo{ 239 { 240 Format: "xdelta3", 241 FromRevision: 19, 242 ToRevision: 21, 243 DownloadURL: "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_19_21_xdelta3.delta", 244 Size: 9999, 245 Sha3_384: "29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4df", 246 }, 247 }, 248 }, 249 Prices: map[string]float64{ 250 "USD": 9.99, 251 }, 252 Media: []snap.MediaInfo{ 253 {Type: "icon", URL: "https://dashboard.snapcraft.io/site_media/appmedia/2017/12/Thingy.png"}, 254 {Type: "screenshot", URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_01.png"}, 255 {Type: "screenshot", URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_02.png", Width: 600, Height: 200}, 256 }, 257 CommonIDs: []string{"org.thingy"}, 258 Website: "http://example.com/thingy", 259 StoreURL: "https://snapcraft.io/thingy", 260 }) 261 262 // validate the plugs/slots 263 c.Assert(info.Plugs, HasLen, 1) 264 plug := info.Plugs["shared-content-plug"] 265 c.Check(plug.Name, Equals, "shared-content-plug") 266 c.Check(plug.Snap, Equals, info) 267 c.Check(plug.Apps, HasLen, 1) 268 c.Check(plug.Apps["content-plug"].Command, Equals, "bin/content-plug") 269 270 c.Assert(info.Slots, HasLen, 1) 271 slot := info.Slots["shared-content-slot"] 272 c.Check(slot.Name, Equals, "shared-content-slot") 273 c.Check(slot.Snap, Equals, info) 274 c.Check(slot.Apps, HasLen, 1) 275 c.Check(slot.Apps["content-plug"].Command, Equals, "bin/content-plug") 276 277 // private 278 err = json.Unmarshal([]byte(strings.Replace(thingyStoreJSON, `"private": false`, `"private": true`, 1)), &snp) 279 c.Assert(err, IsNil) 280 281 info, err = infoFromStoreSnap(&snp) 282 c.Assert(err, IsNil) 283 c.Check(snap.Validate(info), IsNil) 284 285 c.Check(info.Private, Equals, true) 286 287 // check that up to few exceptions info is filled 288 expectedZeroFields := []string{ 289 "SuggestedName", 290 "InstanceKey", 291 "OriginalTitle", 292 "OriginalSummary", 293 "OriginalDescription", 294 "OriginalLinks", 295 "Environment", 296 "LicenseAgreement", // XXX go away? 297 "LicenseVersion", // XXX go away? 298 "Apps", 299 "LegacyAliases", 300 "Hooks", 301 "BadInterfaces", 302 "Broken", 303 "MustBuy", 304 "Channels", // handled at a different level (see TestInfo) 305 "Tracks", // handled at a different level (see TestInfo) 306 "Layout", 307 "SideInfo.Channel", 308 "SideInfo.EditedLinks", // TODO: take this value from the store 309 "DownloadInfo.AnonDownloadURL", // TODO: going away at some point 310 "SystemUsernames", 311 } 312 var checker func(string, reflect.Value) 313 checker = func(pfx string, x reflect.Value) { 314 t := x.Type() 315 for i := 0; i < x.NumField(); i++ { 316 f := t.Field(i) 317 if f.PkgPath != "" { 318 // not exported, ignore 319 continue 320 } 321 v := x.Field(i) 322 if f.Anonymous { 323 checker(pfx+f.Name+".", v) 324 continue 325 } 326 name := pfx + f.Name 327 if reflect.DeepEqual(v.Interface(), reflect.Zero(f.Type).Interface()) { 328 c.Check(expectedZeroFields, testutil.Contains, name, Commentf("%s not set", name)) 329 } else { 330 c.Check(expectedZeroFields, Not(testutil.Contains), name, Commentf("%s unexpectedly set", name)) 331 } 332 } 333 } 334 x := reflect.ValueOf(info).Elem() 335 checker("", x) 336 } 337 338 // arg must be a pointer to a struct 339 func fillStruct(a interface{}, c *C) { 340 if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct { 341 k := t.Kind() 342 if k == reflect.Ptr { 343 k = t.Elem().Kind() 344 } 345 c.Fatalf("first argument must be expected a pointer to a struct, not %s", k) 346 } 347 va := reflect.ValueOf(a) 348 n := va.Elem().NumField() 349 for i := 0; i < n; i++ { 350 field := va.Elem().Field(i) 351 var x interface{} 352 switch field.Interface().(type) { 353 case string: 354 x = "foo" 355 case []string: 356 x = []string{"foo"} 357 case safejson.String: 358 var s safejson.String 359 c.Assert(json.Unmarshal([]byte(`"foo"`), &s), IsNil) 360 x = s 361 case safejson.Paragraph: 362 var p safejson.Paragraph 363 c.Assert(json.Unmarshal([]byte(`"foo"`), &p), IsNil) 364 x = p 365 case storeSnapDownload: 366 x = storeSnapDownload{ 367 URL: "http://example.com/foo", 368 Size: 42, 369 Sha3_384: "foo", 370 } 371 case snap.Epoch: 372 x = snap.E("1") 373 case map[string]string: 374 x = map[string]string{"foo": "bar"} 375 case bool: 376 x = true 377 case snap.StoreAccount: 378 x = snap.StoreAccount{ 379 ID: "foo-id", 380 Username: "foo", 381 DisplayName: "Foo Bar", 382 Validation: "VALIDATION", 383 } 384 case int: 385 x = 42 386 case snap.Type: 387 x = snap.Type("invalid") 388 case []storeSnapMedia: 389 x = []storeSnapMedia{{ 390 Type: "potato", 391 URL: "http://example.com/foo.pot", 392 }} 393 default: 394 c.Fatalf("unhandled field type %T", field.Interface()) 395 } 396 field.Set(reflect.ValueOf(x)) 397 } 398 } 399 400 func (s *detailsV2Suite) TestCopyNonZero(c *C) { 401 // a is a storeSnap with everything non-zero 402 a := storeSnap{} 403 fillStruct(&a, c) 404 // b is all zeros 405 b := storeSnap{} 406 407 aCopy := a 408 bCopy := b 409 410 // sanity check 411 c.Check(a, DeepEquals, aCopy) 412 c.Check(b, DeepEquals, bCopy) 413 c.Check(a, Not(DeepEquals), b) 414 415 // copying from b to a does nothing: 416 copyNonZeroFrom(&b, &a) 417 c.Check(a, DeepEquals, aCopy) 418 c.Check(b, DeepEquals, bCopy) 419 420 // copying from a to b does its thing: 421 copyNonZeroFrom(&a, &b) 422 c.Check(a, DeepEquals, b) 423 c.Check(b, Not(DeepEquals), bCopy) 424 }