mirror of https://github.com/docker/docs.git
Merge pull request #349 from endophage/server_snapshot_bugfixes
fixing bugs raised by @mtrmac
This commit is contained in:
commit
3d54349e4a
4
Makefile
4
Makefile
|
@ -43,8 +43,8 @@ GO_VERSION = $(shell go version | awk '{print $$3}')
|
||||||
.DEFAULT: default
|
.DEFAULT: default
|
||||||
|
|
||||||
go_version:
|
go_version:
|
||||||
ifneq ("$(GO_VERSION)", "go1.5.1")
|
ifeq (,$(findstring go1.5.,$(GO_VERSION)))
|
||||||
$(error Requires go version 1.5.1 - found $(GO_VERSION))
|
$(error Requires go version 1.5.x - found $(GO_VERSION))
|
||||||
else
|
else
|
||||||
@echo
|
@echo
|
||||||
endif
|
endif
|
||||||
|
|
|
@ -81,9 +81,21 @@ func validateUpdate(cs signed.CryptoService, gun string, updates []storage.MetaU
|
||||||
return nil, validation.ErrBadTargets{Msg: err.Error()}
|
return nil, validation.ErrBadTargets{Msg: err.Error()}
|
||||||
}
|
}
|
||||||
repo.SetTargets(targetsRole, t)
|
repo.SetTargets(targetsRole, t)
|
||||||
|
} else {
|
||||||
|
targetsJSON, err := store.GetCurrent(gun, data.CanonicalTargetsRole)
|
||||||
|
if err != nil {
|
||||||
|
return nil, validation.ErrValidation{Msg: err.Error()}
|
||||||
|
}
|
||||||
|
targets := &data.SignedTargets{}
|
||||||
|
err = json.Unmarshal(targetsJSON, targets)
|
||||||
|
if err != nil {
|
||||||
|
return nil, validation.ErrValidation{Msg: err.Error()}
|
||||||
|
}
|
||||||
|
repo.SetTargets(data.CanonicalTargetsRole, targets)
|
||||||
}
|
}
|
||||||
logrus.Debug("Successfully validated targets")
|
logrus.Debug("Successfully validated targets")
|
||||||
|
|
||||||
|
// At this point, root and targets must have been loaded into the repo
|
||||||
if _, ok := roles[snapshotRole]; ok {
|
if _, ok := roles[snapshotRole]; ok {
|
||||||
var oldSnap *data.SignedSnapshot
|
var oldSnap *data.SignedSnapshot
|
||||||
oldSnapJSON, err := store.GetCurrent(gun, snapshotRole)
|
oldSnapJSON, err := store.GetCurrent(gun, snapshotRole)
|
||||||
|
@ -111,10 +123,6 @@ func validateUpdate(cs signed.CryptoService, gun string, updates []storage.MetaU
|
||||||
// Then:
|
// Then:
|
||||||
// - generate a new snapshot
|
// - generate a new snapshot
|
||||||
// - add it to the updates
|
// - add it to the updates
|
||||||
err := prepRepo(gun, repo, store)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
update, err := generateSnapshot(gun, kdb, repo, store)
|
update, err := generateSnapshot(gun, kdb, repo, store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -124,34 +132,6 @@ func validateUpdate(cs signed.CryptoService, gun string, updates []storage.MetaU
|
||||||
return updates, nil
|
return updates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepRepo(gun string, repo *tuf.Repo, store storage.MetaStore) error {
|
|
||||||
if repo.Root == nil {
|
|
||||||
rootJSON, err := store.GetCurrent(gun, data.CanonicalRootRole)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not load repo for snapshot generation: %v", err)
|
|
||||||
}
|
|
||||||
root := &data.SignedRoot{}
|
|
||||||
err = json.Unmarshal(rootJSON, root)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not load repo for snapshot generation: %v", err)
|
|
||||||
}
|
|
||||||
repo.SetRoot(root)
|
|
||||||
}
|
|
||||||
if repo.Targets[data.CanonicalTargetsRole] == nil {
|
|
||||||
targetsJSON, err := store.GetCurrent(gun, data.CanonicalTargetsRole)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not load repo for snapshot generation: %v", err)
|
|
||||||
}
|
|
||||||
targets := &data.SignedTargets{}
|
|
||||||
err = json.Unmarshal(targetsJSON, targets)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not load repo for snapshot generation: %v", err)
|
|
||||||
}
|
|
||||||
repo.SetTargets(data.CanonicalTargetsRole, targets)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateSnapshot(gun string, kdb *keys.KeyDB, repo *tuf.Repo, store storage.MetaStore) (*storage.MetaUpdate, error) {
|
func generateSnapshot(gun string, kdb *keys.KeyDB, repo *tuf.Repo, store storage.MetaStore) (*storage.MetaUpdate, error) {
|
||||||
role := kdb.GetRole(data.RoleName(data.CanonicalSnapshotRole))
|
role := kdb.GetRole(data.RoleName(data.CanonicalSnapshotRole))
|
||||||
if role == nil {
|
if role == nil {
|
||||||
|
@ -159,8 +139,8 @@ func generateSnapshot(gun string, kdb *keys.KeyDB, repo *tuf.Repo, store storage
|
||||||
}
|
}
|
||||||
|
|
||||||
algo, keyBytes, err := store.GetKey(gun, data.CanonicalSnapshotRole)
|
algo, keyBytes, err := store.GetKey(gun, data.CanonicalSnapshotRole)
|
||||||
if role == nil {
|
if err != nil {
|
||||||
return nil, validation.ErrBadRoot{Msg: "root did not include snapshot key"}
|
return nil, validation.ErrBadHierarchy{Msg: "could not retrieve snapshot key. client must provide snapshot"}
|
||||||
}
|
}
|
||||||
foundK := data.NewPublicKey(algo, keyBytes)
|
foundK := data.NewPublicKey(algo, keyBytes)
|
||||||
|
|
||||||
|
@ -180,7 +160,7 @@ func generateSnapshot(gun string, kdb *keys.KeyDB, repo *tuf.Repo, store storage
|
||||||
currentJSON, err := store.GetCurrent(gun, data.CanonicalSnapshotRole)
|
currentJSON, err := store.GetCurrent(gun, data.CanonicalSnapshotRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*storage.ErrNotFound); !ok {
|
if _, ok := err.(*storage.ErrNotFound); !ok {
|
||||||
return nil, fmt.Errorf("could not retrieve previous snapshot: %v", err)
|
return nil, validation.ErrValidation{Msg: err.Error()}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var sn *data.SignedSnapshot
|
var sn *data.SignedSnapshot
|
||||||
|
@ -188,25 +168,25 @@ func generateSnapshot(gun string, kdb *keys.KeyDB, repo *tuf.Repo, store storage
|
||||||
sn = new(data.SignedSnapshot)
|
sn = new(data.SignedSnapshot)
|
||||||
err := json.Unmarshal(currentJSON, sn)
|
err := json.Unmarshal(currentJSON, sn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not retrieve previous snapshot: %v", err)
|
return nil, validation.ErrValidation{Msg: err.Error()}
|
||||||
}
|
}
|
||||||
err = repo.SetSnapshot(sn)
|
err = repo.SetSnapshot(sn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not load previous snapshot: %v", err)
|
return nil, validation.ErrValidation{Msg: err.Error()}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err := repo.InitSnapshot()
|
err := repo.InitSnapshot()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not initialize snapshot: %v", err)
|
return nil, validation.ErrBadSnapshot{Msg: err.Error()}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sgnd, err := repo.SignSnapshot(data.DefaultExpires(data.CanonicalSnapshotRole))
|
sgnd, err := repo.SignSnapshot(data.DefaultExpires(data.CanonicalSnapshotRole))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not sign snapshot: %v", err)
|
return nil, validation.ErrBadSnapshot{Msg: err.Error()}
|
||||||
}
|
}
|
||||||
sgndJSON, err := json.Marshal(sgnd)
|
sgndJSON, err := json.Marshal(sgnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not save snapshot: %v", err)
|
return nil, validation.ErrBadSnapshot{Msg: err.Error()}
|
||||||
}
|
}
|
||||||
return &storage.MetaUpdate{
|
return &storage.MetaUpdate{
|
||||||
Role: data.CanonicalSnapshotRole,
|
Role: data.CanonicalSnapshotRole,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/notary/trustmanager"
|
"github.com/docker/notary/trustmanager"
|
||||||
"github.com/docker/notary/tuf"
|
|
||||||
"github.com/docker/notary/tuf/data"
|
"github.com/docker/notary/tuf/data"
|
||||||
"github.com/docker/notary/tuf/keys"
|
"github.com/docker/notary/tuf/keys"
|
||||||
"github.com/docker/notary/tuf/signed"
|
"github.com/docker/notary/tuf/signed"
|
||||||
|
@ -377,71 +376,6 @@ func TestValidateSnapshotGenerateLoadRootTargets(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrepRepoLoadRootTargets(t *testing.T) {
|
|
||||||
_, repo, _ := testutils.EmptyRepo()
|
|
||||||
store := storage.NewMemStorage()
|
|
||||||
|
|
||||||
r, tg, sn, ts, err := testutils.Sign(repo)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
root, targets, _, _, err := getUpdates(r, tg, sn, ts)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
store.UpdateCurrent("testGUN", root)
|
|
||||||
store.UpdateCurrent("testGUN", targets)
|
|
||||||
|
|
||||||
toPrep := tuf.NewRepo(keys.NewDB(), nil)
|
|
||||||
assert.Nil(t, toPrep.Root)
|
|
||||||
assert.Nil(t, toPrep.Targets[data.CanonicalTargetsRole])
|
|
||||||
err = prepRepo("testGUN", toPrep, store)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, toPrep.Root)
|
|
||||||
assert.NotNil(t, toPrep.Targets[data.CanonicalTargetsRole])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPrepRepoLoadRootCorrupt(t *testing.T) {
|
|
||||||
_, repo, _ := testutils.EmptyRepo()
|
|
||||||
store := storage.NewMemStorage()
|
|
||||||
|
|
||||||
r, tg, sn, ts, err := testutils.Sign(repo)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
root, targets, _, _, err := getUpdates(r, tg, sn, ts)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
root.Data = root.Data[:1]
|
|
||||||
store.UpdateCurrent("testGUN", root)
|
|
||||||
store.UpdateCurrent("testGUN", targets)
|
|
||||||
|
|
||||||
toPrep := tuf.NewRepo(keys.NewDB(), nil)
|
|
||||||
err = prepRepo("testGUN", toPrep, store)
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPrepRepoLoadTargetsCorrupt(t *testing.T) {
|
|
||||||
_, repo, _ := testutils.EmptyRepo()
|
|
||||||
store := storage.NewMemStorage()
|
|
||||||
|
|
||||||
r, tg, sn, ts, err := testutils.Sign(repo)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
root, targets, _, _, err := getUpdates(r, tg, sn, ts)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
targets.Data = targets.Data[:1]
|
|
||||||
store.UpdateCurrent("testGUN", root)
|
|
||||||
store.UpdateCurrent("testGUN", targets)
|
|
||||||
|
|
||||||
toPrep := tuf.NewRepo(keys.NewDB(), nil)
|
|
||||||
err = prepRepo("testGUN", toPrep, store)
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPrepRepoLoadRootMissing(t *testing.T) {
|
|
||||||
store := storage.NewMemStorage()
|
|
||||||
|
|
||||||
toPrep := tuf.NewRepo(nil, nil)
|
|
||||||
err := prepRepo("testGUN", toPrep, store)
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no timestamp key in the store, validation fails. This could
|
// If there is no timestamp key in the store, validation fails. This could
|
||||||
// happen if pushing an existing repository from one server to another that
|
// happen if pushing an existing repository from one server to another that
|
||||||
// does not have the repo.
|
// does not have the repo.
|
||||||
|
@ -831,3 +765,22 @@ func TestValidateTargetsModifiedHash(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ### End snapshot hash mismatch negative tests ###
|
// ### End snapshot hash mismatch negative tests ###
|
||||||
|
|
||||||
|
// ### generateSnapshot tests ###
|
||||||
|
func TestGenerateSnapshotNoRole(t *testing.T) {
|
||||||
|
kdb := keys.NewDB()
|
||||||
|
_, err := generateSnapshot("gun", kdb, nil, nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.IsType(t, validation.ErrBadRoot{}, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateSnapshotNoKey(t *testing.T) {
|
||||||
|
kdb, _, _ := testutils.EmptyRepo()
|
||||||
|
store := storage.NewMemStorage()
|
||||||
|
|
||||||
|
_, err := generateSnapshot("gun", kdb, nil, store)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.IsType(t, validation.ErrBadHierarchy{}, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ### End generateSnapshot tests ###
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package snapshot
|
package snapshot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/docker/notary/server/storage"
|
"github.com/docker/notary/server/storage"
|
||||||
|
@ -43,7 +43,8 @@ func GetOrCreateSnapshotKey(gun string, store storage.KeyStore, crypto signed.Cr
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrCreateSnapshot either returns the exisiting latest snapshot, or uses
|
// GetOrCreateSnapshot either returns the exisiting latest snapshot, or uses
|
||||||
// whatever the most recent snapshot is to generate a new one.
|
// whatever the most recent snapshot is to create the next one, only updating
|
||||||
|
// the expiry time and version.
|
||||||
func GetOrCreateSnapshot(gun string, store storage.MetaStore, cryptoService signed.CryptoService) ([]byte, error) {
|
func GetOrCreateSnapshot(gun string, store storage.MetaStore, cryptoService signed.CryptoService) ([]byte, error) {
|
||||||
|
|
||||||
d, err := store.GetCurrent(gun, "snapshot")
|
d, err := store.GetCurrent(gun, "snapshot")
|
||||||
|
@ -58,7 +59,8 @@ func GetOrCreateSnapshot(gun string, store storage.MetaStore, cryptoService sign
|
||||||
logrus.Error("Failed to unmarshal existing snapshot")
|
logrus.Error("Failed to unmarshal existing snapshot")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !snapshotExpired(sn) && !contentExpired(gun, sn, store) {
|
|
||||||
|
if !snapshotExpired(sn) {
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,57 +87,13 @@ func snapshotExpired(sn *data.SignedSnapshot) bool {
|
||||||
return signed.IsExpired(sn.Signed.Expires)
|
return signed.IsExpired(sn.Signed.Expires)
|
||||||
}
|
}
|
||||||
|
|
||||||
// contentExpired checks to see if any of the roles already in the snapshot
|
|
||||||
// have been updated. It will update any roles that have changed as it goes
|
|
||||||
// so that we don't have to run through all this again a second time.
|
|
||||||
func contentExpired(gun string, sn *data.SignedSnapshot, store storage.MetaStore) bool {
|
|
||||||
expired := false
|
|
||||||
updatedMeta := make(data.Files)
|
|
||||||
for role, meta := range sn.Signed.Meta {
|
|
||||||
curr, err := store.GetCurrent(gun, role)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
roleExp, newHash := roleExpired(curr, meta)
|
|
||||||
if roleExp {
|
|
||||||
updatedMeta[role] = data.FileMeta{
|
|
||||||
Length: int64(len(curr)),
|
|
||||||
Hashes: data.Hashes{
|
|
||||||
"sha256": newHash,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expired = expired || roleExp
|
|
||||||
}
|
|
||||||
if expired {
|
|
||||||
sn.Signed.Meta = updatedMeta
|
|
||||||
}
|
|
||||||
return expired
|
|
||||||
}
|
|
||||||
|
|
||||||
// roleExpired checks if the content for a specific role differs from
|
|
||||||
// the snapshot
|
|
||||||
func roleExpired(roleData []byte, meta data.FileMeta) (bool, []byte) {
|
|
||||||
currMeta, err := data.NewFileMeta(bytes.NewReader(roleData), "sha256")
|
|
||||||
if err != nil {
|
|
||||||
// if we can't generate FileMeta from the current roleData, we should
|
|
||||||
// continue to serve the old role if it isn't time expired
|
|
||||||
// because we won't be able to generate a new one.
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
hash := currMeta.Hashes["sha256"]
|
|
||||||
return !bytes.Equal(hash, meta.Hashes["sha256"]), hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// createSnapshot uses an existing snapshot to create a new one.
|
// createSnapshot uses an existing snapshot to create a new one.
|
||||||
// Important things to be aware of:
|
// Important things to be aware of:
|
||||||
// - It requires that a snapshot already exists. We create snapshots
|
// - It requires that a snapshot already exists. We create snapshots
|
||||||
// on upload so there should always be an existing snapshot if this
|
// on upload so there should always be an existing snapshot if this
|
||||||
// gets called.
|
// gets called.
|
||||||
// - It doesn't update what roles are present in the snapshot, as those
|
// - It doesn't update what roles are present in the snapshot, as those
|
||||||
// were validated during upload. We also updated the hashes of the
|
// were validated during upload.
|
||||||
// already present roles as part of our checks on whether we could
|
|
||||||
// serve the previous version of the snapshot
|
|
||||||
func createSnapshot(gun string, sn *data.SignedSnapshot, store storage.MetaStore, cryptoService signed.CryptoService) (*data.Signed, int, error) {
|
func createSnapshot(gun string, sn *data.SignedSnapshot, store storage.MetaStore, cryptoService signed.CryptoService) (*data.Signed, int, error) {
|
||||||
algorithm, public, err := store.GetKey(gun, data.CanonicalSnapshotRole)
|
algorithm, public, err := store.GetKey(gun, data.CanonicalSnapshotRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -114,32 +114,6 @@ func TestGetSnapshotKeyExistsOnSet(t *testing.T) {
|
||||||
assert.NotNil(t, k2, "Key should not be nil")
|
assert.NotNil(t, k2, "Key should not be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRoleExpired(t *testing.T) {
|
|
||||||
meta := data.FileMeta{
|
|
||||||
Hashes: data.Hashes{
|
|
||||||
"sha256": []byte{1},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
newData := []byte{2}
|
|
||||||
res, _ := roleExpired(newData, meta)
|
|
||||||
assert.True(t, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRoleNotExpired(t *testing.T) {
|
|
||||||
newData := []byte{2}
|
|
||||||
currMeta, err := data.NewFileMeta(bytes.NewReader(newData), "sha256")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
meta := data.FileMeta{
|
|
||||||
Hashes: data.Hashes{
|
|
||||||
"sha256": currMeta.Hashes["sha256"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
res, _ := roleExpired(newData, meta)
|
|
||||||
assert.False(t, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetSnapshotNotExists(t *testing.T) {
|
func TestGetSnapshotNotExists(t *testing.T) {
|
||||||
store := storage.NewMemStorage()
|
store := storage.NewMemStorage()
|
||||||
crypto := signed.NewEd25519()
|
crypto := signed.NewEd25519()
|
||||||
|
@ -211,3 +185,24 @@ func TestGetSnapshotCurrCorrupt(t *testing.T) {
|
||||||
_, err = GetOrCreateSnapshot("gun", store, crypto)
|
_, err = GetOrCreateSnapshot("gun", store, crypto)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateSnapshotNoKeyInStorage(t *testing.T) {
|
||||||
|
store := storage.NewMemStorage()
|
||||||
|
crypto := signed.NewEd25519()
|
||||||
|
|
||||||
|
_, _, err := createSnapshot("gun", nil, store, crypto)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSnapshotNoKeyInCrypto(t *testing.T) {
|
||||||
|
store := storage.NewMemStorage()
|
||||||
|
crypto := signed.NewEd25519()
|
||||||
|
|
||||||
|
_, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key)
|
||||||
|
|
||||||
|
// reset crypto so the store has the key but crypto doesn't
|
||||||
|
crypto = signed.NewEd25519()
|
||||||
|
|
||||||
|
_, _, err = createSnapshot("gun", &data.SignedSnapshot{}, store, crypto)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue