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  }