enable groups integration tests (#4161)
enables nightly cli e2e tests, operations layer integration tests, and sanity tests for groups and teams. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🤖 Supportability/Tests #### Issue(s) * #3989 #### Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
59c541a7db
commit
d3eda28989
3
.github/workflows/sanity-test.yaml
vendored
3
.github/workflows/sanity-test.yaml
vendored
@ -330,7 +330,6 @@ jobs:
|
|||||||
|
|
||||||
# generate new entries for test
|
# generate new entries for test
|
||||||
- name: Groups - Create new data
|
- name: Groups - Create new data
|
||||||
if: false # TODO: enable when ready
|
|
||||||
id: new-data-creation-groups
|
id: new-data-creation-groups
|
||||||
working-directory: ./src/cmd/factory
|
working-directory: ./src/cmd/factory
|
||||||
run: |
|
run: |
|
||||||
@ -347,7 +346,6 @@ jobs:
|
|||||||
echo result="${suffix}" >> $GITHUB_OUTPUT
|
echo result="${suffix}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Groups - Backup
|
- name: Groups - Backup
|
||||||
if: false # TODO: enable when ready
|
|
||||||
id: groups-backup
|
id: groups-backup
|
||||||
uses: ./.github/actions/backup-restore-test
|
uses: ./.github/actions/backup-restore-test
|
||||||
with:
|
with:
|
||||||
@ -358,7 +356,6 @@ jobs:
|
|||||||
log-dir: ${{ env.CORSO_LOG_DIR }}
|
log-dir: ${{ env.CORSO_LOG_DIR }}
|
||||||
|
|
||||||
- name: Teams - Backup
|
- name: Teams - Backup
|
||||||
if: false # TODO: enable when ready
|
|
||||||
id: teams-backup
|
id: teams-backup
|
||||||
uses: ./.github/actions/backup-restore-test
|
uses: ./.github/actions/backup-restore-test
|
||||||
with:
|
with:
|
||||||
|
|||||||
615
src/cli/backup/groups_e2e_test.go
Normal file
615
src/cli/backup/groups_e2e_test.go
Normal file
@ -0,0 +1,615 @@
|
|||||||
|
package backup_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/cli"
|
||||||
|
"github.com/alcionai/corso/src/cli/config"
|
||||||
|
"github.com/alcionai/corso/src/cli/flags"
|
||||||
|
"github.com/alcionai/corso/src/cli/print"
|
||||||
|
cliTD "github.com/alcionai/corso/src/cli/testdata"
|
||||||
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
|
"github.com/alcionai/corso/src/internal/operations"
|
||||||
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
|
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
|
selTD "github.com/alcionai/corso/src/pkg/selectors/testdata"
|
||||||
|
storeTD "github.com/alcionai/corso/src/pkg/storage/testdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
channelMessages = path.ChannelMessagesCategory
|
||||||
|
libraries = path.LibrariesCategory
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tests that require no existing backups
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type NoBackupGroupsE2ESuite struct {
|
||||||
|
tester.Suite
|
||||||
|
dpnd dependencies
|
||||||
|
its intgTesterSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoBackupGroupsE2ESuite(t *testing.T) {
|
||||||
|
suite.Run(t, &BackupGroupsE2ESuite{Suite: tester.NewE2ESuite(
|
||||||
|
t,
|
||||||
|
[][]string{storeTD.AWSStorageCredEnvs, tconfig.M365AcctCredEnvs},
|
||||||
|
)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NoBackupGroupsE2ESuite) SetupSuite() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.its = newIntegrationTesterSetup(t)
|
||||||
|
suite.dpnd = prepM365Test(t, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NoBackupGroupsE2ESuite) TestGroupsBackupListCmd_noBackups() {
|
||||||
|
t := suite.T()
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "list", "groups",
|
||||||
|
"--config-file", suite.dpnd.configFilePath)
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
cmd.SetErr(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
result := suite.dpnd.recorder.String()
|
||||||
|
|
||||||
|
// as an offhand check: the result should contain the m365 group id
|
||||||
|
assert.True(t, strings.HasSuffix(result, "No backups available\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tests with no prior backup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type BackupGroupsE2ESuite struct {
|
||||||
|
tester.Suite
|
||||||
|
dpnd dependencies
|
||||||
|
its intgTesterSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackupGroupsE2ESuite(t *testing.T) {
|
||||||
|
suite.Run(t, &BackupGroupsE2ESuite{Suite: tester.NewE2ESuite(
|
||||||
|
t,
|
||||||
|
[][]string{storeTD.AWSStorageCredEnvs, tconfig.M365AcctCredEnvs},
|
||||||
|
)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupGroupsE2ESuite) SetupSuite() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.its = newIntegrationTesterSetup(t)
|
||||||
|
suite.dpnd = prepM365Test(t, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupGroupsE2ESuite) TestGroupsBackupCmd_channelMessages() {
|
||||||
|
runGroupsBackupCategoryTest(suite, channelMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupGroupsE2ESuite) TestGroupsBackupCmd_libraries() {
|
||||||
|
runGroupsBackupCategoryTest(suite, libraries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGroupsBackupCategoryTest(suite *BackupGroupsE2ESuite, category path.CategoryType) {
|
||||||
|
recorder := strings.Builder{}
|
||||||
|
recorder.Reset()
|
||||||
|
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cmd, ctx := buildGroupsBackupCmd(
|
||||||
|
ctx,
|
||||||
|
suite.dpnd.configFilePath,
|
||||||
|
suite.its.group.ID,
|
||||||
|
category.String(),
|
||||||
|
&recorder)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
result := recorder.String()
|
||||||
|
t.Log("backup results", result)
|
||||||
|
|
||||||
|
// as an offhand check: the result should contain the m365 group id
|
||||||
|
assert.Contains(t, result, suite.its.group.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupGroupsE2ESuite) TestGroupsBackupCmd_groupNotFound_channelMessages() {
|
||||||
|
runGroupsBackupGroupNotFoundTest(suite, channelMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupGroupsE2ESuite) TestGroupsBackupCmd_groupNotFound_libraries() {
|
||||||
|
runGroupsBackupGroupNotFoundTest(suite, libraries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGroupsBackupGroupNotFoundTest(suite *BackupGroupsE2ESuite, category path.CategoryType) {
|
||||||
|
recorder := strings.Builder{}
|
||||||
|
recorder.Reset()
|
||||||
|
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cmd, ctx := buildGroupsBackupCmd(
|
||||||
|
ctx,
|
||||||
|
suite.dpnd.configFilePath,
|
||||||
|
"foo@not-there.com",
|
||||||
|
category.String(),
|
||||||
|
&recorder)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.Error(t, err, clues.ToCore(err))
|
||||||
|
assert.Contains(
|
||||||
|
t,
|
||||||
|
err.Error(),
|
||||||
|
"not found in tenant", "error missing group not found")
|
||||||
|
assert.NotContains(t, err.Error(), "runtime error", "panic happened")
|
||||||
|
|
||||||
|
t.Logf("backup error message: %s", err.Error())
|
||||||
|
|
||||||
|
result := recorder.String()
|
||||||
|
t.Log("backup results", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupGroupsE2ESuite) TestBackupCreateGroups_badAzureClientIDFlag() {
|
||||||
|
t := suite.T()
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "create", "groups",
|
||||||
|
"--group", suite.its.group.ID,
|
||||||
|
"--azure-client-id", "invalid-value")
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
cmd.SetErr(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.Error(t, err, clues.ToCore(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupGroupsE2ESuite) TestBackupCreateGroups_fromConfigFile() {
|
||||||
|
t := suite.T()
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "create", "groups",
|
||||||
|
"--group", suite.its.group.ID,
|
||||||
|
"--config-file", suite.dpnd.configFilePath)
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
cmd.SetOut(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
result := suite.dpnd.recorder.String()
|
||||||
|
t.Log("backup results", result)
|
||||||
|
|
||||||
|
// as an offhand check: the result should contain the m365 group id
|
||||||
|
assert.Contains(t, result, suite.its.group.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AWS flags
|
||||||
|
func (suite *BackupGroupsE2ESuite) TestBackupCreateGroups_badAWSFlags() {
|
||||||
|
t := suite.T()
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "create", "groups",
|
||||||
|
"--group", suite.its.group.ID,
|
||||||
|
"--aws-access-key", "invalid-value",
|
||||||
|
"--aws-secret-access-key", "some-invalid-value",
|
||||||
|
)
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
cmd.SetOut(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
// since invalid aws creds are explicitly set, should see a failure
|
||||||
|
require.Error(t, err, clues.ToCore(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tests prepared with a previous backup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type PreparedBackupGroupsE2ESuite struct {
|
||||||
|
tester.Suite
|
||||||
|
dpnd dependencies
|
||||||
|
backupOps map[path.CategoryType]string
|
||||||
|
its intgTesterSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreparedBackupGroupsE2ESuite(t *testing.T) {
|
||||||
|
suite.Run(t, &PreparedBackupGroupsE2ESuite{
|
||||||
|
Suite: tester.NewE2ESuite(
|
||||||
|
t,
|
||||||
|
[][]string{storeTD.AWSStorageCredEnvs, tconfig.M365AcctCredEnvs}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupGroupsE2ESuite) SetupSuite() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.its = newIntegrationTesterSetup(t)
|
||||||
|
suite.dpnd = prepM365Test(t, ctx)
|
||||||
|
suite.backupOps = make(map[path.CategoryType]string)
|
||||||
|
|
||||||
|
var (
|
||||||
|
groups = []string{suite.its.group.ID}
|
||||||
|
ins = idname.NewCache(map[string]string{suite.its.group.ID: suite.its.group.ID})
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, set := range []path.CategoryType{channelMessages, libraries} {
|
||||||
|
var (
|
||||||
|
sel = selectors.NewGroupsBackup(groups)
|
||||||
|
scopes []selectors.GroupsScope
|
||||||
|
)
|
||||||
|
|
||||||
|
switch set {
|
||||||
|
case channelMessages:
|
||||||
|
scopes = selTD.GroupsBackupChannelScope(sel)
|
||||||
|
|
||||||
|
case libraries:
|
||||||
|
scopes = selTD.GroupsBackupLibraryFolderScope(sel)
|
||||||
|
}
|
||||||
|
|
||||||
|
sel.Include(scopes)
|
||||||
|
|
||||||
|
bop, err := suite.dpnd.repo.NewBackupWithLookup(ctx, sel.Selector, ins)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
err = bop.Run(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
bIDs := string(bop.Results.BackupID)
|
||||||
|
|
||||||
|
// sanity check, ensure we can find the backup and its details immediately
|
||||||
|
b, err := suite.dpnd.repo.Backup(ctx, string(bop.Results.BackupID))
|
||||||
|
require.NoError(t, err, "retrieving recent backup by ID")
|
||||||
|
require.Equal(t, bIDs, string(b.ID), "repo backup matches results id")
|
||||||
|
|
||||||
|
_, b, errs := suite.dpnd.repo.GetBackupDetails(ctx, bIDs)
|
||||||
|
require.NoError(t, errs.Failure(), "retrieving recent backup details by ID")
|
||||||
|
require.Empty(t, errs.Recovered(), "retrieving recent backup details by ID")
|
||||||
|
require.Equal(t, bIDs, string(b.ID), "repo details matches results id")
|
||||||
|
|
||||||
|
suite.backupOps[set] = string(b.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupGroupsE2ESuite) TestGroupsListCmd_channelMessages() {
|
||||||
|
runGroupsListCmdTest(suite, channelMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupGroupsE2ESuite) TestGroupsListCmd_libraries() {
|
||||||
|
runGroupsListCmdTest(suite, libraries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGroupsListCmdTest(suite *PreparedBackupGroupsE2ESuite, category path.CategoryType) {
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "list", "groups",
|
||||||
|
"--config-file", suite.dpnd.configFilePath)
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
cmd.SetOut(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
// compare the output
|
||||||
|
result := suite.dpnd.recorder.String()
|
||||||
|
assert.Contains(t, result, suite.backupOps[category])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupGroupsE2ESuite) TestGroupsListCmd_singleID_channelMessages() {
|
||||||
|
runGroupsListSingleCmdTest(suite, channelMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupGroupsE2ESuite) TestGroupsListCmd_singleID_libraries() {
|
||||||
|
runGroupsListSingleCmdTest(suite, libraries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGroupsListSingleCmdTest(suite *PreparedBackupGroupsE2ESuite, category path.CategoryType) {
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
bID := suite.backupOps[category]
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "list", "groups",
|
||||||
|
"--config-file", suite.dpnd.configFilePath,
|
||||||
|
"--backup", string(bID))
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
cmd.SetOut(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
// compare the output
|
||||||
|
result := suite.dpnd.recorder.String()
|
||||||
|
assert.Contains(t, result, bID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupGroupsE2ESuite) TestGroupsListCmd_badID() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "list", "groups",
|
||||||
|
"--config-file", suite.dpnd.configFilePath,
|
||||||
|
"--backup", "smarfs")
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.Error(t, err, clues.ToCore(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupGroupsE2ESuite) TestGroupsDetailsCmd_channelMessages() {
|
||||||
|
runGroupsDetailsCmdTest(suite, channelMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupGroupsE2ESuite) TestGroupsDetailsCmd_libraries() {
|
||||||
|
runGroupsDetailsCmdTest(suite, libraries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGroupsDetailsCmdTest(suite *PreparedBackupGroupsE2ESuite, category path.CategoryType) {
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
bID := suite.backupOps[category]
|
||||||
|
|
||||||
|
// fetch the details from the repo first
|
||||||
|
deets, _, errs := suite.dpnd.repo.GetBackupDetails(ctx, string(bID))
|
||||||
|
require.NoError(t, errs.Failure(), clues.ToCore(errs.Failure()))
|
||||||
|
require.Empty(t, errs.Recovered())
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "details", "groups",
|
||||||
|
"--config-file", suite.dpnd.configFilePath,
|
||||||
|
"--"+flags.BackupFN, string(bID))
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
cmd.SetOut(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
// compare the output
|
||||||
|
result := suite.dpnd.recorder.String()
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
foundFolders := 0
|
||||||
|
|
||||||
|
for _, ent := range deets.Entries {
|
||||||
|
// Skip folders as they don't mean anything to the end group.
|
||||||
|
if ent.Folder != nil {
|
||||||
|
foundFolders++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Run(fmt.Sprintf("detail %d", i), func() {
|
||||||
|
assert.Contains(suite.T(), result, ent.ShortRef)
|
||||||
|
})
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only backup the default folder for each category so there should be at
|
||||||
|
// least that folder (we don't make details entries for prefix folders).
|
||||||
|
assert.GreaterOrEqual(t, foundFolders, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tests for deleting backups
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type BackupDeleteGroupsE2ESuite struct {
|
||||||
|
tester.Suite
|
||||||
|
dpnd dependencies
|
||||||
|
backupOp operations.BackupOperation
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackupDeleteGroupsE2ESuite(t *testing.T) {
|
||||||
|
suite.Run(t, &BackupDeleteGroupsE2ESuite{
|
||||||
|
Suite: tester.NewE2ESuite(
|
||||||
|
t,
|
||||||
|
[][]string{storeTD.AWSStorageCredEnvs, tconfig.M365AcctCredEnvs},
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupDeleteGroupsE2ESuite) SetupSuite() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.dpnd = prepM365Test(t, ctx)
|
||||||
|
|
||||||
|
m365GroupID := tconfig.M365GroupID(t)
|
||||||
|
groups := []string{m365GroupID}
|
||||||
|
|
||||||
|
// some tests require an existing backup
|
||||||
|
sel := selectors.NewGroupsBackup(groups)
|
||||||
|
sel.Include(selTD.GroupsBackupChannelScope(sel))
|
||||||
|
|
||||||
|
backupOp, err := suite.dpnd.repo.NewBackup(ctx, sel.Selector)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
suite.backupOp = backupOp
|
||||||
|
|
||||||
|
err = suite.backupOp.Run(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupDeleteGroupsE2ESuite) TestGroupsBackupDeleteCmd() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "delete", "groups",
|
||||||
|
"--config-file", suite.dpnd.configFilePath,
|
||||||
|
"--"+flags.BackupFN, string(suite.backupOp.Results.BackupID))
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
// a follow-up details call should fail, due to the backup ID being deleted
|
||||||
|
cmd = cliTD.StubRootCmd(
|
||||||
|
"backup", "details", "groups",
|
||||||
|
"--config-file", suite.dpnd.configFilePath,
|
||||||
|
"--backup", string(suite.backupOp.Results.BackupID))
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
err = cmd.ExecuteContext(ctx)
|
||||||
|
require.Error(t, err, clues.ToCore(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupDeleteGroupsE2ESuite) TestGroupsBackupDeleteCmd_UnknownID() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "delete", "groups",
|
||||||
|
"--config-file", suite.dpnd.configFilePath,
|
||||||
|
"--"+flags.BackupFN, uuid.NewString())
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
// unknown backupIDs should error since the modelStore can't find the backup
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.Error(t, err, clues.ToCore(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func buildGroupsBackupCmd(
|
||||||
|
ctx context.Context,
|
||||||
|
configFile, group, category string,
|
||||||
|
recorder *strings.Builder,
|
||||||
|
) (*cobra.Command, context.Context) {
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "create", "groups",
|
||||||
|
"--config-file", configFile,
|
||||||
|
"--"+flags.GroupFN, group,
|
||||||
|
"--"+flags.CategoryDataFN, category)
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
cmd.SetOut(recorder)
|
||||||
|
|
||||||
|
return cmd, print.SetRootCmd(ctx, cmd)
|
||||||
|
}
|
||||||
610
src/cli/backup/teams_e2e_test.go
Normal file
610
src/cli/backup/teams_e2e_test.go
Normal file
@ -0,0 +1,610 @@
|
|||||||
|
package backup_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/cli"
|
||||||
|
"github.com/alcionai/corso/src/cli/config"
|
||||||
|
"github.com/alcionai/corso/src/cli/flags"
|
||||||
|
"github.com/alcionai/corso/src/cli/print"
|
||||||
|
cliTD "github.com/alcionai/corso/src/cli/testdata"
|
||||||
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
|
"github.com/alcionai/corso/src/internal/operations"
|
||||||
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
|
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
|
selTD "github.com/alcionai/corso/src/pkg/selectors/testdata"
|
||||||
|
storeTD "github.com/alcionai/corso/src/pkg/storage/testdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tests that require no existing backups
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type NoBackupTeamsE2ESuite struct {
|
||||||
|
tester.Suite
|
||||||
|
dpnd dependencies
|
||||||
|
its intgTesterSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoBackupTeamsE2ESuite(t *testing.T) {
|
||||||
|
suite.Run(t, &BackupTeamsE2ESuite{Suite: tester.NewE2ESuite(
|
||||||
|
t,
|
||||||
|
[][]string{storeTD.AWSStorageCredEnvs, tconfig.M365AcctCredEnvs},
|
||||||
|
)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NoBackupTeamsE2ESuite) SetupSuite() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.its = newIntegrationTesterSetup(t)
|
||||||
|
suite.dpnd = prepM365Test(t, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NoBackupTeamsE2ESuite) TestTeamsBackupListCmd_noBackups() {
|
||||||
|
t := suite.T()
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "list", "teams",
|
||||||
|
"--config-file", suite.dpnd.configFilePath)
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
cmd.SetErr(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
result := suite.dpnd.recorder.String()
|
||||||
|
|
||||||
|
// as an offhand check: the result should contain the m365 team id
|
||||||
|
assert.True(t, strings.HasSuffix(result, "No backups available\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tests with no prior backup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type BackupTeamsE2ESuite struct {
|
||||||
|
tester.Suite
|
||||||
|
dpnd dependencies
|
||||||
|
its intgTesterSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackupTeamsE2ESuite(t *testing.T) {
|
||||||
|
suite.Run(t, &BackupTeamsE2ESuite{Suite: tester.NewE2ESuite(
|
||||||
|
t,
|
||||||
|
[][]string{storeTD.AWSStorageCredEnvs, tconfig.M365AcctCredEnvs},
|
||||||
|
)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupTeamsE2ESuite) SetupSuite() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.its = newIntegrationTesterSetup(t)
|
||||||
|
suite.dpnd = prepM365Test(t, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupTeamsE2ESuite) TestTeamsBackupCmd_channelMessages() {
|
||||||
|
runTeamsBackupCategoryTest(suite, channelMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupTeamsE2ESuite) TestTeamsBackupCmd_libraries() {
|
||||||
|
runTeamsBackupCategoryTest(suite, libraries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTeamsBackupCategoryTest(suite *BackupTeamsE2ESuite, category path.CategoryType) {
|
||||||
|
recorder := strings.Builder{}
|
||||||
|
recorder.Reset()
|
||||||
|
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cmd, ctx := buildTeamsBackupCmd(
|
||||||
|
ctx,
|
||||||
|
suite.dpnd.configFilePath,
|
||||||
|
suite.its.team.ID,
|
||||||
|
category.String(),
|
||||||
|
&recorder)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
result := recorder.String()
|
||||||
|
t.Log("backup results", result)
|
||||||
|
|
||||||
|
// as an offhand check: the result should contain the m365 team id
|
||||||
|
assert.Contains(t, result, suite.its.team.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupTeamsE2ESuite) TestTeamsBackupCmd_teamNotFound_channelMessages() {
|
||||||
|
runTeamsBackupTeamNotFoundTest(suite, channelMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupTeamsE2ESuite) TestTeamsBackupCmd_teamNotFound_libraries() {
|
||||||
|
runTeamsBackupTeamNotFoundTest(suite, libraries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTeamsBackupTeamNotFoundTest(suite *BackupTeamsE2ESuite, category path.CategoryType) {
|
||||||
|
recorder := strings.Builder{}
|
||||||
|
recorder.Reset()
|
||||||
|
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cmd, ctx := buildTeamsBackupCmd(
|
||||||
|
ctx,
|
||||||
|
suite.dpnd.configFilePath,
|
||||||
|
"foo@not-there.com",
|
||||||
|
category.String(),
|
||||||
|
&recorder)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.Error(t, err, clues.ToCore(err))
|
||||||
|
assert.Contains(
|
||||||
|
t,
|
||||||
|
err.Error(),
|
||||||
|
"not found in tenant", "error missing team not found")
|
||||||
|
assert.NotContains(t, err.Error(), "runtime error", "panic happened")
|
||||||
|
|
||||||
|
t.Logf("backup error message: %s", err.Error())
|
||||||
|
|
||||||
|
result := recorder.String()
|
||||||
|
t.Log("backup results", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupTeamsE2ESuite) TestBackupCreateTeams_badAzureClientIDFlag() {
|
||||||
|
t := suite.T()
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "create", "teams",
|
||||||
|
"--team", suite.its.team.ID,
|
||||||
|
"--azure-client-id", "invalid-value")
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
cmd.SetErr(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.Error(t, err, clues.ToCore(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupTeamsE2ESuite) TestBackupCreateTeams_fromConfigFile() {
|
||||||
|
t := suite.T()
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "create", "teams",
|
||||||
|
"--team", suite.its.team.ID,
|
||||||
|
"--config-file", suite.dpnd.configFilePath)
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
cmd.SetOut(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
result := suite.dpnd.recorder.String()
|
||||||
|
t.Log("backup results", result)
|
||||||
|
|
||||||
|
// as an offhand check: the result should contain the m365 team id
|
||||||
|
assert.Contains(t, result, suite.its.team.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AWS flags
|
||||||
|
func (suite *BackupTeamsE2ESuite) TestBackupCreateTeams_badAWSFlags() {
|
||||||
|
t := suite.T()
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "create", "teams",
|
||||||
|
"--team", suite.its.team.ID,
|
||||||
|
"--aws-access-key", "invalid-value",
|
||||||
|
"--aws-secret-access-key", "some-invalid-value",
|
||||||
|
)
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
cmd.SetOut(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
// since invalid aws creds are explicitly set, should see a failure
|
||||||
|
require.Error(t, err, clues.ToCore(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tests prepared with a previous backup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type PreparedBackupTeamsE2ESuite struct {
|
||||||
|
tester.Suite
|
||||||
|
dpnd dependencies
|
||||||
|
backupOps map[path.CategoryType]string
|
||||||
|
its intgTesterSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreparedBackupTeamsE2ESuite(t *testing.T) {
|
||||||
|
suite.Run(t, &PreparedBackupTeamsE2ESuite{
|
||||||
|
Suite: tester.NewE2ESuite(
|
||||||
|
t,
|
||||||
|
[][]string{storeTD.AWSStorageCredEnvs, tconfig.M365AcctCredEnvs}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupTeamsE2ESuite) SetupSuite() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.its = newIntegrationTesterSetup(t)
|
||||||
|
suite.dpnd = prepM365Test(t, ctx)
|
||||||
|
suite.backupOps = make(map[path.CategoryType]string)
|
||||||
|
|
||||||
|
var (
|
||||||
|
teams = []string{suite.its.team.ID}
|
||||||
|
ins = idname.NewCache(map[string]string{suite.its.team.ID: suite.its.team.ID})
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, set := range []path.CategoryType{channelMessages, libraries} {
|
||||||
|
var (
|
||||||
|
sel = selectors.NewGroupsBackup(teams)
|
||||||
|
scopes []selectors.GroupsScope
|
||||||
|
)
|
||||||
|
|
||||||
|
switch set {
|
||||||
|
case channelMessages:
|
||||||
|
scopes = selTD.GroupsBackupChannelScope(sel)
|
||||||
|
|
||||||
|
case libraries:
|
||||||
|
scopes = selTD.GroupsBackupLibraryFolderScope(sel)
|
||||||
|
}
|
||||||
|
|
||||||
|
sel.Include(scopes)
|
||||||
|
|
||||||
|
bop, err := suite.dpnd.repo.NewBackupWithLookup(ctx, sel.Selector, ins)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
err = bop.Run(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
bIDs := string(bop.Results.BackupID)
|
||||||
|
|
||||||
|
// sanity check, ensure we can find the backup and its details immediately
|
||||||
|
b, err := suite.dpnd.repo.Backup(ctx, string(bop.Results.BackupID))
|
||||||
|
require.NoError(t, err, "retrieving recent backup by ID")
|
||||||
|
require.Equal(t, bIDs, string(b.ID), "repo backup matches results id")
|
||||||
|
|
||||||
|
_, b, errs := suite.dpnd.repo.GetBackupDetails(ctx, bIDs)
|
||||||
|
require.NoError(t, errs.Failure(), "retrieving recent backup details by ID")
|
||||||
|
require.Empty(t, errs.Recovered(), "retrieving recent backup details by ID")
|
||||||
|
require.Equal(t, bIDs, string(b.ID), "repo details matches results id")
|
||||||
|
|
||||||
|
suite.backupOps[set] = string(b.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupTeamsE2ESuite) TestTeamsListCmd_channelMessages() {
|
||||||
|
runTeamsListCmdTest(suite, channelMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupTeamsE2ESuite) TestTeamsListCmd_libraries() {
|
||||||
|
runTeamsListCmdTest(suite, libraries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTeamsListCmdTest(suite *PreparedBackupTeamsE2ESuite, category path.CategoryType) {
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "list", "teams",
|
||||||
|
"--config-file", suite.dpnd.configFilePath)
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
cmd.SetOut(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
// compare the output
|
||||||
|
result := suite.dpnd.recorder.String()
|
||||||
|
assert.Contains(t, result, suite.backupOps[category])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupTeamsE2ESuite) TestTeamsListCmd_singleID_channelMessages() {
|
||||||
|
runTeamsListSingleCmdTest(suite, channelMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupTeamsE2ESuite) TestTeamsListCmd_singleID_libraries() {
|
||||||
|
runTeamsListSingleCmdTest(suite, libraries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTeamsListSingleCmdTest(suite *PreparedBackupTeamsE2ESuite, category path.CategoryType) {
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
bID := suite.backupOps[category]
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "list", "teams",
|
||||||
|
"--config-file", suite.dpnd.configFilePath,
|
||||||
|
"--backup", string(bID))
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
cmd.SetOut(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
// compare the output
|
||||||
|
result := suite.dpnd.recorder.String()
|
||||||
|
assert.Contains(t, result, bID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupTeamsE2ESuite) TestTeamsListCmd_badID() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "list", "teams",
|
||||||
|
"--config-file", suite.dpnd.configFilePath,
|
||||||
|
"--backup", "smarfs")
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.Error(t, err, clues.ToCore(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupTeamsE2ESuite) TestTeamsDetailsCmd_channelMessages() {
|
||||||
|
runTeamsDetailsCmdTest(suite, channelMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PreparedBackupTeamsE2ESuite) TestTeamsDetailsCmd_libraries() {
|
||||||
|
runTeamsDetailsCmdTest(suite, libraries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTeamsDetailsCmdTest(suite *PreparedBackupTeamsE2ESuite, category path.CategoryType) {
|
||||||
|
suite.dpnd.recorder.Reset()
|
||||||
|
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
bID := suite.backupOps[category]
|
||||||
|
|
||||||
|
// fetch the details from the repo first
|
||||||
|
deets, _, errs := suite.dpnd.repo.GetBackupDetails(ctx, string(bID))
|
||||||
|
require.NoError(t, errs.Failure(), clues.ToCore(errs.Failure()))
|
||||||
|
require.Empty(t, errs.Recovered())
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "details", "teams",
|
||||||
|
"--config-file", suite.dpnd.configFilePath,
|
||||||
|
"--"+flags.BackupFN, string(bID))
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
cmd.SetOut(&suite.dpnd.recorder)
|
||||||
|
|
||||||
|
ctx = print.SetRootCmd(ctx, cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
// compare the output
|
||||||
|
result := suite.dpnd.recorder.String()
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
foundFolders := 0
|
||||||
|
|
||||||
|
for _, ent := range deets.Entries {
|
||||||
|
// Skip folders as they don't mean anything to the end team.
|
||||||
|
if ent.Folder != nil {
|
||||||
|
foundFolders++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Run(fmt.Sprintf("detail %d", i), func() {
|
||||||
|
assert.Contains(suite.T(), result, ent.ShortRef)
|
||||||
|
})
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only backup the default folder for each category so there should be at
|
||||||
|
// least that folder (we don't make details entries for prefix folders).
|
||||||
|
assert.GreaterOrEqual(t, foundFolders, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tests for deleting backups
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type BackupDeleteTeamsE2ESuite struct {
|
||||||
|
tester.Suite
|
||||||
|
dpnd dependencies
|
||||||
|
backupOp operations.BackupOperation
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackupDeleteTeamsE2ESuite(t *testing.T) {
|
||||||
|
suite.Run(t, &BackupDeleteTeamsE2ESuite{
|
||||||
|
Suite: tester.NewE2ESuite(
|
||||||
|
t,
|
||||||
|
[][]string{storeTD.AWSStorageCredEnvs, tconfig.M365AcctCredEnvs},
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupDeleteTeamsE2ESuite) SetupSuite() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
suite.dpnd = prepM365Test(t, ctx)
|
||||||
|
|
||||||
|
m365TeamID := tconfig.M365TeamID(t)
|
||||||
|
teams := []string{m365TeamID}
|
||||||
|
|
||||||
|
// some tests require an existing backup
|
||||||
|
sel := selectors.NewGroupsBackup(teams)
|
||||||
|
sel.Include(selTD.GroupsBackupChannelScope(sel))
|
||||||
|
|
||||||
|
backupOp, err := suite.dpnd.repo.NewBackup(ctx, sel.Selector)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
suite.backupOp = backupOp
|
||||||
|
|
||||||
|
err = suite.backupOp.Run(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupDeleteTeamsE2ESuite) TestTeamsBackupDeleteCmd() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "delete", "teams",
|
||||||
|
"--config-file", suite.dpnd.configFilePath,
|
||||||
|
"--"+flags.BackupFN, string(suite.backupOp.Results.BackupID))
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
// a follow-up details call should fail, due to the backup ID being deleted
|
||||||
|
cmd = cliTD.StubRootCmd(
|
||||||
|
"backup", "details", "teams",
|
||||||
|
"--config-file", suite.dpnd.configFilePath,
|
||||||
|
"--backup", string(suite.backupOp.Results.BackupID))
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
err = cmd.ExecuteContext(ctx)
|
||||||
|
require.Error(t, err, clues.ToCore(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupDeleteTeamsE2ESuite) TestTeamsBackupDeleteCmd_UnknownID() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
ctx = config.SetViper(ctx, suite.dpnd.vpr)
|
||||||
|
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "delete", "teams",
|
||||||
|
"--config-file", suite.dpnd.configFilePath,
|
||||||
|
"--"+flags.BackupFN, uuid.NewString())
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
// unknown backupIDs should error since the modelStore can't find the backup
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
require.Error(t, err, clues.ToCore(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func buildTeamsBackupCmd(
|
||||||
|
ctx context.Context,
|
||||||
|
configFile, team, category string,
|
||||||
|
recorder *strings.Builder,
|
||||||
|
) (*cobra.Command, context.Context) {
|
||||||
|
cmd := cliTD.StubRootCmd(
|
||||||
|
"backup", "create", "teams",
|
||||||
|
"--config-file", configFile,
|
||||||
|
"--"+flags.TeamFN, team,
|
||||||
|
"--"+flags.CategoryDataFN, category)
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
cmd.SetOut(recorder)
|
||||||
|
|
||||||
|
return cmd, print.SetRootCmd(ctx, cmd)
|
||||||
|
}
|
||||||
@ -19,6 +19,7 @@ import (
|
|||||||
odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts"
|
odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts"
|
||||||
"github.com/alcionai/corso/src/internal/m365/support"
|
"github.com/alcionai/corso/src/internal/m365/support"
|
||||||
"github.com/alcionai/corso/src/internal/observe"
|
"github.com/alcionai/corso/src/internal/observe"
|
||||||
|
bupMD "github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
@ -120,10 +121,10 @@ func deserializeMetadata(
|
|||||||
)
|
)
|
||||||
|
|
||||||
switch item.ID() {
|
switch item.ID() {
|
||||||
case graph.PreviousPathFileName:
|
case bupMD.PreviousPathFileName:
|
||||||
err = deserializeMap(item.ToReader(), prevFolders)
|
err = deserializeMap(item.ToReader(), prevFolders)
|
||||||
|
|
||||||
case graph.DeltaURLsFileName:
|
case bupMD.DeltaURLsFileName:
|
||||||
err = deserializeMap(item.ToReader(), prevDeltas)
|
err = deserializeMap(item.ToReader(), prevDeltas)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -449,8 +450,8 @@ func (c *Collections) Get(
|
|||||||
md, err := graph.MakeMetadataCollection(
|
md, err := graph.MakeMetadataCollection(
|
||||||
pathPrefix,
|
pathPrefix,
|
||||||
[]graph.MetadataCollectionEntry{
|
[]graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(graph.PreviousPathFileName, folderPaths),
|
graph.NewMetadataEntry(bupMD.PreviousPathFileName, folderPaths),
|
||||||
graph.NewMetadataEntry(graph.DeltaURLsFileName, deltaURLs),
|
graph.NewMetadataEntry(bupMD.DeltaURLsFileName, deltaURLs),
|
||||||
},
|
},
|
||||||
c.statusUpdater)
|
c.statusUpdater)
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/m365/service/onedrive/mock"
|
"github.com/alcionai/corso/src/internal/m365/service/onedrive/mock"
|
||||||
"github.com/alcionai/corso/src/internal/m365/support"
|
"github.com/alcionai/corso/src/internal/m365/support"
|
||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
|
bupMD "github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
@ -814,10 +815,10 @@ func (suite *OneDriveCollectionsUnitSuite) TestDeserializeMetadata() {
|
|||||||
func() []graph.MetadataCollectionEntry {
|
func() []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.DeltaURLsFileName,
|
bupMD.DeltaURLsFileName,
|
||||||
map[string]string{driveID1: deltaURL1}),
|
map[string]string{driveID1: deltaURL1}),
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.PreviousPathFileName,
|
bupMD.PreviousPathFileName,
|
||||||
map[string]map[string]string{
|
map[string]map[string]string{
|
||||||
driveID1: {
|
driveID1: {
|
||||||
folderID1: path1,
|
folderID1: path1,
|
||||||
@ -843,7 +844,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestDeserializeMetadata() {
|
|||||||
func() []graph.MetadataCollectionEntry {
|
func() []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.DeltaURLsFileName,
|
bupMD.DeltaURLsFileName,
|
||||||
map[string]string{driveID1: deltaURL1}),
|
map[string]string{driveID1: deltaURL1}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -859,7 +860,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestDeserializeMetadata() {
|
|||||||
func() []graph.MetadataCollectionEntry {
|
func() []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.PreviousPathFileName,
|
bupMD.PreviousPathFileName,
|
||||||
map[string]map[string]string{
|
map[string]map[string]string{
|
||||||
driveID1: {
|
driveID1: {
|
||||||
folderID1: path1,
|
folderID1: path1,
|
||||||
@ -886,10 +887,10 @@ func (suite *OneDriveCollectionsUnitSuite) TestDeserializeMetadata() {
|
|||||||
func() []graph.MetadataCollectionEntry {
|
func() []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.DeltaURLsFileName,
|
bupMD.DeltaURLsFileName,
|
||||||
map[string]string{driveID1: deltaURL1}),
|
map[string]string{driveID1: deltaURL1}),
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.PreviousPathFileName,
|
bupMD.PreviousPathFileName,
|
||||||
map[string]map[string]string{
|
map[string]map[string]string{
|
||||||
driveID1: {},
|
driveID1: {},
|
||||||
}),
|
}),
|
||||||
@ -910,12 +911,12 @@ func (suite *OneDriveCollectionsUnitSuite) TestDeserializeMetadata() {
|
|||||||
func() []graph.MetadataCollectionEntry {
|
func() []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.DeltaURLsFileName,
|
bupMD.DeltaURLsFileName,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
driveID1: "",
|
driveID1: "",
|
||||||
}),
|
}),
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.PreviousPathFileName,
|
bupMD.PreviousPathFileName,
|
||||||
map[string]map[string]string{
|
map[string]map[string]string{
|
||||||
driveID1: {
|
driveID1: {
|
||||||
folderID1: path1,
|
folderID1: path1,
|
||||||
@ -939,10 +940,10 @@ func (suite *OneDriveCollectionsUnitSuite) TestDeserializeMetadata() {
|
|||||||
func() []graph.MetadataCollectionEntry {
|
func() []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.DeltaURLsFileName,
|
bupMD.DeltaURLsFileName,
|
||||||
map[string]string{driveID1: deltaURL1}),
|
map[string]string{driveID1: deltaURL1}),
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.PreviousPathFileName,
|
bupMD.PreviousPathFileName,
|
||||||
map[string]map[string]string{
|
map[string]map[string]string{
|
||||||
driveID1: {
|
driveID1: {
|
||||||
folderID1: path1,
|
folderID1: path1,
|
||||||
@ -953,10 +954,10 @@ func (suite *OneDriveCollectionsUnitSuite) TestDeserializeMetadata() {
|
|||||||
func() []graph.MetadataCollectionEntry {
|
func() []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.DeltaURLsFileName,
|
bupMD.DeltaURLsFileName,
|
||||||
map[string]string{driveID2: deltaURL2}),
|
map[string]string{driveID2: deltaURL2}),
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.PreviousPathFileName,
|
bupMD.PreviousPathFileName,
|
||||||
map[string]map[string]string{
|
map[string]map[string]string{
|
||||||
driveID2: {
|
driveID2: {
|
||||||
folderID2: path2,
|
folderID2: path2,
|
||||||
@ -988,7 +989,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestDeserializeMetadata() {
|
|||||||
func() []graph.MetadataCollectionEntry {
|
func() []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.PreviousPathFileName,
|
bupMD.PreviousPathFileName,
|
||||||
map[string]string{driveID1: deltaURL1}),
|
map[string]string{driveID1: deltaURL1}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1004,10 +1005,10 @@ func (suite *OneDriveCollectionsUnitSuite) TestDeserializeMetadata() {
|
|||||||
func() []graph.MetadataCollectionEntry {
|
func() []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.DeltaURLsFileName,
|
bupMD.DeltaURLsFileName,
|
||||||
map[string]string{driveID1: deltaURL1}),
|
map[string]string{driveID1: deltaURL1}),
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.PreviousPathFileName,
|
bupMD.PreviousPathFileName,
|
||||||
map[string]map[string]string{
|
map[string]map[string]string{
|
||||||
driveID1: {
|
driveID1: {
|
||||||
folderID1: path1,
|
folderID1: path1,
|
||||||
@ -1036,10 +1037,10 @@ func (suite *OneDriveCollectionsUnitSuite) TestDeserializeMetadata() {
|
|||||||
func() []graph.MetadataCollectionEntry {
|
func() []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.DeltaURLsFileName,
|
bupMD.DeltaURLsFileName,
|
||||||
map[string]string{driveID1: deltaURL1}),
|
map[string]string{driveID1: deltaURL1}),
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.PreviousPathFileName,
|
bupMD.PreviousPathFileName,
|
||||||
map[string]map[string]string{
|
map[string]map[string]string{
|
||||||
driveID1: {
|
driveID1: {
|
||||||
folderID1: path1,
|
folderID1: path1,
|
||||||
@ -1050,7 +1051,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestDeserializeMetadata() {
|
|||||||
func() []graph.MetadataCollectionEntry {
|
func() []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.PreviousPathFileName,
|
bupMD.PreviousPathFileName,
|
||||||
map[string]map[string]string{
|
map[string]map[string]string{
|
||||||
driveID1: {
|
driveID1: {
|
||||||
folderID2: path2,
|
folderID2: path2,
|
||||||
@ -1070,10 +1071,10 @@ func (suite *OneDriveCollectionsUnitSuite) TestDeserializeMetadata() {
|
|||||||
func() []graph.MetadataCollectionEntry {
|
func() []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.DeltaURLsFileName,
|
bupMD.DeltaURLsFileName,
|
||||||
map[string]string{driveID1: deltaURL1}),
|
map[string]string{driveID1: deltaURL1}),
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.PreviousPathFileName,
|
bupMD.PreviousPathFileName,
|
||||||
map[string]map[string]string{
|
map[string]map[string]string{
|
||||||
driveID1: {
|
driveID1: {
|
||||||
folderID1: path1,
|
folderID1: path1,
|
||||||
@ -1084,7 +1085,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestDeserializeMetadata() {
|
|||||||
func() []graph.MetadataCollectionEntry {
|
func() []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.DeltaURLsFileName,
|
bupMD.DeltaURLsFileName,
|
||||||
map[string]string{driveID1: deltaURL2}),
|
map[string]string{driveID1: deltaURL2}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2281,13 +2282,13 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
|
|||||||
pathPrefix,
|
pathPrefix,
|
||||||
[]graph.MetadataCollectionEntry{
|
[]graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.DeltaURLsFileName,
|
bupMD.DeltaURLsFileName,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
driveID1: prevDelta,
|
driveID1: prevDelta,
|
||||||
driveID2: prevDelta,
|
driveID2: prevDelta,
|
||||||
}),
|
}),
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.PreviousPathFileName,
|
bupMD.PreviousPathFileName,
|
||||||
test.prevFolderPaths),
|
test.prevFolderPaths),
|
||||||
},
|
},
|
||||||
func(*support.ControllerOperationStatus) {})
|
func(*support.ControllerOperationStatus) {})
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/m365/support"
|
"github.com/alcionai/corso/src/internal/m365/support"
|
||||||
"github.com/alcionai/corso/src/internal/observe"
|
"github.com/alcionai/corso/src/internal/observe"
|
||||||
"github.com/alcionai/corso/src/internal/operations/inject"
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
@ -29,7 +30,7 @@ func CreateCollections(
|
|||||||
handlers map[path.CategoryType]backupHandler,
|
handlers map[path.CategoryType]backupHandler,
|
||||||
tenantID string,
|
tenantID string,
|
||||||
scope selectors.ExchangeScope,
|
scope selectors.ExchangeScope,
|
||||||
dps DeltaPaths,
|
dps metadata.DeltaPaths,
|
||||||
su support.StatusUpdater,
|
su support.StatusUpdater,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]data.BackupCollection, error) {
|
) ([]data.BackupCollection, error) {
|
||||||
@ -98,7 +99,7 @@ func populateCollections(
|
|||||||
statusUpdater support.StatusUpdater,
|
statusUpdater support.StatusUpdater,
|
||||||
resolver graph.ContainerResolver,
|
resolver graph.ContainerResolver,
|
||||||
scope selectors.ExchangeScope,
|
scope selectors.ExchangeScope,
|
||||||
dps DeltaPaths,
|
dps metadata.DeltaPaths,
|
||||||
ctrlOpts control.Options,
|
ctrlOpts control.Options,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) (map[string]data.BackupCollection, error) {
|
) (map[string]data.BackupCollection, error) {
|
||||||
@ -280,8 +281,8 @@ func populateCollections(
|
|||||||
col, err := graph.MakeMetadataCollection(
|
col, err := graph.MakeMetadataCollection(
|
||||||
pathPrefix,
|
pathPrefix,
|
||||||
[]graph.MetadataCollectionEntry{
|
[]graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(graph.PreviousPathFileName, currPaths),
|
graph.NewMetadataEntry(metadata.PreviousPathFileName, currPaths),
|
||||||
graph.NewMetadataEntry(graph.DeltaURLsFileName, deltaURLs),
|
graph.NewMetadataEntry(metadata.DeltaURLsFileName, deltaURLs),
|
||||||
},
|
},
|
||||||
statusUpdater)
|
statusUpdater)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -296,7 +297,7 @@ func populateCollections(
|
|||||||
// produces a set of id:path pairs from the deltapaths map.
|
// produces a set of id:path pairs from the deltapaths map.
|
||||||
// Each entry in the set will, if not removed, produce a collection
|
// Each entry in the set will, if not removed, produce a collection
|
||||||
// that will delete the tombstone by path.
|
// that will delete the tombstone by path.
|
||||||
func makeTombstones(dps DeltaPaths) map[string]string {
|
func makeTombstones(dps metadata.DeltaPaths) map[string]string {
|
||||||
r := make(map[string]string, len(dps))
|
r := make(map[string]string, len(dps))
|
||||||
|
|
||||||
for id, v := range dps {
|
for id, v := range dps {
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
||||||
"github.com/alcionai/corso/src/internal/version"
|
"github.com/alcionai/corso/src/internal/version"
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
@ -150,24 +151,24 @@ func (suite *DataCollectionsUnitSuite) TestParseMetadataCollections() {
|
|||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
data []fileValues
|
data []fileValues
|
||||||
expect map[string]DeltaPath
|
expect map[string]metadata.DeltaPath
|
||||||
canUsePreviousBackup bool
|
canUsePreviousBackup bool
|
||||||
expectError assert.ErrorAssertionFunc
|
expectError assert.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "delta urls only",
|
name: "delta urls only",
|
||||||
data: []fileValues{
|
data: []fileValues{
|
||||||
{graph.DeltaURLsFileName, "delta-link"},
|
{metadata.DeltaURLsFileName, "delta-link"},
|
||||||
},
|
},
|
||||||
expect: map[string]DeltaPath{},
|
expect: map[string]metadata.DeltaPath{},
|
||||||
canUsePreviousBackup: true,
|
canUsePreviousBackup: true,
|
||||||
expectError: assert.NoError,
|
expectError: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple delta urls",
|
name: "multiple delta urls",
|
||||||
data: []fileValues{
|
data: []fileValues{
|
||||||
{graph.DeltaURLsFileName, "delta-link"},
|
{metadata.DeltaURLsFileName, "delta-link"},
|
||||||
{graph.DeltaURLsFileName, "delta-link-2"},
|
{metadata.DeltaURLsFileName, "delta-link-2"},
|
||||||
},
|
},
|
||||||
canUsePreviousBackup: false,
|
canUsePreviousBackup: false,
|
||||||
expectError: assert.Error,
|
expectError: assert.Error,
|
||||||
@ -175,9 +176,9 @@ func (suite *DataCollectionsUnitSuite) TestParseMetadataCollections() {
|
|||||||
{
|
{
|
||||||
name: "previous path only",
|
name: "previous path only",
|
||||||
data: []fileValues{
|
data: []fileValues{
|
||||||
{graph.PreviousPathFileName, "prev-path"},
|
{metadata.PreviousPathFileName, "prev-path"},
|
||||||
},
|
},
|
||||||
expect: map[string]DeltaPath{
|
expect: map[string]metadata.DeltaPath{
|
||||||
"key": {
|
"key": {
|
||||||
Delta: "delta-link",
|
Delta: "delta-link",
|
||||||
Path: "prev-path",
|
Path: "prev-path",
|
||||||
@ -189,8 +190,8 @@ func (suite *DataCollectionsUnitSuite) TestParseMetadataCollections() {
|
|||||||
{
|
{
|
||||||
name: "multiple previous paths",
|
name: "multiple previous paths",
|
||||||
data: []fileValues{
|
data: []fileValues{
|
||||||
{graph.PreviousPathFileName, "prev-path"},
|
{metadata.PreviousPathFileName, "prev-path"},
|
||||||
{graph.PreviousPathFileName, "prev-path-2"},
|
{metadata.PreviousPathFileName, "prev-path-2"},
|
||||||
},
|
},
|
||||||
canUsePreviousBackup: false,
|
canUsePreviousBackup: false,
|
||||||
expectError: assert.Error,
|
expectError: assert.Error,
|
||||||
@ -198,10 +199,10 @@ func (suite *DataCollectionsUnitSuite) TestParseMetadataCollections() {
|
|||||||
{
|
{
|
||||||
name: "delta urls and previous paths",
|
name: "delta urls and previous paths",
|
||||||
data: []fileValues{
|
data: []fileValues{
|
||||||
{graph.DeltaURLsFileName, "delta-link"},
|
{metadata.DeltaURLsFileName, "delta-link"},
|
||||||
{graph.PreviousPathFileName, "prev-path"},
|
{metadata.PreviousPathFileName, "prev-path"},
|
||||||
},
|
},
|
||||||
expect: map[string]DeltaPath{
|
expect: map[string]metadata.DeltaPath{
|
||||||
"key": {
|
"key": {
|
||||||
Delta: "delta-link",
|
Delta: "delta-link",
|
||||||
Path: "prev-path",
|
Path: "prev-path",
|
||||||
@ -213,20 +214,20 @@ func (suite *DataCollectionsUnitSuite) TestParseMetadataCollections() {
|
|||||||
{
|
{
|
||||||
name: "delta urls and empty previous paths",
|
name: "delta urls and empty previous paths",
|
||||||
data: []fileValues{
|
data: []fileValues{
|
||||||
{graph.DeltaURLsFileName, "delta-link"},
|
{metadata.DeltaURLsFileName, "delta-link"},
|
||||||
{graph.PreviousPathFileName, ""},
|
{metadata.PreviousPathFileName, ""},
|
||||||
},
|
},
|
||||||
expect: map[string]DeltaPath{},
|
expect: map[string]metadata.DeltaPath{},
|
||||||
canUsePreviousBackup: true,
|
canUsePreviousBackup: true,
|
||||||
expectError: assert.NoError,
|
expectError: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty delta urls and previous paths",
|
name: "empty delta urls and previous paths",
|
||||||
data: []fileValues{
|
data: []fileValues{
|
||||||
{graph.DeltaURLsFileName, ""},
|
{metadata.DeltaURLsFileName, ""},
|
||||||
{graph.PreviousPathFileName, "prev-path"},
|
{metadata.PreviousPathFileName, "prev-path"},
|
||||||
},
|
},
|
||||||
expect: map[string]DeltaPath{
|
expect: map[string]metadata.DeltaPath{
|
||||||
"key": {
|
"key": {
|
||||||
Delta: "delta-link",
|
Delta: "delta-link",
|
||||||
Path: "prev-path",
|
Path: "prev-path",
|
||||||
@ -238,10 +239,10 @@ func (suite *DataCollectionsUnitSuite) TestParseMetadataCollections() {
|
|||||||
{
|
{
|
||||||
name: "delta urls with special chars",
|
name: "delta urls with special chars",
|
||||||
data: []fileValues{
|
data: []fileValues{
|
||||||
{graph.DeltaURLsFileName, "`!@#$%^&*()_[]{}/\"\\"},
|
{metadata.DeltaURLsFileName, "`!@#$%^&*()_[]{}/\"\\"},
|
||||||
{graph.PreviousPathFileName, "prev-path"},
|
{metadata.PreviousPathFileName, "prev-path"},
|
||||||
},
|
},
|
||||||
expect: map[string]DeltaPath{
|
expect: map[string]metadata.DeltaPath{
|
||||||
"key": {
|
"key": {
|
||||||
Delta: "`!@#$%^&*()_[]{}/\"\\",
|
Delta: "`!@#$%^&*()_[]{}/\"\\",
|
||||||
Path: "prev-path",
|
Path: "prev-path",
|
||||||
@ -253,10 +254,10 @@ func (suite *DataCollectionsUnitSuite) TestParseMetadataCollections() {
|
|||||||
{
|
{
|
||||||
name: "delta urls with escaped chars",
|
name: "delta urls with escaped chars",
|
||||||
data: []fileValues{
|
data: []fileValues{
|
||||||
{graph.DeltaURLsFileName, `\n\r\t\b\f\v\0\\`},
|
{metadata.DeltaURLsFileName, `\n\r\t\b\f\v\0\\`},
|
||||||
{graph.PreviousPathFileName, "prev-path"},
|
{metadata.PreviousPathFileName, "prev-path"},
|
||||||
},
|
},
|
||||||
expect: map[string]DeltaPath{
|
expect: map[string]metadata.DeltaPath{
|
||||||
"key": {
|
"key": {
|
||||||
Delta: "\\n\\r\\t\\b\\f\\v\\0\\\\",
|
Delta: "\\n\\r\\t\\b\\f\\v\\0\\\\",
|
||||||
Path: "prev-path",
|
Path: "prev-path",
|
||||||
@ -271,10 +272,10 @@ func (suite *DataCollectionsUnitSuite) TestParseMetadataCollections() {
|
|||||||
// rune(92) = \, rune(110) = n. Ensuring it's not possible to
|
// rune(92) = \, rune(110) = n. Ensuring it's not possible to
|
||||||
// error in serializing/deserializing and produce a single newline
|
// error in serializing/deserializing and produce a single newline
|
||||||
// character from those two runes.
|
// character from those two runes.
|
||||||
{graph.DeltaURLsFileName, string([]rune{rune(92), rune(110)})},
|
{metadata.DeltaURLsFileName, string([]rune{rune(92), rune(110)})},
|
||||||
{graph.PreviousPathFileName, "prev-path"},
|
{metadata.PreviousPathFileName, "prev-path"},
|
||||||
},
|
},
|
||||||
expect: map[string]DeltaPath{
|
expect: map[string]metadata.DeltaPath{
|
||||||
"key": {
|
"key": {
|
||||||
Delta: "\\n",
|
Delta: "\\n",
|
||||||
Path: "prev-path",
|
Path: "prev-path",
|
||||||
@ -485,7 +486,7 @@ func (suite *BackupIntgSuite) TestMailFetch() {
|
|||||||
handlers,
|
handlers,
|
||||||
suite.tenantID,
|
suite.tenantID,
|
||||||
test.scope,
|
test.scope,
|
||||||
DeltaPaths{},
|
metadata.DeltaPaths{},
|
||||||
func(status *support.ControllerOperationStatus) {},
|
func(status *support.ControllerOperationStatus) {},
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
@ -565,7 +566,7 @@ func (suite *BackupIntgSuite) TestDelta() {
|
|||||||
handlers,
|
handlers,
|
||||||
suite.tenantID,
|
suite.tenantID,
|
||||||
test.scope,
|
test.scope,
|
||||||
DeltaPaths{},
|
metadata.DeltaPaths{},
|
||||||
func(status *support.ControllerOperationStatus) {},
|
func(status *support.ControllerOperationStatus) {},
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
@ -649,7 +650,7 @@ func (suite *BackupIntgSuite) TestMailSerializationRegression() {
|
|||||||
handlers,
|
handlers,
|
||||||
suite.tenantID,
|
suite.tenantID,
|
||||||
sel.Scopes()[0],
|
sel.Scopes()[0],
|
||||||
DeltaPaths{},
|
metadata.DeltaPaths{},
|
||||||
newStatusUpdater(t, &wg),
|
newStatusUpdater(t, &wg),
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
@ -730,7 +731,7 @@ func (suite *BackupIntgSuite) TestContactSerializationRegression() {
|
|||||||
handlers,
|
handlers,
|
||||||
suite.tenantID,
|
suite.tenantID,
|
||||||
test.scope,
|
test.scope,
|
||||||
DeltaPaths{},
|
metadata.DeltaPaths{},
|
||||||
newStatusUpdater(t, &wg),
|
newStatusUpdater(t, &wg),
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
@ -858,7 +859,7 @@ func (suite *BackupIntgSuite) TestEventsSerializationRegression() {
|
|||||||
handlers,
|
handlers,
|
||||||
suite.tenantID,
|
suite.tenantID,
|
||||||
test.scope,
|
test.scope,
|
||||||
DeltaPaths{},
|
metadata.DeltaPaths{},
|
||||||
newStatusUpdater(t, &wg),
|
newStatusUpdater(t, &wg),
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
@ -923,7 +924,7 @@ func (suite *CollectionPopulationSuite) TestPopulateCollections() {
|
|||||||
}
|
}
|
||||||
statusUpdater = func(*support.ControllerOperationStatus) {}
|
statusUpdater = func(*support.ControllerOperationStatus) {}
|
||||||
allScope = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0]
|
allScope = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0]
|
||||||
dps = DeltaPaths{} // incrementals are tested separately
|
dps = metadata.DeltaPaths{} // incrementals are tested separately
|
||||||
commonResult = mockGetterResults{
|
commonResult = mockGetterResults{
|
||||||
added: []string{"a1", "a2", "a3"},
|
added: []string{"a1", "a2", "a3"},
|
||||||
removed: []string{"r1", "r2", "r3"},
|
removed: []string{"r1", "r2", "r3"},
|
||||||
@ -1192,7 +1193,7 @@ func checkMetadata(
|
|||||||
t *testing.T,
|
t *testing.T,
|
||||||
ctx context.Context, //revive:disable-line:context-as-argument
|
ctx context.Context, //revive:disable-line:context-as-argument
|
||||||
cat path.CategoryType,
|
cat path.CategoryType,
|
||||||
expect DeltaPaths,
|
expect metadata.DeltaPaths,
|
||||||
c data.BackupCollection,
|
c data.BackupCollection,
|
||||||
) {
|
) {
|
||||||
catPaths, _, err := ParseMetadataCollections(
|
catPaths, _, err := ParseMetadataCollections(
|
||||||
@ -1313,10 +1314,10 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_D
|
|||||||
name string
|
name string
|
||||||
getter mockGetter
|
getter mockGetter
|
||||||
resolver graph.ContainerResolver
|
resolver graph.ContainerResolver
|
||||||
inputMetadata func(t *testing.T, cat path.CategoryType) DeltaPaths
|
inputMetadata func(t *testing.T, cat path.CategoryType) metadata.DeltaPaths
|
||||||
expectNewColls int
|
expectNewColls int
|
||||||
expectDeleted int
|
expectDeleted int
|
||||||
expectMetadata func(t *testing.T, cat path.CategoryType) DeltaPaths
|
expectMetadata func(t *testing.T, cat path.CategoryType) metadata.DeltaPaths
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "1 moved to duplicate",
|
name: "1 moved to duplicate",
|
||||||
@ -1327,25 +1328,25 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_D
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolver: newMockResolver(container1, container2),
|
resolver: newMockResolver(container1, container2),
|
||||||
inputMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
|
inputMetadata: func(t *testing.T, cat path.CategoryType) metadata.DeltaPaths {
|
||||||
return DeltaPaths{
|
return metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "old_delta",
|
Delta: "old_delta",
|
||||||
Path: oldPath1(t, cat).String(),
|
Path: oldPath1(t, cat).String(),
|
||||||
},
|
},
|
||||||
"2": DeltaPath{
|
"2": metadata.DeltaPath{
|
||||||
Delta: "old_delta",
|
Delta: "old_delta",
|
||||||
Path: idPath2(t, cat).String(),
|
Path: idPath2(t, cat).String(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
expectMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
|
expectMetadata: func(t *testing.T, cat path.CategoryType) metadata.DeltaPaths {
|
||||||
return DeltaPaths{
|
return metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "delta_url",
|
Delta: "delta_url",
|
||||||
Path: idPath1(t, cat).String(),
|
Path: idPath1(t, cat).String(),
|
||||||
},
|
},
|
||||||
"2": DeltaPath{
|
"2": metadata.DeltaPath{
|
||||||
Delta: "delta_url2",
|
Delta: "delta_url2",
|
||||||
Path: idPath2(t, cat).String(),
|
Path: idPath2(t, cat).String(),
|
||||||
},
|
},
|
||||||
@ -1361,25 +1362,25 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_D
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolver: newMockResolver(container1, container2),
|
resolver: newMockResolver(container1, container2),
|
||||||
inputMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
|
inputMetadata: func(t *testing.T, cat path.CategoryType) metadata.DeltaPaths {
|
||||||
return DeltaPaths{
|
return metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "old_delta",
|
Delta: "old_delta",
|
||||||
Path: oldPath1(t, cat).String(),
|
Path: oldPath1(t, cat).String(),
|
||||||
},
|
},
|
||||||
"2": DeltaPath{
|
"2": metadata.DeltaPath{
|
||||||
Delta: "old_delta",
|
Delta: "old_delta",
|
||||||
Path: oldPath2(t, cat).String(),
|
Path: oldPath2(t, cat).String(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
expectMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
|
expectMetadata: func(t *testing.T, cat path.CategoryType) metadata.DeltaPaths {
|
||||||
return DeltaPaths{
|
return metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "delta_url",
|
Delta: "delta_url",
|
||||||
Path: idPath1(t, cat).String(),
|
Path: idPath1(t, cat).String(),
|
||||||
},
|
},
|
||||||
"2": DeltaPath{
|
"2": metadata.DeltaPath{
|
||||||
Delta: "delta_url2",
|
Delta: "delta_url2",
|
||||||
Path: idPath2(t, cat).String(),
|
Path: idPath2(t, cat).String(),
|
||||||
},
|
},
|
||||||
@ -1395,17 +1396,17 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_D
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolver: newMockResolver(container1, container2),
|
resolver: newMockResolver(container1, container2),
|
||||||
inputMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
|
inputMetadata: func(t *testing.T, cat path.CategoryType) metadata.DeltaPaths {
|
||||||
return DeltaPaths{}
|
return metadata.DeltaPaths{}
|
||||||
},
|
},
|
||||||
expectNewColls: 2,
|
expectNewColls: 2,
|
||||||
expectMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
|
expectMetadata: func(t *testing.T, cat path.CategoryType) metadata.DeltaPaths {
|
||||||
return DeltaPaths{
|
return metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "delta_url",
|
Delta: "delta_url",
|
||||||
Path: idPath1(t, cat).String(),
|
Path: idPath1(t, cat).String(),
|
||||||
},
|
},
|
||||||
"2": DeltaPath{
|
"2": metadata.DeltaPath{
|
||||||
Delta: "delta_url2",
|
Delta: "delta_url2",
|
||||||
Path: idPath2(t, cat).String(),
|
Path: idPath2(t, cat).String(),
|
||||||
},
|
},
|
||||||
@ -1420,9 +1421,9 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_D
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolver: newMockResolver(container1),
|
resolver: newMockResolver(container1),
|
||||||
inputMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
|
inputMetadata: func(t *testing.T, cat path.CategoryType) metadata.DeltaPaths {
|
||||||
return DeltaPaths{
|
return metadata.DeltaPaths{
|
||||||
"2": DeltaPath{
|
"2": metadata.DeltaPath{
|
||||||
Delta: "old_delta",
|
Delta: "old_delta",
|
||||||
Path: idPath2(t, cat).String(),
|
Path: idPath2(t, cat).String(),
|
||||||
},
|
},
|
||||||
@ -1430,9 +1431,9 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_D
|
|||||||
},
|
},
|
||||||
expectNewColls: 1,
|
expectNewColls: 1,
|
||||||
expectDeleted: 1,
|
expectDeleted: 1,
|
||||||
expectMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
|
expectMetadata: func(t *testing.T, cat path.CategoryType) metadata.DeltaPaths {
|
||||||
return DeltaPaths{
|
return metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "delta_url",
|
Delta: "delta_url",
|
||||||
Path: idPath1(t, cat).String(),
|
Path: idPath1(t, cat).String(),
|
||||||
},
|
},
|
||||||
@ -1604,7 +1605,7 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_r
|
|||||||
}
|
}
|
||||||
statusUpdater = func(*support.ControllerOperationStatus) {}
|
statusUpdater = func(*support.ControllerOperationStatus) {}
|
||||||
allScope = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0]
|
allScope = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0]
|
||||||
dps = DeltaPaths{} // incrementals are tested separately
|
dps = metadata.DeltaPaths{} // incrementals are tested separately
|
||||||
container1 = mockContainer{
|
container1 = mockContainer{
|
||||||
id: strPtr("1"),
|
id: strPtr("1"),
|
||||||
displayName: strPtr("display_name_1"),
|
displayName: strPtr("display_name_1"),
|
||||||
@ -1718,7 +1719,7 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_i
|
|||||||
name string
|
name string
|
||||||
getter mockGetter
|
getter mockGetter
|
||||||
resolver graph.ContainerResolver
|
resolver graph.ContainerResolver
|
||||||
dps DeltaPaths
|
dps metadata.DeltaPaths
|
||||||
expect map[string]endState
|
expect map[string]endState
|
||||||
skipWhenForcedNoDelta bool
|
skipWhenForcedNoDelta bool
|
||||||
}{
|
}{
|
||||||
@ -1735,7 +1736,7 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_i
|
|||||||
p: path.Builder{}.Append("1", "new"),
|
p: path.Builder{}.Append("1", "new"),
|
||||||
l: path.Builder{}.Append("1", "new"),
|
l: path.Builder{}.Append("1", "new"),
|
||||||
}),
|
}),
|
||||||
dps: DeltaPaths{},
|
dps: metadata.DeltaPaths{},
|
||||||
expect: map[string]endState{
|
expect: map[string]endState{
|
||||||
"1": {data.NewState, false},
|
"1": {data.NewState, false},
|
||||||
},
|
},
|
||||||
@ -1753,8 +1754,8 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_i
|
|||||||
p: path.Builder{}.Append("1", "not_moved"),
|
p: path.Builder{}.Append("1", "not_moved"),
|
||||||
l: path.Builder{}.Append("1", "not_moved"),
|
l: path.Builder{}.Append("1", "not_moved"),
|
||||||
}),
|
}),
|
||||||
dps: DeltaPaths{
|
dps: metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "old_delta_url",
|
Delta: "old_delta_url",
|
||||||
Path: prevPath(suite.T(), "1", "not_moved").String(),
|
Path: prevPath(suite.T(), "1", "not_moved").String(),
|
||||||
},
|
},
|
||||||
@ -1776,8 +1777,8 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_i
|
|||||||
p: path.Builder{}.Append("1", "moved"),
|
p: path.Builder{}.Append("1", "moved"),
|
||||||
l: path.Builder{}.Append("1", "moved"),
|
l: path.Builder{}.Append("1", "moved"),
|
||||||
}),
|
}),
|
||||||
dps: DeltaPaths{
|
dps: metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "old_delta_url",
|
Delta: "old_delta_url",
|
||||||
Path: prevPath(suite.T(), "1", "prev").String(),
|
Path: prevPath(suite.T(), "1", "prev").String(),
|
||||||
},
|
},
|
||||||
@ -1792,8 +1793,8 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_i
|
|||||||
results: map[string]mockGetterResults{},
|
results: map[string]mockGetterResults{},
|
||||||
},
|
},
|
||||||
resolver: newMockResolver(),
|
resolver: newMockResolver(),
|
||||||
dps: DeltaPaths{
|
dps: metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "old_delta_url",
|
Delta: "old_delta_url",
|
||||||
Path: prevPath(suite.T(), "1", "deleted").String(),
|
Path: prevPath(suite.T(), "1", "deleted").String(),
|
||||||
},
|
},
|
||||||
@ -1815,8 +1816,8 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_i
|
|||||||
p: path.Builder{}.Append("2", "new"),
|
p: path.Builder{}.Append("2", "new"),
|
||||||
l: path.Builder{}.Append("2", "new"),
|
l: path.Builder{}.Append("2", "new"),
|
||||||
}),
|
}),
|
||||||
dps: DeltaPaths{
|
dps: metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "old_delta_url",
|
Delta: "old_delta_url",
|
||||||
Path: prevPath(suite.T(), "1", "deleted").String(),
|
Path: prevPath(suite.T(), "1", "deleted").String(),
|
||||||
},
|
},
|
||||||
@ -1839,8 +1840,8 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_i
|
|||||||
p: path.Builder{}.Append("2", "same"),
|
p: path.Builder{}.Append("2", "same"),
|
||||||
l: path.Builder{}.Append("2", "same"),
|
l: path.Builder{}.Append("2", "same"),
|
||||||
}),
|
}),
|
||||||
dps: DeltaPaths{
|
dps: metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "old_delta_url",
|
Delta: "old_delta_url",
|
||||||
Path: prevPath(suite.T(), "1", "same").String(),
|
Path: prevPath(suite.T(), "1", "same").String(),
|
||||||
},
|
},
|
||||||
@ -1871,8 +1872,8 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_i
|
|||||||
p: path.Builder{}.Append("2", "prev"),
|
p: path.Builder{}.Append("2", "prev"),
|
||||||
l: path.Builder{}.Append("2", "prev"),
|
l: path.Builder{}.Append("2", "prev"),
|
||||||
}),
|
}),
|
||||||
dps: DeltaPaths{
|
dps: metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "old_delta_url",
|
Delta: "old_delta_url",
|
||||||
Path: prevPath(suite.T(), "1", "prev").String(),
|
Path: prevPath(suite.T(), "1", "prev").String(),
|
||||||
},
|
},
|
||||||
@ -1895,12 +1896,12 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_i
|
|||||||
p: path.Builder{}.Append("1", "not_moved"),
|
p: path.Builder{}.Append("1", "not_moved"),
|
||||||
l: path.Builder{}.Append("1", "not_moved"),
|
l: path.Builder{}.Append("1", "not_moved"),
|
||||||
}),
|
}),
|
||||||
dps: DeltaPaths{
|
dps: metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "old_delta_url",
|
Delta: "old_delta_url",
|
||||||
Path: "1/fnords/mc/smarfs",
|
Path: "1/fnords/mc/smarfs",
|
||||||
},
|
},
|
||||||
"2": DeltaPath{
|
"2": metadata.DeltaPath{
|
||||||
Delta: "old_delta_url",
|
Delta: "old_delta_url",
|
||||||
Path: "2/fnords/mc/smarfs",
|
Path: "2/fnords/mc/smarfs",
|
||||||
},
|
},
|
||||||
@ -1922,8 +1923,8 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_i
|
|||||||
p: path.Builder{}.Append("1", "same"),
|
p: path.Builder{}.Append("1", "same"),
|
||||||
l: path.Builder{}.Append("1", "same"),
|
l: path.Builder{}.Append("1", "same"),
|
||||||
}),
|
}),
|
||||||
dps: DeltaPaths{
|
dps: metadata.DeltaPaths{
|
||||||
"1": DeltaPath{
|
"1": metadata.DeltaPath{
|
||||||
Delta: "old_delta_url",
|
Delta: "old_delta_url",
|
||||||
Path: prevPath(suite.T(), "1", "same").String(),
|
Path: prevPath(suite.T(), "1", "same").String(),
|
||||||
},
|
},
|
||||||
@ -1969,20 +1970,20 @@ func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_i
|
|||||||
p: path.Builder{}.Append("4", "moved"),
|
p: path.Builder{}.Append("4", "moved"),
|
||||||
l: path.Builder{}.Append("4", "moved"),
|
l: path.Builder{}.Append("4", "moved"),
|
||||||
}),
|
}),
|
||||||
dps: DeltaPaths{
|
dps: metadata.DeltaPaths{
|
||||||
"2": DeltaPath{
|
"2": metadata.DeltaPath{
|
||||||
Delta: "old_delta_url",
|
Delta: "old_delta_url",
|
||||||
Path: prevPath(suite.T(), "2", "not_moved").String(),
|
Path: prevPath(suite.T(), "2", "not_moved").String(),
|
||||||
},
|
},
|
||||||
"3": DeltaPath{
|
"3": metadata.DeltaPath{
|
||||||
Delta: "old_delta_url",
|
Delta: "old_delta_url",
|
||||||
Path: prevPath(suite.T(), "3", "prev").String(),
|
Path: prevPath(suite.T(), "3", "prev").String(),
|
||||||
},
|
},
|
||||||
"4": DeltaPath{
|
"4": metadata.DeltaPath{
|
||||||
Delta: "old_delta_url",
|
Delta: "old_delta_url",
|
||||||
Path: prevPath(suite.T(), "4", "prev").String(),
|
Path: prevPath(suite.T(), "4", "prev").String(),
|
||||||
},
|
},
|
||||||
"5": DeltaPath{
|
"5": metadata.DeltaPath{
|
||||||
Delta: "old_delta_url",
|
Delta: "old_delta_url",
|
||||||
Path: prevPath(suite.T(), "5", "deleted").String(),
|
Path: prevPath(suite.T(), "5", "deleted").String(),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/internal/m365/graph"
|
"github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
@ -17,50 +17,22 @@ import (
|
|||||||
// store graph metadata such as delta tokens and folderID->path references.
|
// store graph metadata such as delta tokens and folderID->path references.
|
||||||
func MetadataFileNames(cat path.CategoryType) []string {
|
func MetadataFileNames(cat path.CategoryType) []string {
|
||||||
switch cat {
|
switch cat {
|
||||||
|
// TODO: should this include events?
|
||||||
case path.EmailCategory, path.ContactsCategory:
|
case path.EmailCategory, path.ContactsCategory:
|
||||||
return []string{graph.DeltaURLsFileName, graph.PreviousPathFileName}
|
return []string{metadata.DeltaURLsFileName, metadata.PreviousPathFileName}
|
||||||
default:
|
default:
|
||||||
return []string{graph.PreviousPathFileName}
|
return []string{metadata.PreviousPathFileName}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CatDeltaPaths map[path.CategoryType]DeltaPaths
|
|
||||||
|
|
||||||
type DeltaPaths map[string]DeltaPath
|
|
||||||
|
|
||||||
func (dps DeltaPaths) AddDelta(k, d string) {
|
|
||||||
dp, ok := dps[k]
|
|
||||||
if !ok {
|
|
||||||
dp = DeltaPath{}
|
|
||||||
}
|
|
||||||
|
|
||||||
dp.Delta = d
|
|
||||||
dps[k] = dp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dps DeltaPaths) AddPath(k, p string) {
|
|
||||||
dp, ok := dps[k]
|
|
||||||
if !ok {
|
|
||||||
dp = DeltaPath{}
|
|
||||||
}
|
|
||||||
|
|
||||||
dp.Path = p
|
|
||||||
dps[k] = dp
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeltaPath struct {
|
|
||||||
Delta string
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseMetadataCollections produces a map of structs holding delta
|
// ParseMetadataCollections produces a map of structs holding delta
|
||||||
// and path lookup maps.
|
// and path lookup maps.
|
||||||
func ParseMetadataCollections(
|
func ParseMetadataCollections(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
colls []data.RestoreCollection,
|
colls []data.RestoreCollection,
|
||||||
) (CatDeltaPaths, bool, error) {
|
) (metadata.CatDeltaPaths, bool, error) {
|
||||||
// cdp stores metadata
|
// cdp stores metadata
|
||||||
cdp := CatDeltaPaths{
|
cdp := metadata.CatDeltaPaths{
|
||||||
path.ContactsCategory: {},
|
path.ContactsCategory: {},
|
||||||
path.EmailCategory: {},
|
path.EmailCategory: {},
|
||||||
path.EventsCategory: {},
|
path.EventsCategory: {},
|
||||||
@ -107,8 +79,8 @@ func ParseMetadataCollections(
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch item.ID() {
|
switch item.ID() {
|
||||||
case graph.PreviousPathFileName:
|
case metadata.PreviousPathFileName:
|
||||||
if _, ok := found[category]["path"]; ok {
|
if _, ok := found[category][metadata.PathKey]; ok {
|
||||||
return nil, false, clues.Wrap(clues.New(category.String()), "multiple versions of path metadata").WithClues(ctx)
|
return nil, false, clues.Wrap(clues.New(category.String()), "multiple versions of path metadata").WithClues(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,10 +88,10 @@ func ParseMetadataCollections(
|
|||||||
cdps.AddPath(k, p)
|
cdps.AddPath(k, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
found[category]["path"] = struct{}{}
|
found[category][metadata.PathKey] = struct{}{}
|
||||||
|
|
||||||
case graph.DeltaURLsFileName:
|
case metadata.DeltaURLsFileName:
|
||||||
if _, ok := found[category]["delta"]; ok {
|
if _, ok := found[category][metadata.DeltaKey]; ok {
|
||||||
return nil, false, clues.Wrap(clues.New(category.String()), "multiple versions of delta metadata").WithClues(ctx)
|
return nil, false, clues.Wrap(clues.New(category.String()), "multiple versions of delta metadata").WithClues(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +99,7 @@ func ParseMetadataCollections(
|
|||||||
cdps.AddDelta(k, d)
|
cdps.AddDelta(k, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
found[category]["delta"] = struct{}{}
|
found[category][metadata.DeltaKey] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
cdp[category] = cdps
|
cdp[category] = cdps
|
||||||
@ -142,7 +114,7 @@ func ParseMetadataCollections(
|
|||||||
if errs.Failure() != nil {
|
if errs.Failure() != nil {
|
||||||
logger.CtxErr(ctx, errs.Failure()).Info("reading metadata collection items")
|
logger.CtxErr(ctx, errs.Failure()).Info("reading metadata collection items")
|
||||||
|
|
||||||
return CatDeltaPaths{
|
return metadata.CatDeltaPaths{
|
||||||
path.ContactsCategory: {},
|
path.ContactsCategory: {},
|
||||||
path.EmailCategory: {},
|
path.EmailCategory: {},
|
||||||
path.EventsCategory: {},
|
path.EventsCategory: {},
|
||||||
|
|||||||
@ -5,14 +5,15 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/pii"
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/internal/m365/graph"
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
||||||
"github.com/alcionai/corso/src/internal/m365/support"
|
"github.com/alcionai/corso/src/internal/m365/support"
|
||||||
"github.com/alcionai/corso/src/internal/observe"
|
"github.com/alcionai/corso/src/internal/observe"
|
||||||
"github.com/alcionai/corso/src/internal/operations/inject"
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
@ -34,10 +35,9 @@ func CreateCollections(
|
|||||||
bh backupHandler,
|
bh backupHandler,
|
||||||
tenantID string,
|
tenantID string,
|
||||||
scope selectors.GroupsScope,
|
scope selectors.GroupsScope,
|
||||||
// dps DeltaPaths,
|
|
||||||
su support.StatusUpdater,
|
su support.StatusUpdater,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]data.BackupCollection, error) {
|
) ([]data.BackupCollection, bool, error) {
|
||||||
ctx = clues.Add(ctx, "category", scope.Category().PathType())
|
ctx = clues.Add(ctx, "category", scope.Category().PathType())
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -50,6 +50,13 @@ func CreateCollections(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cdps, canUsePreviousBackup, err := parseMetadataCollections(ctx, bpc.MetadataCollections)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = clues.Add(ctx, "can_use_previous_backup", canUsePreviousBackup)
|
||||||
|
|
||||||
catProgress := observe.MessageWithCompletion(
|
catProgress := observe.MessageWithCompletion(
|
||||||
ctx,
|
ctx,
|
||||||
observe.Bulletf("%s", qp.Category))
|
observe.Bulletf("%s", qp.Category))
|
||||||
@ -57,7 +64,7 @@ func CreateCollections(
|
|||||||
|
|
||||||
channels, err := bh.getChannels(ctx)
|
channels, err := bh.getChannels(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Stack(err)
|
return nil, false, clues.Stack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
collections, err := populateCollections(
|
collections, err := populateCollections(
|
||||||
@ -67,18 +74,18 @@ func CreateCollections(
|
|||||||
su,
|
su,
|
||||||
channels,
|
channels,
|
||||||
scope,
|
scope,
|
||||||
// dps,
|
cdps[scope.Category().PathType()],
|
||||||
bpc.Options,
|
bpc.Options,
|
||||||
errs)
|
errs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "filling collections")
|
return nil, false, clues.Wrap(err, "filling collections")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, coll := range collections {
|
for _, coll := range collections {
|
||||||
allCollections = append(allCollections, coll)
|
allCollections = append(allCollections, coll)
|
||||||
}
|
}
|
||||||
|
|
||||||
return allCollections, nil
|
return allCollections, canUsePreviousBackup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateCollections(
|
func populateCollections(
|
||||||
@ -88,79 +95,76 @@ func populateCollections(
|
|||||||
statusUpdater support.StatusUpdater,
|
statusUpdater support.StatusUpdater,
|
||||||
channels []models.Channelable,
|
channels []models.Channelable,
|
||||||
scope selectors.GroupsScope,
|
scope selectors.GroupsScope,
|
||||||
// dps DeltaPaths,
|
dps metadata.DeltaPaths,
|
||||||
ctrlOpts control.Options,
|
ctrlOpts control.Options,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) (map[string]data.BackupCollection, error) {
|
) (map[string]data.BackupCollection, error) {
|
||||||
|
var (
|
||||||
// channel ID -> BackupCollection.
|
// channel ID -> BackupCollection.
|
||||||
channelCollections := map[string]data.BackupCollection{}
|
collections = map[string]data.BackupCollection{}
|
||||||
|
|
||||||
// channel ID -> delta url or folder path lookups
|
// channel ID -> delta url or folder path lookups
|
||||||
// deltaURLs = map[string]string{}
|
deltaURLs = map[string]string{}
|
||||||
// currPaths = map[string]string{}
|
currPaths = map[string]string{}
|
||||||
// copy of previousPaths. every channel present in the slice param
|
// copy of previousPaths. every channel present in the slice param
|
||||||
// gets removed from this map; the remaining channels at the end of
|
// gets removed from this map; the remaining channels at the end of
|
||||||
// the process have been deleted.
|
// the process have been deleted.
|
||||||
// tombstones = makeTombstones(dps)
|
tombstones = makeTombstones(dps)
|
||||||
|
el = errs.Local()
|
||||||
|
)
|
||||||
|
|
||||||
logger.Ctx(ctx).Info("filling collections")
|
logger.Ctx(ctx).Info("filling collections", "len_deltapaths", len(dps))
|
||||||
// , "len_deltapaths", len(dps))
|
|
||||||
|
|
||||||
el := errs.Local()
|
|
||||||
|
|
||||||
for _, c := range channels {
|
for _, c := range channels {
|
||||||
if el.Failure() != nil {
|
if el.Failure() != nil {
|
||||||
return nil, el.Failure()
|
return nil, el.Failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete(tombstones, cID)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cID = ptr.Val(c.GetId())
|
cID = ptr.Val(c.GetId())
|
||||||
cName = ptr.Val(c.GetDisplayName())
|
cName = ptr.Val(c.GetDisplayName())
|
||||||
err error
|
err error
|
||||||
// dp = dps[cID]
|
dp = dps[cID]
|
||||||
// prevDelta = dp.Delta
|
prevDelta = dp.Delta
|
||||||
// prevPathStr = dp.Path // do not log: pii; log prevPath instead
|
prevPathStr = dp.Path // do not log: pii; log prevPath instead
|
||||||
// prevPath path.Path
|
prevPath path.Path
|
||||||
ictx = clues.Add(
|
ictx = clues.Add(
|
||||||
ctx,
|
ctx,
|
||||||
"channel_id", cID)
|
"channel_id", cID,
|
||||||
// "previous_delta", pii.SafeURL{
|
"previous_delta", pii.SafeURL{
|
||||||
// URL: prevDelta,
|
URL: prevDelta,
|
||||||
// SafePathElems: graph.SafeURLPathParams,
|
SafePathElems: graph.SafeURLPathParams,
|
||||||
// SafeQueryKeys: graph.SafeURLQueryParams,
|
SafeQueryKeys: graph.SafeURLQueryParams,
|
||||||
// })
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
delete(tombstones, cID)
|
||||||
|
|
||||||
// Only create a collection if the path matches the scope.
|
// Only create a collection if the path matches the scope.
|
||||||
if !bh.includeContainer(ictx, qp, c, scope) {
|
if !bh.includeContainer(ictx, qp, c, scope) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// if len(prevPathStr) > 0 {
|
if len(prevPathStr) > 0 {
|
||||||
// if prevPath, err = pathFromPrevString(prevPathStr); err != nil {
|
if prevPath, err = pathFromPrevString(prevPathStr); err != nil {
|
||||||
// logger.CtxErr(ictx, err).Error("parsing prev path")
|
logger.CtxErr(ictx, err).Error("parsing prev path")
|
||||||
// // if the previous path is unusable, then the delta must be, too.
|
// if the previous path is unusable, then the delta must be, too.
|
||||||
// prevDelta = ""
|
prevDelta = ""
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// ictx = clues.Add(ictx, "previous_path", prevPath)
|
ictx = clues.Add(ictx, "previous_path", prevPath)
|
||||||
|
|
||||||
items, _, err := bh.getChannelMessageIDsDelta(ctx, cID, "")
|
added, removed, du, err := bh.getChannelMessageIDsDelta(ctx, cID, prevDelta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
el.AddRecoverable(ctx, clues.Stack(err))
|
el.AddRecoverable(ctx, clues.Stack(err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// if len(newDelta.URL) > 0 {
|
if len(du.URL) > 0 {
|
||||||
// deltaURLs[cID] = newDelta.URL
|
deltaURLs[cID] = du.URL
|
||||||
// } else if !newDelta.Reset {
|
} else if !du.Reset {
|
||||||
// logger.Ctx(ictx).Info("missing delta url")
|
logger.Ctx(ictx).Info("missing delta url")
|
||||||
// }
|
}
|
||||||
|
|
||||||
var prevPath path.Path
|
|
||||||
|
|
||||||
currPath, err := bh.canonicalPath(path.Builder{}.Append(cID), qp.TenantID)
|
currPath, err := bh.canonicalPath(path.Builder{}.Append(cID), qp.TenantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -168,6 +172,13 @@ func populateCollections(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove any deleted IDs from the set of added IDs because items that are
|
||||||
|
// deleted and then restored will have a different ID than they did
|
||||||
|
// originally.
|
||||||
|
for remove := range removed {
|
||||||
|
delete(added, remove)
|
||||||
|
}
|
||||||
|
|
||||||
edc := NewCollection(
|
edc := NewCollection(
|
||||||
bh,
|
bh,
|
||||||
qp.ProtectedResource.ID(),
|
qp.ProtectedResource.ID(),
|
||||||
@ -175,50 +186,93 @@ func populateCollections(
|
|||||||
prevPath,
|
prevPath,
|
||||||
path.Builder{}.Append(cName),
|
path.Builder{}.Append(cName),
|
||||||
qp.Category,
|
qp.Category,
|
||||||
|
added,
|
||||||
|
removed,
|
||||||
statusUpdater,
|
statusUpdater,
|
||||||
ctrlOpts)
|
ctrlOpts,
|
||||||
|
du.Reset)
|
||||||
|
|
||||||
channelCollections[cID] = &edc
|
collections[cID] = &edc
|
||||||
|
|
||||||
// TODO: handle deleted items for v1 backup.
|
// add the current path for the container ID to be used in the next backup
|
||||||
// // Remove any deleted IDs from the set of added IDs because items that are
|
// as the "previous path", for reference in case of a rename or relocation.
|
||||||
// // deleted and then restored will have a different ID than they did
|
currPaths[cID] = currPath.String()
|
||||||
// // originally.
|
|
||||||
// for _, remove := range removed {
|
|
||||||
// delete(edc.added, remove)
|
|
||||||
// edc.removed[remove] = struct{}{}
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // add the current path for the container ID to be used in the next backup
|
|
||||||
// // as the "previous path", for reference in case of a rename or relocation.
|
|
||||||
// currPaths[cID] = currPath.String()
|
|
||||||
|
|
||||||
// FIXME: normally this goes before removal, but the linters require no bottom comments
|
|
||||||
maps.Copy(edc.added, items)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: handle tombstones here
|
// A tombstone is a channel that needs to be marked for deletion.
|
||||||
|
// The only situation where a tombstone should appear is if the channel exists
|
||||||
|
// in the `previousPath` set, but does not exist in the enumeration.
|
||||||
|
for id, p := range tombstones {
|
||||||
|
if el.Failure() != nil {
|
||||||
|
return nil, el.Failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
ictx = clues.Add(ctx, "tombstone_id", id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if collections[id] != nil {
|
||||||
|
el.AddRecoverable(ctx, clues.Wrap(err, "conflict: tombstone exists for a live collection").WithClues(ictx))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// only occurs if it was a new folder that we picked up during the container
|
||||||
|
// resolver phase that got deleted in flight by the time we hit this stage.
|
||||||
|
if len(p) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
prevPath, err := pathFromPrevString(p)
|
||||||
|
if err != nil {
|
||||||
|
// technically shouldn't ever happen. But just in case...
|
||||||
|
logger.CtxErr(ictx, err).Error("parsing tombstone prev path")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
edc := NewCollection(
|
||||||
|
bh,
|
||||||
|
qp.ProtectedResource.ID(),
|
||||||
|
nil, // marks the collection as deleted
|
||||||
|
prevPath,
|
||||||
|
nil, // tombstones don't need a location
|
||||||
|
qp.Category,
|
||||||
|
nil, // no items added
|
||||||
|
nil, // this deletes a directory, so no items deleted either
|
||||||
|
statusUpdater,
|
||||||
|
ctrlOpts,
|
||||||
|
false)
|
||||||
|
|
||||||
|
collections[id] = &edc
|
||||||
|
}
|
||||||
|
|
||||||
logger.Ctx(ctx).Infow(
|
logger.Ctx(ctx).Infow(
|
||||||
"adding metadata collection entries",
|
"adding metadata collection entries",
|
||||||
// "num_deltas_entries", len(deltaURLs),
|
"num_deltas_entries", len(deltaURLs),
|
||||||
"num_paths_entries", len(channelCollections))
|
"num_paths_entries", len(collections))
|
||||||
|
|
||||||
// col, err := graph.MakeMetadataCollection(
|
pathPrefix, err := path.Builder{}.ToServiceCategoryMetadataPath(
|
||||||
// qp.TenantID,
|
qp.TenantID,
|
||||||
// qp.ProtectedResource.ID(),
|
qp.ProtectedResource.ID(),
|
||||||
// path.ExchangeService,
|
path.GroupsService,
|
||||||
// qp.Category,
|
qp.Category,
|
||||||
// []graph.MetadataCollectionEntry{
|
false)
|
||||||
// graph.NewMetadataEntry(graph.PreviousPathFileName, currPaths),
|
if err != nil {
|
||||||
// graph.NewMetadataEntry(graph.DeltaURLsFileName, deltaURLs),
|
return nil, clues.Wrap(err, "making metadata path")
|
||||||
// },
|
}
|
||||||
// statusUpdater)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, clues.Wrap(err, "making metadata collection")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// channelCollections["metadata"] = col
|
col, err := graph.MakeMetadataCollection(
|
||||||
|
pathPrefix,
|
||||||
|
[]graph.MetadataCollectionEntry{
|
||||||
|
graph.NewMetadataEntry(metadata.PreviousPathFileName, currPaths),
|
||||||
|
graph.NewMetadataEntry(metadata.DeltaURLsFileName, deltaURLs),
|
||||||
|
},
|
||||||
|
statusUpdater)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "making metadata collection")
|
||||||
|
}
|
||||||
|
|
||||||
return channelCollections, el.Failure()
|
collections["metadata"] = col
|
||||||
|
|
||||||
|
return collections, el.Failure()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/version"
|
"github.com/alcionai/corso/src/internal/version"
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
@ -40,6 +41,7 @@ type mockBackupHandler struct {
|
|||||||
channels []models.Channelable
|
channels []models.Channelable
|
||||||
channelsErr error
|
channelsErr error
|
||||||
messageIDs map[string]struct{}
|
messageIDs map[string]struct{}
|
||||||
|
deletedMsgIDs map[string]struct{}
|
||||||
messagesErr error
|
messagesErr error
|
||||||
messages map[string]models.ChatMessageable
|
messages map[string]models.ChatMessageable
|
||||||
info map[string]*details.GroupsInfo
|
info map[string]*details.GroupsInfo
|
||||||
@ -54,8 +56,8 @@ func (bh mockBackupHandler) getChannels(context.Context) ([]models.Channelable,
|
|||||||
func (bh mockBackupHandler) getChannelMessageIDsDelta(
|
func (bh mockBackupHandler) getChannelMessageIDsDelta(
|
||||||
_ context.Context,
|
_ context.Context,
|
||||||
_, _ string,
|
_, _ string,
|
||||||
) (map[string]struct{}, api.DeltaUpdate, error) {
|
) (map[string]struct{}, map[string]struct{}, api.DeltaUpdate, error) {
|
||||||
return bh.messageIDs, api.DeltaUpdate{}, bh.messagesErr
|
return bh.messageIDs, bh.deletedMsgIDs, api.DeltaUpdate{}, bh.messagesErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bh mockBackupHandler) includeContainer(
|
func (bh mockBackupHandler) includeContainer(
|
||||||
@ -115,19 +117,15 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
TenantID: suite.creds.AzureTenantID,
|
TenantID: suite.creds.AzureTenantID,
|
||||||
}
|
}
|
||||||
statusUpdater = func(*support.ControllerOperationStatus) {}
|
statusUpdater = func(*support.ControllerOperationStatus) {}
|
||||||
allScope = selectors.NewGroupsBackup(nil).Channels(selectors.Any())[0]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
mock mockBackupHandler
|
mock mockBackupHandler
|
||||||
scope selectors.GroupsScope
|
|
||||||
failFast control.FailurePolicy
|
|
||||||
expectErr require.ErrorAssertionFunc
|
expectErr require.ErrorAssertionFunc
|
||||||
expectColls int
|
expectColls int
|
||||||
expectNewColls int
|
expectNewColls int
|
||||||
expectMetadataColls int
|
expectMetadataColls int
|
||||||
expectDoNotMergeColls int
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "happy path, one container",
|
name: "happy path, one container",
|
||||||
@ -135,12 +133,21 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
channels: testdata.StubChannels("one"),
|
channels: testdata.StubChannels("one"),
|
||||||
messageIDs: map[string]struct{}{"msg-one": {}},
|
messageIDs: map[string]struct{}{"msg-one": {}},
|
||||||
},
|
},
|
||||||
scope: allScope,
|
|
||||||
expectErr: require.NoError,
|
expectErr: require.NoError,
|
||||||
expectColls: 1,
|
expectColls: 2,
|
||||||
expectNewColls: 1,
|
expectNewColls: 1,
|
||||||
expectMetadataColls: 0,
|
expectMetadataColls: 1,
|
||||||
expectDoNotMergeColls: 1,
|
},
|
||||||
|
{
|
||||||
|
name: "happy path, one container, only deleted messages",
|
||||||
|
mock: mockBackupHandler{
|
||||||
|
channels: testdata.StubChannels("one"),
|
||||||
|
deletedMsgIDs: map[string]struct{}{"msg-one": {}},
|
||||||
|
},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 2,
|
||||||
|
expectNewColls: 1,
|
||||||
|
expectMetadataColls: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "happy path, many containers",
|
name: "happy path, many containers",
|
||||||
@ -148,12 +155,10 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
channels: testdata.StubChannels("one", "two"),
|
channels: testdata.StubChannels("one", "two"),
|
||||||
messageIDs: map[string]struct{}{"msg-one": {}},
|
messageIDs: map[string]struct{}{"msg-one": {}},
|
||||||
},
|
},
|
||||||
scope: allScope,
|
|
||||||
expectErr: require.NoError,
|
expectErr: require.NoError,
|
||||||
expectColls: 2,
|
expectColls: 3,
|
||||||
expectNewColls: 2,
|
expectNewColls: 2,
|
||||||
expectMetadataColls: 0,
|
expectMetadataColls: 1,
|
||||||
expectDoNotMergeColls: 2,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no containers pass scope",
|
name: "no containers pass scope",
|
||||||
@ -161,34 +166,28 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
channels: testdata.StubChannels("one"),
|
channels: testdata.StubChannels("one"),
|
||||||
doNotInclude: true,
|
doNotInclude: true,
|
||||||
},
|
},
|
||||||
scope: selectors.NewGroupsBackup(nil).Channels(selectors.None())[0],
|
|
||||||
expectErr: require.NoError,
|
expectErr: require.NoError,
|
||||||
expectColls: 0,
|
expectColls: 1,
|
||||||
expectNewColls: 0,
|
expectNewColls: 0,
|
||||||
expectMetadataColls: 0,
|
expectMetadataColls: 1,
|
||||||
expectDoNotMergeColls: 0,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no channels",
|
name: "no channels",
|
||||||
mock: mockBackupHandler{},
|
mock: mockBackupHandler{},
|
||||||
scope: allScope,
|
|
||||||
expectErr: require.NoError,
|
expectErr: require.NoError,
|
||||||
expectColls: 0,
|
expectColls: 1,
|
||||||
expectNewColls: 0,
|
expectNewColls: 0,
|
||||||
expectMetadataColls: 0,
|
expectMetadataColls: 1,
|
||||||
expectDoNotMergeColls: 0,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no channel messages",
|
name: "no channel messages",
|
||||||
mock: mockBackupHandler{
|
mock: mockBackupHandler{
|
||||||
channels: testdata.StubChannels("one"),
|
channels: testdata.StubChannels("one"),
|
||||||
},
|
},
|
||||||
scope: allScope,
|
|
||||||
expectErr: require.NoError,
|
expectErr: require.NoError,
|
||||||
expectColls: 1,
|
expectColls: 2,
|
||||||
expectNewColls: 1,
|
expectNewColls: 1,
|
||||||
expectMetadataColls: 0,
|
expectMetadataColls: 1,
|
||||||
expectDoNotMergeColls: 1,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "err: deleted in flight",
|
name: "err: deleted in flight",
|
||||||
@ -196,12 +195,10 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
channels: testdata.StubChannels("one"),
|
channels: testdata.StubChannels("one"),
|
||||||
messagesErr: graph.ErrDeletedInFlight,
|
messagesErr: graph.ErrDeletedInFlight,
|
||||||
},
|
},
|
||||||
scope: allScope,
|
|
||||||
expectErr: require.Error,
|
expectErr: require.Error,
|
||||||
expectColls: 0,
|
expectColls: 1,
|
||||||
expectNewColls: 0,
|
expectNewColls: 0,
|
||||||
expectMetadataColls: 0,
|
expectMetadataColls: 1,
|
||||||
expectDoNotMergeColls: 0,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "err: other error",
|
name: "err: other error",
|
||||||
@ -209,32 +206,20 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
channels: testdata.StubChannels("one"),
|
channels: testdata.StubChannels("one"),
|
||||||
messagesErr: assert.AnError,
|
messagesErr: assert.AnError,
|
||||||
},
|
},
|
||||||
scope: allScope,
|
|
||||||
expectErr: require.Error,
|
expectErr: require.Error,
|
||||||
expectColls: 0,
|
expectColls: 1,
|
||||||
expectNewColls: 0,
|
expectNewColls: 0,
|
||||||
expectMetadataColls: 0,
|
expectMetadataColls: 1,
|
||||||
expectDoNotMergeColls: 0,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
// for _, canMakeDeltaQueries := range []bool{true, false} {
|
suite.Run(test.name, func() {
|
||||||
name := test.name
|
|
||||||
|
|
||||||
// if canMakeDeltaQueries {
|
|
||||||
// name += "-delta"
|
|
||||||
// } else {
|
|
||||||
// name += "-non-delta"
|
|
||||||
// }
|
|
||||||
|
|
||||||
suite.Run(name, func() {
|
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
|
|
||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
ctrlOpts := control.Options{FailureHandling: test.failFast}
|
ctrlOpts := control.Options{FailureHandling: control.FailFast}
|
||||||
// ctrlOpts.ToggleFeatures.DisableDelta = !canMakeDeltaQueries
|
|
||||||
|
|
||||||
collections, err := populateCollections(
|
collections, err := populateCollections(
|
||||||
ctx,
|
ctx,
|
||||||
@ -242,7 +227,8 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
test.mock,
|
test.mock,
|
||||||
statusUpdater,
|
statusUpdater,
|
||||||
test.mock.channels,
|
test.mock.channels,
|
||||||
test.scope,
|
selectors.NewGroupsBackup(nil).Channels(selectors.Any())[0],
|
||||||
|
nil,
|
||||||
ctrlOpts,
|
ctrlOpts,
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
test.expectErr(t, err, clues.ToCore(err))
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
@ -273,12 +259,168 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
assert.Zero(t, deleteds, "deleted collections")
|
assert.Zero(t, deleteds, "deleted collections")
|
||||||
assert.Equal(t, test.expectNewColls, news, "new collections")
|
assert.Equal(t, test.expectNewColls, news, "new collections")
|
||||||
assert.Equal(t, test.expectMetadataColls, metadatas, "metadata collections")
|
assert.Equal(t, test.expectMetadataColls, metadatas, "metadata collections")
|
||||||
assert.Equal(t, test.expectDoNotMergeColls, doNotMerges, "doNotMerge collections")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// }
|
func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
|
||||||
|
var (
|
||||||
|
qp = graph.QueryParams{
|
||||||
|
Category: path.ChannelMessagesCategory, // doesn't matter which one we use.
|
||||||
|
ProtectedResource: inMock.NewProvider("group_id", "user_name"),
|
||||||
|
TenantID: suite.creds.AzureTenantID,
|
||||||
|
}
|
||||||
|
statusUpdater = func(*support.ControllerOperationStatus) {}
|
||||||
|
allScope = selectors.NewGroupsBackup(nil).Channels(selectors.Any())[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
chanPath, err := path.Build("tid", "grp", path.GroupsService, path.ChannelMessagesCategory, false, "chan")
|
||||||
|
require.NoError(suite.T(), err, clues.ToCore(err))
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
mock mockBackupHandler
|
||||||
|
deltaPaths metadata.DeltaPaths
|
||||||
|
expectErr require.ErrorAssertionFunc
|
||||||
|
expectColls int
|
||||||
|
expectNewColls int
|
||||||
|
expectTombstoneCols int
|
||||||
|
expectMetadataColls int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "non incremental",
|
||||||
|
mock: mockBackupHandler{
|
||||||
|
channels: testdata.StubChannels("chan"),
|
||||||
|
messageIDs: map[string]struct{}{"msg": {}},
|
||||||
|
},
|
||||||
|
deltaPaths: metadata.DeltaPaths{},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 2,
|
||||||
|
expectNewColls: 1,
|
||||||
|
expectTombstoneCols: 0,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incremental",
|
||||||
|
mock: mockBackupHandler{
|
||||||
|
channels: testdata.StubChannels("chan"),
|
||||||
|
deletedMsgIDs: map[string]struct{}{"msg": {}},
|
||||||
|
},
|
||||||
|
deltaPaths: metadata.DeltaPaths{
|
||||||
|
"chan": {
|
||||||
|
Delta: "chan",
|
||||||
|
Path: chanPath.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 2,
|
||||||
|
expectNewColls: 0,
|
||||||
|
expectTombstoneCols: 0,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incremental no new messages",
|
||||||
|
mock: mockBackupHandler{
|
||||||
|
channels: testdata.StubChannels("chan"),
|
||||||
|
},
|
||||||
|
deltaPaths: metadata.DeltaPaths{
|
||||||
|
"chan": {
|
||||||
|
Delta: "chan",
|
||||||
|
Path: chanPath.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 2,
|
||||||
|
expectNewColls: 0,
|
||||||
|
expectTombstoneCols: 0,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incremental deleted channel",
|
||||||
|
mock: mockBackupHandler{
|
||||||
|
channels: testdata.StubChannels(),
|
||||||
|
},
|
||||||
|
deltaPaths: metadata.DeltaPaths{
|
||||||
|
"chan": {
|
||||||
|
Delta: "chan",
|
||||||
|
Path: chanPath.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 2,
|
||||||
|
expectNewColls: 0,
|
||||||
|
expectTombstoneCols: 1,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incremental new and deleted channel",
|
||||||
|
mock: mockBackupHandler{
|
||||||
|
channels: testdata.StubChannels("chan2"),
|
||||||
|
messageIDs: map[string]struct{}{"msg": {}},
|
||||||
|
},
|
||||||
|
deltaPaths: metadata.DeltaPaths{
|
||||||
|
"chan": {
|
||||||
|
Delta: "chan",
|
||||||
|
Path: chanPath.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 3,
|
||||||
|
expectNewColls: 1,
|
||||||
|
expectTombstoneCols: 1,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
ctrlOpts := control.Options{FailureHandling: control.FailFast}
|
||||||
|
|
||||||
|
collections, err := populateCollections(
|
||||||
|
ctx,
|
||||||
|
qp,
|
||||||
|
test.mock,
|
||||||
|
statusUpdater,
|
||||||
|
test.mock.channels,
|
||||||
|
allScope,
|
||||||
|
test.deltaPaths,
|
||||||
|
ctrlOpts,
|
||||||
|
fault.New(true))
|
||||||
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
|
assert.Len(t, collections, test.expectColls, "number of collections")
|
||||||
|
|
||||||
|
// collection assertions
|
||||||
|
|
||||||
|
tombstones, news, metadatas, doNotMerges := 0, 0, 0, 0
|
||||||
|
for _, c := range collections {
|
||||||
|
if c.FullPath() != nil && c.FullPath().Service() == path.GroupsMetadataService {
|
||||||
|
metadatas++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.State() == data.DeletedState {
|
||||||
|
tombstones++
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.State() == data.NewState {
|
||||||
|
news++
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DoNotMergeItems() {
|
||||||
|
doNotMerges++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, test.expectNewColls, news, "new collections")
|
||||||
|
assert.Equal(t, test.expectTombstoneCols, tombstones, "tombstone collections")
|
||||||
|
assert.Equal(t, test.expectMetadataColls, metadatas, "metadata collections")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Integration tests
|
// Integration tests
|
||||||
@ -330,15 +472,13 @@ func (suite *BackupIntgSuite) TestCreateCollections() {
|
|||||||
name string
|
name string
|
||||||
scope selectors.GroupsScope
|
scope selectors.GroupsScope
|
||||||
channelNames map[string]struct{}
|
channelNames map[string]struct{}
|
||||||
canMakeDeltaQueries bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "channel messages non-delta",
|
name: "channel messages",
|
||||||
scope: selTD.GroupsBackupChannelScope(selectors.NewGroupsBackup(resources))[0],
|
scope: selTD.GroupsBackupChannelScope(selectors.NewGroupsBackup(resources))[0],
|
||||||
channelNames: map[string]struct{}{
|
channelNames: map[string]struct{}{
|
||||||
selTD.TestChannelName: {},
|
selTD.TestChannelName: {},
|
||||||
},
|
},
|
||||||
canMakeDeltaQueries: false,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,7 +490,6 @@ func (suite *BackupIntgSuite) TestCreateCollections() {
|
|||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
ctrlOpts := control.DefaultOptions()
|
ctrlOpts := control.DefaultOptions()
|
||||||
ctrlOpts.ToggleFeatures.DisableDelta = !test.canMakeDeltaQueries
|
|
||||||
|
|
||||||
sel := selectors.NewGroupsBackup([]string{protectedResource})
|
sel := selectors.NewGroupsBackup([]string{protectedResource})
|
||||||
sel.Include(selTD.GroupsBackupChannelScope(sel))
|
sel.Include(selTD.GroupsBackupChannelScope(sel))
|
||||||
@ -362,7 +501,7 @@ func (suite *BackupIntgSuite) TestCreateCollections() {
|
|||||||
Selector: sel.Selector,
|
Selector: sel.Selector,
|
||||||
}
|
}
|
||||||
|
|
||||||
collections, err := CreateCollections(
|
collections, _, err := CreateCollections(
|
||||||
ctx,
|
ctx,
|
||||||
bpc,
|
bpc,
|
||||||
handler,
|
handler,
|
||||||
|
|||||||
@ -39,7 +39,7 @@ func (bh channelsBackupHandler) getChannels(
|
|||||||
func (bh channelsBackupHandler) getChannelMessageIDsDelta(
|
func (bh channelsBackupHandler) getChannelMessageIDsDelta(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
channelID, prevDelta string,
|
channelID, prevDelta string,
|
||||||
) (map[string]struct{}, api.DeltaUpdate, error) {
|
) (map[string]struct{}, map[string]struct{}, api.DeltaUpdate, error) {
|
||||||
return bh.ac.GetChannelMessageIDsDelta(ctx, bh.protectedResource, channelID, prevDelta)
|
return bh.ac.GetChannelMessageIDsDelta(ctx, bh.protectedResource, channelID, prevDelta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -64,7 +64,7 @@ type Collection struct {
|
|||||||
state data.CollectionState
|
state data.CollectionState
|
||||||
|
|
||||||
// doNotMergeItems should only be true if the old delta token expired.
|
// doNotMergeItems should only be true if the old delta token expired.
|
||||||
// doNotMergeItems bool
|
doNotMergeItems bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewExchangeDataCollection creates an ExchangeDataCollection.
|
// NewExchangeDataCollection creates an ExchangeDataCollection.
|
||||||
@ -79,20 +79,22 @@ func NewCollection(
|
|||||||
curr, prev path.Path,
|
curr, prev path.Path,
|
||||||
location *path.Builder,
|
location *path.Builder,
|
||||||
category path.CategoryType,
|
category path.CategoryType,
|
||||||
|
added map[string]struct{},
|
||||||
|
removed map[string]struct{},
|
||||||
statusUpdater support.StatusUpdater,
|
statusUpdater support.StatusUpdater,
|
||||||
ctrlOpts control.Options,
|
ctrlOpts control.Options,
|
||||||
// doNotMergeItems bool,
|
doNotMergeItems bool,
|
||||||
) Collection {
|
) Collection {
|
||||||
collection := Collection{
|
collection := Collection{
|
||||||
added: map[string]struct{}{},
|
added: added,
|
||||||
category: category,
|
category: category,
|
||||||
ctrl: ctrlOpts,
|
ctrl: ctrlOpts,
|
||||||
// doNotMergeItems: doNotMergeItems,
|
doNotMergeItems: doNotMergeItems,
|
||||||
fullPath: curr,
|
fullPath: curr,
|
||||||
getter: getter,
|
getter: getter,
|
||||||
locationPath: location,
|
locationPath: location,
|
||||||
prevPath: prev,
|
prevPath: prev,
|
||||||
removed: make(map[string]struct{}, 0),
|
removed: removed,
|
||||||
state: data.StateOf(prev, curr),
|
state: data.StateOf(prev, curr),
|
||||||
statusUpdater: statusUpdater,
|
statusUpdater: statusUpdater,
|
||||||
stream: make(chan data.Item, collectionChannelBufferSize),
|
stream: make(chan data.Item, collectionChannelBufferSize),
|
||||||
|
|||||||
@ -124,8 +124,10 @@ func (suite *CollectionSuite) TestNewCollection_state() {
|
|||||||
"g",
|
"g",
|
||||||
test.curr, test.prev, test.loc,
|
test.curr, test.prev, test.loc,
|
||||||
0,
|
0,
|
||||||
|
nil, nil,
|
||||||
nil,
|
nil,
|
||||||
control.DefaultOptions())
|
control.DefaultOptions(),
|
||||||
|
false)
|
||||||
assert.Equal(t, test.expect, c.State(), "collection state")
|
assert.Equal(t, test.expect, c.State(), "collection state")
|
||||||
assert.Equal(t, test.curr, c.fullPath, "full path")
|
assert.Equal(t, test.curr, c.fullPath, "full path")
|
||||||
assert.Equal(t, test.prev, c.prevPath, "prev path")
|
assert.Equal(t, test.prev, c.prevPath, "prev path")
|
||||||
|
|||||||
@ -24,7 +24,7 @@ type backupHandler interface {
|
|||||||
getChannelMessageIDsDelta(
|
getChannelMessageIDsDelta(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
channelID, prevDelta string,
|
channelID, prevDelta string,
|
||||||
) (map[string]struct{}, api.DeltaUpdate, error)
|
) (map[string]struct{}, map[string]struct{}, api.DeltaUpdate, error)
|
||||||
|
|
||||||
// includeContainer evaluates whether the channel is included
|
// includeContainer evaluates whether the channel is included
|
||||||
// in the provided scope.
|
// in the provided scope.
|
||||||
|
|||||||
130
src/internal/m365/collection/groups/metadata.go
Normal file
130
src/internal/m365/collection/groups/metadata.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package groups
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseMetadataCollections produces a map of structs holding delta
|
||||||
|
// and path lookup maps.
|
||||||
|
func parseMetadataCollections(
|
||||||
|
ctx context.Context,
|
||||||
|
colls []data.RestoreCollection,
|
||||||
|
) (metadata.CatDeltaPaths, bool, error) {
|
||||||
|
// cdp stores metadata
|
||||||
|
cdp := metadata.CatDeltaPaths{
|
||||||
|
path.ChannelMessagesCategory: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// found tracks the metadata we've loaded, to make sure we don't
|
||||||
|
// fetch overlapping copies.
|
||||||
|
found := map[path.CategoryType]map[string]struct{}{
|
||||||
|
path.ChannelMessagesCategory: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// errors from metadata items should not stop the backup,
|
||||||
|
// but it should prevent us from using previous backups
|
||||||
|
errs := fault.New(true)
|
||||||
|
|
||||||
|
for _, coll := range colls {
|
||||||
|
var (
|
||||||
|
breakLoop bool
|
||||||
|
items = coll.Items(ctx, errs)
|
||||||
|
category = coll.FullPath().Category()
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, false, clues.Wrap(ctx.Err(), "parsing collection metadata").WithClues(ctx)
|
||||||
|
|
||||||
|
case item, ok := <-items:
|
||||||
|
if !ok || errs.Failure() != nil {
|
||||||
|
breakLoop = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
m = map[string]string{}
|
||||||
|
cdps, wantedCategory = cdp[category]
|
||||||
|
)
|
||||||
|
|
||||||
|
// avoid sharepoint site deltapaths
|
||||||
|
if !wantedCategory {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.NewDecoder(item.ToReader()).Decode(&m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, clues.New("decoding metadata json").WithClues(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch item.ID() {
|
||||||
|
case metadata.PreviousPathFileName:
|
||||||
|
// no-op at this time, previous paths not needed
|
||||||
|
|
||||||
|
case metadata.DeltaURLsFileName:
|
||||||
|
if _, ok := found[category][metadata.DeltaKey]; ok {
|
||||||
|
return nil, false, clues.Wrap(clues.New(category.String()), "multiple versions of delta metadata").WithClues(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, d := range m {
|
||||||
|
cdps.AddDelta(k, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
found[category][metadata.DeltaKey] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
cdp[category] = cdps
|
||||||
|
}
|
||||||
|
|
||||||
|
if breakLoop {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs.Failure() != nil {
|
||||||
|
logger.CtxErr(ctx, errs.Failure()).Info("reading metadata collection items")
|
||||||
|
|
||||||
|
return metadata.CatDeltaPaths{
|
||||||
|
path.ChannelMessagesCategory: {},
|
||||||
|
}, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not remove entries that contain only a path or a delta, but not both.
|
||||||
|
// This condition is expected. Channels only record their path. Messages
|
||||||
|
// only record their deltas.
|
||||||
|
|
||||||
|
return cdp, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// produces a set of id:path pairs from the deltapaths map.
|
||||||
|
// Each entry in the set will, if not removed, produce a collection
|
||||||
|
// that will delete the tombstone by path.
|
||||||
|
func makeTombstones(dps metadata.DeltaPaths) map[string]string {
|
||||||
|
r := make(map[string]string, len(dps))
|
||||||
|
|
||||||
|
for id, v := range dps {
|
||||||
|
r[id] = v.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathFromPrevString(ps string) (path.Path, error) {
|
||||||
|
p, err := path.FromDataLayerPath(ps, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "parsing previous path string")
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
@ -7,13 +7,13 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StubChannels(names ...string) []models.Channelable {
|
func StubChannels(ids ...string) []models.Channelable {
|
||||||
sl := make([]models.Channelable, 0, len(names))
|
sl := make([]models.Channelable, 0, len(ids))
|
||||||
|
|
||||||
for _, name := range names {
|
for _, id := range ids {
|
||||||
ch := models.NewChannel()
|
ch := models.NewChannel()
|
||||||
ch.SetDisplayName(ptr.To(name))
|
ch.SetDisplayName(ptr.To(id))
|
||||||
ch.SetId(ptr.To(uuid.NewString()))
|
ch.SetId(ptr.To(id))
|
||||||
|
|
||||||
sl = append(sl, ch)
|
sl = append(sl, ch)
|
||||||
}
|
}
|
||||||
@ -21,15 +21,15 @@ func StubChannels(names ...string) []models.Channelable {
|
|||||||
return sl
|
return sl
|
||||||
}
|
}
|
||||||
|
|
||||||
func StubChatMessages(names ...string) []models.ChatMessageable {
|
func StubChatMessages(ids ...string) []models.ChatMessageable {
|
||||||
sl := make([]models.ChatMessageable, 0, len(names))
|
sl := make([]models.ChatMessageable, 0, len(ids))
|
||||||
|
|
||||||
for _, name := range names {
|
for _, id := range ids {
|
||||||
cm := models.NewChatMessage()
|
cm := models.NewChatMessage()
|
||||||
cm.SetId(ptr.To(uuid.NewString()))
|
cm.SetId(ptr.To(uuid.NewString()))
|
||||||
|
|
||||||
body := models.NewItemBody()
|
body := models.NewItemBody()
|
||||||
body.SetContent(ptr.To(name))
|
body.SetContent(ptr.To(id))
|
||||||
|
|
||||||
cm.SetBody(body)
|
cm.SetBody(body)
|
||||||
|
|
||||||
|
|||||||
@ -34,20 +34,6 @@ const (
|
|||||||
AddtlDataRemoved = "@removed"
|
AddtlDataRemoved = "@removed"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Metadata Files
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DeltaURLsFileName is the name of the file containing delta token(s) for a
|
|
||||||
// given endpoint. The endpoint granularity varies by service.
|
|
||||||
DeltaURLsFileName = "delta"
|
|
||||||
|
|
||||||
// PreviousPathFileName is the name of the file containing previous path(s) for a
|
|
||||||
// given endpoint.
|
|
||||||
PreviousPathFileName = "previouspath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Runtime Configuration
|
// Runtime Configuration
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -37,12 +37,6 @@ const (
|
|||||||
defaultHTTPClientTimeout = 1 * time.Hour
|
defaultHTTPClientTimeout = 1 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
// AllMetadataFileNames produces the standard set of filenames used to store graph
|
|
||||||
// metadata such as delta tokens and folderID->path references.
|
|
||||||
func AllMetadataFileNames() []string {
|
|
||||||
return []string{DeltaURLsFileName, PreviousPathFileName}
|
|
||||||
}
|
|
||||||
|
|
||||||
type QueryParams struct {
|
type QueryParams struct {
|
||||||
Category path.CategoryType
|
Category path.CategoryType
|
||||||
ProtectedResource idname.Provider
|
ProtectedResource idname.Provider
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/observe"
|
"github.com/alcionai/corso/src/internal/observe"
|
||||||
"github.com/alcionai/corso/src/internal/operations/inject"
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
@ -107,7 +108,7 @@ func ProduceBackupCollections(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case path.ChannelMessagesCategory:
|
case path.ChannelMessagesCategory:
|
||||||
dbcs, err = groups.CreateCollections(
|
dbcs, canUsePreviousBackup, err = groups.CreateCollections(
|
||||||
ctx,
|
ctx,
|
||||||
bpc,
|
bpc,
|
||||||
groups.NewChannelBackupHandler(bpc.ProtectedResource.ID(), ac.Channels()),
|
groups.NewChannelBackupHandler(bpc.ProtectedResource.ID(), ac.Channels()),
|
||||||
@ -183,7 +184,7 @@ func getSitesMetadataCollection(
|
|||||||
md, err := graph.MakeMetadataCollection(
|
md, err := graph.MakeMetadataCollection(
|
||||||
p,
|
p,
|
||||||
[]graph.MetadataCollectionEntry{
|
[]graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(graph.PreviousPathFileName, sites),
|
graph.NewMetadataEntry(metadata.PreviousPathFileName, sites),
|
||||||
},
|
},
|
||||||
su)
|
su)
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
deeTD "github.com/alcionai/corso/src/pkg/backup/details/testdata"
|
deeTD "github.com/alcionai/corso/src/pkg/backup/details/testdata"
|
||||||
"github.com/alcionai/corso/src/pkg/backup/identity"
|
"github.com/alcionai/corso/src/pkg/backup/identity"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/control/repository"
|
"github.com/alcionai/corso/src/pkg/control/repository"
|
||||||
"github.com/alcionai/corso/src/pkg/extensions"
|
"github.com/alcionai/corso/src/pkg/extensions"
|
||||||
@ -1608,10 +1609,10 @@ func makeMetadataCollectionEntries(
|
|||||||
) []graph.MetadataCollectionEntry {
|
) []graph.MetadataCollectionEntry {
|
||||||
return []graph.MetadataCollectionEntry{
|
return []graph.MetadataCollectionEntry{
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.DeltaURLsFileName,
|
metadata.DeltaURLsFileName,
|
||||||
map[string]string{driveID: deltaURL}),
|
map[string]string{driveID: deltaURL}),
|
||||||
graph.NewMetadataEntry(
|
graph.NewMetadataEntry(
|
||||||
graph.PreviousPathFileName,
|
metadata.PreviousPathFileName,
|
||||||
map[string]map[string]string{
|
map[string]map[string]string{
|
||||||
driveID: {
|
driveID: {
|
||||||
folderID: p.PlainString(),
|
folderID: p.PlainString(),
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/internal/kopia"
|
"github.com/alcionai/corso/src/internal/kopia"
|
||||||
"github.com/alcionai/corso/src/internal/kopia/inject"
|
"github.com/alcionai/corso/src/internal/kopia/inject"
|
||||||
"github.com/alcionai/corso/src/internal/m365/graph"
|
|
||||||
"github.com/alcionai/corso/src/pkg/backup/identity"
|
"github.com/alcionai/corso/src/pkg/backup/identity"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
@ -63,7 +63,7 @@ func getManifestsAndMetadata(
|
|||||||
) (kopia.BackupBases, []data.RestoreCollection, bool, error) {
|
) (kopia.BackupBases, []data.RestoreCollection, bool, error) {
|
||||||
var (
|
var (
|
||||||
tags = map[string]string{kopia.TagBackupCategory: ""}
|
tags = map[string]string{kopia.TagBackupCategory: ""}
|
||||||
metadataFiles = graph.AllMetadataFileNames()
|
metadataFiles = metadata.AllMetadataFileNames()
|
||||||
collections []data.RestoreCollection
|
collections []data.RestoreCollection
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -23,8 +23,6 @@ type GroupsBackupIntgSuite struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGroupsBackupIntgSuite(t *testing.T) {
|
func TestGroupsBackupIntgSuite(t *testing.T) {
|
||||||
t.Skip("todo: enable")
|
|
||||||
|
|
||||||
suite.Run(t, &GroupsBackupIntgSuite{
|
suite.Run(t, &GroupsBackupIntgSuite{
|
||||||
Suite: tester.NewIntegrationSuite(
|
Suite: tester.NewIntegrationSuite(
|
||||||
t,
|
t,
|
||||||
|
|||||||
@ -193,7 +193,14 @@ func runAndCheckBackup(
|
|||||||
acceptNoData bool,
|
acceptNoData bool,
|
||||||
) {
|
) {
|
||||||
err := bo.Run(ctx)
|
err := bo.Run(ctx)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
if !assert.NoError(t, err, clues.ToCore(err)) {
|
||||||
|
for i, err := range bo.Errors.Recovered() {
|
||||||
|
t.Logf("recoverable err %d, %+v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Fail(t, "not allowed to error")
|
||||||
|
}
|
||||||
|
|
||||||
require.NotEmpty(t, bo.Results, "the backup had non-zero results")
|
require.NotEmpty(t, bo.Results, "the backup had non-zero results")
|
||||||
require.NotEmpty(t, bo.Results.BackupID, "the backup generated an ID")
|
require.NotEmpty(t, bo.Results.BackupID, "the backup generated an ID")
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/backup"
|
"github.com/alcionai/corso/src/pkg/backup"
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
deeTD "github.com/alcionai/corso/src/pkg/backup/details/testdata"
|
deeTD "github.com/alcionai/corso/src/pkg/backup/details/testdata"
|
||||||
|
bupMD "github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
ctrlTD "github.com/alcionai/corso/src/pkg/control/testdata"
|
ctrlTD "github.com/alcionai/corso/src/pkg/control/testdata"
|
||||||
"github.com/alcionai/corso/src/pkg/count"
|
"github.com/alcionai/corso/src/pkg/count"
|
||||||
@ -173,7 +174,7 @@ func runDriveIncrementalTest(
|
|||||||
now = dttm.FormatNow(dttm.SafeForTesting)
|
now = dttm.FormatNow(dttm.SafeForTesting)
|
||||||
|
|
||||||
categories = map[path.CategoryType][]string{
|
categories = map[path.CategoryType][]string{
|
||||||
category: {graph.DeltaURLsFileName, graph.PreviousPathFileName},
|
category: {bupMD.DeltaURLsFileName, bupMD.PreviousPathFileName},
|
||||||
}
|
}
|
||||||
container1 = fmt.Sprintf("%s%d_%s", incrementalsDestContainerPrefix, 1, now)
|
container1 = fmt.Sprintf("%s%d_%s", incrementalsDestContainerPrefix, 1, now)
|
||||||
container2 = fmt.Sprintf("%s%d_%s", incrementalsDestContainerPrefix, 2, now)
|
container2 = fmt.Sprintf("%s%d_%s", incrementalsDestContainerPrefix, 2, now)
|
||||||
@ -787,7 +788,7 @@ func (suite *OneDriveBackupIntgSuite) TestBackup_Run_oneDriveOwnerMigration() {
|
|||||||
mb = evmock.NewBus()
|
mb = evmock.NewBus()
|
||||||
|
|
||||||
categories = map[path.CategoryType][]string{
|
categories = map[path.CategoryType][]string{
|
||||||
path.FilesCategory: {graph.DeltaURLsFileName, graph.PreviousPathFileName},
|
path.FilesCategory: {bupMD.DeltaURLsFileName, bupMD.PreviousPathFileName},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -124,8 +124,13 @@ func (i *GroupsInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *GroupsInfo) updateFolder(f *FolderInfo) error {
|
func (i *GroupsInfo) updateFolder(f *FolderInfo) error {
|
||||||
if i.ItemType == SharePointLibrary {
|
f.DataType = i.ItemType
|
||||||
|
|
||||||
|
switch i.ItemType {
|
||||||
|
case SharePointLibrary:
|
||||||
return updateFolderWithinDrive(SharePointLibrary, i.DriveName, i.DriveID, f)
|
return updateFolderWithinDrive(SharePointLibrary, i.DriveName, i.DriveID, f)
|
||||||
|
case GroupsChannelMessage:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return clues.New("unsupported ItemType for GroupsInfo").With("item_type", i.ItemType)
|
return clues.New("unsupported ItemType for GroupsInfo").With("item_type", i.ItemType)
|
||||||
|
|||||||
51
src/pkg/backup/metadata/metadata.go
Normal file
51
src/pkg/backup/metadata/metadata.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
import "github.com/alcionai/corso/src/pkg/path"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DeltaURLsFileName is the name of the file containing delta token(s) for a
|
||||||
|
// given endpoint. The endpoint granularity varies by service.
|
||||||
|
DeltaURLsFileName = "delta"
|
||||||
|
|
||||||
|
// PreviousPathFileName is the name of the file containing previous path(s) for a
|
||||||
|
// given endpoint.
|
||||||
|
PreviousPathFileName = "previouspath"
|
||||||
|
|
||||||
|
PathKey = "path"
|
||||||
|
DeltaKey = "delta"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
CatDeltaPaths map[path.CategoryType]DeltaPaths
|
||||||
|
DeltaPaths map[string]DeltaPath
|
||||||
|
DeltaPath struct {
|
||||||
|
Delta string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (dps DeltaPaths) AddDelta(k, d string) {
|
||||||
|
dp, ok := dps[k]
|
||||||
|
if !ok {
|
||||||
|
dp = DeltaPath{}
|
||||||
|
}
|
||||||
|
|
||||||
|
dp.Delta = d
|
||||||
|
dps[k] = dp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dps DeltaPaths) AddPath(k, p string) {
|
||||||
|
dp, ok := dps[k]
|
||||||
|
if !ok {
|
||||||
|
dp = DeltaPath{}
|
||||||
|
}
|
||||||
|
|
||||||
|
dp.Path = p
|
||||||
|
dps[k] = dp
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllMetadataFileNames produces the standard set of filenames used to store graph
|
||||||
|
// metadata such as delta tokens and folderID->path references.
|
||||||
|
func AllMetadataFileNames() []string {
|
||||||
|
return []string{DeltaURLsFileName, PreviousPathFileName}
|
||||||
|
}
|
||||||
@ -26,7 +26,7 @@ const (
|
|||||||
LibrariesCategory CategoryType = 6 // libraries
|
LibrariesCategory CategoryType = 6 // libraries
|
||||||
PagesCategory CategoryType = 7 // pages
|
PagesCategory CategoryType = 7 // pages
|
||||||
DetailsCategory CategoryType = 8 // details
|
DetailsCategory CategoryType = 8 // details
|
||||||
ChannelMessagesCategory CategoryType = 9 // channel messages
|
ChannelMessagesCategory CategoryType = 9 // channelMessages
|
||||||
)
|
)
|
||||||
|
|
||||||
func ToCategoryType(category string) CategoryType {
|
func ToCategoryType(category string) CategoryType {
|
||||||
|
|||||||
@ -20,9 +20,9 @@ func _() {
|
|||||||
_ = x[ChannelMessagesCategory-9]
|
_ = x[ChannelMessagesCategory-9]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _CategoryType_name = "UnknownCategoryemailcontactseventsfileslistslibrariespagesdetailschannel messages"
|
const _CategoryType_name = "UnknownCategoryemailcontactseventsfileslistslibrariespagesdetailschannelMessages"
|
||||||
|
|
||||||
var _CategoryType_index = [...]uint8{0, 15, 20, 28, 34, 39, 44, 53, 58, 65, 81}
|
var _CategoryType_index = [...]uint8{0, 15, 20, 28, 34, 39, 44, 53, 58, 65, 80}
|
||||||
|
|
||||||
func (i CategoryType) String() string {
|
func (i CategoryType) String() string {
|
||||||
if i < 0 || i >= CategoryType(len(_CategoryType_index)-1) {
|
if i < 0 || i >= CategoryType(len(_CategoryType_index)-1) {
|
||||||
|
|||||||
@ -159,6 +159,8 @@ func ChannelMessageInfo(
|
|||||||
from := msg.GetFrom()
|
from := msg.GetFrom()
|
||||||
|
|
||||||
switch true {
|
switch true {
|
||||||
|
case from == nil:
|
||||||
|
// not all messages have a populated 'from'. Namely, system messages do not.
|
||||||
case from.GetApplication() != nil:
|
case from.GetApplication() != nil:
|
||||||
msgCreator = ptr.Val(from.GetApplication().GetDisplayName())
|
msgCreator = ptr.Val(from.GetApplication().GetDisplayName())
|
||||||
case from.GetDevice() != nil:
|
case from.GetDevice() != nil:
|
||||||
|
|||||||
@ -86,12 +86,14 @@ func (c Channels) NewChannelMessageDeltaPager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetChannelMessageIDsDelta fetches a delta of all messages in the channel.
|
// GetChannelMessageIDsDelta fetches a delta of all messages in the channel.
|
||||||
|
// returns two maps: addedItems, deletedItems
|
||||||
func (c Channels) GetChannelMessageIDsDelta(
|
func (c Channels) GetChannelMessageIDsDelta(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
teamID, channelID, prevDelta string,
|
teamID, channelID, prevDelta string,
|
||||||
) (map[string]struct{}, DeltaUpdate, error) {
|
) (map[string]struct{}, map[string]struct{}, DeltaUpdate, error) {
|
||||||
var (
|
var (
|
||||||
vs = map[string]struct{}{}
|
added = map[string]struct{}{}
|
||||||
|
deleted = map[string]struct{}{}
|
||||||
// select is not currently allowed on messages
|
// select is not currently allowed on messages
|
||||||
// this func will still isolate to the ID, however,
|
// this func will still isolate to the ID, however,
|
||||||
// because we need the follow-up get request to gather
|
// because we need the follow-up get request to gather
|
||||||
@ -109,7 +111,8 @@ func (c Channels) GetChannelMessageIDsDelta(
|
|||||||
logger.Ctx(ctx).Infow("Invalid previous delta", "delta_link", prevDelta)
|
logger.Ctx(ctx).Infow("Invalid previous delta", "delta_link", prevDelta)
|
||||||
|
|
||||||
invalidPrevDelta = true
|
invalidPrevDelta = true
|
||||||
vs = map[string]struct{}{}
|
added = map[string]struct{}{}
|
||||||
|
deleted = map[string]struct{}{}
|
||||||
|
|
||||||
pager.Reset(ctx)
|
pager.Reset(ctx)
|
||||||
|
|
||||||
@ -117,16 +120,20 @@ func (c Channels) GetChannelMessageIDsDelta(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, DeltaUpdate{}, graph.Wrap(ctx, err, "retrieving page of channel messages")
|
return nil, nil, DeltaUpdate{}, graph.Wrap(ctx, err, "retrieving page of channel messages")
|
||||||
}
|
}
|
||||||
|
|
||||||
vals, err := pager.ValuesIn(page)
|
vals, err := pager.ValuesIn(page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, DeltaUpdate{}, graph.Wrap(ctx, err, "extracting channel messages from response")
|
return nil, nil, DeltaUpdate{}, graph.Wrap(ctx, err, "extracting channel messages from response")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range vals {
|
for _, v := range vals {
|
||||||
vs[ptr.Val(v.GetId())] = struct{}{}
|
if v.GetAdditionalData()[graph.AddtlDataRemoved] == nil {
|
||||||
|
added[ptr.Val(v.GetId())] = struct{}{}
|
||||||
|
} else {
|
||||||
|
deleted[ptr.Val(v.GetId())] = struct{}{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nextLink, deltaLink := NextAndDeltaLink(page)
|
nextLink, deltaLink := NextAndDeltaLink(page)
|
||||||
@ -142,14 +149,14 @@ func (c Channels) GetChannelMessageIDsDelta(
|
|||||||
pager.SetNext(nextLink)
|
pager.SetNext(nextLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Ctx(ctx).Debugf("retrieved %d channel messages", len(vs))
|
logger.Ctx(ctx).Debugf("retrieved %d channel messages", len(added))
|
||||||
|
|
||||||
du := DeltaUpdate{
|
du := DeltaUpdate{
|
||||||
URL: newDeltaLink,
|
URL: newDeltaLink,
|
||||||
Reset: invalidPrevDelta,
|
Reset: invalidPrevDelta,
|
||||||
}
|
}
|
||||||
|
|
||||||
return vs, du, nil
|
return added, deleted, du, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -56,27 +56,28 @@ func (suite *ChannelsPagerIntgSuite) TestEnumerateChannelMessages() {
|
|||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
msgIDs, du, err := ac.GetChannelMessageIDsDelta(
|
addedIDs, _, du, err := ac.GetChannelMessageIDsDelta(
|
||||||
ctx,
|
ctx,
|
||||||
suite.its.group.id,
|
suite.its.group.id,
|
||||||
suite.its.group.testContainerID,
|
suite.its.group.testContainerID,
|
||||||
"")
|
"")
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
require.NotEmpty(t, msgIDs)
|
require.NotEmpty(t, addedIDs)
|
||||||
require.NotZero(t, du.URL, "delta link")
|
require.NotZero(t, du.URL, "delta link")
|
||||||
require.True(t, du.Reset, "reset due to empty prev delta link")
|
require.True(t, du.Reset, "reset due to empty prev delta link")
|
||||||
|
|
||||||
msgIDs, du, err = ac.GetChannelMessageIDsDelta(
|
addedIDs, deletedIDs, du, err := ac.GetChannelMessageIDsDelta(
|
||||||
ctx,
|
ctx,
|
||||||
suite.its.group.id,
|
suite.its.group.id,
|
||||||
suite.its.group.testContainerID,
|
suite.its.group.testContainerID,
|
||||||
du.URL)
|
du.URL)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
require.Empty(t, msgIDs, "should have no new messages from delta")
|
require.Empty(t, addedIDs, "should have no new messages from delta")
|
||||||
|
require.Empty(t, deletedIDs, "should have no deleted messages from delta")
|
||||||
require.NotZero(t, du.URL, "delta link")
|
require.NotZero(t, du.URL, "delta link")
|
||||||
require.False(t, du.Reset, "prev delta link should be valid")
|
require.False(t, du.Reset, "prev delta link should be valid")
|
||||||
|
|
||||||
for id := range msgIDs {
|
for id := range addedIDs {
|
||||||
suite.Run(id+"-replies", func() {
|
suite.Run(id+"-replies", func() {
|
||||||
testEnumerateChannelMessageReplies(
|
testEnumerateChannelMessageReplies(
|
||||||
suite.T(),
|
suite.T(),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user