Handle previous paths for group mailbox (#5154)
<!-- PR description--> * Tombstone collections weren't being added because we were not processing prev paths for group mailbox category. * Hence deleted conversations were being carried forward even if `--disable-incrementals` was used. --- #### Does this PR need a docs update or release note? - [ ] ✅ Yes, it's included - [ ] 🕐 Yes, but in a later PR - [x] ⛔ No #### Type of change <!--- Please check the type of change your PR introduces: ---> - [ ] 🌻 Feature - [x] 🐛 Bugfix - [ ] 🗺️ Documentation - [ ] 🤖 Supportability/Tests - [ ] 💻 CI/Deployment - [ ] 🧹 Tech Debt/Cleanup #### Issue(s) <!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. --> * #<issue> #### Test Plan <!-- How will this be tested prior to merging.--> - [x] 💪 Manual - [x] ⚡ Unit test - [ ] 💚 E2E
This commit is contained in:
parent
de22131b23
commit
53a0525bfd
@ -39,22 +39,24 @@ import (
|
|||||||
// mocks
|
// mocks
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
var _ backupHandler[models.Channelable, models.ChatMessageable] = &mockBackupHandler{}
|
var _ backupHandler[models.Channelable, models.ChatMessageable] = &mockChannelsBH{}
|
||||||
|
|
||||||
//lint:ignore U1000 false linter issue due to generics
|
//lint:ignore U1000 false linter issue due to generics
|
||||||
type mockBackupHandler struct {
|
type mockChannelsBH struct {
|
||||||
channels []models.Channelable
|
channels []models.Channelable
|
||||||
|
conversations []models.Conversationable
|
||||||
messageIDs []string
|
messageIDs []string
|
||||||
deletedMsgIDs []string
|
deletedMsgIDs []string
|
||||||
messagesErr error
|
messagesErr error
|
||||||
messages map[string]models.ChatMessageable
|
messages map[string]models.ChatMessageable
|
||||||
|
posts map[string]models.Postable
|
||||||
info map[string]*details.GroupsInfo
|
info map[string]*details.GroupsInfo
|
||||||
getMessageErr map[string]error
|
getMessageErr map[string]error
|
||||||
doNotInclude bool
|
doNotInclude bool
|
||||||
}
|
}
|
||||||
|
|
||||||
//lint:ignore U1000 false linter issue due to generics
|
//lint:ignore U1000 false linter issue due to generics
|
||||||
func (bh mockBackupHandler) augmentItemInfo(
|
func (bh mockChannelsBH) augmentItemInfo(
|
||||||
*details.GroupsInfo,
|
*details.GroupsInfo,
|
||||||
models.Channelable,
|
models.Channelable,
|
||||||
) {
|
) {
|
||||||
@ -62,15 +64,15 @@ func (bh mockBackupHandler) augmentItemInfo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
//lint:ignore U1000 false linter issue due to generics
|
//lint:ignore U1000 false linter issue due to generics
|
||||||
func (bh mockBackupHandler) supportsItemMetadata() bool {
|
func (bh mockChannelsBH) supportsItemMetadata() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bh mockBackupHandler) canMakeDeltaQueries() bool {
|
func (bh mockChannelsBH) canMakeDeltaQueries() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bh mockBackupHandler) containers() []container[models.Channelable] {
|
func (bh mockChannelsBH) containers() []container[models.Channelable] {
|
||||||
containers := make([]container[models.Channelable], 0, len(bh.channels))
|
containers := make([]container[models.Channelable], 0, len(bh.channels))
|
||||||
|
|
||||||
for _, ch := range bh.channels {
|
for _, ch := range bh.channels {
|
||||||
@ -81,14 +83,14 @@ func (bh mockBackupHandler) containers() []container[models.Channelable] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//lint:ignore U1000 required for interface compliance
|
//lint:ignore U1000 required for interface compliance
|
||||||
func (bh mockBackupHandler) getContainers(
|
func (bh mockChannelsBH) getContainers(
|
||||||
context.Context,
|
context.Context,
|
||||||
api.CallConfig,
|
api.CallConfig,
|
||||||
) ([]container[models.Channelable], error) {
|
) ([]container[models.Channelable], error) {
|
||||||
return bh.containers(), nil
|
return bh.containers(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bh mockBackupHandler) getContainerItemIDs(
|
func (bh mockChannelsBH) getContainerItemIDs(
|
||||||
_ context.Context,
|
_ context.Context,
|
||||||
_ path.Elements,
|
_ path.Elements,
|
||||||
_ string,
|
_ string,
|
||||||
@ -111,14 +113,14 @@ func (bh mockBackupHandler) getContainerItemIDs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
//lint:ignore U1000 required for interface compliance
|
//lint:ignore U1000 required for interface compliance
|
||||||
func (bh mockBackupHandler) includeContainer(
|
func (bh mockChannelsBH) includeContainer(
|
||||||
models.Channelable,
|
models.Channelable,
|
||||||
selectors.GroupsScope,
|
selectors.GroupsScope,
|
||||||
) bool {
|
) bool {
|
||||||
return !bh.doNotInclude
|
return !bh.doNotInclude
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bh mockBackupHandler) canonicalPath(
|
func (bh mockChannelsBH) canonicalPath(
|
||||||
storageDirFolders path.Elements,
|
storageDirFolders path.Elements,
|
||||||
tenantID string,
|
tenantID string,
|
||||||
) (path.Path, error) {
|
) (path.Path, error) {
|
||||||
@ -133,7 +135,7 @@ func (bh mockBackupHandler) canonicalPath(
|
|||||||
}
|
}
|
||||||
|
|
||||||
//lint:ignore U1000 false linter issue due to generics
|
//lint:ignore U1000 false linter issue due to generics
|
||||||
func (bh mockBackupHandler) getItem(
|
func (bh mockChannelsBH) getItem(
|
||||||
_ context.Context,
|
_ context.Context,
|
||||||
_ string,
|
_ string,
|
||||||
_ path.Elements,
|
_ path.Elements,
|
||||||
@ -142,7 +144,7 @@ func (bh mockBackupHandler) getItem(
|
|||||||
return bh.messages[itemID], bh.info[itemID], bh.getMessageErr[itemID]
|
return bh.messages[itemID], bh.info[itemID], bh.getMessageErr[itemID]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bh mockBackupHandler) getItemMetadata(
|
func (bh mockChannelsBH) getItemMetadata(
|
||||||
_ context.Context,
|
_ context.Context,
|
||||||
_ models.Channelable,
|
_ models.Channelable,
|
||||||
) (io.ReadCloser, int, error) {
|
) (io.ReadCloser, int, error) {
|
||||||
@ -150,7 +152,7 @@ func (bh mockBackupHandler) getItemMetadata(
|
|||||||
}
|
}
|
||||||
|
|
||||||
//lint:ignore U1000 false linter issue due to generics
|
//lint:ignore U1000 false linter issue due to generics
|
||||||
func (bh mockBackupHandler) makeTombstones(
|
func (bh mockChannelsBH) makeTombstones(
|
||||||
dps metadata.DeltaPaths,
|
dps metadata.DeltaPaths,
|
||||||
) (map[string]string, error) {
|
) (map[string]string, error) {
|
||||||
return makeTombstones(dps), nil
|
return makeTombstones(dps), nil
|
||||||
@ -176,6 +178,10 @@ func (suite *BackupUnitSuite) SetupSuite() {
|
|||||||
suite.creds = m365
|
suite.creds = m365
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Channels tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func (suite *BackupUnitSuite) TestPopulateCollections() {
|
func (suite *BackupUnitSuite) TestPopulateCollections() {
|
||||||
var (
|
var (
|
||||||
qp = graph.QueryParams{
|
qp = graph.QueryParams{
|
||||||
@ -188,7 +194,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
mock mockBackupHandler
|
mock mockChannelsBH
|
||||||
expectErr require.ErrorAssertionFunc
|
expectErr require.ErrorAssertionFunc
|
||||||
expectColls int
|
expectColls int
|
||||||
expectNewColls int
|
expectNewColls int
|
||||||
@ -196,7 +202,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "happy path, one container",
|
name: "happy path, one container",
|
||||||
mock: mockBackupHandler{
|
mock: mockChannelsBH{
|
||||||
channels: testdata.StubChannels("one"),
|
channels: testdata.StubChannels("one"),
|
||||||
messageIDs: []string{"msg-one"},
|
messageIDs: []string{"msg-one"},
|
||||||
},
|
},
|
||||||
@ -207,7 +213,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "happy path, one container, only deleted messages",
|
name: "happy path, one container, only deleted messages",
|
||||||
mock: mockBackupHandler{
|
mock: mockChannelsBH{
|
||||||
channels: testdata.StubChannels("one"),
|
channels: testdata.StubChannels("one"),
|
||||||
deletedMsgIDs: []string{"msg-one"},
|
deletedMsgIDs: []string{"msg-one"},
|
||||||
},
|
},
|
||||||
@ -218,7 +224,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "happy path, many containers",
|
name: "happy path, many containers",
|
||||||
mock: mockBackupHandler{
|
mock: mockChannelsBH{
|
||||||
channels: testdata.StubChannels("one", "two"),
|
channels: testdata.StubChannels("one", "two"),
|
||||||
messageIDs: []string{"msg-one"},
|
messageIDs: []string{"msg-one"},
|
||||||
},
|
},
|
||||||
@ -229,7 +235,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no containers pass scope",
|
name: "no containers pass scope",
|
||||||
mock: mockBackupHandler{
|
mock: mockChannelsBH{
|
||||||
channels: testdata.StubChannels("one"),
|
channels: testdata.StubChannels("one"),
|
||||||
doNotInclude: true,
|
doNotInclude: true,
|
||||||
},
|
},
|
||||||
@ -240,7 +246,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no channels",
|
name: "no channels",
|
||||||
mock: mockBackupHandler{},
|
mock: mockChannelsBH{},
|
||||||
expectErr: require.NoError,
|
expectErr: require.NoError,
|
||||||
expectColls: 1,
|
expectColls: 1,
|
||||||
expectNewColls: 0,
|
expectNewColls: 0,
|
||||||
@ -248,7 +254,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no channel messages",
|
name: "no channel messages",
|
||||||
mock: mockBackupHandler{
|
mock: mockChannelsBH{
|
||||||
channels: testdata.StubChannels("one"),
|
channels: testdata.StubChannels("one"),
|
||||||
},
|
},
|
||||||
expectErr: require.NoError,
|
expectErr: require.NoError,
|
||||||
@ -258,7 +264,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "err: deleted in flight",
|
name: "err: deleted in flight",
|
||||||
mock: mockBackupHandler{
|
mock: mockChannelsBH{
|
||||||
channels: testdata.StubChannels("one"),
|
channels: testdata.StubChannels("one"),
|
||||||
messagesErr: core.ErrNotFound,
|
messagesErr: core.ErrNotFound,
|
||||||
},
|
},
|
||||||
@ -269,7 +275,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "err: other error",
|
name: "err: other error",
|
||||||
mock: mockBackupHandler{
|
mock: mockChannelsBH{
|
||||||
channels: testdata.StubChannels("one"),
|
channels: testdata.StubChannels("one"),
|
||||||
messagesErr: assert.AnError,
|
messagesErr: assert.AnError,
|
||||||
},
|
},
|
||||||
@ -348,7 +354,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
|
|||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
mock mockBackupHandler
|
mock mockChannelsBH
|
||||||
deltaPaths metadata.DeltaPaths
|
deltaPaths metadata.DeltaPaths
|
||||||
expectErr require.ErrorAssertionFunc
|
expectErr require.ErrorAssertionFunc
|
||||||
expectColls int
|
expectColls int
|
||||||
@ -358,7 +364,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "non incremental",
|
name: "non incremental",
|
||||||
mock: mockBackupHandler{
|
mock: mockChannelsBH{
|
||||||
channels: testdata.StubChannels("chan"),
|
channels: testdata.StubChannels("chan"),
|
||||||
messageIDs: []string{"msg"},
|
messageIDs: []string{"msg"},
|
||||||
},
|
},
|
||||||
@ -371,7 +377,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "incremental",
|
name: "incremental",
|
||||||
mock: mockBackupHandler{
|
mock: mockChannelsBH{
|
||||||
channels: testdata.StubChannels("chan"),
|
channels: testdata.StubChannels("chan"),
|
||||||
deletedMsgIDs: []string{"msg"},
|
deletedMsgIDs: []string{"msg"},
|
||||||
},
|
},
|
||||||
@ -389,7 +395,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "incremental no new messages",
|
name: "incremental no new messages",
|
||||||
mock: mockBackupHandler{
|
mock: mockChannelsBH{
|
||||||
channels: testdata.StubChannels("chan"),
|
channels: testdata.StubChannels("chan"),
|
||||||
},
|
},
|
||||||
deltaPaths: metadata.DeltaPaths{
|
deltaPaths: metadata.DeltaPaths{
|
||||||
@ -406,7 +412,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "incremental deleted channel",
|
name: "incremental deleted channel",
|
||||||
mock: mockBackupHandler{
|
mock: mockChannelsBH{
|
||||||
channels: testdata.StubChannels(),
|
channels: testdata.StubChannels(),
|
||||||
},
|
},
|
||||||
deltaPaths: metadata.DeltaPaths{
|
deltaPaths: metadata.DeltaPaths{
|
||||||
@ -423,7 +429,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "incremental new and deleted channel",
|
name: "incremental new and deleted channel",
|
||||||
mock: mockBackupHandler{
|
mock: mockChannelsBH{
|
||||||
channels: testdata.StubChannels("chan2"),
|
channels: testdata.StubChannels("chan2"),
|
||||||
messageIDs: []string{"msg"},
|
messageIDs: []string{"msg"},
|
||||||
},
|
},
|
||||||
@ -493,6 +499,465 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Conversations tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var _ backupHandler[models.Conversationable, models.Postable] = &mockConversationsBH{}
|
||||||
|
|
||||||
|
//lint:ignore U1000 false linter issue due to generics
|
||||||
|
type mockConversationsBH struct {
|
||||||
|
conversations []models.Conversationable
|
||||||
|
// Assume all conversations have the same thread object under them for simplicty.
|
||||||
|
// It doesn't impact the tests.
|
||||||
|
thread models.ConversationThreadable
|
||||||
|
postIDs []string
|
||||||
|
deletedPostIDs []string
|
||||||
|
PostsErr error
|
||||||
|
Posts map[string]models.Postable
|
||||||
|
info map[string]*details.GroupsInfo
|
||||||
|
getPostErr map[string]error
|
||||||
|
doNotInclude bool
|
||||||
|
}
|
||||||
|
|
||||||
|
//lint:ignore U1000 false linter issue due to generics
|
||||||
|
func (bh mockConversationsBH) augmentItemInfo(
|
||||||
|
*details.GroupsInfo,
|
||||||
|
models.Conversationable,
|
||||||
|
) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bh mockConversationsBH) canMakeDeltaQueries() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bh mockConversationsBH) containers() []container[models.Conversationable] {
|
||||||
|
containers := make([]container[models.Conversationable], 0, len(bh.conversations))
|
||||||
|
|
||||||
|
for _, ch := range bh.conversations {
|
||||||
|
containers = append(containers, conversationThreadContainer(ch, bh.thread))
|
||||||
|
}
|
||||||
|
|
||||||
|
return containers
|
||||||
|
}
|
||||||
|
|
||||||
|
//lint:ignore U1000 required for interface compliance
|
||||||
|
func (bh mockConversationsBH) getContainers(
|
||||||
|
context.Context,
|
||||||
|
api.CallConfig,
|
||||||
|
) ([]container[models.Conversationable], error) {
|
||||||
|
return bh.containers(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bh mockConversationsBH) getContainerItemIDs(
|
||||||
|
_ context.Context,
|
||||||
|
_ path.Elements,
|
||||||
|
_ string,
|
||||||
|
_ api.CallConfig,
|
||||||
|
) (pagers.AddedAndRemoved, error) {
|
||||||
|
idRes := make(map[string]time.Time, len(bh.postIDs))
|
||||||
|
|
||||||
|
for _, id := range bh.postIDs {
|
||||||
|
idRes[id] = time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
aar := pagers.AddedAndRemoved{
|
||||||
|
Added: idRes,
|
||||||
|
Removed: bh.deletedPostIDs,
|
||||||
|
ValidModTimes: true,
|
||||||
|
DU: pagers.DeltaUpdate{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return aar, bh.PostsErr
|
||||||
|
}
|
||||||
|
|
||||||
|
//lint:ignore U1000 required for interface compliance
|
||||||
|
func (bh mockConversationsBH) includeContainer(
|
||||||
|
models.Conversationable,
|
||||||
|
selectors.GroupsScope,
|
||||||
|
) bool {
|
||||||
|
return !bh.doNotInclude
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bh mockConversationsBH) canonicalPath(
|
||||||
|
storageDirFolders path.Elements,
|
||||||
|
tenantID string,
|
||||||
|
) (path.Path, error) {
|
||||||
|
return storageDirFolders.
|
||||||
|
Builder().
|
||||||
|
ToDataLayerPath(
|
||||||
|
tenantID,
|
||||||
|
"protectedResource",
|
||||||
|
path.GroupsService,
|
||||||
|
path.ConversationPostsCategory,
|
||||||
|
false)
|
||||||
|
}
|
||||||
|
|
||||||
|
//lint:ignore U1000 false linter issue due to generics
|
||||||
|
func (bh mockConversationsBH) getItem(
|
||||||
|
_ context.Context,
|
||||||
|
_ string,
|
||||||
|
_ path.Elements,
|
||||||
|
itemID string,
|
||||||
|
) (models.Postable, *details.GroupsInfo, error) {
|
||||||
|
return bh.Posts[itemID], bh.info[itemID], bh.getPostErr[itemID]
|
||||||
|
}
|
||||||
|
|
||||||
|
//lint:ignore U1000 false linter issue due to generics
|
||||||
|
func (bh mockConversationsBH) supportsItemMetadata() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bh mockConversationsBH) getItemMetadata(
|
||||||
|
_ context.Context,
|
||||||
|
_ models.Conversationable,
|
||||||
|
) (io.ReadCloser, int, error) {
|
||||||
|
return nil, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//lint:ignore U1000 false linter issue due to generics
|
||||||
|
func (bh mockConversationsBH) makeTombstones(
|
||||||
|
dps metadata.DeltaPaths,
|
||||||
|
) (map[string]string, error) {
|
||||||
|
r := make(map[string]string, len(dps))
|
||||||
|
|
||||||
|
for id, v := range dps {
|
||||||
|
elems := path.Split(id)
|
||||||
|
if len(elems) != 2 {
|
||||||
|
return nil, clues.New("invalid prev path")
|
||||||
|
}
|
||||||
|
|
||||||
|
r[elems[0]] = v.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupUnitSuite) TestPopulateCollections_Conversations() {
|
||||||
|
var (
|
||||||
|
qp = graph.QueryParams{
|
||||||
|
Category: path.ConversationPostsCategory, // doesn't matter which one we use.
|
||||||
|
ProtectedResource: inMock.NewProvider("group_id", "user_name"),
|
||||||
|
TenantID: suite.creds.AzureTenantID,
|
||||||
|
}
|
||||||
|
statusUpdater = func(*support.ControllerOperationStatus) {}
|
||||||
|
)
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
mock mockConversationsBH
|
||||||
|
expectErr require.ErrorAssertionFunc
|
||||||
|
expectColls int
|
||||||
|
expectNewColls int
|
||||||
|
expectMetadataColls int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path, one container",
|
||||||
|
mock: mockConversationsBH{
|
||||||
|
conversations: testdata.StubConversations("one"),
|
||||||
|
thread: testdata.StubConversationThreads("t-one")[0],
|
||||||
|
postIDs: []string{"msg-one"},
|
||||||
|
},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 2,
|
||||||
|
expectNewColls: 1,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy path, one container, only deleted messages",
|
||||||
|
mock: mockConversationsBH{
|
||||||
|
conversations: testdata.StubConversations("one"),
|
||||||
|
thread: testdata.StubConversationThreads("t-one")[0],
|
||||||
|
deletedPostIDs: []string{"msg-one"},
|
||||||
|
},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 2,
|
||||||
|
expectNewColls: 1,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy path, many containers",
|
||||||
|
mock: mockConversationsBH{
|
||||||
|
conversations: testdata.StubConversations("one", "two"),
|
||||||
|
thread: testdata.StubConversationThreads("t-one")[0],
|
||||||
|
postIDs: []string{"msg-one"},
|
||||||
|
},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 3,
|
||||||
|
expectNewColls: 2,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no containers pass scope",
|
||||||
|
mock: mockConversationsBH{
|
||||||
|
conversations: testdata.StubConversations("one"),
|
||||||
|
thread: testdata.StubConversationThreads("t-one")[0],
|
||||||
|
doNotInclude: true,
|
||||||
|
},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 1,
|
||||||
|
expectNewColls: 0,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no conversations",
|
||||||
|
mock: mockConversationsBH{},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 1,
|
||||||
|
expectNewColls: 0,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no conv posts",
|
||||||
|
mock: mockConversationsBH{
|
||||||
|
conversations: testdata.StubConversations("one"),
|
||||||
|
thread: testdata.StubConversationThreads("t-one")[0],
|
||||||
|
},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 2,
|
||||||
|
expectNewColls: 1,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "err: deleted in flight",
|
||||||
|
mock: mockConversationsBH{
|
||||||
|
conversations: testdata.StubConversations("one"),
|
||||||
|
thread: testdata.StubConversationThreads("t-one")[0],
|
||||||
|
PostsErr: core.ErrNotFound,
|
||||||
|
},
|
||||||
|
expectErr: require.Error,
|
||||||
|
expectColls: 1,
|
||||||
|
expectNewColls: 0,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "err: other error",
|
||||||
|
mock: mockConversationsBH{
|
||||||
|
conversations: testdata.StubConversations("one"),
|
||||||
|
thread: testdata.StubConversationThreads("t-one")[0],
|
||||||
|
PostsErr: assert.AnError,
|
||||||
|
},
|
||||||
|
expectErr: require.Error,
|
||||||
|
expectColls: 1,
|
||||||
|
expectNewColls: 0,
|
||||||
|
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.containers(),
|
||||||
|
selectors.NewGroupsBackup(nil).Channels(selectors.Any())[0],
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
ctrlOpts,
|
||||||
|
count.New(),
|
||||||
|
fault.New(true))
|
||||||
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
|
assert.Len(t, collections, test.expectColls, "number of collections")
|
||||||
|
|
||||||
|
// collection assertions
|
||||||
|
|
||||||
|
deleteds, news, metadatas, doNotMerges := 0, 0, 0, 0
|
||||||
|
for _, c := range collections {
|
||||||
|
if c.FullPath().Service() == path.GroupsMetadataService {
|
||||||
|
metadatas++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.State() == data.DeletedState {
|
||||||
|
deleteds++
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.State() == data.NewState {
|
||||||
|
news++
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DoNotMergeItems() {
|
||||||
|
doNotMerges++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Zero(t, deleteds, "deleted collections")
|
||||||
|
assert.Equal(t, test.expectNewColls, news, "new collections")
|
||||||
|
assert.Equal(t, test.expectMetadataColls, metadatas, "metadata collections")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupUnitSuite) TestPopulateCollections_ConversationsIncremental() {
|
||||||
|
var (
|
||||||
|
qp = graph.QueryParams{
|
||||||
|
Category: path.ConversationPostsCategory, // 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).Conversation(selectors.Any())[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
convPath, err := path.Build("t", "g", path.GroupsService, path.ConversationPostsCategory, false, "conv0", "thread0")
|
||||||
|
require.NoError(suite.T(), err, clues.ToCore(err))
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
mock mockConversationsBH
|
||||||
|
deltaPaths metadata.DeltaPaths
|
||||||
|
expectErr require.ErrorAssertionFunc
|
||||||
|
expectColls int
|
||||||
|
expectNewColls int
|
||||||
|
expectTombstoneCols int
|
||||||
|
expectMetadataColls int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "non incremental",
|
||||||
|
mock: mockConversationsBH{
|
||||||
|
conversations: testdata.StubConversations("conv0"),
|
||||||
|
thread: testdata.StubConversationThreads("t0")[0],
|
||||||
|
postIDs: []string{"msg"},
|
||||||
|
},
|
||||||
|
deltaPaths: metadata.DeltaPaths{},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 2,
|
||||||
|
expectNewColls: 1,
|
||||||
|
expectTombstoneCols: 0,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incremental",
|
||||||
|
mock: mockConversationsBH{
|
||||||
|
conversations: testdata.StubConversations("conv0"),
|
||||||
|
thread: testdata.StubConversationThreads("t0")[0],
|
||||||
|
postIDs: []string{"msg"},
|
||||||
|
},
|
||||||
|
deltaPaths: metadata.DeltaPaths{
|
||||||
|
"conv0/thread0": {
|
||||||
|
Path: convPath.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 2,
|
||||||
|
expectNewColls: 1, // No delta support
|
||||||
|
expectTombstoneCols: 0,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incremental no new posts",
|
||||||
|
mock: mockConversationsBH{
|
||||||
|
conversations: testdata.StubConversations("conv0"),
|
||||||
|
thread: testdata.StubConversationThreads("t0")[0],
|
||||||
|
},
|
||||||
|
deltaPaths: metadata.DeltaPaths{
|
||||||
|
"conv0/thread0": {
|
||||||
|
Path: convPath.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 2,
|
||||||
|
expectNewColls: 1, // No delta support
|
||||||
|
expectTombstoneCols: 0,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incremental deleted conversation",
|
||||||
|
mock: mockConversationsBH{
|
||||||
|
conversations: testdata.StubConversations(),
|
||||||
|
},
|
||||||
|
deltaPaths: metadata.DeltaPaths{
|
||||||
|
"conv0/thread0": {
|
||||||
|
Path: convPath.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: require.NoError,
|
||||||
|
expectColls: 2,
|
||||||
|
expectNewColls: 0,
|
||||||
|
expectTombstoneCols: 1,
|
||||||
|
expectMetadataColls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incremental new and deleted conversations",
|
||||||
|
mock: mockConversationsBH{
|
||||||
|
conversations: testdata.StubConversations("conv1"),
|
||||||
|
thread: testdata.StubConversationThreads("t1")[0],
|
||||||
|
postIDs: []string{"msg"},
|
||||||
|
},
|
||||||
|
deltaPaths: metadata.DeltaPaths{
|
||||||
|
"conv0/thread0": {
|
||||||
|
Path: convPath.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.containers(),
|
||||||
|
allScope,
|
||||||
|
test.deltaPaths,
|
||||||
|
false,
|
||||||
|
ctrlOpts,
|
||||||
|
count.New(),
|
||||||
|
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
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -21,13 +21,15 @@ func parseMetadataCollections(
|
|||||||
) (metadata.CatDeltaPaths, bool, error) {
|
) (metadata.CatDeltaPaths, bool, error) {
|
||||||
// cdp stores metadata
|
// cdp stores metadata
|
||||||
cdp := metadata.CatDeltaPaths{
|
cdp := metadata.CatDeltaPaths{
|
||||||
path.ChannelMessagesCategory: {},
|
path.ChannelMessagesCategory: {},
|
||||||
|
path.ConversationPostsCategory: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// found tracks the metadata we've loaded, to make sure we don't
|
// found tracks the metadata we've loaded, to make sure we don't
|
||||||
// fetch overlapping copies.
|
// fetch overlapping copies.
|
||||||
found := map[path.CategoryType]map[string]struct{}{
|
found := map[path.CategoryType]map[string]struct{}{
|
||||||
path.ChannelMessagesCategory: {},
|
path.ChannelMessagesCategory: {},
|
||||||
|
path.ConversationPostsCategory: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// errors from metadata items should not stop the backup,
|
// errors from metadata items should not stop the backup,
|
||||||
@ -104,7 +106,8 @@ func parseMetadataCollections(
|
|||||||
logger.CtxErr(ctx, errs.Failure()).Info("reading metadata collection items")
|
logger.CtxErr(ctx, errs.Failure()).Info("reading metadata collection items")
|
||||||
|
|
||||||
return metadata.CatDeltaPaths{
|
return metadata.CatDeltaPaths{
|
||||||
path.ChannelMessagesCategory: {},
|
path.ChannelMessagesCategory: {},
|
||||||
|
path.ConversationPostsCategory: {},
|
||||||
}, false, nil
|
}, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
52
src/internal/m365/collection/groups/testdata/conversations.go
vendored
Normal file
52
src/internal/m365/collection/groups/testdata/conversations.go
vendored
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package testdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StubConversations(ids ...string) []models.Conversationable {
|
||||||
|
sl := make([]models.Conversationable, 0, len(ids))
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
c := models.NewConversation()
|
||||||
|
c.SetId(ptr.To(id))
|
||||||
|
|
||||||
|
sl = append(sl, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sl
|
||||||
|
}
|
||||||
|
|
||||||
|
func StubConversationThreads(ids ...string) []models.ConversationThreadable {
|
||||||
|
sl := make([]models.ConversationThreadable, 0, len(ids))
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
ct := models.NewConversationThread()
|
||||||
|
ct.SetId(ptr.To(id))
|
||||||
|
|
||||||
|
sl = append(sl, ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sl
|
||||||
|
}
|
||||||
|
|
||||||
|
func StubPosts(ids ...string) []models.Postable {
|
||||||
|
sl := make([]models.Postable, 0, len(ids))
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
p := models.NewPost()
|
||||||
|
p.SetId(ptr.To(uuid.NewString()))
|
||||||
|
|
||||||
|
body := models.NewItemBody()
|
||||||
|
body.SetContent(ptr.To(id))
|
||||||
|
|
||||||
|
p.SetBody(body)
|
||||||
|
|
||||||
|
sl = append(sl, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sl
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user