Restore/backup test for email attachments (#1057)

## Description

Test to check that attachments are properly populated and fetched during restore and backup

Some notes:
* make slice element comparison function generic so it can compare locations and attachments
* very hacky comparison for attachment content using reflection as it's not easily accessible otherwise. Changes to the underlying struct will cause runtime failures in the test case if/when they occur

## Type of change

<!--- Please check the type of change your PR introduces: --->
- [ ] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [x] 🤖 Test
- [ ] 💻 CI/Deployment
- [ ] 🐹 Trivial/Minor

## Issue(s)

* #913 

## Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2022-10-05 16:11:54 -07:00 committed by GitHub
parent 79f50cad82
commit 4ba8f50c53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 144 additions and 63 deletions

View File

@ -71,6 +71,70 @@ func testEmptyOrEqual[T any](t *testing.T, expected *T, got *T, msg string) {
assert.Equal(t, *expected, *got, msg)
}
func testElementsMatch[T any](
t *testing.T,
expected []T,
got []T,
equalityCheck func(expectedItem, gotItem T) bool,
) {
t.Helper()
pending := make([]*T, len(expected))
for i := 0; i < len(expected); i++ {
pending[i] = &expected[i]
}
unexpected := []T{}
for i := 0; i < len(got); i++ {
found := false
for j, maybe := range pending {
if maybe == nil {
// Already matched with something in got.
continue
}
// Item matched, break out of inner loop and move to next item in got.
if equalityCheck(*maybe, got[i]) {
pending[j] = nil
found = true
break
}
}
if !found {
unexpected = append(unexpected, got[i])
}
}
// Print differences.
missing := []T{}
for _, p := range pending {
if p == nil {
continue
}
missing = append(missing, *p)
}
if len(unexpected) == 0 && len(missing) == 0 {
return
}
assert.Failf(
t,
"contain different elements",
"missing items: (%T)%v\nunexpected items: (%T)%v\n",
expected,
missing,
got,
unexpected,
)
}
type itemInfo struct {
// lookupKey is a string that can be used to find this data from a set of
// other data in the same collection. This key should be something that will
@ -92,11 +156,50 @@ type colInfo struct {
items []itemInfo
}
func attachmentEqual(
expected models.Attachmentable,
got models.Attachmentable,
) bool {
// This is super hacky, but seems like it would be good to have a comparison
// of the actual content. I think the only other way to really get it is to
// serialize both structs to JSON and pull it from there or something though.
expectedData := reflect.Indirect(reflect.ValueOf(expected)).FieldByName("contentBytes").Bytes()
gotData := reflect.Indirect(reflect.ValueOf(got)).FieldByName("contentBytes").Bytes()
if !reflect.DeepEqual(expectedData, gotData) {
return false
}
if !emptyOrEqual(expected.GetContentType(), got.GetContentType()) {
return false
}
// Skip Id as it's tied to this specific instance of the item.
if !emptyOrEqual(expected.GetIsInline(), got.GetIsInline()) {
return false
}
// Skip LastModifiedDateTime as it's tied to this specific instance of the item.
if !emptyOrEqual(expected.GetName(), got.GetName()) {
return false
}
// Skip Size as the server clobbers whatever value we give it. It's unknown
// how they populate size though as it's not just the length of the byte
// array backing the content.
return true
}
func checkMessage(
t *testing.T,
expected models.Messageable,
got models.Messageable,
) {
testElementsMatch(t, expected.GetAttachments(), got.GetAttachments(), attachmentEqual)
assert.Equal(t, expected.GetBccRecipients(), got.GetBccRecipients(), "BccRecipients")
testEmptyOrEqual(t, expected.GetBody().GetContentType(), got.GetBody().GetContentType(), "Body.ContentType")
@ -253,67 +356,6 @@ func checkContact(
testEmptyOrEqual(t, expected.GetYomiSurname(), got.GetYomiSurname(), "YomiSurname")
}
func checkLocations(
t *testing.T,
expected []models.Locationable,
got []models.Locationable,
) {
pending := make([]*models.Locationable, len(expected))
for i := 0; i < len(expected); i++ {
pending[i] = &expected[i]
}
unexpected := []models.Locationable{}
for i := 0; i < len(got); i++ {
found := false
for j, maybe := range pending {
if maybe == nil {
// Already matched with something in got.
continue
}
// Item matched, break out of inner loop and move to next item in got.
if locationEqual(*maybe, got[i]) {
pending[j] = nil
found = true
break
}
}
if !found {
unexpected = append(unexpected, got[i])
}
}
// Print differences.
missing := []models.Locationable{}
for _, p := range pending {
if p == nil {
continue
}
missing = append(missing, *p)
}
if len(unexpected) == 0 && len(missing) == 0 {
return
}
assert.Failf(
t,
"contain different elements",
"missing items: (%T)%v\nunexpected items: (%T)%v\n",
expected,
missing,
got,
unexpected,
)
}
func locationEqual(expected, got models.Locationable) bool {
if !reflect.DeepEqual(expected.GetAddress(), got.GetAddress()) {
return false
@ -406,13 +448,14 @@ func checkEvent(
// Cheating a little here in the name of code-reuse. model.Location needs
// custom compare logic because it has fields marked as "internal use only"
// that seem to change.
checkLocations(
testElementsMatch(
t,
[]models.Locationable{expected.GetLocation()},
[]models.Locationable{got.GetLocation()},
locationEqual,
)
checkLocations(t, expected.GetLocations(), got.GetLocations())
testElementsMatch(t, expected.GetLocations(), got.GetLocations(), locationEqual)
assert.Equal(t, expected.GetOnlineMeeting(), got.GetOnlineMeeting(), "OnlineMeeting")

View File

@ -438,6 +438,44 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup() {
backupSelFunc func(dest control.RestoreDestination, backupUser string) selectors.Selector
expectedRestoreFolders int
}{
{
name: "EmailsWithAttachments",
service: path.ExchangeService,
expectedRestoreFolders: 1,
collections: []colInfo{
{
pathElements: []string{"Inbox"},
category: path.EmailCategory,
items: []itemInfo{
{
name: "someencodeditemID",
data: mockconnector.GetMockMessageWithDirectAttachment(
subjectText + "-1",
),
lookupKey: subjectText + "-1",
},
{
name: "someencodeditemID2",
data: mockconnector.GetMockMessageWithTwoAttachments(
subjectText + "-2",
),
lookupKey: subjectText + "-2",
},
},
},
},
// TODO(ashmrtn): Generalize this once we know the path transforms that
// occur during restore.
backupSelFunc: func(dest control.RestoreDestination, backupUser string) selectors.Selector {
backupSel := selectors.NewExchangeBackup()
backupSel.Include(backupSel.MailFolders(
[]string{backupUser},
[]string{dest.ContainerName},
))
return backupSel.Selector
},
},
{
name: "MultipleEmailsSingleFolder",
service: path.ExchangeService,