get replies when getting message (#4232)

This was somehow sliced out of changes persisted
in prior branch merges.  It re-adds persisting replies as part of message content retrieval.

---

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

- [x]  No

#### Type of change

- [x] 🐛 Bugfix

#### Issue(s)

* #3989

#### Test Plan

- [x] 💪 Manual
- [x]  Unit test
This commit is contained in:
Keepers 2023-09-14 15:42:05 -06:00 committed by GitHub
parent d11eea5f9c
commit 14416094e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 619 additions and 105 deletions

View File

@ -208,7 +208,7 @@ func (oc *Collection) Items(
ctx context.Context, ctx context.Context,
errs *fault.Bus, errs *fault.Bus,
) <-chan data.Item { ) <-chan data.Item {
go oc.populateItems(ctx, errs) go oc.streamItems(ctx, errs)
return oc.data return oc.data
} }
@ -411,9 +411,9 @@ type driveStats struct {
itemsFound int64 itemsFound int64
} }
// populateItems iterates through items added to the collection // streamItems iterates through items added to the collection
// and uses the collection `itemReader` to read the item // and uses the collection `itemReader` to read the item
func (oc *Collection) populateItems(ctx context.Context, errs *fault.Bus) { func (oc *Collection) streamItems(ctx context.Context, errs *fault.Bus) {
var ( var (
stats driveStats stats driveStats
wg sync.WaitGroup wg sync.WaitGroup
@ -453,7 +453,7 @@ func (oc *Collection) populateItems(ctx context.Context, errs *fault.Bus) {
defer func() { <-semaphoreCh }() defer func() { <-semaphoreCh }()
// Read the item // Read the item
oc.populateDriveItem( oc.streamDriveItem(
ctx, ctx,
parentPath, parentPath,
item, item,
@ -470,7 +470,7 @@ func (oc *Collection) populateItems(ctx context.Context, errs *fault.Bus) {
oc.reportAsCompleted(ctx, int(stats.itemsFound), int(stats.itemsRead), stats.byteCount) oc.reportAsCompleted(ctx, int(stats.itemsFound), int(stats.itemsRead), stats.byteCount)
} }
func (oc *Collection) populateDriveItem( func (oc *Collection) streamDriveItem(
ctx context.Context, ctx context.Context,
parentPath *path.Builder, parentPath *path.Builder,
item models.DriveItemable, item models.DriveItemable,

View File

@ -38,16 +38,16 @@ import (
// tests // tests
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
type CollectionUnitTestSuite struct { type CollectionUnitSuite struct {
tester.Suite tester.Suite
} }
func TestCollectionUnitTestSuite(t *testing.T) { func TestCollectionUnitSuite(t *testing.T) {
suite.Run(t, &CollectionUnitTestSuite{Suite: tester.NewUnitSuite(t)}) suite.Run(t, &CollectionUnitSuite{Suite: tester.NewUnitSuite(t)})
} }
// Returns a status update function that signals the specified WaitGroup when it is done // Returns a status update function that signals the specified WaitGroup when it is done
func (suite *CollectionUnitTestSuite) testStatusUpdater( func (suite *CollectionUnitSuite) testStatusUpdater(
wg *sync.WaitGroup, wg *sync.WaitGroup,
statusToUpdate *support.ControllerOperationStatus, statusToUpdate *support.ControllerOperationStatus,
) support.StatusUpdater { ) support.StatusUpdater {
@ -59,7 +59,7 @@ func (suite *CollectionUnitTestSuite) testStatusUpdater(
} }
} }
func (suite *CollectionUnitTestSuite) TestCollection() { func (suite *CollectionUnitSuite) TestCollection() {
var ( var (
now = time.Now() now = time.Now()
@ -281,7 +281,7 @@ func (suite *CollectionUnitTestSuite) TestCollection() {
} }
} }
func (suite *CollectionUnitTestSuite) TestCollectionReadError() { func (suite *CollectionUnitSuite) TestCollectionReadError() {
var ( var (
t = suite.T() t = suite.T()
stubItemID = "fakeItemID" stubItemID = "fakeItemID"
@ -349,7 +349,7 @@ func (suite *CollectionUnitTestSuite) TestCollectionReadError() {
require.Equal(t, 1, collStatus.Metrics.Successes, "TODO: should be 0, but allowing 1 to reduce async management") require.Equal(t, 1, collStatus.Metrics.Successes, "TODO: should be 0, but allowing 1 to reduce async management")
} }
func (suite *CollectionUnitTestSuite) TestCollectionReadUnauthorizedErrorRetry() { func (suite *CollectionUnitSuite) TestCollectionReadUnauthorizedErrorRetry() {
var ( var (
t = suite.T() t = suite.T()
stubItemID = "fakeItemID" stubItemID = "fakeItemID"
@ -417,7 +417,7 @@ func (suite *CollectionUnitTestSuite) TestCollectionReadUnauthorizedErrorRetry()
} }
// Ensure metadata file always uses latest time for mod time // Ensure metadata file always uses latest time for mod time
func (suite *CollectionUnitTestSuite) TestCollectionPermissionBackupLatestModTime() { func (suite *CollectionUnitSuite) TestCollectionPermissionBackupLatestModTime() {
var ( var (
t = suite.T() t = suite.T()
stubItemID = "fakeItemID" stubItemID = "fakeItemID"
@ -779,7 +779,7 @@ func (suite *GetDriveItemUnitTestSuite) TestDownloadContent() {
} }
} }
func (suite *CollectionUnitTestSuite) TestItemExtensions() { func (suite *CollectionUnitSuite) TestItemExtensions() {
type verifyExtensionOutput func( type verifyExtensionOutput func(
t *testing.T, t *testing.T,
info details.ItemInfo, info details.ItemInfo,

View File

@ -2,59 +2,33 @@ package exchange
import ( import (
"bytes" "bytes"
"context"
"testing" "testing"
"time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/m365/collection/exchange/mock"
"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/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/backup/details"
"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"
) )
type mockItemer struct { type CollectionUnitSuite struct {
getCount int
serializeCount int
getErr error
serializeErr error
}
func (mi *mockItemer) GetItem(
context.Context,
string, string,
bool,
*fault.Bus,
) (serialization.Parsable, *details.ExchangeInfo, error) {
mi.getCount++
return nil, nil, mi.getErr
}
func (mi *mockItemer) Serialize(
context.Context,
serialization.Parsable,
string, string,
) ([]byte, error) {
mi.serializeCount++
return nil, mi.serializeErr
}
type CollectionSuite struct {
tester.Suite tester.Suite
} }
func TestCollectionSuite(t *testing.T) { func TestCollectionUnitSuite(t *testing.T) {
suite.Run(t, &CollectionSuite{Suite: tester.NewUnitSuite(t)}) suite.Run(t, &CollectionUnitSuite{Suite: tester.NewUnitSuite(t)})
} }
func (suite *CollectionSuite) TestReader_Valid() { func (suite *CollectionUnitSuite) TestReader_Valid() {
m := []byte("test message") m := []byte("test message")
description := "aFile" description := "aFile"
ed := &Item{id: description, message: m} ed := &Item{id: description, message: m}
@ -66,7 +40,7 @@ func (suite *CollectionSuite) TestReader_Valid() {
assert.Equal(suite.T(), description, ed.ID()) assert.Equal(suite.T(), description, ed.ID())
} }
func (suite *CollectionSuite) TestReader_Empty() { func (suite *CollectionUnitSuite) TestReader_Empty() {
var ( var (
empty []byte empty []byte
expected int64 expected int64
@ -81,7 +55,7 @@ func (suite *CollectionSuite) TestReader_Empty() {
assert.NoError(t, err, clues.ToCore(err)) assert.NoError(t, err, clues.ToCore(err))
} }
func (suite *CollectionSuite) TestCollection_NewCollection() { func (suite *CollectionUnitSuite) TestCollection_NewCollection() {
t := suite.T() t := suite.T()
tenant := "a-tenant" tenant := "a-tenant"
user := "a-user" user := "a-user"
@ -105,7 +79,7 @@ func (suite *CollectionSuite) TestCollection_NewCollection() {
assert.Equal(t, fullPath, edc.FullPath()) assert.Equal(t, fullPath, edc.FullPath())
} }
func (suite *CollectionSuite) TestNewCollection_state() { func (suite *CollectionUnitSuite) TestNewCollection_state() {
fooP, err := path.Build("t", "u", path.ExchangeService, path.EmailCategory, false, "foo") fooP, err := path.Build("t", "u", path.ExchangeService, path.EmailCategory, false, "foo")
require.NoError(suite.T(), err, clues.ToCore(err)) require.NoError(suite.T(), err, clues.ToCore(err))
barP, err := path.Build("t", "u", path.ExchangeService, path.EmailCategory, false, "bar") barP, err := path.Build("t", "u", path.ExchangeService, path.EmailCategory, false, "bar")
@ -154,7 +128,8 @@ func (suite *CollectionSuite) TestNewCollection_state() {
"u", "u",
test.curr, test.prev, test.loc, test.curr, test.prev, test.loc,
0, 0,
&mockItemer{}, nil, mock.DefaultItemGetSerialize(),
nil,
control.DefaultOptions(), control.DefaultOptions(),
false) false)
assert.Equal(t, test.expect, c.State(), "collection state") assert.Equal(t, test.expect, c.State(), "collection state")
@ -165,16 +140,16 @@ func (suite *CollectionSuite) TestNewCollection_state() {
} }
} }
func (suite *CollectionSuite) TestGetItemWithRetries() { func (suite *CollectionUnitSuite) TestGetItemWithRetries() {
table := []struct { table := []struct {
name string name string
items *mockItemer items *mock.ItemGetSerialize
expectErr func(*testing.T, error) expectErr func(*testing.T, error)
expectGetCalls int expectGetCalls int
}{ }{
{ {
name: "happy", name: "happy",
items: &mockItemer{}, items: mock.DefaultItemGetSerialize(),
expectErr: func(t *testing.T, err error) { expectErr: func(t *testing.T, err error) {
assert.NoError(t, err, clues.ToCore(err)) assert.NoError(t, err, clues.ToCore(err))
}, },
@ -182,7 +157,7 @@ func (suite *CollectionSuite) TestGetItemWithRetries() {
}, },
{ {
name: "an error", name: "an error",
items: &mockItemer{getErr: assert.AnError}, items: &mock.ItemGetSerialize{GetErr: assert.AnError},
expectErr: func(t *testing.T, err error) { expectErr: func(t *testing.T, err error) {
assert.Error(t, err, clues.ToCore(err)) assert.Error(t, err, clues.ToCore(err))
}, },
@ -190,8 +165,8 @@ func (suite *CollectionSuite) TestGetItemWithRetries() {
}, },
{ {
name: "deleted in flight", name: "deleted in flight",
items: &mockItemer{ items: &mock.ItemGetSerialize{
getErr: graph.ErrDeletedInFlight, GetErr: graph.ErrDeletedInFlight,
}, },
expectErr: func(t *testing.T, err error) { expectErr: func(t *testing.T, err error) {
assert.True(t, graph.IsErrDeletedInFlight(err), "is ErrDeletedInFlight") assert.True(t, graph.IsErrDeletedInFlight(err), "is ErrDeletedInFlight")
@ -212,3 +187,107 @@ func (suite *CollectionSuite) TestGetItemWithRetries() {
}) })
} }
} }
func (suite *CollectionUnitSuite) TestCollection_streamItems() {
var (
t = suite.T()
start = time.Now().Add(-1 * time.Second)
statusUpdater = func(*support.ControllerOperationStatus) {}
)
fullPath, err := path.Build("t", "pr", path.ExchangeService, path.EmailCategory, false, "fnords", "smarf")
require.NoError(t, err, clues.ToCore(err))
locPath, err := path.Build("t", "pr", path.ExchangeService, path.EmailCategory, false, "fnords", "smarf")
require.NoError(t, err, clues.ToCore(err))
table := []struct {
name string
added map[string]struct{}
removed map[string]struct{}
}{
{
name: "no items",
added: map[string]struct{}{},
removed: map[string]struct{}{},
},
{
name: "only added items",
added: map[string]struct{}{
"fisher": {},
"flannigan": {},
"fitzbog": {},
},
removed: map[string]struct{}{},
},
{
name: "only removed items",
added: map[string]struct{}{},
removed: map[string]struct{}{
"princess": {},
"poppy": {},
"petunia": {},
},
},
{
name: "added and removed items",
added: map[string]struct{}{},
removed: map[string]struct{}{
"general": {},
"goose": {},
"grumbles": {},
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
var (
t = suite.T()
errs = fault.New(true)
itemCount int
)
ctx, flush := tester.NewContext(t)
defer flush()
col := &Collection{
added: test.added,
removed: test.removed,
ctrl: control.DefaultOptions(),
getter: &mock.ItemGetSerialize{},
stream: make(chan data.Item),
fullPath: fullPath,
locationPath: locPath.ToBuilder(),
statusUpdater: statusUpdater,
}
go col.streamItems(ctx, errs)
for item := range col.stream {
itemCount++
_, aok := test.added[item.ID()]
if aok {
assert.False(t, item.Deleted(), "additions should not be marked as deleted")
}
_, rok := test.removed[item.ID()]
if rok {
assert.True(t, item.Deleted(), "removals should be marked as deleted")
dimt, ok := item.(data.ItemModTime)
require.True(t, ok, "item implements data.ItemModTime")
assert.True(t, dimt.ModTime().After(start), "deleted items should set mod time to now()")
}
assert.True(t, aok || rok, "item must be either added or removed: %q", item.ID())
}
assert.NoError(t, errs.Failure())
assert.Equal(
t,
len(test.added)+len(test.removed),
itemCount,
"should see all expected items")
})
}
}

View File

@ -0,0 +1,40 @@
package mock
import (
"context"
"github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/fault"
)
type ItemGetSerialize struct {
GetCount int
GetErr error
SerializeCount int
SerializeErr error
}
func (m *ItemGetSerialize) GetItem(
context.Context,
string, string,
bool,
*fault.Bus,
) (serialization.Parsable, *details.ExchangeInfo, error) {
m.GetCount++
return nil, &details.ExchangeInfo{}, m.GetErr
}
func (m *ItemGetSerialize) Serialize(
context.Context,
serialization.Parsable,
string, string,
) ([]byte, error) {
m.SerializeCount++
return nil, m.SerializeErr
}
func DefaultItemGetSerialize() *ItemGetSerialize {
return &ItemGetSerialize{}
}

View File

@ -83,7 +83,7 @@ func (bh mockBackupHandler) canonicalPath(
false) false)
} }
func (bh mockBackupHandler) getChannelMessage( func (bh mockBackupHandler) GetChannelMessage(
_ context.Context, _ context.Context,
_, _, itemID string, _, _, itemID string,
) (models.ChatMessageable, *details.GroupsInfo, error) { ) (models.ChatMessageable, *details.GroupsInfo, error) {

View File

@ -66,7 +66,7 @@ func (bh channelsBackupHandler) canonicalPath(
false) false)
} }
func (bh channelsBackupHandler) getChannelMessage( func (bh channelsBackupHandler) GetChannelMessage(
ctx context.Context, ctx context.Context,
teamID, channelID, itemID string, teamID, channelID, itemID string,
) (models.ChatMessageable, *details.GroupsInfo, error) { ) (models.ChatMessageable, *details.GroupsInfo, error) {

View File

@ -143,11 +143,8 @@ func (col Collection) DoNotMergeItems() bool {
// Item represents a single item retrieved from exchange // Item represents a single item retrieved from exchange
type Item struct { type Item struct {
id string id string
// TODO: We may need this to be a "oneOf" of `message`, `contact`, etc.
// going forward. Using []byte for now but I assume we'll have
// some structured type in here (serialization to []byte can be done in `Read`)
message []byte message []byte
info *details.GroupsInfo // temporary change to bring populate function into directory info *details.GroupsInfo
// TODO(ashmrtn): Can probably eventually be sourced from info as there's a // TODO(ashmrtn): Can probably eventually be sourced from info as there's a
// request to provide modtime in ItemInfo structs. // request to provide modtime in ItemInfo structs.
modTime time.Time modTime time.Time
@ -220,31 +217,30 @@ func (col *Collection) streamItems(ctx context.Context, errs *fault.Bus) {
semaphoreCh := make(chan struct{}, col.ctrl.Parallelism.ItemFetch) semaphoreCh := make(chan struct{}, col.ctrl.Parallelism.ItemFetch)
defer close(semaphoreCh) defer close(semaphoreCh)
// TODO: add for v1 with incrementals
// delete all removed items // delete all removed items
// for id := range col.removed { for id := range col.removed {
// semaphoreCh <- struct{}{} semaphoreCh <- struct{}{}
// wg.Add(1) wg.Add(1)
// go func(id string) { go func(id string) {
// defer wg.Done() defer wg.Done()
// defer func() { <-semaphoreCh }() defer func() { <-semaphoreCh }()
// col.stream <- &Item{ col.stream <- &Item{
// id: id, id: id,
// modTime: time.Now().UTC(), // removed items have no modTime entry. modTime: time.Now().UTC(), // removed items have no modTime entry.
// deleted: true, deleted: true,
// } }
// atomic.AddInt64(&streamedItems, 1) atomic.AddInt64(&streamedItems, 1)
// atomic.AddInt64(&totalBytes, 0) atomic.AddInt64(&totalBytes, 0)
// if colProgress != nil { if colProgress != nil {
// colProgress <- struct{}{} colProgress <- struct{}{}
// } }
// }(id) }(id)
// } }
// add any new items // add any new items
for id := range col.added { for id := range col.added {
@ -265,7 +261,7 @@ func (col *Collection) streamItems(ctx context.Context, errs *fault.Bus) {
flds := col.fullPath.Folders() flds := col.fullPath.Folders()
parentFolderID := flds[len(flds)-1] parentFolderID := flds[len(flds)-1]
item, info, err := col.getter.getChannelMessage( item, info, err := col.getter.GetChannelMessage(
ctx, ctx,
col.protectedResource, col.protectedResource,
parentFolderID, parentFolderID,

View File

@ -3,6 +3,7 @@ package groups
import ( import (
"bytes" "bytes"
"testing" "testing"
"time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -10,20 +11,23 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/m365/collection/groups/mock"
"github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"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/path" "github.com/alcionai/corso/src/pkg/path"
) )
type CollectionSuite struct { type CollectionUnitSuite struct {
tester.Suite tester.Suite
} }
func TestCollectionSuite(t *testing.T) { func TestCollectionUnitSuite(t *testing.T) {
suite.Run(t, &CollectionSuite{Suite: tester.NewUnitSuite(t)}) suite.Run(t, &CollectionUnitSuite{Suite: tester.NewUnitSuite(t)})
} }
func (suite *CollectionSuite) TestReader_Valid() { func (suite *CollectionUnitSuite) TestReader_Valid() {
m := []byte("test message") m := []byte("test message")
description := "aFile" description := "aFile"
ed := &Item{id: description, message: m} ed := &Item{id: description, message: m}
@ -35,7 +39,7 @@ func (suite *CollectionSuite) TestReader_Valid() {
assert.Equal(suite.T(), description, ed.ID()) assert.Equal(suite.T(), description, ed.ID())
} }
func (suite *CollectionSuite) TestReader_Empty() { func (suite *CollectionUnitSuite) TestReader_Empty() {
var ( var (
empty []byte empty []byte
expected int64 expected int64
@ -50,7 +54,7 @@ func (suite *CollectionSuite) TestReader_Empty() {
assert.NoError(t, err, clues.ToCore(err)) assert.NoError(t, err, clues.ToCore(err))
} }
func (suite *CollectionSuite) TestCollection_NewCollection() { func (suite *CollectionUnitSuite) TestCollection_NewCollection() {
t := suite.T() t := suite.T()
tenant := "a-tenant" tenant := "a-tenant"
protectedResource := "a-protectedResource" protectedResource := "a-protectedResource"
@ -74,7 +78,7 @@ func (suite *CollectionSuite) TestCollection_NewCollection() {
assert.Equal(t, fullPath, edc.FullPath()) assert.Equal(t, fullPath, edc.FullPath())
} }
func (suite *CollectionSuite) TestNewCollection_state() { func (suite *CollectionUnitSuite) TestNewCollection_state() {
fooP, err := path.Build("t", "u", path.GroupsService, path.ChannelMessagesCategory, false, "foo") fooP, err := path.Build("t", "u", path.GroupsService, path.ChannelMessagesCategory, false, "foo")
require.NoError(suite.T(), err, clues.ToCore(err)) require.NoError(suite.T(), err, clues.ToCore(err))
barP, err := path.Build("t", "u", path.GroupsService, path.ChannelMessagesCategory, false, "bar") barP, err := path.Build("t", "u", path.GroupsService, path.ChannelMessagesCategory, false, "bar")
@ -135,3 +139,107 @@ func (suite *CollectionSuite) TestNewCollection_state() {
}) })
} }
} }
func (suite *CollectionUnitSuite) TestCollection_streamItems() {
var (
t = suite.T()
start = time.Now().Add(-1 * time.Second)
statusUpdater = func(*support.ControllerOperationStatus) {}
)
fullPath, err := path.Build("t", "pr", path.GroupsService, path.ChannelMessagesCategory, false, "fnords", "smarf")
require.NoError(t, err, clues.ToCore(err))
locPath, err := path.Build("t", "pr", path.GroupsService, path.ChannelMessagesCategory, false, "fnords", "smarf")
require.NoError(t, err, clues.ToCore(err))
table := []struct {
name string
added map[string]struct{}
removed map[string]struct{}
}{
{
name: "no items",
added: map[string]struct{}{},
removed: map[string]struct{}{},
},
{
name: "only added items",
added: map[string]struct{}{
"fisher": {},
"flannigan": {},
"fitzbog": {},
},
removed: map[string]struct{}{},
},
{
name: "only removed items",
added: map[string]struct{}{},
removed: map[string]struct{}{
"princess": {},
"poppy": {},
"petunia": {},
},
},
{
name: "added and removed items",
added: map[string]struct{}{},
removed: map[string]struct{}{
"general": {},
"goose": {},
"grumbles": {},
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
var (
t = suite.T()
errs = fault.New(true)
itemCount int
)
ctx, flush := tester.NewContext(t)
defer flush()
col := &Collection{
added: test.added,
removed: test.removed,
ctrl: control.DefaultOptions(),
getter: mock.GetChannelMessage{},
stream: make(chan data.Item),
fullPath: fullPath,
locationPath: locPath.ToBuilder(),
statusUpdater: statusUpdater,
}
go col.streamItems(ctx, errs)
for item := range col.stream {
itemCount++
_, aok := test.added[item.ID()]
if aok {
assert.False(t, item.Deleted(), "additions should not be marked as deleted")
}
_, rok := test.removed[item.ID()]
if rok {
assert.True(t, item.Deleted(), "removals should be marked as deleted")
dimt, ok := item.(data.ItemModTime)
require.True(t, ok, "item implements data.ItemModTime")
assert.True(t, dimt.ModTime().After(start), "deleted items should set mod time to now()")
}
assert.True(t, aok || rok, "item must be either added or removed: %q", item.ID())
}
assert.NoError(t, errs.Failure())
assert.Equal(
t,
len(test.added)+len(test.removed),
itemCount,
"should see all expected items")
})
}
}

View File

@ -45,7 +45,7 @@ type backupHandler interface {
} }
type getChannelMessager interface { type getChannelMessager interface {
getChannelMessage( GetChannelMessage(
ctx context.Context, ctx context.Context,
teamID, channelID, itemID string, teamID, channelID, itemID string,
) (models.ChatMessageable, *details.GroupsInfo, error) ) (models.ChatMessageable, *details.GroupsInfo, error)

View File

@ -0,0 +1,24 @@
package mock
import (
"context"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/pkg/backup/details"
)
type GetChannelMessage struct {
Err error
}
func (m GetChannelMessage) GetChannelMessage(
ctx context.Context,
teamID, channelID, itemID string,
) (models.ChatMessageable, *details.GroupsInfo, error) {
msg := models.NewChatMessage()
msg.SetId(ptr.To(itemID))
return msg, &details.GroupsInfo{}, m.Err
}

View File

@ -691,11 +691,22 @@ func verifyExtensionData(
) { ) {
require.NotNil(t, itemInfo.Extension, "nil extension") require.NotNil(t, itemInfo.Extension, "nil extension")
assert.NotNil(t, itemInfo.Extension.Data[extensions.KNumBytes], "key not found in extension") assert.NotNil(t, itemInfo.Extension.Data[extensions.KNumBytes], "key not found in extension")
actualSize := int64(itemInfo.Extension.Data[extensions.KNumBytes].(float64))
if p == path.SharePointService { var (
assert.Equal(t, itemInfo.SharePoint.Size, actualSize, "incorrect data in extension") detailsSize int64
} else { extensionSize = int64(itemInfo.Extension.Data[extensions.KNumBytes].(float64))
assert.Equal(t, itemInfo.OneDrive.Size, actualSize, "incorrect data in extension") )
switch p {
case path.SharePointService:
detailsSize = itemInfo.SharePoint.Size
case path.OneDriveService:
detailsSize = itemInfo.OneDrive.Size
case path.GroupsService:
detailsSize = itemInfo.Groups.Size
default:
assert.Fail(t, "unrecognized data type")
} }
assert.Equal(t, extensionSize, detailsSize, "incorrect size in extension")
} }

View File

@ -108,8 +108,6 @@ func (c Channels) GetChannelMessage(
ctx context.Context, ctx context.Context,
teamID, channelID, messageID string, teamID, channelID, messageID string,
) (models.ChatMessageable, *details.GroupsInfo, error) { ) (models.ChatMessageable, *details.GroupsInfo, error) {
var size int64
message, err := c.Stable. message, err := c.Stable.
Client(). Client().
Teams(). Teams().
@ -123,7 +121,14 @@ func (c Channels) GetChannelMessage(
return nil, nil, graph.Stack(ctx, err) return nil, nil, graph.Stack(ctx, err)
} }
info := ChannelMessageInfo(message, size) replies, err := c.GetChannelMessageReplies(ctx, teamID, channelID, messageID)
if err != nil {
return nil, nil, graph.Wrap(ctx, err, "retrieving message replies")
}
message.SetReplies(replies)
info := ChannelMessageInfo(message)
return message, info, nil return message, info, nil
} }
@ -134,12 +139,12 @@ func (c Channels) GetChannelMessage(
func ChannelMessageInfo( func ChannelMessageInfo(
msg models.ChatMessageable, msg models.ChatMessageable,
size int64,
) *details.GroupsInfo { ) *details.GroupsInfo {
var ( var (
lastReply time.Time lastReply time.Time
modTime = ptr.OrNow(msg.GetLastModifiedDateTime()) modTime = ptr.OrNow(msg.GetLastModifiedDateTime())
msgCreator string msgCreator string
content string
) )
for _, r := range msg.GetReplies() { for _, r := range msg.GetReplies() {
@ -169,15 +174,19 @@ func ChannelMessageInfo(
msgCreator = ptr.Val(from.GetUser().GetDisplayName()) msgCreator = ptr.Val(from.GetUser().GetDisplayName())
} }
if msg.GetBody() != nil {
content = ptr.Val(msg.GetBody().GetContent())
}
return &details.GroupsInfo{ return &details.GroupsInfo{
ItemType: details.GroupsChannelMessage, ItemType: details.GroupsChannelMessage,
Created: ptr.Val(msg.GetCreatedDateTime()), Created: ptr.Val(msg.GetCreatedDateTime()),
LastReplyAt: lastReply, LastReplyAt: lastReply,
Modified: modTime, Modified: modTime,
MessageCreator: msgCreator, MessageCreator: msgCreator,
MessagePreview: str.Preview(ptr.Val(msg.GetBody().GetContent()), 16), MessagePreview: str.Preview(content, 16),
ReplyCount: len(msg.GetReplies()), ReplyCount: len(msg.GetReplies()),
Size: size, Size: int64(len(content)),
} }
} }

View File

@ -214,7 +214,9 @@ func (c Channels) GetChannelMessageReplies(
ctx context.Context, ctx context.Context,
teamID, channelID, messageID string, teamID, channelID, messageID string,
) ([]models.ChatMessageable, error) { ) ([]models.ChatMessageable, error) {
return enumerateItems[models.ChatMessageable](ctx, c.NewChannelMessageRepliesPager(teamID, channelID, messageID)) return enumerateItems[models.ChatMessageable](
ctx,
c.NewChannelMessageRepliesPager(teamID, channelID, messageID))
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -0,0 +1,241 @@
package api_test
import (
"testing"
"time"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
type ChannelsAPIUnitSuite struct {
tester.Suite
}
func TestChannelsAPIUnitSuitee(t *testing.T) {
suite.Run(t, &ChannelsAPIUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() {
var (
initial = time.Now().Add(-24 * time.Hour)
mid = time.Now().Add(-1 * time.Hour)
curr = time.Now()
content = "content"
body = models.NewItemBody()
)
body.SetContent(ptr.To(content))
tests := []struct {
name string
msgAndInfo func() (models.ChatMessageable, *details.GroupsInfo)
}{
{
name: "No body",
msgAndInfo: func() (models.ChatMessageable, *details.GroupsInfo) {
msg := models.NewChatMessage()
msg.SetCreatedDateTime(&initial)
msg.SetLastModifiedDateTime(&initial)
iden := models.NewIdentity()
iden.SetDisplayName(ptr.To("user"))
from := models.NewChatMessageFromIdentitySet()
from.SetUser(iden)
msg.SetFrom(from)
i := &details.GroupsInfo{
ItemType: details.GroupsChannelMessage,
Created: initial,
Modified: initial,
LastReplyAt: time.Time{},
ReplyCount: 0,
MessageCreator: "user",
}
return msg, i
},
},
{
name: "No Replies - created by user",
msgAndInfo: func() (models.ChatMessageable, *details.GroupsInfo) {
msg := models.NewChatMessage()
msg.SetCreatedDateTime(&initial)
msg.SetLastModifiedDateTime(&initial)
msg.SetBody(body)
iden := models.NewIdentity()
iden.SetDisplayName(ptr.To("user"))
from := models.NewChatMessageFromIdentitySet()
from.SetUser(iden)
msg.SetFrom(from)
i := &details.GroupsInfo{
ItemType: details.GroupsChannelMessage,
Created: initial,
Modified: initial,
LastReplyAt: time.Time{},
ReplyCount: 0,
MessageCreator: "user",
Size: int64(len(content)),
MessagePreview: content,
}
return msg, i
},
},
{
name: "No Replies - created by application",
msgAndInfo: func() (models.ChatMessageable, *details.GroupsInfo) {
msg := models.NewChatMessage()
msg.SetCreatedDateTime(&initial)
msg.SetLastModifiedDateTime(&initial)
msg.SetBody(body)
iden := models.NewIdentity()
iden.SetDisplayName(ptr.To("app"))
from := models.NewChatMessageFromIdentitySet()
from.SetApplication(iden)
msg.SetFrom(from)
i := &details.GroupsInfo{
ItemType: details.GroupsChannelMessage,
Created: initial,
Modified: initial,
LastReplyAt: time.Time{},
ReplyCount: 0,
MessageCreator: "app",
Size: int64(len(content)),
MessagePreview: content,
}
return msg, i
},
},
{
name: "No Replies - created by device",
msgAndInfo: func() (models.ChatMessageable, *details.GroupsInfo) {
msg := models.NewChatMessage()
msg.SetCreatedDateTime(&initial)
msg.SetLastModifiedDateTime(&initial)
msg.SetBody(body)
iden := models.NewIdentity()
iden.SetDisplayName(ptr.To("device"))
from := models.NewChatMessageFromIdentitySet()
from.SetDevice(iden)
msg.SetFrom(from)
i := &details.GroupsInfo{
ItemType: details.GroupsChannelMessage,
Created: initial,
Modified: initial,
LastReplyAt: time.Time{},
ReplyCount: 0,
MessageCreator: "device",
Size: int64(len(content)),
MessagePreview: content,
}
return msg, i
},
},
{
name: "One Reply",
msgAndInfo: func() (models.ChatMessageable, *details.GroupsInfo) {
msg := models.NewChatMessage()
msg.SetCreatedDateTime(&initial)
msg.SetLastModifiedDateTime(&initial)
msg.SetBody(body)
iden := models.NewIdentity()
iden.SetDisplayName(ptr.To("user"))
from := models.NewChatMessageFromIdentitySet()
from.SetUser(iden)
msg.SetFrom(from)
reply := models.NewChatMessage()
reply.SetCreatedDateTime(&curr)
reply.SetLastModifiedDateTime(&curr)
msg.SetReplies([]models.ChatMessageable{reply})
i := &details.GroupsInfo{
ItemType: details.GroupsChannelMessage,
Created: initial,
Modified: curr,
LastReplyAt: curr,
ReplyCount: 1,
MessageCreator: "user",
Size: int64(len(content)),
MessagePreview: content,
}
return msg, i
},
},
{
name: "Many Replies",
msgAndInfo: func() (models.ChatMessageable, *details.GroupsInfo) {
msg := models.NewChatMessage()
msg.SetCreatedDateTime(&initial)
msg.SetLastModifiedDateTime(&initial)
msg.SetBody(body)
iden := models.NewIdentity()
iden.SetDisplayName(ptr.To("user"))
from := models.NewChatMessageFromIdentitySet()
from.SetUser(iden)
msg.SetFrom(from)
reply1 := models.NewChatMessage()
reply1.SetCreatedDateTime(&mid)
reply1.SetLastModifiedDateTime(&mid)
reply2 := models.NewChatMessage()
reply2.SetCreatedDateTime(&curr)
reply2.SetLastModifiedDateTime(&curr)
msg.SetReplies([]models.ChatMessageable{reply1, reply2})
i := &details.GroupsInfo{
ItemType: details.GroupsChannelMessage,
Created: initial,
Modified: curr,
LastReplyAt: curr,
ReplyCount: 2,
MessageCreator: "user",
Size: int64(len(content)),
MessagePreview: content,
}
return msg, i
},
},
}
for _, test := range tests {
suite.Run(test.name, func() {
chMsg, expected := test.msgAndInfo()
assert.Equal(suite.T(), expected, api.ChannelMessageInfo(chMsg))
})
}
}

View File

@ -46,9 +46,11 @@ func (suite *ContactsAPIUnitSuite) TestContactInfo() {
Created: initial, Created: initial,
Modified: initial, Modified: initial,
} }
return contact, i return contact, i
}, },
}, { },
{
name: "Only Name", name: "Only Name",
contactAndRP: func() (models.Contactable, *details.ExchangeInfo) { contactAndRP: func() (models.Contactable, *details.ExchangeInfo) {
aPerson := "Whole Person" aPerson := "Whole Person"
@ -56,12 +58,14 @@ func (suite *ContactsAPIUnitSuite) TestContactInfo() {
contact.SetCreatedDateTime(&initial) contact.SetCreatedDateTime(&initial)
contact.SetLastModifiedDateTime(&initial) contact.SetLastModifiedDateTime(&initial)
contact.SetDisplayName(&aPerson) contact.SetDisplayName(&aPerson)
i := &details.ExchangeInfo{ i := &details.ExchangeInfo{
ItemType: details.ExchangeContact, ItemType: details.ExchangeContact,
ContactName: aPerson, ContactName: aPerson,
Created: initial, Created: initial,
Modified: initial, Modified: initial,
} }
return contact, i return contact, i
}, },
}, },