add indeets test helper, implement in exchange op (#3295)

Adds a helper for building expected details entries and checking
them after a backup.  Implements the helper in the exchange backup
tests in operations/backup integration.  Will follow with a onedrive
implementation.

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🤖 Supportability/Tests

#### Issue(s)

* #3240

#### Test Plan

- [x] 💪 Manual
- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-05-10 20:28:18 -06:00 committed by GitHub
parent c0725b9cf9
commit c5b388a721
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1170 additions and 149 deletions

View File

@ -112,7 +112,7 @@ func runDisplayM365JSON(
creds account.M365Config,
user, itemID string,
) error {
drive, err := api.GetDriveByID(ctx, srv, user)
drive, err := api.GetUsersDrive(ctx, srv, user)
if err != nil {
return err
}

View File

@ -441,10 +441,7 @@ func restoreCollection(
continue
}
locationRef := &path.Builder{}
if category == path.ContactsCategory {
locationRef = locationRef.Append(itemPath.Folders()...)
}
locationRef := path.Builder{}.Append(itemPath.Folders()...)
err = deets.Add(
itemPath,

View File

@ -336,18 +336,33 @@ func GetItemPermission(
return perm, nil
}
func GetDriveByID(
func GetUsersDrive(
ctx context.Context,
srv graph.Servicer,
userID string,
user string,
) (models.Driveable, error) {
//revive:enable:context-as-argument
d, err := srv.Client().
UsersById(userID).
UsersById(user).
Drive().
Get(ctx, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "getting drive")
return nil, graph.Wrap(ctx, err, "getting user's drive")
}
return d, nil
}
func GetSitesDefaultDrive(
ctx context.Context,
srv graph.Servicer,
site string,
) (models.Driveable, error) {
d, err := srv.Client().
SitesById(site).
Drive().
Get(ctx, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "getting site's drive")
}
return d, nil

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,368 @@
package testdata
import (
"context"
"strings"
"testing"
"github.com/alcionai/clues"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
"github.com/alcionai/corso/src/internal/kopia"
"github.com/alcionai/corso/src/internal/model"
"github.com/alcionai/corso/src/internal/streamstore"
"github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path"
)
// ---------------------------------------------------------------------------
// location set handling
// ---------------------------------------------------------------------------
var exists = struct{}{}
type locSet struct {
// map [locationRef] map [itemRef] {}
// refs may be either the canonical ent refs, or something else,
// so long as they are consistent for the test in question
Locations map[string]map[string]struct{}
Deleted map[string]map[string]struct{}
}
func newLocSet() *locSet {
return &locSet{
Locations: map[string]map[string]struct{}{},
Deleted: map[string]map[string]struct{}{},
}
}
func (ls *locSet) AddItem(locationRef, itemRef string) {
ls.AddLocation(locationRef)
ls.Locations[locationRef][itemRef] = exists
delete(ls.Deleted[locationRef], itemRef)
}
func (ls *locSet) RemoveItem(locationRef, itemRef string) {
delete(ls.Locations[locationRef], itemRef)
if _, ok := ls.Deleted[locationRef]; !ok {
ls.Deleted[locationRef] = map[string]struct{}{}
}
ls.Deleted[locationRef][itemRef] = exists
}
func (ls *locSet) MoveItem(fromLocation, toLocation, ir string) {
ls.RemoveItem(fromLocation, ir)
ls.AddItem(toLocation, ir)
}
func (ls *locSet) AddLocation(locationRef string) {
if _, ok := ls.Locations[locationRef]; !ok {
ls.Locations[locationRef] = map[string]struct{}{}
}
// don't purge previously deleted items, or child locations.
// Assumption is that their itemRef is unique, and still deleted.
delete(ls.Deleted, locationRef)
}
func (ls *locSet) RemoveLocation(locationRef string) {
ss := ls.Subset(locationRef)
for lr := range ss.Locations {
items := ls.Locations[lr]
delete(ls.Locations, lr)
if _, ok := ls.Deleted[lr]; !ok {
ls.Deleted[lr] = map[string]struct{}{}
}
for ir := range items {
ls.Deleted[lr][ir] = exists
}
}
}
// MoveLocation takes the LAST elemet in the fromLocation (and all)
// children matching the prefix, and relocates it as a child of toLocation.
// ex: MoveLocation("/a/b/c", "/d") will move all entries with the prefix
// "/a/b/c" into "/d/c". This also deletes all "/a/b/c" entries and children.
// assumes item IDs don't change across the migration. If item IDs do change,
// that difference will need to be handled manually by the caller.
// returns the base folder's new location (ex: /d/c)
func (ls *locSet) MoveLocation(fromLocation, toLocation string) string {
fromBuilder := path.Builder{}.Append(path.Split(fromLocation)...)
toBuilder := path.Builder{}.Append(path.Split(toLocation)...).Append(fromBuilder.LastElem())
ls.RenameLocation(fromBuilder.String(), toBuilder.String())
return toBuilder.String()
}
func (ls *locSet) RenameLocation(fromLocation, toLocation string) {
ss := ls.Subset(fromLocation)
fromBuilder := path.Builder{}.Append(path.Split(fromLocation)...)
toBuilder := path.Builder{}.Append(path.Split(toLocation)...)
for lr, items := range ss.Locations {
lrBuilder := path.Builder{}.Append(path.Split(lr)...)
lrBuilder.UpdateParent(fromBuilder, toBuilder)
newLoc := lrBuilder.String()
for ir := range items {
ls.RemoveItem(lr, ir)
ls.AddItem(newLoc, ir)
}
ls.RemoveLocation(lr)
ls.AddLocation(newLoc)
}
}
// Subset produces a new locSet containing only Items and Locations
// whose location matches the locationPfx
func (ls *locSet) Subset(locationPfx string) *locSet {
ss := newLocSet()
for lr, items := range ls.Locations {
if strings.HasPrefix(lr, locationPfx) {
ss.AddLocation(lr)
for ir := range items {
ss.AddItem(lr, ir)
}
}
}
return ss
}
// ---------------------------------------------------------------------------
// The goal of InDeets is to provide a struct and interface which allows
// tests to predict not just the elements within a set of details entries,
// but also their changes (relocation, renaming, etc) in a way that consolidates
// building an "expected set" of details entries that can be compared against
// the details results after a backup.
// ---------------------------------------------------------------------------
// InDeets is a helper for comparing details state in tests
// across backup instances.
type InDeets struct {
// only: tenantID/service/resourceOwnerID
RRPrefix string
// map of container setting the uniqueness boundary for location
// ref entries (eg, data type like email, contacts, etc, or
// drive id) to the unique entries in that set.
Sets map[string]*locSet
}
func NewInDeets(repoRefPrefix string) *InDeets {
return &InDeets{
RRPrefix: repoRefPrefix,
Sets: map[string]*locSet{},
}
}
func (id *InDeets) getSet(set string) *locSet {
s, ok := id.Sets[set]
if ok {
return s
}
return newLocSet()
}
func (id *InDeets) AddAll(deets details.Details, ws whatSet) {
if id.Sets == nil {
id.Sets = map[string]*locSet{}
}
for _, ent := range deets.Entries {
set, err := ws(ent)
if err != nil {
set = err.Error()
}
dir := ent.LocationRef
if ent.Folder != nil {
dir = dir + ent.Folder.DisplayName
id.AddLocation(set, dir)
} else {
id.AddItem(set, ent.LocationRef, ent.ItemRef)
}
}
}
func (id *InDeets) AddItem(set, locationRef, itemRef string) {
id.getSet(set).AddItem(locationRef, itemRef)
}
func (id *InDeets) RemoveItem(set, locationRef, itemRef string) {
id.getSet(set).RemoveItem(locationRef, itemRef)
}
func (id *InDeets) MoveItem(set, fromLocation, toLocation, ir string) {
id.getSet(set).MoveItem(fromLocation, toLocation, ir)
}
func (id *InDeets) AddLocation(set, locationRef string) {
id.getSet(set).AddLocation(locationRef)
}
// RemoveLocation removes the provided location, and all children
// of that location.
func (id *InDeets) RemoveLocation(set, locationRef string) {
id.getSet(set).RemoveLocation(locationRef)
}
// MoveLocation takes the LAST elemet in the fromLocation (and all)
// children matching the prefix, and relocates it as a child of toLocation.
// ex: MoveLocation("/a/b/c", "/d") will move all entries with the prefix
// "/a/b/c" into "/d/c". This also deletes all "/a/b/c" entries and children.
// assumes item IDs don't change across the migration. If item IDs do change,
// that difference will need to be handled manually by the caller.
// returns the base folder's new location (ex: /d/c)
func (id *InDeets) MoveLocation(set, fromLocation, toLocation string) string {
return id.getSet(set).MoveLocation(fromLocation, toLocation)
}
func (id *InDeets) RenameLocation(set, fromLocation, toLocation string) {
id.getSet(set).RenameLocation(fromLocation, toLocation)
}
// Subset produces a new locSet containing only Items and Locations
// whose location matches the locationPfx
func (id *InDeets) Subset(set, locationPfx string) *locSet {
return id.getSet(set).Subset(locationPfx)
}
// ---------------------------------------------------------------------------
// whatSet helpers for extracting a set identifier from an arbitrary repoRef
// ---------------------------------------------------------------------------
type whatSet func(details.Entry) (string, error)
// common whatSet parser that extracts the service category from
// a repoRef.
func CategoryFromRepoRef(ent details.Entry) (string, error) {
p, err := path.FromDataLayerPath(ent.RepoRef, false)
if err != nil {
return "", err
}
return p.Category().String(), nil
}
// common whatSet parser that extracts the driveID from a repoRef.
func DriveIDFromRepoRef(ent details.Entry) (string, error) {
p, err := path.FromDataLayerPath(ent.RepoRef, false)
if err != nil {
return "", err
}
odp, err := path.ToDrivePath(p)
if err != nil {
return "", err
}
return odp.DriveID, nil
}
// ---------------------------------------------------------------------------
// helpers and comparators
// ---------------------------------------------------------------------------
func CheckBackupDetails(
t *testing.T,
ctx context.Context, //revive:disable-line:context-as-argument
backupID model.StableID,
ws whatSet,
ms *kopia.ModelStore,
ssr streamstore.Reader,
expect *InDeets,
// standard check is assert.Subset due to issues of external data cross-
// pollination. This should be true if the backup contains a unique directory
// of data.
mustEqualFolders bool,
) {
deets, result := GetDeetsInBackup(t, ctx, backupID, "", "", path.UnknownService, ws, ms, ssr)
t.Log("details entries in result")
for _, ent := range deets.Entries {
if ent.Folder == nil {
t.Log(ent.LocationRef)
t.Log(ent.ItemRef)
}
assert.Truef(
t,
strings.HasPrefix(ent.RepoRef, expect.RRPrefix),
"all details should begin with the expected prefix\nwant: %s\ngot: %s",
expect.RRPrefix, ent.RepoRef)
}
for set := range expect.Sets {
check := assert.Subsetf
if mustEqualFolders {
check = assert.ElementsMatchf
}
check(
t,
maps.Keys(result.Sets[set].Locations),
maps.Keys(expect.Sets[set].Locations),
"results in %s missing expected location", set)
for lr, items := range expect.Sets[set].Deleted {
_, ok := result.Sets[set].Locations[lr]
assert.Falsef(t, ok, "deleted location in %s found in result: %s", set, lr)
for ir := range items {
_, ok := result.Sets[set].Locations[lr][ir]
assert.Falsef(t, ok, "deleted item in %s found in result: %s", set, lr)
}
}
}
}
func GetDeetsInBackup(
t *testing.T,
ctx context.Context, //revive:disable-line:context-as-argument
backupID model.StableID,
tid, resourceOwner string,
service path.ServiceType,
ws whatSet,
ms *kopia.ModelStore,
ssr streamstore.Reader,
) (details.Details, *InDeets) {
bup := backup.Backup{}
err := ms.Get(ctx, model.BackupSchema, backupID, &bup)
require.NoError(t, err, clues.ToCore(err))
ssid := bup.StreamStoreID
require.NotEmpty(t, ssid, "stream store ID")
var deets details.Details
err = ssr.Read(
ctx,
ssid,
streamstore.DetailsReader(details.UnmarshalTo(&deets)),
fault.New(true))
require.NoError(t, err, clues.ToCore(err))
id := NewInDeets(path.Builder{}.Append(tid, service.String(), resourceOwner).String())
id.AddAll(deets, ws)
return deets, id
}

View File

@ -0,0 +1,445 @@
package testdata
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"golang.org/x/exp/maps"
"github.com/alcionai/corso/src/internal/tester"
)
type LocSetUnitSuite struct {
tester.Suite
}
func TestLocSetUnitSuite(t *testing.T) {
suite.Run(t, &LocSetUnitSuite{Suite: tester.NewUnitSuite(t)})
}
const (
l1 = "lr_1"
l2 = "lr_2"
l13 = "lr_1/lr_3"
l14 = "lr_1/lr_4"
i1 = "ir_1"
i2 = "ir_2"
i3 = "ir_3"
i4 = "ir_4"
)
func (suite *LocSetUnitSuite) TestAdd() {
t := suite.T()
ls := newLocSet()
ls.AddItem(l1, i1)
ls.AddLocation(l2)
assert.ElementsMatch(t, []string{l1, l2}, maps.Keys(ls.Locations))
assert.ElementsMatch(t, []string{i1}, maps.Keys(ls.Locations[l1]))
assert.Empty(t, maps.Keys(ls.Locations[l2]))
assert.Empty(t, maps.Keys(ls.Locations[l13]))
}
func (suite *LocSetUnitSuite) TestRemove() {
t := suite.T()
ls := newLocSet()
ls.AddItem(l1, i1)
ls.AddItem(l1, i2)
ls.AddLocation(l13)
ls.AddItem(l14, i3)
ls.AddItem(l14, i4)
assert.ElementsMatch(t, []string{l1, l13, l14}, maps.Keys(ls.Locations))
assert.ElementsMatch(t, []string{i1, i2}, maps.Keys(ls.Locations[l1]))
assert.Empty(t, maps.Keys(ls.Locations[l13]))
assert.ElementsMatch(t, []string{i3, i4}, maps.Keys(ls.Locations[l14]))
// nop removal
ls.RemoveItem(l2, i1)
assert.ElementsMatch(t, []string{i1, i2}, maps.Keys(ls.Locations[l1]))
// item removal
ls.RemoveItem(l1, i2)
assert.ElementsMatch(t, []string{i1}, maps.Keys(ls.Locations[l1]))
// nop location removal
ls.RemoveLocation(l2)
assert.ElementsMatch(t, []string{l1, l13, l14}, maps.Keys(ls.Locations))
// non-cascading location removal
ls.RemoveLocation(l13)
assert.ElementsMatch(t, []string{l1, l14}, maps.Keys(ls.Locations))
assert.ElementsMatch(t, []string{i1}, maps.Keys(ls.Locations[l1]))
assert.ElementsMatch(t, []string{i3, i4}, maps.Keys(ls.Locations[l14]))
// cascading location removal
ls.RemoveLocation(l1)
assert.Empty(t, maps.Keys(ls.Locations))
assert.Empty(t, maps.Keys(ls.Locations[l1]))
assert.Empty(t, maps.Keys(ls.Locations[l13]))
assert.Empty(t, maps.Keys(ls.Locations[l14]))
}
func (suite *LocSetUnitSuite) TestSubset() {
ls := newLocSet()
ls.AddItem(l1, i1)
ls.AddItem(l1, i2)
ls.AddLocation(l13)
ls.AddItem(l14, i3)
ls.AddItem(l14, i4)
table := []struct {
name string
locPfx string
expect func(*testing.T, *locSet)
}{
{
name: "nop",
locPfx: l2,
expect: func(t *testing.T, ss *locSet) {
assert.Empty(t, maps.Keys(ss.Locations))
},
},
{
name: "no items",
locPfx: l13,
expect: func(t *testing.T, ss *locSet) {
assert.ElementsMatch(t, []string{l13}, maps.Keys(ss.Locations))
assert.Empty(t, maps.Keys(ss.Locations[l13]))
},
},
{
name: "non-cascading",
locPfx: l14,
expect: func(t *testing.T, ss *locSet) {
assert.ElementsMatch(t, []string{l14}, maps.Keys(ss.Locations))
assert.ElementsMatch(t, []string{i3, i4}, maps.Keys(ss.Locations[l14]))
},
},
{
name: "cascading",
locPfx: l1,
expect: func(t *testing.T, ss *locSet) {
assert.ElementsMatch(t, []string{l1, l13, l14}, maps.Keys(ss.Locations))
assert.ElementsMatch(t, []string{i1, i2}, maps.Keys(ss.Locations[l1]))
assert.ElementsMatch(t, []string{i3, i4}, maps.Keys(ss.Locations[l14]))
assert.Empty(t, maps.Keys(ss.Locations[l13]))
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
test.expect(t, ls.Subset(test.locPfx))
})
}
}
func (suite *LocSetUnitSuite) TestRename() {
t := suite.T()
makeSet := func() *locSet {
ls := newLocSet()
ls.AddItem(l1, i1)
ls.AddItem(l1, i2)
ls.AddLocation(l13)
ls.AddItem(l14, i3)
ls.AddItem(l14, i4)
return ls
}
ts := makeSet()
assert.ElementsMatch(t, []string{l1, l13, l14}, maps.Keys(ts.Locations))
assert.ElementsMatch(t, []string{i1, i2}, maps.Keys(ts.Locations[l1]))
assert.Empty(t, maps.Keys(ts.Locations[l13]))
assert.ElementsMatch(t, []string{i3, i4}, maps.Keys(ts.Locations[l14]))
table := []struct {
name string
from string
to string
expect func(*testing.T, *locSet)
}{
{
name: "nop",
from: l2,
to: "foo",
expect: func(t *testing.T, ls *locSet) {
assert.ElementsMatch(t, []string{l1, l13, l14}, maps.Keys(ls.Locations))
assert.Empty(t, maps.Keys(ls.Locations[l2]))
assert.Empty(t, maps.Keys(ls.Locations["foo"]))
},
},
{
name: "no items",
from: l13,
to: "foo",
expect: func(t *testing.T, ls *locSet) {
assert.ElementsMatch(t, []string{l1, "foo", l14}, maps.Keys(ls.Locations))
assert.Empty(t, maps.Keys(ls.Locations[l13]))
assert.Empty(t, maps.Keys(ls.Locations["foo"]))
},
},
{
name: "with items",
from: l14,
to: "foo",
expect: func(t *testing.T, ls *locSet) {
assert.ElementsMatch(t, []string{l1, l13, "foo"}, maps.Keys(ls.Locations))
assert.Empty(t, maps.Keys(ls.Locations[l14]))
assert.ElementsMatch(t, []string{i3, i4}, maps.Keys(ls.Locations["foo"]))
},
},
{
name: "cascading locations",
from: l1,
to: "foo",
expect: func(t *testing.T, ls *locSet) {
assert.ElementsMatch(t, []string{"foo", "foo/lr_3", "foo/lr_4"}, maps.Keys(ls.Locations))
assert.Empty(t, maps.Keys(ls.Locations[l1]))
assert.Empty(t, maps.Keys(ls.Locations[l14]))
assert.Empty(t, maps.Keys(ls.Locations[l13]))
assert.ElementsMatch(t, []string{i1, i2}, maps.Keys(ls.Locations["foo"]))
assert.Empty(t, maps.Keys(ls.Locations["foo/lr_3"]))
assert.ElementsMatch(t, []string{i3, i4}, maps.Keys(ls.Locations["foo/lr_4"]))
},
},
{
name: "to existing location",
from: l14,
to: l1,
expect: func(t *testing.T, ls *locSet) {
assert.ElementsMatch(t, []string{l1, l13}, maps.Keys(ls.Locations))
assert.Empty(t, maps.Keys(ls.Locations[l14]))
assert.ElementsMatch(t, []string{i1, i2, i3, i4}, maps.Keys(ls.Locations[l1]))
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ls := makeSet()
ls.RenameLocation(test.from, test.to)
test.expect(t, ls)
})
}
}
func (suite *LocSetUnitSuite) TestItem() {
t := suite.T()
b4 := "bar/lr_4"
makeSet := func() *locSet {
ls := newLocSet()
ls.AddItem(l1, i1)
ls.AddItem(l1, i2)
ls.AddLocation(l13)
ls.AddItem(l14, i3)
ls.AddItem(l14, i4)
ls.AddItem(b4, "fnord")
return ls
}
ts := makeSet()
assert.ElementsMatch(t, []string{l1, l13, l14, b4}, maps.Keys(ts.Locations))
assert.ElementsMatch(t, []string{i1, i2}, maps.Keys(ts.Locations[l1]))
assert.Empty(t, maps.Keys(ts.Locations[l13]))
assert.ElementsMatch(t, []string{i3, i4}, maps.Keys(ts.Locations[l14]))
assert.ElementsMatch(t, []string{"fnord"}, maps.Keys(ts.Locations[b4]))
table := []struct {
name string
item string
from string
to string
expect func(*testing.T, *locSet)
}{
{
name: "nop item",
item: "floob",
from: l2,
to: l1,
expect: func(t *testing.T, ls *locSet) {
assert.ElementsMatch(t, []string{i1, i2, "floob"}, maps.Keys(ls.Locations[l1]))
assert.Empty(t, maps.Keys(ls.Locations[l2]))
},
},
{
name: "nop origin",
item: i1,
from: "smarf",
to: l2,
expect: func(t *testing.T, ls *locSet) {
assert.ElementsMatch(t, []string{i1, i2}, maps.Keys(ls.Locations[l1]))
assert.ElementsMatch(t, []string{i1}, maps.Keys(ls.Locations[l2]))
assert.Empty(t, maps.Keys(ls.Locations["smarf"]))
},
},
{
name: "new location",
item: i1,
from: l1,
to: "fnords",
expect: func(t *testing.T, ls *locSet) {
assert.ElementsMatch(t, []string{i2}, maps.Keys(ls.Locations[l1]))
assert.ElementsMatch(t, []string{i1}, maps.Keys(ls.Locations["fnords"]))
},
},
{
name: "existing location",
item: i1,
from: l1,
to: l2,
expect: func(t *testing.T, ls *locSet) {
assert.ElementsMatch(t, []string{i2}, maps.Keys(ls.Locations[l1]))
assert.ElementsMatch(t, []string{i1}, maps.Keys(ls.Locations[l2]))
},
},
{
name: "same location",
item: i1,
from: l1,
to: l1,
expect: func(t *testing.T, ls *locSet) {
assert.ElementsMatch(t, []string{i1, i2}, maps.Keys(ls.Locations[l1]))
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ls := makeSet()
ls.MoveItem(test.from, test.to, test.item)
test.expect(t, ls)
})
}
}
func (suite *LocSetUnitSuite) TestMoveLocation() {
t := suite.T()
b4 := "bar/lr_4"
makeSet := func() *locSet {
ls := newLocSet()
ls.AddItem(l1, i1)
ls.AddItem(l1, i2)
ls.AddLocation(l13)
ls.AddItem(l14, i3)
ls.AddItem(l14, i4)
ls.AddItem(b4, "fnord")
return ls
}
ts := makeSet()
assert.ElementsMatch(t, []string{l1, l13, l14, b4}, maps.Keys(ts.Locations))
assert.ElementsMatch(t, []string{i1, i2}, maps.Keys(ts.Locations[l1]))
assert.Empty(t, maps.Keys(ts.Locations[l13]))
assert.ElementsMatch(t, []string{i3, i4}, maps.Keys(ts.Locations[l14]))
assert.ElementsMatch(t, []string{"fnord"}, maps.Keys(ts.Locations[b4]))
table := []struct {
name string
from string
to string
expect func(*testing.T, *locSet)
expectNewLoc string
}{
{
name: "nop root",
from: l2,
to: "",
expect: func(t *testing.T, ls *locSet) {
assert.ElementsMatch(t, []string{l1, l13, l14, b4}, maps.Keys(ls.Locations))
assert.Empty(t, maps.Keys(ls.Locations[l2]))
},
expectNewLoc: l2,
},
{
name: "nop child",
from: l2,
to: "foo",
expect: func(t *testing.T, ls *locSet) {
assert.ElementsMatch(t, []string{l1, l13, l14, b4}, maps.Keys(ls.Locations))
assert.Empty(t, maps.Keys(ls.Locations["foo"]))
assert.Empty(t, maps.Keys(ls.Locations["foo/"+l2]))
},
expectNewLoc: "foo/" + l2,
},
{
name: "no items",
from: l13,
to: "foo",
expect: func(t *testing.T, ls *locSet) {
newLoc := "foo/lr_3"
assert.ElementsMatch(t, []string{l1, newLoc, l14, b4}, maps.Keys(ls.Locations))
assert.Empty(t, maps.Keys(ls.Locations[l13]))
assert.Empty(t, maps.Keys(ls.Locations[newLoc]))
},
expectNewLoc: "foo/lr_3",
},
{
name: "with items",
from: l14,
to: "foo",
expect: func(t *testing.T, ls *locSet) {
newLoc := "foo/lr_4"
assert.ElementsMatch(t, []string{l1, l13, newLoc, b4}, maps.Keys(ls.Locations))
assert.Empty(t, maps.Keys(ls.Locations[l14]))
assert.ElementsMatch(t, []string{i3, i4}, maps.Keys(ls.Locations[newLoc]))
},
expectNewLoc: "foo/lr_4",
},
{
name: "cascading locations",
from: l1,
to: "foo",
expect: func(t *testing.T, ls *locSet) {
pfx := "foo/"
assert.ElementsMatch(t, []string{pfx + l1, pfx + l13, pfx + l14, b4}, maps.Keys(ls.Locations))
assert.Empty(t, maps.Keys(ls.Locations[l1]))
assert.Empty(t, maps.Keys(ls.Locations[l14]))
assert.Empty(t, maps.Keys(ls.Locations[l13]))
assert.ElementsMatch(t, []string{i1, i2}, maps.Keys(ls.Locations[pfx+l1]))
assert.Empty(t, maps.Keys(ls.Locations[pfx+l13]))
assert.ElementsMatch(t, []string{i3, i4}, maps.Keys(ls.Locations[pfx+l14]))
},
expectNewLoc: "foo/" + l1,
},
{
name: "to existing location",
from: l14,
to: "bar",
expect: func(t *testing.T, ls *locSet) {
assert.ElementsMatch(t, []string{l1, l13, b4}, maps.Keys(ls.Locations))
assert.Empty(t, maps.Keys(ls.Locations[l14]))
assert.Empty(t, maps.Keys(ls.Locations["bar"]))
assert.ElementsMatch(t, []string{"fnord", i3, i4}, maps.Keys(ls.Locations[b4]))
},
expectNewLoc: b4,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ls := makeSet()
newLoc := ls.MoveLocation(test.from, test.to)
test.expect(t, ls)
assert.Equal(t, test.expectNewLoc, newLoc)
})
}
}

View File

@ -85,7 +85,7 @@ type Path interface {
Category() CategoryType
Tenant() string
ResourceOwner() string
Folder(bool) string
Folder(escaped bool) string
Folders() Elements
Item() string
// UpdateParent updates parent from old to new if the item/folder was