package kopia import ( "context" "fmt" "testing" "time" "github.com/alcionai/clues" "github.com/kopia/kopia/repo/manifest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/model" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup/identity" "github.com/alcionai/corso/src/pkg/path" ) type BackupCleanupUnitSuite struct { tester.Suite } func TestBackupCleanupUnitSuite(t *testing.T) { suite.Run(t, &BackupCleanupUnitSuite{Suite: tester.NewUnitSuite(t)}) } type mockManifestFinder struct { t *testing.T manifests []*manifest.EntryMetadata err error } func (mmf mockManifestFinder) FindManifests( ctx context.Context, tags map[string]string, ) ([]*manifest.EntryMetadata, error) { assert.Equal( mmf.t, map[string]string{"type": "snapshot"}, tags, "snapshot search tags") return mmf.manifests, clues.Stack(mmf.err).OrNil() } type mockStorer struct { t *testing.T details []*model.BaseModel detailsErr error backups []backupRes backupListErr error expectDeleteIDs []manifest.ID deleteErr error } func (ms mockStorer) Delete(context.Context, model.Schema, model.StableID) error { return clues.New("not implemented") } func (ms mockStorer) Get(context.Context, model.Schema, model.StableID, model.Model) error { return clues.New("not implemented") } func (ms mockStorer) Put(context.Context, model.Schema, model.Model) error { return clues.New("not implemented") } func (ms mockStorer) Update(context.Context, model.Schema, model.Model) error { return clues.New("not implemented") } func (ms mockStorer) GetIDsForType( _ context.Context, s model.Schema, tags map[string]string, ) ([]*model.BaseModel, error) { assert.Empty(ms.t, tags, "model search tags") switch s { case model.BackupDetailsSchema: return ms.details, clues.Stack(ms.detailsErr).OrNil() case model.BackupSchema: var bases []*model.BaseModel for _, b := range ms.backups { bases = append(bases, &b.bup.BaseModel) } return bases, clues.Stack(ms.backupListErr).OrNil() } return nil, clues.New(fmt.Sprintf("unknown type: %s", s.String())) } func (ms mockStorer) GetWithModelStoreID( _ context.Context, s model.Schema, id manifest.ID, m model.Model, ) error { assert.Equal(ms.t, model.BackupSchema, s, "model get schema") d := m.(*backup.Backup) for _, b := range ms.backups { if id == b.bup.ModelStoreID { *d = *b.bup return clues.Stack(b.err).OrNil() } } return clues.Stack(data.ErrNotFound) } func (ms mockStorer) DeleteWithModelStoreIDs( _ context.Context, ids ...manifest.ID, ) error { assert.ElementsMatch(ms.t, ms.expectDeleteIDs, ids, "model delete IDs") return clues.Stack(ms.deleteErr).OrNil() } // backupRes represents an individual return value for an item in GetIDsForType // or the result of GetWithModelStoreID. err is used for GetWithModelStoreID // only. type backupRes struct { bup *backup.Backup err error } func (suite *BackupCleanupUnitSuite) TestCleanupOrphanedData() { backupTag, _ := makeTagKV(TagBackupCategory) // Current backup and snapshots. bupCurrent := func() *backup.Backup { return &backup.Backup{ BaseModel: model.BaseModel{ ID: model.StableID("current-bup-id"), ModelStoreID: manifest.ID("current-bup-msid"), }, SnapshotID: "current-snap-msid", StreamStoreID: "current-deets-msid", } } snapCurrent := func() *manifest.EntryMetadata { return &manifest.EntryMetadata{ ID: "current-snap-msid", Labels: map[string]string{ backupTag: "0", }, } } deetsCurrent := func() *manifest.EntryMetadata { return &manifest.EntryMetadata{ ID: "current-deets-msid", } } bupCurrent2 := func() *backup.Backup { return &backup.Backup{ BaseModel: model.BaseModel{ ID: model.StableID("current-bup-id-2"), ModelStoreID: manifest.ID("current-bup-msid-2"), }, SnapshotID: "current-snap-msid-2", StreamStoreID: "current-deets-msid-2", } } snapCurrent2 := func() *manifest.EntryMetadata { return &manifest.EntryMetadata{ ID: "current-snap-msid-2", Labels: map[string]string{ backupTag: "0", }, } } deetsCurrent2 := func() *manifest.EntryMetadata { return &manifest.EntryMetadata{ ID: "current-deets-msid-2", } } bupCurrent3 := func() *backup.Backup { return &backup.Backup{ BaseModel: model.BaseModel{ ID: model.StableID("current-bup-id-3"), ModelStoreID: manifest.ID("current-bup-msid-3"), }, SnapshotID: "current-snap-msid-3", StreamStoreID: "current-deets-msid-3", } } snapCurrent3 := func() *manifest.EntryMetadata { return &manifest.EntryMetadata{ ID: "current-snap-msid-3", Labels: map[string]string{ backupTag: "0", }, } } deetsCurrent3 := func() *manifest.EntryMetadata { return &manifest.EntryMetadata{ ID: "current-deets-msid-3", } } // Legacy backup with details in separate model. bupLegacy := func() *backup.Backup { return &backup.Backup{ BaseModel: model.BaseModel{ ID: model.StableID("legacy-bup-id"), ModelStoreID: manifest.ID("legacy-bup-msid"), }, SnapshotID: "legacy-snap-msid", DetailsID: "legacy-deets-msid", } } snapLegacy := func() *manifest.EntryMetadata { return &manifest.EntryMetadata{ ID: "legacy-snap-msid", Labels: map[string]string{ backupTag: "0", }, } } deetsLegacy := func() *model.BaseModel { return &model.BaseModel{ ID: "legacy-deets-id", ModelStoreID: "legacy-deets-msid", } } // Incomplete backup missing data snapshot. bupNoSnapshot := func() *backup.Backup { return &backup.Backup{ BaseModel: model.BaseModel{ ID: model.StableID("ns-bup-id"), ModelStoreID: manifest.ID("ns-bup-id-msid"), }, StreamStoreID: "ns-deets-msid", } } deetsNoSnapshot := func() *manifest.EntryMetadata { return &manifest.EntryMetadata{ ID: "ns-deets-msid", } } // Legacy incomplete backup missing data snapshot. bupLegacyNoSnapshot := func() *backup.Backup { return &backup.Backup{ BaseModel: model.BaseModel{ ID: model.StableID("ns-legacy-bup-id"), ModelStoreID: manifest.ID("ns-legacy-bup-id-msid"), }, DetailsID: "ns-legacy-deets-msid", } } deetsLegacyNoSnapshot := func() *model.BaseModel { return &model.BaseModel{ ID: "ns-legacy-deets-id", ModelStoreID: "ns-legacy-deets-msid", } } // Incomplete backup missing details. bupNoDetails := func() *backup.Backup { return &backup.Backup{ BaseModel: model.BaseModel{ ID: model.StableID("nssid-bup-id"), ModelStoreID: manifest.ID("nssid-bup-msid"), }, SnapshotID: "nssid-snap-msid", } } snapNoDetails := func() *manifest.EntryMetadata { return &manifest.EntryMetadata{ ID: "nssid-snap-msid", Labels: map[string]string{ backupTag: "0", }, } } // Get some stable time so that we can do everything relative to this in the // tests. Mostly just makes reasoning/viewing times easier because the only // differences will be the changes we make. baseTime := time.Now() manifestWithTime := func( mt time.Time, m *manifest.EntryMetadata, ) *manifest.EntryMetadata { res := *m res.ModTime = mt return &res } manifestWithReasons := func( m *manifest.EntryMetadata, tenantID string, reasons ...identity.Reasoner, ) *manifest.EntryMetadata { res := *m if res.Labels == nil { res.Labels = map[string]string{} } res.Labels[kopiaPathLabel] = encodeAsPath(tenantID) // Add the given reasons. for _, r := range reasons { for _, k := range tagKeys(r) { key, _ := makeTagKV(k) res.Labels[key] = "0" } } // Also add other common reasons on item data snapshots. k, _ := makeTagKV(TagBackupCategory) res.Labels[k] = "0" return &res } backupWithTime := func(mt time.Time, b *backup.Backup) *backup.Backup { res := *b res.ModTime = mt res.CreationTime = mt return &res } backupWithResource := func(protectedResource string, isAssist bool, b *backup.Backup) *backup.Backup { res := *b res.ProtectedResourceID = protectedResource t := model.MergeBackup if isAssist { t = model.AssistBackup } if res.Tags == nil { res.Tags = map[string]string{} } res.Tags[model.BackupTypeTag] = t return &res } backupWithLegacyResource := func(protectedResource string, b *backup.Backup) *backup.Backup { res := *b res.ResourceOwnerID = protectedResource return &res } table := []struct { name string snapshots []*manifest.EntryMetadata snapshotFetchErr error // only need BaseModel here since we never look inside the details items. detailsModels []*model.BaseModel detailsModelListErr error backups []backupRes backupListErr error deleteErr error time time.Time buffer time.Duration expectDeleteIDs []manifest.ID expectErr assert.ErrorAssertionFunc }{ { name: "EmptyRepo", time: baseTime, expectErr: assert.NoError, }, { name: "OnlyCompleteBackups Noops", snapshots: []*manifest.EntryMetadata{ snapCurrent(), deetsCurrent(), snapLegacy(), }, detailsModels: []*model.BaseModel{ deetsLegacy(), }, backups: []backupRes{ {bup: bupCurrent()}, {bup: bupLegacy()}, }, time: baseTime, expectErr: assert.NoError, }, { name: "MissingFieldsInBackup CausesCleanup", snapshots: []*manifest.EntryMetadata{ snapNoDetails(), deetsNoSnapshot(), }, detailsModels: []*model.BaseModel{ deetsLegacyNoSnapshot(), }, backups: []backupRes{ {bup: bupNoSnapshot()}, {bup: bupLegacyNoSnapshot()}, {bup: bupNoDetails()}, }, expectDeleteIDs: []manifest.ID{ manifest.ID(bupNoSnapshot().ModelStoreID), manifest.ID(bupLegacyNoSnapshot().ModelStoreID), manifest.ID(bupNoDetails().ModelStoreID), manifest.ID(deetsLegacyNoSnapshot().ModelStoreID), snapNoDetails().ID, deetsNoSnapshot().ID, }, time: baseTime, expectErr: assert.NoError, }, { name: "MissingSnapshot CausesCleanup", snapshots: []*manifest.EntryMetadata{ deetsCurrent(), }, detailsModels: []*model.BaseModel{ deetsLegacy(), }, backups: []backupRes{ {bup: bupCurrent()}, {bup: bupLegacy()}, }, expectDeleteIDs: []manifest.ID{ manifest.ID(bupCurrent().ModelStoreID), deetsCurrent().ID, manifest.ID(bupLegacy().ModelStoreID), manifest.ID(deetsLegacy().ModelStoreID), }, time: baseTime, expectErr: assert.NoError, }, { name: "MissingDetails CausesCleanup", snapshots: []*manifest.EntryMetadata{ snapCurrent(), snapLegacy(), }, backups: []backupRes{ {bup: bupCurrent()}, {bup: bupLegacy()}, }, expectDeleteIDs: []manifest.ID{ manifest.ID(bupCurrent().ModelStoreID), manifest.ID(bupLegacy().ModelStoreID), snapCurrent().ID, snapLegacy().ID, }, time: baseTime, expectErr: assert.NoError, }, // Tests with various errors from Storer. { name: "SnapshotsListError Fails", snapshotFetchErr: assert.AnError, backups: []backupRes{ {bup: bupCurrent()}, }, expectErr: assert.Error, }, { name: "LegacyDetailsListError Fails", snapshots: []*manifest.EntryMetadata{ snapCurrent(), }, detailsModelListErr: assert.AnError, backups: []backupRes{ {bup: bupCurrent()}, }, time: baseTime, expectErr: assert.Error, }, { name: "BackupIDsListError Fails", snapshots: []*manifest.EntryMetadata{ snapCurrent(), deetsCurrent(), }, backupListErr: assert.AnError, time: baseTime, expectErr: assert.Error, }, { name: "BackupModelGetErrorNotFound CausesCleanup", snapshots: []*manifest.EntryMetadata{ snapCurrent(), deetsCurrent(), snapLegacy(), snapNoDetails(), }, detailsModels: []*model.BaseModel{ deetsLegacy(), }, backups: []backupRes{ {bup: bupCurrent()}, { bup: bupLegacy(), err: data.ErrNotFound, }, { bup: bupNoDetails(), err: data.ErrNotFound, }, }, // Backup IDs are still included in here because they're added to the // deletion set prior to attempting to fetch models. The model store // delete operation should ignore missing models though so there's no // issue. expectDeleteIDs: []manifest.ID{ snapLegacy().ID, manifest.ID(deetsLegacy().ModelStoreID), manifest.ID(bupLegacy().ModelStoreID), snapNoDetails().ID, manifest.ID(bupNoDetails().ModelStoreID), }, time: baseTime, expectErr: assert.NoError, }, { name: "BackupModelGetError Fails", snapshots: []*manifest.EntryMetadata{ snapCurrent(), deetsCurrent(), snapLegacy(), snapNoDetails(), }, detailsModels: []*model.BaseModel{ deetsLegacy(), }, backups: []backupRes{ {bup: bupCurrent()}, { bup: bupLegacy(), err: assert.AnError, }, {bup: bupNoDetails()}, }, time: baseTime, expectErr: assert.Error, }, { name: "DeleteError Fails", snapshots: []*manifest.EntryMetadata{ snapCurrent(), deetsCurrent(), snapLegacy(), snapNoDetails(), }, detailsModels: []*model.BaseModel{ deetsLegacy(), }, backups: []backupRes{ {bup: bupCurrent()}, {bup: bupLegacy()}, {bup: bupNoDetails()}, }, expectDeleteIDs: []manifest.ID{ snapNoDetails().ID, manifest.ID(bupNoDetails().ModelStoreID), }, deleteErr: assert.AnError, time: baseTime, expectErr: assert.Error, }, // Tests dealing with buffer times. { name: "MissingSnapshot BarelyTooYoungForCleanup Noops", snapshots: []*manifest.EntryMetadata{ manifestWithTime(baseTime, deetsCurrent()), }, backups: []backupRes{ {bup: backupWithTime(baseTime, bupCurrent())}, }, time: baseTime.Add(24 * time.Hour), buffer: 24 * time.Hour, expectErr: assert.NoError, }, { name: "MissingSnapshot BarelyOldEnough CausesCleanup", snapshots: []*manifest.EntryMetadata{ manifestWithTime(baseTime, deetsCurrent()), }, backups: []backupRes{ {bup: backupWithTime(baseTime, bupCurrent())}, }, expectDeleteIDs: []manifest.ID{ deetsCurrent().ID, manifest.ID(bupCurrent().ModelStoreID), }, time: baseTime.Add((24 * time.Hour) + time.Second), buffer: 24 * time.Hour, expectErr: assert.NoError, }, { name: "BackupGetErrorNotFound TooYoung Noops", snapshots: []*manifest.EntryMetadata{ manifestWithTime(baseTime, snapCurrent()), manifestWithTime(baseTime, deetsCurrent()), }, backups: []backupRes{ { bup: backupWithTime(baseTime, bupCurrent()), err: data.ErrNotFound, }, }, time: baseTime, buffer: 24 * time.Hour, expectErr: assert.NoError, }, // Tests dealing with assist base cleanup. { // Test that even if we have multiple assist bases with the same // Reason(s), none of them are garbage collected if they are within the // buffer period used to exclude recently created backups from garbage // collection. name: "AssistBase NotYoungest InBufferTime Noops", snapshots: []*manifest.EntryMetadata{ manifestWithReasons( manifestWithTime(baseTime, snapCurrent()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime, deetsCurrent()), manifestWithReasons( manifestWithTime(baseTime.Add(time.Second), snapCurrent2()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()), }, backups: []backupRes{ {bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))}, {bup: backupWithResource("ro", true, backupWithTime(baseTime.Add(time.Second), bupCurrent2()))}, }, time: baseTime, buffer: 24 * time.Hour, expectErr: assert.NoError, }, { // Test that an assist base that has the same Reasons as a newer assist // base is garbage collected when it's outside the buffer period. Also // check the most recent assist base isn't garbage collected if it's // younger than the most recent merge base. name: "AssistAndMergeBases MixedAges CausesCleanup", snapshots: []*manifest.EntryMetadata{ manifestWithReasons( manifestWithTime(baseTime, snapCurrent()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime, deetsCurrent()), manifestWithReasons( manifestWithTime(baseTime.Add(time.Second), snapCurrent2()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()), manifestWithReasons( manifestWithTime(baseTime.Add(time.Minute), snapCurrent3()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime.Add(time.Minute), deetsCurrent3()), }, backups: []backupRes{ {bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))}, {bup: backupWithResource("ro", false, backupWithTime(baseTime.Add(time.Second), bupCurrent2()))}, {bup: backupWithResource("ro", true, backupWithTime(baseTime.Add(time.Minute), bupCurrent3()))}, }, expectDeleteIDs: []manifest.ID{ snapCurrent().ID, deetsCurrent().ID, manifest.ID(bupCurrent().ModelStoreID), }, time: baseTime.Add(48 * time.Hour), buffer: 24 * time.Hour, expectErr: assert.NoError, }, { // Test that an assist base that has the same Reasons as a newer merge // base but the merge base is from an older version of corso for some // reason doesn't cause the assist base to be garbage collected. This is // not ideal, but some older versions of corso didn't even populate the // resource owner ID. // // Worst case, the assist base will be cleaned up when the user upgrades // corso and generates either a new assist base or merge base with the // same reason. name: "AssistAndLegacyMergeBases NotYoungest Noops", snapshots: []*manifest.EntryMetadata{ manifestWithReasons( manifestWithTime(baseTime, snapCurrent()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime, deetsCurrent()), manifestWithReasons( manifestWithTime(baseTime.Add(time.Second), snapCurrent2()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()), }, backups: []backupRes{ {bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))}, {bup: backupWithLegacyResource("ro", backupWithTime(baseTime.Add(time.Second), bupCurrent2()))}, }, time: baseTime.Add(48 * time.Hour), buffer: 24 * time.Hour, expectErr: assert.NoError, }, { // Test that an assist base that has the same Reasons as a newer merge // base but the merge base is from an older version of corso for some // reason and an even newer merge base from a current version of corso // causes the assist base to be garbage collected. // // This also tests that bases without a merge or assist tag are not // garbage collected as an assist base. name: "AssistAndLegacyAndCurrentMergeBases NotYoungest CausesCleanup", snapshots: []*manifest.EntryMetadata{ manifestWithReasons( manifestWithTime(baseTime, snapCurrent()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime, deetsCurrent()), manifestWithReasons( manifestWithTime(baseTime.Add(time.Second), snapCurrent2()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()), manifestWithReasons( manifestWithTime(baseTime.Add(time.Minute), snapCurrent3()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime.Add(time.Minute), deetsCurrent3()), }, backups: []backupRes{ {bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))}, {bup: backupWithLegacyResource("ro", backupWithTime(baseTime.Add(time.Second), bupCurrent2()))}, {bup: backupWithResource("ro", false, backupWithTime(baseTime.Add(time.Minute), bupCurrent3()))}, }, time: baseTime.Add(48 * time.Hour), buffer: 24 * time.Hour, expectDeleteIDs: []manifest.ID{ snapCurrent().ID, deetsCurrent().ID, manifest.ID(bupCurrent().ModelStoreID), }, expectErr: assert.NoError, }, { // Test that the most recent assist base is garbage collected if there's a // newer merge base that has the same Reasons as the assist base. name: "AssistBasesAndMergeBases NotYoungest CausesCleanupForAssistBase", snapshots: []*manifest.EntryMetadata{ manifestWithReasons( manifestWithTime(baseTime, snapCurrent()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime, deetsCurrent()), manifestWithReasons( manifestWithTime(baseTime.Add(time.Minute), snapCurrent2()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime.Add(time.Minute), deetsCurrent2()), manifestWithReasons( manifestWithTime(baseTime.Add(time.Second), snapCurrent3()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime.Add(time.Second), deetsCurrent3()), }, backups: []backupRes{ {bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))}, {bup: backupWithResource("ro", false, backupWithTime(baseTime.Add(time.Minute), bupCurrent2()))}, {bup: backupWithResource("ro", false, backupWithTime(baseTime.Add(-time.Second), bupCurrent3()))}, }, expectDeleteIDs: []manifest.ID{ snapCurrent().ID, deetsCurrent().ID, manifest.ID(bupCurrent().ModelStoreID), }, time: baseTime.Add(48 * time.Hour), buffer: 24 * time.Hour, expectErr: assert.NoError, }, { // Test that an assist base that is not the most recent for Reason A but // is the most recent for Reason B is not garbage collected. name: "AssistBases YoungestInOneReason Noops", snapshots: []*manifest.EntryMetadata{ manifestWithReasons( manifestWithTime(baseTime, snapCurrent()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory), identity.NewReason("", "ro", path.ExchangeService, path.ContactsCategory)), manifestWithTime(baseTime, deetsCurrent()), manifestWithReasons( manifestWithTime(baseTime.Add(time.Second), snapCurrent2()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()), }, backups: []backupRes{ {bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))}, {bup: backupWithResource("ro", true, backupWithTime(baseTime.Add(time.Second), bupCurrent2()))}, }, time: baseTime.Add(48 * time.Hour), buffer: 24 * time.Hour, expectErr: assert.NoError, }, { // Test that assist bases that have the same tenant, service, and category // but different protected resources are not garbage collected. This is // a test to ensure the Reason field is properly handled when finding the // most recent assist base. name: "AssistBases DifferentProtectedResources Noops", snapshots: []*manifest.EntryMetadata{ manifestWithReasons( manifestWithTime(baseTime, snapCurrent()), "tenant1", identity.NewReason("", "ro1", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime, deetsCurrent()), manifestWithReasons( manifestWithTime(baseTime.Add(time.Second), snapCurrent2()), "tenant1", identity.NewReason("", "ro2", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()), }, backups: []backupRes{ {bup: backupWithResource("ro1", true, backupWithTime(baseTime, bupCurrent()))}, {bup: backupWithResource("ro2", true, backupWithTime(baseTime.Add(time.Second), bupCurrent2()))}, }, time: baseTime.Add(48 * time.Hour), buffer: 24 * time.Hour, expectErr: assert.NoError, }, { // Test that assist bases that have the same protected resource, service, // and category but different tenants are not garbage collected. This is a // test to ensure the Reason field is properly handled when finding the // most recent assist base. name: "AssistBases DifferentTenants Noops", snapshots: []*manifest.EntryMetadata{ manifestWithReasons( manifestWithTime(baseTime, snapCurrent()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime, deetsCurrent()), manifestWithReasons( manifestWithTime(baseTime.Add(time.Second), snapCurrent2()), "tenant2", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()), }, backups: []backupRes{ {bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))}, {bup: backupWithResource("ro", true, backupWithTime(baseTime.Add(time.Second), bupCurrent2()))}, }, time: baseTime.Add(48 * time.Hour), buffer: 24 * time.Hour, expectErr: assert.NoError, }, { // Test that if the tenant is not available for a given assist base that // it's excluded from the garbage collection set. This behavior is // conservative because it's quite likely that we could garbage collect // the base without issue. name: "AssistBases NoTenant SkipsBackup", snapshots: []*manifest.EntryMetadata{ manifestWithReasons( manifestWithTime(baseTime, snapCurrent()), "", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime, deetsCurrent()), manifestWithReasons( manifestWithTime(baseTime.Add(time.Second), snapCurrent2()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()), manifestWithReasons( manifestWithTime(baseTime.Add(time.Minute), snapCurrent3()), "tenant1", identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)), manifestWithTime(baseTime.Add(time.Minute), deetsCurrent3()), }, backups: []backupRes{ {bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))}, {bup: backupWithResource("ro", true, backupWithTime(baseTime.Add(time.Second), bupCurrent2()))}, {bup: backupWithResource("ro", true, backupWithTime(baseTime.Add(time.Minute), bupCurrent3()))}, }, time: baseTime.Add(48 * time.Hour), buffer: 24 * time.Hour, expectDeleteIDs: []manifest.ID{ snapCurrent2().ID, deetsCurrent2().ID, manifest.ID(bupCurrent2().ModelStoreID), }, expectErr: assert.NoError, }, } for _, test := range table { suite.Run(test.name, func() { t := suite.T() ctx, flush := tester.NewContext(t) defer flush() mbs := mockStorer{ t: t, details: test.detailsModels, detailsErr: test.detailsModelListErr, backups: test.backups, backupListErr: test.backupListErr, expectDeleteIDs: test.expectDeleteIDs, deleteErr: test.deleteErr, } mmf := mockManifestFinder{ t: t, manifests: test.snapshots, err: test.snapshotFetchErr, } err := cleanupOrphanedData( ctx, mbs, mmf, test.buffer, func() time.Time { return test.time }) test.expectErr(t, err, clues.ToCore(err)) }) } }