Sanity tests for email exports (#4741)
<!-- PR description--> --- #### 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 - [ ] 🐛 Bugfix - [ ] 🗺️ Documentation - [x] 🤖 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. --> * closes https://github.com/alcionai/corso/issues/4652 #### Test Plan <!-- How will this be tested prior to merging.--> - [ ] 💪 Manual - [ ] ⚡ Unit test - [ ] 💚 E2E
This commit is contained in:
parent
925c70d9d2
commit
9efe413e35
4
.github/workflows/sanity-test.yaml
vendored
4
.github/workflows/sanity-test.yaml
vendored
@ -196,6 +196,7 @@ jobs:
|
|||||||
restore-args: '--email-folder ${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
|
restore-args: '--email-folder ${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
|
||||||
restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
|
restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
|
||||||
log-dir: ${{ env.CORSO_LOG_DIR }}
|
log-dir: ${{ env.CORSO_LOG_DIR }}
|
||||||
|
with-export: true
|
||||||
|
|
||||||
- name: Exchange - Incremental backup
|
- name: Exchange - Incremental backup
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
@ -209,6 +210,7 @@ jobs:
|
|||||||
restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
|
restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
|
||||||
backup-id: ${{ steps.exchange-backup.outputs.backup-id }}
|
backup-id: ${{ steps.exchange-backup.outputs.backup-id }}
|
||||||
log-dir: ${{ env.CORSO_LOG_DIR }}
|
log-dir: ${{ env.CORSO_LOG_DIR }}
|
||||||
|
with-export: true
|
||||||
|
|
||||||
- name: Exchange - Non delta backup
|
- name: Exchange - Non delta backup
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
@ -222,6 +224,7 @@ jobs:
|
|||||||
restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
|
restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
|
||||||
backup-id: ${{ steps.exchange-backup.outputs.backup-id }}
|
backup-id: ${{ steps.exchange-backup.outputs.backup-id }}
|
||||||
log-dir: ${{ env.CORSO_LOG_DIR }}
|
log-dir: ${{ env.CORSO_LOG_DIR }}
|
||||||
|
with-export: true
|
||||||
|
|
||||||
- name: Exchange - Incremental backup after non-delta
|
- name: Exchange - Incremental backup after non-delta
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
@ -235,6 +238,7 @@ jobs:
|
|||||||
restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
|
restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
|
||||||
backup-id: ${{ steps.exchange-backup.outputs.backup-id }}
|
backup-id: ${{ steps.exchange-backup.outputs.backup-id }}
|
||||||
log-dir: ${{ env.CORSO_LOG_DIR }}
|
log-dir: ${{ env.CORSO_LOG_DIR }}
|
||||||
|
with-export: true
|
||||||
|
|
||||||
|
|
||||||
##########################################################################################################################################
|
##########################################################################################################################################
|
||||||
|
|||||||
@ -58,6 +58,7 @@ func BuildFilepathSanitree(
|
|||||||
Children: map[string]*Sanitree[fs.FileInfo, fs.FileInfo]{},
|
Children: map[string]*Sanitree[fs.FileInfo, fs.FileInfo]{},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
node.CountLeaves++
|
||||||
node.Leaves[info.Name()] = &Sanileaf[fs.FileInfo, fs.FileInfo]{
|
node.Leaves[info.Name()] = &Sanileaf[fs.FileInfo, fs.FileInfo]{
|
||||||
Parent: node,
|
Parent: node,
|
||||||
Self: info,
|
Self: info,
|
||||||
|
|||||||
64
src/cmd/sanity_test/export/exchange.go
Normal file
64
src/cmd/sanity_test/export/exchange.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package export
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/cmd/sanity_test/common"
|
||||||
|
"github.com/alcionai/corso/src/cmd/sanity_test/restore"
|
||||||
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CheckEmailExport(
|
||||||
|
ctx context.Context,
|
||||||
|
ac api.Client,
|
||||||
|
envs common.Envs,
|
||||||
|
) {
|
||||||
|
sourceTree := restore.BuildEmailSanitree(ctx, ac, envs.UserID, envs.SourceContainer)
|
||||||
|
|
||||||
|
emailsExportDir := filepath.Join(envs.RestoreContainer, "Emails")
|
||||||
|
exportedTree := common.BuildFilepathSanitree(ctx, emailsExportDir)
|
||||||
|
|
||||||
|
ctx = clues.Add(
|
||||||
|
ctx,
|
||||||
|
"export_container_id", exportedTree.ID,
|
||||||
|
"export_container_name", exportedTree.Name,
|
||||||
|
"source_container_id", sourceTree.ID,
|
||||||
|
"source_container_name", sourceTree.Name)
|
||||||
|
|
||||||
|
comparator := func(
|
||||||
|
ctx context.Context,
|
||||||
|
expect *common.Sanitree[models.MailFolderable, any],
|
||||||
|
result *common.Sanitree[fs.FileInfo, fs.FileInfo],
|
||||||
|
) {
|
||||||
|
modifiedExpectedLeaves := map[string]*common.Sanileaf[models.MailFolderable, any]{}
|
||||||
|
modifiedResultLeaves := map[string]*common.Sanileaf[fs.FileInfo, fs.FileInfo]{}
|
||||||
|
|
||||||
|
for key, val := range expect.Leaves {
|
||||||
|
val.Size = 0 // we cannot match up sizes
|
||||||
|
modifiedExpectedLeaves[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, val := range result.Leaves {
|
||||||
|
fixedName := strings.TrimSuffix(key, ".eml")
|
||||||
|
val.Size = 0
|
||||||
|
|
||||||
|
modifiedResultLeaves[fixedName] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
common.CompareLeaves(ctx, expect.Leaves, modifiedResultLeaves, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
common.CompareDiffTrees(
|
||||||
|
ctx,
|
||||||
|
sourceTree,
|
||||||
|
exportedTree.Children[envs.SourceContainer],
|
||||||
|
comparator)
|
||||||
|
|
||||||
|
common.Infof(ctx, "Success")
|
||||||
|
}
|
||||||
@ -19,8 +19,8 @@ func CheckEmailRestoration(
|
|||||||
ac api.Client,
|
ac api.Client,
|
||||||
envs common.Envs,
|
envs common.Envs,
|
||||||
) {
|
) {
|
||||||
restoredTree := buildSanitree(ctx, ac, envs.UserID, envs.RestoreContainer)
|
restoredTree := BuildEmailSanitree(ctx, ac, envs.UserID, envs.RestoreContainer)
|
||||||
sourceTree := buildSanitree(ctx, ac, envs.UserID, envs.SourceContainer)
|
sourceTree := BuildEmailSanitree(ctx, ac, envs.UserID, envs.SourceContainer)
|
||||||
|
|
||||||
ctx = clues.Add(
|
ctx = clues.Add(
|
||||||
ctx,
|
ctx,
|
||||||
@ -39,7 +39,7 @@ func CheckEmailRestoration(
|
|||||||
common.Infof(ctx, "Success")
|
common.Infof(ctx, "Success")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSanitree(
|
func BuildEmailSanitree(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
ac api.Client,
|
ac api.Client,
|
||||||
userID, folderName string,
|
userID, folderName string,
|
||||||
@ -69,9 +69,37 @@ func buildSanitree(
|
|||||||
ID: ptr.Val(mmf.GetId()),
|
ID: ptr.Val(mmf.GetId()),
|
||||||
Name: ptr.Val(mmf.GetDisplayName()),
|
Name: ptr.Val(mmf.GetDisplayName()),
|
||||||
CountLeaves: int(ptr.Val(mmf.GetTotalItemCount())),
|
CountLeaves: int(ptr.Val(mmf.GetTotalItemCount())),
|
||||||
|
Leaves: map[string]*common.Sanileaf[models.MailFolderable, any]{},
|
||||||
Children: map[string]*common.Sanitree[models.MailFolderable, any]{},
|
Children: map[string]*common.Sanitree[models.MailFolderable, any]{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mails, err := ac.Mail().GetItemsInContainer(
|
||||||
|
ctx,
|
||||||
|
userID,
|
||||||
|
root.ID)
|
||||||
|
if err != nil {
|
||||||
|
common.Fatal(ctx, "getting child containers", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mails) != root.CountLeaves {
|
||||||
|
common.Fatal(
|
||||||
|
ctx,
|
||||||
|
"mails count mismatch",
|
||||||
|
clues.New("mail message count mismatch from API"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mail := range mails {
|
||||||
|
m := &common.Sanileaf[models.MailFolderable, any]{
|
||||||
|
Parent: root,
|
||||||
|
Self: mail,
|
||||||
|
ID: ptr.Val(mail.GetId()),
|
||||||
|
Name: ptr.Val(mail.GetSubject()),
|
||||||
|
Size: int64(len(ptr.Val(mail.GetBody().GetContent()))),
|
||||||
|
}
|
||||||
|
|
||||||
|
root.Leaves[m.ID] = m
|
||||||
|
}
|
||||||
|
|
||||||
recursivelyBuildTree(ctx, ac, root, userID, root.Name+"/")
|
recursivelyBuildTree(ctx, ac, root, userID, root.Name+"/")
|
||||||
|
|
||||||
return root
|
return root
|
||||||
@ -94,6 +122,11 @@ func recursivelyBuildTree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, child := range childFolders {
|
for _, child := range childFolders {
|
||||||
|
if int(ptr.Val(child.GetTotalItemCount())) == 0 {
|
||||||
|
common.Infof(ctx, "skipped empty folder: %s/%s", location, ptr.Val(child.GetDisplayName()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
c := &common.Sanitree[models.MailFolderable, any]{
|
c := &common.Sanitree[models.MailFolderable, any]{
|
||||||
Parent: stree,
|
Parent: stree,
|
||||||
Self: child,
|
Self: child,
|
||||||
|
|||||||
@ -65,6 +65,7 @@ func main() {
|
|||||||
|
|
||||||
expCMD.AddCommand(exportOneDriveCMD())
|
expCMD.AddCommand(exportOneDriveCMD())
|
||||||
expCMD.AddCommand(exportSharePointCMD())
|
expCMD.AddCommand(exportSharePointCMD())
|
||||||
|
expCMD.AddCommand(exportExchangeCMD())
|
||||||
expCMD.AddCommand(exportGroupsCMD())
|
expCMD.AddCommand(exportGroupsCMD())
|
||||||
root.AddCommand(expCMD)
|
root.AddCommand(expCMD)
|
||||||
|
|
||||||
@ -176,6 +177,29 @@ func sanityTestExportSharePoint(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func exportExchangeCMD() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "exchange",
|
||||||
|
Short: "run the exchange export sanity tests",
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
RunE: sanityTestExportExchange,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanityTestExportExchange(cmd *cobra.Command, args []string) error {
|
||||||
|
ctx := common.SetDebug(cmd.Context())
|
||||||
|
envs := common.EnvVars(ctx)
|
||||||
|
|
||||||
|
ac, err := common.GetAC()
|
||||||
|
if err != nil {
|
||||||
|
return print.Only(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
export.CheckEmailExport(ctx, ac, envs)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// service commands - restore
|
// service commands - restore
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -147,6 +147,21 @@ func (c Mail) GetItemsInContainerByCollisionKey(
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Mail) GetItemsInContainer(
|
||||||
|
ctx context.Context,
|
||||||
|
userID, containerID string,
|
||||||
|
) ([]models.Messageable, error) {
|
||||||
|
ctx = clues.Add(ctx, "container_id", containerID)
|
||||||
|
pager := c.NewMailPager(userID, containerID, false)
|
||||||
|
|
||||||
|
items, err := pagers.BatchEnumerateItems(ctx, pager)
|
||||||
|
if err != nil {
|
||||||
|
return nil, graph.Wrap(ctx, err, "enumerating mails")
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c Mail) GetItemIDsInContainer(
|
func (c Mail) GetItemIDsInContainer(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID, containerID string,
|
userID, containerID string,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user