461 lines
14 KiB
Go
461 lines
14 KiB
Go
package storage
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/theupdateframework/notary/tuf/data"
|
|
)
|
|
|
|
type StoredTUFMeta struct {
|
|
Gun data.GUN
|
|
Role data.RoleName
|
|
SHA256 string
|
|
Data []byte
|
|
Version int
|
|
}
|
|
|
|
func SampleCustomTUFObj(gun data.GUN, role data.RoleName, version int, tufdata []byte) StoredTUFMeta {
|
|
if tufdata == nil {
|
|
tufdata = []byte(fmt.Sprintf("%s_%s_%d", gun, role, version))
|
|
}
|
|
checksum := sha256.Sum256(tufdata)
|
|
hexChecksum := hex.EncodeToString(checksum[:])
|
|
return StoredTUFMeta{
|
|
Gun: gun,
|
|
Role: role,
|
|
Version: version,
|
|
SHA256: hexChecksum,
|
|
Data: tufdata,
|
|
}
|
|
}
|
|
|
|
func MakeUpdate(tufObj StoredTUFMeta) MetaUpdate {
|
|
return MetaUpdate{
|
|
Role: tufObj.Role,
|
|
Version: tufObj.Version,
|
|
Data: tufObj.Data,
|
|
}
|
|
}
|
|
|
|
func assertExpectedTUFMetaInStore(t *testing.T, s MetaStore, expected []StoredTUFMeta, current bool) {
|
|
for _, tufObj := range expected {
|
|
var prevTime *time.Time
|
|
if current {
|
|
cDate, tufdata, err := s.GetCurrent(tufObj.Gun, tufObj.Role)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tufObj.Data, tufdata)
|
|
|
|
// the update date was sometime wthin the last minute
|
|
require.True(t, cDate.After(time.Now().Add(-1*time.Minute)))
|
|
require.True(t, cDate.Before(time.Now().Add(5*time.Second)))
|
|
prevTime = cDate
|
|
}
|
|
|
|
checksumBytes := sha256.Sum256(tufObj.Data)
|
|
checksum := hex.EncodeToString(checksumBytes[:])
|
|
|
|
cDate, tufdata, err := s.GetChecksum(tufObj.Gun, tufObj.Role, checksum)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tufObj.Data, tufdata)
|
|
|
|
if current {
|
|
require.True(t, prevTime.Equal(*cDate), "%s should be equal to %s", prevTime, cDate)
|
|
} else {
|
|
// the update date was sometime wthin the last minute
|
|
require.True(t, cDate.After(time.Now().Add(-1*time.Minute)))
|
|
require.True(t, cDate.Before(time.Now().Add(5*time.Second)))
|
|
}
|
|
}
|
|
}
|
|
|
|
// UpdateCurrent should succeed if there was no previous metadata of the same
|
|
// gun and role. They should be gettable.
|
|
func testUpdateCurrentEmptyStore(t *testing.T, s MetaStore) []StoredTUFMeta {
|
|
expected := make([]StoredTUFMeta, 0, 10)
|
|
for _, role := range append(data.BaseRoles, "targets/a") {
|
|
for _, gun := range []data.GUN{"gun1", "gun2"} {
|
|
// Adding a new TUF file should succeed
|
|
tufObj := SampleCustomTUFObj(gun, role, 1, nil)
|
|
require.NoError(t, s.UpdateCurrent(tufObj.Gun, MakeUpdate(tufObj)))
|
|
expected = append(expected, tufObj)
|
|
}
|
|
}
|
|
|
|
assertExpectedTUFMetaInStore(t, s, expected, true)
|
|
return expected
|
|
}
|
|
|
|
// UpdateCurrent will successfully add a new (higher) version of an existing TUF file,
|
|
// but will return an error if there is an older version of a TUF file. oldVersionExists
|
|
// specifies whether the older version should already exist in the DB or not.
|
|
func testUpdateCurrentVersionCheck(t *testing.T, s MetaStore, oldVersionExists bool) []StoredTUFMeta {
|
|
role, gun := data.CanonicalRootRole, data.GUN("testGUN")
|
|
|
|
expected := []StoredTUFMeta{
|
|
SampleCustomTUFObj(gun, role, 1, nil),
|
|
SampleCustomTUFObj(gun, role, 2, nil),
|
|
SampleCustomTUFObj(gun, role, 4, nil),
|
|
}
|
|
|
|
// starting meta is version 1
|
|
require.NoError(t, s.UpdateCurrent(gun, MakeUpdate(expected[0])))
|
|
|
|
// inserting meta version immediately above it and skipping ahead will succeed
|
|
require.NoError(t, s.UpdateCurrent(gun, MakeUpdate(expected[1])))
|
|
require.NoError(t, s.UpdateCurrent(gun, MakeUpdate(expected[2])))
|
|
|
|
// Inserting a version that already exists, or that is lower than the current version, will fail
|
|
version := 3
|
|
if oldVersionExists {
|
|
version = 4
|
|
}
|
|
|
|
tufObj := SampleCustomTUFObj(gun, role, version, nil)
|
|
err := s.UpdateCurrent(gun, MakeUpdate(tufObj))
|
|
require.Error(t, err, "Error should not be nil")
|
|
require.IsType(t, ErrOldVersion{}, err,
|
|
"Expected ErrOldVersion error type, got: %v", err)
|
|
|
|
assertExpectedTUFMetaInStore(t, s, expected[:2], false)
|
|
assertExpectedTUFMetaInStore(t, s, expected[2:], true)
|
|
return expected
|
|
}
|
|
|
|
// GetVersion should successfully retrieve a version of an existing TUF file,
|
|
// but will return an error if the requested version does not exist.
|
|
func testGetVersion(t *testing.T, s MetaStore) {
|
|
_, _, err := s.GetVersion("gun", "role", 2)
|
|
require.IsType(t, ErrNotFound{}, err, "Expected error to be ErrNotFound")
|
|
|
|
s.UpdateCurrent("gun", MetaUpdate{"role", 2, []byte("version2")})
|
|
_, d, err := s.GetVersion("gun", "role", 2)
|
|
require.Nil(t, err, "Expected error to be nil")
|
|
require.Equal(t, []byte("version2"), d, "Data was incorrect")
|
|
|
|
// Getting newer version fails
|
|
_, _, err = s.GetVersion("gun", "role", 3)
|
|
require.IsType(t, ErrNotFound{}, err, "Expected error to be ErrNotFound")
|
|
|
|
// Getting another gun/role fails
|
|
_, _, err = s.GetVersion("badgun", "badrole", 2)
|
|
require.IsType(t, ErrNotFound{}, err, "Expected error to be ErrNotFound")
|
|
}
|
|
|
|
// UpdateMany succeeds if the updates do not conflict with each other or with what's
|
|
// already in the DB
|
|
func testUpdateManyNoConflicts(t *testing.T, s MetaStore) []StoredTUFMeta {
|
|
var gun data.GUN = "testGUN"
|
|
firstBatch := make([]StoredTUFMeta, 4)
|
|
updates := make([]MetaUpdate, 4)
|
|
for i, role := range data.BaseRoles {
|
|
firstBatch[i] = SampleCustomTUFObj(gun, role, 1, nil)
|
|
updates[i] = MakeUpdate(firstBatch[i])
|
|
}
|
|
|
|
require.NoError(t, s.UpdateMany(gun, updates))
|
|
assertExpectedTUFMetaInStore(t, s, firstBatch, true)
|
|
|
|
secondBatch := make([]StoredTUFMeta, 4)
|
|
// no conflicts with what's in DB or with itself
|
|
for i, role := range data.BaseRoles {
|
|
secondBatch[i] = SampleCustomTUFObj(gun, role, 2, nil)
|
|
updates[i] = MakeUpdate(secondBatch[i])
|
|
}
|
|
|
|
require.NoError(t, s.UpdateMany(gun, updates))
|
|
// the first batch is still there, but are no longer the current ones
|
|
assertExpectedTUFMetaInStore(t, s, firstBatch, false)
|
|
assertExpectedTUFMetaInStore(t, s, secondBatch, true)
|
|
|
|
// and no conflicts if the same role and gun but different version is included
|
|
// in the same update. Even if they're out of order.
|
|
thirdBatch := make([]StoredTUFMeta, 2)
|
|
role := data.CanonicalRootRole
|
|
updates = updates[:2]
|
|
for i, version := range []int{4, 3} {
|
|
thirdBatch[i] = SampleCustomTUFObj(gun, role, version, nil)
|
|
updates[i] = MakeUpdate(thirdBatch[i])
|
|
}
|
|
|
|
require.NoError(t, s.UpdateMany(gun, updates))
|
|
|
|
// all the other data is still there, but are no longer the current ones
|
|
assertExpectedTUFMetaInStore(t, s, append(firstBatch, secondBatch...), false)
|
|
assertExpectedTUFMetaInStore(t, s, thirdBatch[:1], true)
|
|
assertExpectedTUFMetaInStore(t, s, thirdBatch[1:], false)
|
|
|
|
return append(append(firstBatch, secondBatch...), thirdBatch...)
|
|
}
|
|
|
|
// UpdateMany does not insert any rows (or at least rolls them back) if there
|
|
// are any conflicts.
|
|
func testUpdateManyConflictRollback(t *testing.T, s MetaStore) []StoredTUFMeta {
|
|
blackoutTime = 0
|
|
var gun data.GUN = "testGUN"
|
|
successBatch := make([]StoredTUFMeta, 4)
|
|
updates := make([]MetaUpdate, 4)
|
|
for i, role := range data.BaseRoles {
|
|
successBatch[i] = SampleCustomTUFObj(gun, role, 1, nil)
|
|
updates[i] = MakeUpdate(successBatch[i])
|
|
}
|
|
|
|
require.NoError(t, s.UpdateMany(gun, updates))
|
|
|
|
before, err := s.GetChanges("0", 1000, "")
|
|
require.NoError(t, err)
|
|
|
|
// conflicts with what's in DB
|
|
badBatch := make([]StoredTUFMeta, 4)
|
|
for i, role := range data.BaseRoles {
|
|
version := 2
|
|
if role == data.CanonicalTargetsRole {
|
|
version = 1
|
|
}
|
|
tufdata := []byte(fmt.Sprintf("%s_%s_%d_bad", gun, role, version))
|
|
badBatch[i] = SampleCustomTUFObj(gun, role, version, tufdata)
|
|
updates[i] = MakeUpdate(badBatch[i])
|
|
}
|
|
|
|
// check no changes were written when there was a conflict+rollback
|
|
after, err := s.GetChanges("0", 1000, "")
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(before), len(after))
|
|
|
|
err = s.UpdateMany(gun, updates)
|
|
require.Error(t, err)
|
|
require.IsType(t, ErrOldVersion{}, err)
|
|
|
|
// self-conflicting, in that it's a duplicate, but otherwise no DB conflicts
|
|
duplicate := SampleCustomTUFObj(gun, data.CanonicalTimestampRole, 3, []byte("duplicate"))
|
|
duplicateUpdate := MakeUpdate(duplicate)
|
|
err = s.UpdateMany(gun, []MetaUpdate{duplicateUpdate, duplicateUpdate})
|
|
require.Error(t, err)
|
|
require.IsType(t, ErrOldVersion{}, err)
|
|
|
|
assertExpectedTUFMetaInStore(t, s, successBatch, true)
|
|
|
|
for _, tufObj := range append(badBatch, duplicate) {
|
|
checksumBytes := sha256.Sum256(tufObj.Data)
|
|
checksum := hex.EncodeToString(checksumBytes[:])
|
|
|
|
_, _, err = s.GetChecksum(tufObj.Gun, tufObj.Role, checksum)
|
|
require.Error(t, err)
|
|
require.IsType(t, ErrNotFound{}, err)
|
|
}
|
|
|
|
return successBatch
|
|
}
|
|
|
|
// Delete will remove all TUF metadata, all versions, associated with a gun
|
|
func testDeleteSuccess(t *testing.T, s MetaStore) {
|
|
var gun data.GUN = "testGUN"
|
|
// If there is nothing in the DB, delete is a no-op success
|
|
require.NoError(t, s.Delete(gun))
|
|
|
|
// If there is data in the DB, all versions are deleted
|
|
unexpected := make([]StoredTUFMeta, 0, 10)
|
|
updates := make([]MetaUpdate, 0, 10)
|
|
for version := 1; version < 3; version++ {
|
|
for _, role := range append(data.BaseRoles, "targets/a") {
|
|
tufObj := SampleCustomTUFObj(gun, role, version, nil)
|
|
unexpected = append(unexpected, tufObj)
|
|
updates = append(updates, MakeUpdate(tufObj))
|
|
}
|
|
}
|
|
require.NoError(t, s.UpdateMany(gun, updates))
|
|
assertExpectedTUFMetaInStore(t, s, unexpected[:5], false)
|
|
assertExpectedTUFMetaInStore(t, s, unexpected[5:], true)
|
|
|
|
require.NoError(t, s.Delete(gun))
|
|
|
|
for _, tufObj := range unexpected {
|
|
_, _, err := s.GetCurrent(tufObj.Gun, tufObj.Role)
|
|
require.IsType(t, ErrNotFound{}, err)
|
|
|
|
checksumBytes := sha256.Sum256(tufObj.Data)
|
|
checksum := hex.EncodeToString(checksumBytes[:])
|
|
|
|
_, _, err = s.GetChecksum(tufObj.Gun, tufObj.Role, checksum)
|
|
require.Error(t, err)
|
|
require.IsType(t, ErrNotFound{}, err)
|
|
}
|
|
|
|
// We can now write the same files without conflicts to the DB
|
|
require.NoError(t, s.UpdateMany(gun, updates))
|
|
assertExpectedTUFMetaInStore(t, s, unexpected[:5], false)
|
|
assertExpectedTUFMetaInStore(t, s, unexpected[5:], true)
|
|
|
|
// And delete them again successfully
|
|
require.NoError(t, s.Delete(gun))
|
|
}
|
|
|
|
func testGetChanges(t *testing.T, s MetaStore) {
|
|
blackoutTime = 0
|
|
// non-int changeID
|
|
c, err := s.GetChanges("foo", 10, "")
|
|
require.Error(t, err)
|
|
require.Len(t, c, 0)
|
|
|
|
// add some records
|
|
require.NoError(t, s.UpdateMany("alpine", []MetaUpdate{
|
|
{
|
|
Role: data.CanonicalTimestampRole,
|
|
Version: 1,
|
|
Data: []byte{'1'},
|
|
},
|
|
}))
|
|
require.NoError(t, s.UpdateMany("alpine", []MetaUpdate{
|
|
{
|
|
Role: data.CanonicalTimestampRole,
|
|
Version: 2,
|
|
Data: []byte{'2'},
|
|
},
|
|
}))
|
|
require.NoError(t, s.UpdateMany("alpine", []MetaUpdate{
|
|
{
|
|
Role: data.CanonicalTimestampRole,
|
|
Version: 3,
|
|
Data: []byte{'3'},
|
|
},
|
|
}))
|
|
require.NoError(t, s.UpdateMany("alpine", []MetaUpdate{
|
|
{
|
|
Role: data.CanonicalTimestampRole,
|
|
Version: 4,
|
|
Data: []byte{'4'},
|
|
},
|
|
}))
|
|
require.NoError(t, s.UpdateMany("busybox", []MetaUpdate{
|
|
{
|
|
Role: data.CanonicalTimestampRole,
|
|
Version: 1,
|
|
Data: []byte{'5'},
|
|
},
|
|
}))
|
|
require.NoError(t, s.UpdateMany("busybox", []MetaUpdate{
|
|
{
|
|
Role: data.CanonicalTimestampRole,
|
|
Version: 2,
|
|
Data: []byte{'6'},
|
|
},
|
|
}))
|
|
require.NoError(t, s.UpdateMany("busybox", []MetaUpdate{
|
|
{
|
|
Role: data.CanonicalTimestampRole,
|
|
Version: 3,
|
|
Data: []byte{'7'},
|
|
},
|
|
}))
|
|
require.NoError(t, s.UpdateMany("busybox", []MetaUpdate{
|
|
{
|
|
Role: data.CanonicalTimestampRole,
|
|
Version: 4,
|
|
Data: []byte{'8'},
|
|
},
|
|
}))
|
|
|
|
// check non-error cases
|
|
c, err = s.GetChanges("0", 8, "")
|
|
require.NoError(t, err)
|
|
require.Len(t, c, 8)
|
|
|
|
for i := 0; i < 4; i++ {
|
|
require.Equal(t, "alpine", c[i].GUN)
|
|
require.Equal(t, i+1, c[i].Version)
|
|
}
|
|
for i := 4; i < 8; i++ {
|
|
require.Equal(t, "busybox", c[i].GUN)
|
|
require.Equal(t, i-3, c[i].Version)
|
|
}
|
|
full := c
|
|
|
|
c, err = s.GetChanges("-1", 4, "")
|
|
require.NoError(t, err)
|
|
require.Len(t, c, 4)
|
|
for i := 0; i < 4; i++ {
|
|
require.Equal(t, "busybox", c[i].GUN)
|
|
require.Equal(t, i+1, c[i].Version)
|
|
}
|
|
|
|
c, err = s.GetChanges(full[7].ID, 4, "")
|
|
require.NoError(t, err)
|
|
require.Len(t, c, 0)
|
|
|
|
c, err = s.GetChanges(full[6].ID, -4, "")
|
|
require.NoError(t, err)
|
|
require.Len(t, c, 4)
|
|
for i := 0; i < 2; i++ {
|
|
require.Equal(t, "alpine", c[i].GUN)
|
|
require.Equal(t, i+3, c[i].Version)
|
|
}
|
|
for i := 2; i < 4; i++ {
|
|
require.Equal(t, "busybox", c[i].GUN)
|
|
require.Equal(t, i-1, c[i].Version)
|
|
}
|
|
|
|
c, err = s.GetChanges("0", 8, "busybox")
|
|
require.NoError(t, err)
|
|
require.Len(t, c, 4)
|
|
for i := 0; i < 4; i++ {
|
|
require.Equal(t, "busybox", c[i].GUN)
|
|
require.Equal(t, i+1, c[i].Version)
|
|
}
|
|
|
|
c, err = s.GetChanges("-1", -8, "busybox")
|
|
require.NoError(t, err)
|
|
require.Len(t, c, 4)
|
|
for i := 0; i < 4; i++ {
|
|
require.Equal(t, "busybox", c[i].GUN)
|
|
require.Equal(t, i+1, c[i].Version)
|
|
}
|
|
|
|
// update a snapshot and confirm the most recent item of the changelist
|
|
// hasn't changed (only timestamps should create changes)
|
|
before, err := s.GetChanges("-1", -1, "")
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.UpdateMany("alpine", []MetaUpdate{
|
|
{
|
|
Role: data.CanonicalSnapshotRole,
|
|
Version: 1,
|
|
Data: []byte{'1'},
|
|
},
|
|
}))
|
|
after, err := s.GetChanges("-1", -1, "")
|
|
require.NoError(t, err)
|
|
require.Equal(t, before, after)
|
|
|
|
_, err1 := s.GetChanges("1000", 0, "")
|
|
_, err2 := s.GetChanges("doesn't exist", 0, "")
|
|
if _, ok := s.(RethinkDB); ok {
|
|
require.Error(t, err1)
|
|
require.Error(t, err2)
|
|
} else {
|
|
require.NoError(t, err1)
|
|
require.Error(t, err2)
|
|
require.IsType(t, ErrBadQuery{}, err2)
|
|
}
|
|
|
|
// do a deletion and check is shows up.
|
|
require.NoError(t, s.Delete("alpine"))
|
|
c, err = s.GetChanges("-1", -1, "")
|
|
require.NoError(t, err)
|
|
require.Len(t, c, 1)
|
|
require.Equal(t, changeCategoryDeletion, c[0].Category)
|
|
require.Equal(t, "alpine", c[0].GUN)
|
|
|
|
// do another deletion and check it doesn't show up because no records were deleted
|
|
// after the first one
|
|
require.NoError(t, s.Delete("alpine"))
|
|
c, err = s.GetChanges("-1", -2, "")
|
|
require.NoError(t, err)
|
|
require.Len(t, c, 2)
|
|
require.NotEqual(t, changeCategoryDeletion, c[0].Category)
|
|
require.NotEqual(t, "alpine", c[0].GUN)
|
|
|
|
}
|