From a1b9f876aecee5d5bd13f59ad47671d35ca57b27 Mon Sep 17 00:00:00 2001 From: Keepers Date: Wed, 17 Aug 2022 12:13:55 -0600 Subject: [PATCH] add final basic cli tests (#578) Adds the base CLI integration tests for backup, list, details, and restore. Also refactors out the global root command value in favor of a ctx-bound reference so that tests may control safely overwriting stdout to scrutinize output. --- src/cli/backup/exchange.go | 36 ++-- src/cli/backup/exchange_integration_test.go | 200 ++++++++++++++++--- src/cli/cli.go | 4 +- src/cli/config/config.go | 4 +- src/cli/print/print.go | 83 ++++---- src/cli/print/print_test.go | 9 +- src/cli/repo/s3.go | 24 +-- src/cli/restore/exchange.go | 12 +- src/cli/restore/exchange_integration_test.go | 100 ++++++++++ src/cmd/purge/purge.go | 22 +- 10 files changed, 379 insertions(+), 115 deletions(-) create mode 100644 src/cli/restore/exchange_integration_test.go diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index 7d10ba3f8..d6c1d0e64 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -171,12 +171,12 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { s, acct, err := config.GetStorageAndAccount(ctx, true, nil) if err != nil { - return Only(err) + return Only(ctx, err) } m365, err := acct.M365Config() if err != nil { - return Only(errors.Wrap(err, "Failed to parse m365 account config")) + return Only(ctx, errors.Wrap(err, "Failed to parse m365 account config")) } logger.Ctx(ctx).Debugw( @@ -187,7 +187,7 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { r, err := repository.Connect(ctx, acct, s) if err != nil { - return errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider) + return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider)) } defer utils.CloseRepo(ctx, r) @@ -195,20 +195,20 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { bo, err := r.NewBackup(ctx, sel, options.Control()) if err != nil { - return Only(errors.Wrap(err, "Failed to initialize Exchange backup")) + return Only(ctx, errors.Wrap(err, "Failed to initialize Exchange backup")) } err = bo.Run(ctx) if err != nil { - return Only(errors.Wrap(err, "Failed to run Exchange backup")) + return Only(ctx, errors.Wrap(err, "Failed to run Exchange backup")) } bu, err := r.Backup(ctx, bo.Results.BackupID) if err != nil { - return errors.Wrap(err, "Unable to retrieve backup results from storage") + return Only(ctx, errors.Wrap(err, "Unable to retrieve backup results from storage")) } - OutputBackup(*bu) + OutputBackup(ctx, *bu) return nil } @@ -272,12 +272,12 @@ func listExchangeCmd(cmd *cobra.Command, args []string) error { s, acct, err := config.GetStorageAndAccount(ctx, true, nil) if err != nil { - return Only(err) + return Only(ctx, err) } m365, err := acct.M365Config() if err != nil { - return Only(errors.Wrap(err, "Failed to parse m365 account config")) + return Only(ctx, errors.Wrap(err, "Failed to parse m365 account config")) } logger.Ctx(ctx).Debugw( @@ -286,16 +286,16 @@ func listExchangeCmd(cmd *cobra.Command, args []string) error { r, err := repository.Connect(ctx, acct, s) if err != nil { - return Only(errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider)) + return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider)) } defer utils.CloseRepo(ctx, r) rps, err := r.Backups(ctx) if err != nil { - return Only(errors.Wrap(err, "Failed to list backups in the repository")) + return Only(ctx, errors.Wrap(err, "Failed to list backups in the repository")) } - OutputBackups(rps) + OutputBackups(ctx, rps) return nil } @@ -335,12 +335,12 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error { s, acct, err := config.GetStorageAndAccount(ctx, true, nil) if err != nil { - return Only(err) + return Only(ctx, err) } m365, err := acct.M365Config() if err != nil { - return Only(errors.Wrap(err, "Failed to parse m365 account config")) + return Only(ctx, errors.Wrap(err, "Failed to parse m365 account config")) } logger.Ctx(ctx).Debugw( @@ -349,13 +349,13 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error { r, err := repository.Connect(ctx, acct, s) if err != nil { - return Only(errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider)) + return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider)) } defer utils.CloseRepo(ctx, r) d, _, err := r.BackupDetails(ctx, backupID) if err != nil { - return Only(errors.Wrap(err, "Failed to get backup details in the repository")) + return Only(ctx, errors.Wrap(err, "Failed to get backup details in the repository")) } sel := selectors.NewExchangeRestore() @@ -381,10 +381,10 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error { ds := sel.Reduce(d) if len(ds.Entries) == 0 { - return Only(errors.New("nothing to display: no items in the backup match the provided selectors")) + return Only(ctx, errors.New("nothing to display: no items in the backup match the provided selectors")) } - OutputEntries(ds.Entries) + OutputEntries(ctx, ds.Entries) return nil } diff --git a/src/cli/backup/exchange_integration_test.go b/src/cli/backup/exchange_integration_test.go index 0027d96ee..e601f0fd7 100644 --- a/src/cli/backup/exchange_integration_test.go +++ b/src/cli/backup/exchange_integration_test.go @@ -1,22 +1,43 @@ package backup_test import ( + "fmt" + "strings" "testing" + "time" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/alcionai/corso/cli" "github.com/alcionai/corso/cli/config" + "github.com/alcionai/corso/cli/print" + "github.com/alcionai/corso/internal/operations" "github.com/alcionai/corso/internal/tester" + "github.com/alcionai/corso/pkg/account" + "github.com/alcionai/corso/pkg/control" "github.com/alcionai/corso/pkg/repository" + "github.com/alcionai/corso/pkg/selectors" + "github.com/alcionai/corso/pkg/storage" ) -type ExchangeIntegrationSuite struct { +// --------------------------------------------------------------------------- +// tests with no prior backup +// --------------------------------------------------------------------------- + +type BackupExchangeIntegrationSuite struct { suite.Suite + acct account.Account + st storage.Storage + vpr *viper.Viper + cfgFP string + repo *repository.Repository + m365UserID string } -func TestExchangeIntegrationSuite(t *testing.T) { +func TestBackupExchangeIntegrationSuite(t *testing.T) { if err := tester.RunOnAny( tester.CorsoCITests, tester.CorsoCLITests, @@ -24,26 +45,21 @@ func TestExchangeIntegrationSuite(t *testing.T) { ); err != nil { t.Skip(err) } - suite.Run(t, new(ExchangeIntegrationSuite)) + suite.Run(t, new(BackupExchangeIntegrationSuite)) } -func (suite *ExchangeIntegrationSuite) SetupSuite() { - _, err := tester.GetRequiredEnvVars( - append( - tester.AWSStorageCredEnvs, - tester.M365AcctCredEnvs..., - )..., - ) - require.NoError(suite.T(), err) -} - -func (suite *ExchangeIntegrationSuite) TestExchangeBackupCmd() { - ctx := tester.NewContext() +func (suite *BackupExchangeIntegrationSuite) SetupSuite() { t := suite.T() + _, err := tester.GetRequiredEnvSls( + tester.AWSStorageCredEnvs, + tester.M365AcctCredEnvs) + require.NoError(t, err) - acct := tester.NewM365Account(t) - st := tester.NewPrefixedS3Storage(t) - cfg, err := st.S3Config() + // prepare common details + suite.acct = tester.NewM365Account(t) + suite.st = tester.NewPrefixedS3Storage(t) + + cfg, err := suite.st.S3Config() require.NoError(t, err) force := map[string]string{ @@ -51,25 +67,155 @@ func (suite *ExchangeIntegrationSuite) TestExchangeBackupCmd() { tester.TestCfgStorageProvider: "S3", tester.TestCfgPrefix: cfg.Prefix, } - vpr, configFP, err := tester.MakeTempTestConfigClone(t, force) + suite.vpr, suite.cfgFP, err = tester.MakeTempTestConfigClone(t, force) require.NoError(t, err) - ctx = config.SetViper(ctx, vpr) + ctx := config.SetViper(tester.NewContext(), suite.vpr) + + suite.m365UserID = tester.M365UserID(t) // init the repo first - _, err = repository.Initialize(ctx, acct, st) + suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st) require.NoError(t, err) +} - m365UserID := tester.M365UserID(t) +func (suite *BackupExchangeIntegrationSuite) TestExchangeBackupCmd() { + ctx := config.SetViper(tester.NewContext(), suite.vpr) + t := suite.T() - // then test it cmd := tester.StubRootCmd( "backup", "create", "exchange", - "--config-file", configFP, - "--user", m365UserID, - "--data", "email", - ) + "--config-file", suite.cfgFP, + "--user", suite.m365UserID, + "--data", "email") cli.BuildCommandTree(cmd) + var recorder strings.Builder + cmd.SetOut(&recorder) + ctx = print.SetRootCmd(ctx, cmd) // run the command require.NoError(t, cmd.ExecuteContext(ctx)) + + // as an offhand check: the result should contain a string with the current hour + result := recorder.String() + assert.Contains(t, result, time.Now().UTC().Format("2006-01-02T15")) + // and the m365 user id + assert.Contains(t, result, suite.m365UserID) +} + +// --------------------------------------------------------------------------- +// tests prepared with a previous backup +// --------------------------------------------------------------------------- + +type PreparedBackupExchangeIntegrationSuite struct { + suite.Suite + acct account.Account + st storage.Storage + vpr *viper.Viper + cfgFP string + repo *repository.Repository + m365UserID string + backupOp operations.BackupOperation +} + +func TestPreparedBackupExchangeIntegrationSuite(t *testing.T) { + if err := tester.RunOnAny( + tester.CorsoCITests, + tester.CorsoCLITests, + tester.CorsoCLIBackupTests, + ); err != nil { + t.Skip(err) + } + suite.Run(t, new(PreparedBackupExchangeIntegrationSuite)) +} + +func (suite *PreparedBackupExchangeIntegrationSuite) SetupSuite() { + t := suite.T() + _, err := tester.GetRequiredEnvSls( + tester.AWSStorageCredEnvs, + tester.M365AcctCredEnvs) + require.NoError(t, err) + + // prepare common details + suite.acct = tester.NewM365Account(t) + suite.st = tester.NewPrefixedS3Storage(t) + + cfg, err := suite.st.S3Config() + require.NoError(t, err) + + force := map[string]string{ + tester.TestCfgAccountProvider: "M365", + tester.TestCfgStorageProvider: "S3", + tester.TestCfgPrefix: cfg.Prefix, + } + suite.vpr, suite.cfgFP, err = tester.MakeTempTestConfigClone(t, force) + require.NoError(t, err) + ctx := config.SetViper(tester.NewContext(), suite.vpr) + + suite.m365UserID = tester.M365UserID(t) + + // init the repo first + suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st) + require.NoError(t, err) + + // some tests require an existing backup + sel := selectors.NewExchangeBackup() + // TODO: only backup the inbox + sel.Include(sel.Users([]string{suite.m365UserID})) + suite.backupOp, err = suite.repo.NewBackup( + ctx, + sel.Selector, + control.NewOptions(false)) + require.NoError(t, suite.backupOp.Run(ctx)) + require.NoError(t, err) + + time.Sleep(3 * time.Second) +} + +func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeListCmd() { + ctx := config.SetViper(tester.NewContext(), suite.vpr) + t := suite.T() + + cmd := tester.StubRootCmd( + "backup", "list", "exchange", + "--config-file", suite.cfgFP) + cli.BuildCommandTree(cmd) + var recorder strings.Builder + cmd.SetOut(&recorder) + ctx = print.SetRootCmd(ctx, cmd) + + // run the command + require.NoError(t, cmd.ExecuteContext(ctx)) + + // compare the output + result := recorder.String() + assert.Contains(t, result, suite.backupOp.Results.BackupID) +} + +func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeDetailsCmd() { + ctx := config.SetViper(tester.NewContext(), suite.vpr) + t := suite.T() + + // fetch the details from the repo first + deets, _, err := suite.repo.BackupDetails(ctx, string(suite.backupOp.Results.BackupID)) + require.NoError(t, err) + + cmd := tester.StubRootCmd( + "backup", "details", "exchange", + "--config-file", suite.cfgFP, + "--backup", string(suite.backupOp.Results.BackupID)) + cli.BuildCommandTree(cmd) + var recorder strings.Builder + cmd.SetOut(&recorder) + ctx = print.SetRootCmd(ctx, cmd) + + // run the command + require.NoError(t, cmd.ExecuteContext(ctx)) + + // compare the output + result := recorder.String() + for i, ent := range deets.Entries { + t.Run(fmt.Sprintf("detail %d", i), func(t *testing.T) { + assert.Contains(t, result, ent.RepoRef) + }) + } } diff --git a/src/cli/cli.go b/src/cli/cli.go index 463747690..ad2421734 100644 --- a/src/cli/cli.go +++ b/src/cli/cli.go @@ -37,7 +37,7 @@ var ( // Produces the same output as `corso --help`. func handleCorsoCmd(cmd *cobra.Command, args []string) error { if version { - print.Infof("Corso\nversion:\tpre-alpha\n") + print.Infof(cmd.Context(), "Corso\nversion:\tpre-alpha\n") return nil } return cmd.Help() @@ -73,9 +73,9 @@ func BuildCommandTree(cmd *cobra.Command) { // Handle builds and executes the cli processor. func Handle() { ctx := config.Seed(context.Background()) + ctx = print.SetRootCmd(ctx, corsoCmd) BuildCommandTree(corsoCmd) - print.SetRootCommand(corsoCmd) ctx, log := logger.Seed(ctx) defer func() { diff --git a/src/cli/config/config.go b/src/cli/config/config.go index 7ff6c0e7f..4112e1a18 100644 --- a/src/cli/config/config.go +++ b/src/cli/config/config.go @@ -35,7 +35,7 @@ func AddConfigFileFlag(cmd *cobra.Command) { fs := cmd.PersistentFlags() homeDir, err := os.UserHomeDir() if err != nil { - Err("finding $HOME directory (default) for config file") + Err(cmd.Context(), "finding $HOME directory (default) for config file") } fs.StringVar( &configFilePath, @@ -133,7 +133,7 @@ func GetViper(ctx context.Context) *viper.Viper { // set up properly. func Read(ctx context.Context) error { if err := viper.ReadInConfig(); err == nil { - Info("Using config file:", viper.ConfigFileUsed()) + Info(ctx, "Using config file:", viper.ConfigFileUsed()) return err } return nil diff --git a/src/cli/print/print.go b/src/cli/print/print.go index 6b7f1ef65..cc364134d 100644 --- a/src/cli/print/print.go +++ b/src/cli/print/print.go @@ -1,6 +1,7 @@ package print import ( + "context" "encoding/json" "fmt" "io" @@ -18,10 +19,24 @@ var ( outputAsJSONDebug bool ) -var rootCmd = &cobra.Command{} +type rootCmdCtx struct{} -func SetRootCommand(root *cobra.Command) { - rootCmd = root +// Adds a root cobra command to the context. +// Used to amend output controls like SilenceUsage or to retrieve +// the command's output writer. +func SetRootCmd(ctx context.Context, root *cobra.Command) context.Context { + return context.WithValue(ctx, rootCmdCtx{}, root) +} + +// Gets the root cobra command from the context. +// If no command is found, returns a new, blank command. +func getRootCmd(ctx context.Context) *cobra.Command { + cmdIface := ctx.Value(rootCmdCtx{}) + cmd, ok := cmdIface.(*cobra.Command) + if cmd == nil || !ok { + return &cobra.Command{} + } + return cmd } // adds the persistent flag --output to the provided command. @@ -38,16 +53,16 @@ func AddOutputFlag(parent *cobra.Command) { // Only tells the CLI to only display this error, preventing the usage // (ie, help) menu from displaying as well. -func Only(e error) error { - rootCmd.SilenceUsage = true +func Only(ctx context.Context, e error) error { + getRootCmd(ctx).SilenceUsage = true return e } // Err prints the params to cobra's error writer (stdErr by default) // if s is nil, prints nothing. // Prepends the message with "Error: " -func Err(s ...any) { - err(rootCmd.ErrOrStderr(), s...) +func Err(ctx context.Context, s ...any) { + err(getRootCmd(ctx).ErrOrStderr(), s...) } // err is the testable core of Err() @@ -61,8 +76,8 @@ func err(w io.Writer, s ...any) { // Info prints the params to cobra's error writer (stdErr by default) // if s is nil, prints nothing. -func Info(s ...any) { - info(rootCmd.ErrOrStderr(), s...) +func Info(ctx context.Context, s ...any) { + info(getRootCmd(ctx).ErrOrStderr(), s...) } // info is the testable core of Info() @@ -76,8 +91,8 @@ func info(w io.Writer, s ...any) { // Info prints the formatted strings to cobra's error writer (stdErr by default) // if t is empty, prints nothing. -func Infof(t string, s ...any) { - infof(rootCmd.ErrOrStderr(), t, s...) +func Infof(ctx context.Context, t string, s ...any) { + infof(getRootCmd(ctx).ErrOrStderr(), t, s...) } // infof is the testable core of Infof() @@ -105,25 +120,25 @@ type Printable interface { } //revive:disable:redefines-builtin-id -func print(p Printable) { +func print(w io.Writer, p Printable) { if outputAsJSON || outputAsJSONDebug { - outputJSON(p, outputAsJSONDebug) + outputJSON(w, p, outputAsJSONDebug) return } - outputTable([]Printable{p}) + outputTable(w, []Printable{p}) } // printAll prints the slice of printable items, // according to the caller's requested format. -func printAll(ps []Printable) { +func printAll(w io.Writer, ps []Printable) { if len(ps) == 0 { return } if outputAsJSON || outputAsJSONDebug { - outputJSONArr(ps, outputAsJSONDebug) + outputJSONArr(w, ps, outputAsJSONDebug) return } - outputTable(ps) + outputTable(w, ps) } // ------------------------------------------------------------------------------------------ @@ -131,26 +146,26 @@ func printAll(ps []Printable) { // ------------------------------------------------------------------------------------------ // Prints the backup to the terminal with stdout. -func OutputBackup(b backup.Backup) { - print(b) +func OutputBackup(ctx context.Context, b backup.Backup) { + print(getRootCmd(ctx).OutOrStdout(), b) } // Prints the backups to the terminal with stdout. -func OutputBackups(bs []backup.Backup) { +func OutputBackups(ctx context.Context, bs []backup.Backup) { ps := []Printable{} for _, b := range bs { ps = append(ps, b) } - printAll(ps) + printAll(getRootCmd(ctx).OutOrStdout(), ps) } // Prints the entries to the terminal with stdout. -func OutputEntries(des []details.DetailsEntry) { +func OutputEntries(ctx context.Context, des []details.DetailsEntry) { ps := []Printable{} for _, de := range des { ps = append(ps, de) } - printAll(ps) + printAll(getRootCmd(ctx).OutOrStdout(), ps) } // ------------------------------------------------------------------------------------------ @@ -158,7 +173,7 @@ func OutputEntries(des []details.DetailsEntry) { // ------------------------------------------------------------------------------------------ // output to stdout the list of printable structs in a table -func outputTable(ps []Printable) { +func outputTable(w io.Writer, ps []Printable) { t := table.Table{ Headers: ps[0].Headers(), Rows: [][]string{}, @@ -167,7 +182,7 @@ func outputTable(ps []Printable) { t.Rows = append(t.Rows, p.Values()) } _ = t.WriteTable( - rootCmd.OutOrStdout(), + w, &table.Config{ ShowIndex: false, Color: false, @@ -179,15 +194,15 @@ func outputTable(ps []Printable) { // JSON // ------------------------------------------------------------------------------------------ -func outputJSON(p Printable, debug bool) { +func outputJSON(w io.Writer, p Printable, debug bool) { if debug { - printJSON(p) + printJSON(w, p) return } - printJSON(p.MinimumPrintable()) + printJSON(w, p.MinimumPrintable()) } -func outputJSONArr(ps []Printable, debug bool) { +func outputJSONArr(w io.Writer, ps []Printable, debug bool) { sl := make([]any, 0, len(ps)) for _, p := range ps { if debug { @@ -196,17 +211,15 @@ func outputJSONArr(ps []Printable, debug bool) { sl = append(sl, p.MinimumPrintable()) } } - printJSON(sl) + printJSON(w, sl) } // output to stdout the list of printable structs as json. -func printJSON(a any) { +func printJSON(w io.Writer, a any) { bs, err := json.Marshal(a) if err != nil { - fmt.Fprintf(rootCmd.OutOrStderr(), "error formatting results to json: %v\n", err) + fmt.Fprintf(w, "error formatting results to json: %v\n", err) return } - fmt.Fprintln( - rootCmd.OutOrStdout(), - string(pretty.Pretty(bs))) + fmt.Fprintln(w, string(pretty.Pretty(bs))) } diff --git a/src/cli/print/print_test.go b/src/cli/print/print_test.go index cbef53916..1416eeedd 100644 --- a/src/cli/print/print_test.go +++ b/src/cli/print/print_test.go @@ -7,6 +7,8 @@ import ( "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/internal/tester" ) type PrintUnitSuite struct { @@ -18,12 +20,11 @@ func TestPrintUnitSuite(t *testing.T) { } func (suite *PrintUnitSuite) TestOnly() { + ctx := tester.NewContext() t := suite.T() c := &cobra.Command{} - oldRoot := rootCmd - defer SetRootCommand(oldRoot) - SetRootCommand(c) - assert.NoError(t, Only(nil)) + ctx = SetRootCmd(ctx, c) + assert.NoError(t, Only(ctx, nil)) assert.True(t, c.SilenceUsage) } diff --git a/src/cli/repo/s3.go b/src/cli/repo/s3.go index b84c9c708..709f5309b 100644 --- a/src/cli/repo/s3.go +++ b/src/cli/repo/s3.go @@ -78,16 +78,16 @@ func initS3Cmd(cmd *cobra.Command, args []string) error { s, a, err := config.GetStorageAndAccount(ctx, false, s3Overrides()) if err != nil { - return Only(err) + return Only(ctx, err) } s3Cfg, err := s.S3Config() if err != nil { - return Only(errors.Wrap(err, "Retrieving s3 configuration")) + return Only(ctx, errors.Wrap(err, "Retrieving s3 configuration")) } m365, err := a.M365Config() if err != nil { - return Only(errors.Wrap(err, "Failed to parse m365 account config")) + return Only(ctx, errors.Wrap(err, "Failed to parse m365 account config")) } log.Debugw( @@ -103,14 +103,14 @@ func initS3Cmd(cmd *cobra.Command, args []string) error { if succeedIfExists && kopia.IsRepoAlreadyExistsError(err) { return nil } - return Only(errors.Wrap(err, "Failed to initialize a new S3 repository")) + return Only(ctx, errors.Wrap(err, "Failed to initialize a new S3 repository")) } defer utils.CloseRepo(ctx, r) - Infof("Initialized a S3 repository within bucket %s.", s3Cfg.Bucket) + Infof(ctx, "Initialized a S3 repository within bucket %s.", s3Cfg.Bucket) if err = config.WriteRepoConfig(ctx, s3Cfg, m365); err != nil { - return Only(errors.Wrap(err, "Failed to write repository configuration")) + return Only(ctx, errors.Wrap(err, "Failed to write repository configuration")) } return nil } @@ -141,15 +141,15 @@ func connectS3Cmd(cmd *cobra.Command, args []string) error { s, a, err := config.GetStorageAndAccount(ctx, true, s3Overrides()) if err != nil { - return Only(err) + return Only(ctx, err) } s3Cfg, err := s.S3Config() if err != nil { - return Only(errors.Wrap(err, "Retrieving s3 configuration")) + return Only(ctx, errors.Wrap(err, "Retrieving s3 configuration")) } m365, err := a.M365Config() if err != nil { - return Only(errors.Wrap(err, "Failed to parse m365 account config")) + return Only(ctx, errors.Wrap(err, "Failed to parse m365 account config")) } log.Debugw( @@ -162,14 +162,14 @@ func connectS3Cmd(cmd *cobra.Command, args []string) error { r, err := repository.Connect(ctx, a, s) if err != nil { - return Only(errors.Wrap(err, "Failed to connect to the S3 repository")) + return Only(ctx, errors.Wrap(err, "Failed to connect to the S3 repository")) } defer utils.CloseRepo(ctx, r) - Infof("Connected to S3 bucket %s.", s3Cfg.Bucket) + Infof(ctx, "Connected to S3 bucket %s.", s3Cfg.Bucket) if err = config.WriteRepoConfig(ctx, s3Cfg, m365); err != nil { - return Only(errors.Wrap(err, "Failed to write repository configuration")) + return Only(ctx, errors.Wrap(err, "Failed to write repository configuration")) } return nil } diff --git a/src/cli/restore/exchange.go b/src/cli/restore/exchange.go index 19ce88807..3343d27f4 100644 --- a/src/cli/restore/exchange.go +++ b/src/cli/restore/exchange.go @@ -125,12 +125,12 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error { s, a, err := config.GetStorageAndAccount(ctx, true, nil) if err != nil { - return Only(err) + return Only(ctx, err) } m365, err := a.M365Config() if err != nil { - return Only(errors.Wrap(err, "Failed to parse m365 account config")) + return Only(ctx, errors.Wrap(err, "Failed to parse m365 account config")) } logger.Ctx(ctx).Debugw( @@ -142,7 +142,7 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error { r, err := repository.Connect(ctx, a, s) if err != nil { - return Only(errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider)) + return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider)) } defer utils.CloseRepo(ctx, r) @@ -169,14 +169,14 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error { ro, err := r.NewRestore(ctx, backupID, sel.Selector, options.Control()) if err != nil { - return Only(errors.Wrap(err, "Failed to initialize Exchange restore")) + return Only(ctx, errors.Wrap(err, "Failed to initialize Exchange restore")) } if err := ro.Run(ctx); err != nil { - return Only(errors.Wrap(err, "Failed to run Exchange restore")) + return Only(ctx, errors.Wrap(err, "Failed to run Exchange restore")) } - Infof("Restored Exchange in %s for user %s.\n", s.Provider, user) + Infof(ctx, "Restored Exchange in %s for user %s.\n", s.Provider, user) return nil } diff --git a/src/cli/restore/exchange_integration_test.go b/src/cli/restore/exchange_integration_test.go new file mode 100644 index 000000000..b4c5009ab --- /dev/null +++ b/src/cli/restore/exchange_integration_test.go @@ -0,0 +1,100 @@ +package restore_test + +import ( + "testing" + "time" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/cli" + "github.com/alcionai/corso/cli/config" + "github.com/alcionai/corso/internal/operations" + "github.com/alcionai/corso/internal/tester" + "github.com/alcionai/corso/pkg/account" + "github.com/alcionai/corso/pkg/control" + "github.com/alcionai/corso/pkg/repository" + "github.com/alcionai/corso/pkg/selectors" + "github.com/alcionai/corso/pkg/storage" +) + +type BackupExchangeIntegrationSuite struct { + suite.Suite + acct account.Account + st storage.Storage + vpr *viper.Viper + cfgFP string + repo *repository.Repository + m365UserID string + backupOp operations.BackupOperation +} + +func TestBackupExchangeIntegrationSuite(t *testing.T) { + if err := tester.RunOnAny( + tester.CorsoCITests, + tester.CorsoCLITests, + tester.CorsoCLIBackupTests, + ); err != nil { + t.Skip(err) + } + suite.Run(t, new(BackupExchangeIntegrationSuite)) +} + +func (suite *BackupExchangeIntegrationSuite) SetupSuite() { + t := suite.T() + _, err := tester.GetRequiredEnvSls( + tester.AWSStorageCredEnvs, + tester.M365AcctCredEnvs) + require.NoError(t, err) + + // aggregate required details + + suite.acct = tester.NewM365Account(t) + suite.st = tester.NewPrefixedS3Storage(t) + + cfg, err := suite.st.S3Config() + require.NoError(t, err) + + force := map[string]string{ + tester.TestCfgAccountProvider: "M365", + tester.TestCfgStorageProvider: "S3", + tester.TestCfgPrefix: cfg.Prefix, + } + suite.vpr, suite.cfgFP, err = tester.MakeTempTestConfigClone(t, force) + require.NoError(t, err) + ctx := config.SetViper(tester.NewContext(), suite.vpr) + + suite.m365UserID = tester.M365UserID(t) + + // init the repo first + suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st) + require.NoError(t, err) + + // restoration requires an existing backup + sel := selectors.NewExchangeBackup() + // TODO: only backup the inbox + sel.Include(sel.Users([]string{suite.m365UserID})) + suite.backupOp, err = suite.repo.NewBackup( + ctx, + sel.Selector, + control.NewOptions(false)) + require.NoError(t, suite.backupOp.Run(ctx)) + require.NoError(t, err) + + time.Sleep(3 * time.Second) +} + +func (suite *BackupExchangeIntegrationSuite) TestExchangeRestoreCmd() { + ctx := config.SetViper(tester.NewContext(), suite.vpr) + t := suite.T() + + cmd := tester.StubRootCmd( + "restore", "exchange", + "--config-file", suite.cfgFP, + "--backup", string(suite.backupOp.Results.BackupID)) + cli.BuildCommandTree(cmd) + + // run the command + require.NoError(t, cmd.ExecuteContext(ctx)) +} diff --git a/src/cmd/purge/purge.go b/src/cmd/purge/purge.go index b72e8079f..a43f298b9 100644 --- a/src/cmd/purge/purge.go +++ b/src/cmd/purge/purge.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "time" @@ -30,6 +31,8 @@ var ( ) func doFolderPurge(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + if utils.HasNoFlagsAndShownHelp(cmd) { return nil } @@ -41,19 +44,19 @@ func doFolderPurge(cmd *cobra.Command, args []string) error { } acct, err := account.NewAccount(account.ProviderM365, m365Cfg) if err != nil { - return Only(errors.Wrap(err, "finding m365 account details")) + return Only(ctx, errors.Wrap(err, "finding m365 account details")) } // build a graph connector gc, err := connector.NewGraphConnector(acct) if err != nil { - return Only(errors.Wrap(err, "connecting to graph api")) + return Only(ctx, errors.Wrap(err, "connecting to graph api")) } // get them folders mfs, err := exchange.GetAllMailFolders(gc.Service(), user, prefix) if err != nil { - return Only(errors.Wrap(err, "retrieving mail folders")) + return Only(ctx, errors.Wrap(err, "retrieving mail folders")) } // format the time input @@ -61,7 +64,7 @@ func doFolderPurge(cmd *cobra.Command, args []string) error { if len(before) > 0 { beforeTime, err = common.ParseTime(before) if err != nil { - return Only(errors.Wrap(err, "parsing before flag to time")) + return Only(ctx, errors.Wrap(err, "parsing before flag to time")) } } stLen := len(common.SimpleDateTimeFormat) @@ -76,7 +79,7 @@ func doFolderPurge(cmd *cobra.Command, args []string) error { dnSuff := mf.DisplayName[dnLen-stLen:] dnTime, err := common.ParseTime(dnSuff) if err != nil { - Info(errors.Wrapf(err, "Error: deleting folder [%s]", mf.DisplayName)) + Info(ctx, errors.Wrapf(err, "Error: deleting folder [%s]", mf.DisplayName)) continue } delete = dnTime.Before(beforeTime) @@ -86,10 +89,10 @@ func doFolderPurge(cmd *cobra.Command, args []string) error { continue } - Info("Deleting folder: ", mf.DisplayName) + Info(ctx, "Deleting folder: ", mf.DisplayName) err = exchange.DeleteMailFolder(gc.Service(), user, mf.ID) if err != nil { - Info(errors.Wrapf(err, "Error: deleting folder [%s]", mf.DisplayName)) + Info(ctx, errors.Wrapf(err, "Error: deleting folder [%s]", mf.DisplayName)) } } @@ -97,6 +100,7 @@ func doFolderPurge(cmd *cobra.Command, args []string) error { } func main() { + ctx := SetRootCmd(context.Background(), purgeCmd) fs := purgeCmd.Flags() fs.StringVar(&before, "before", "", "folders older than this date are deleted. (default: now in UTC)") fs.StringVar(&user, "user", "", "m365 user id whose folders will be deleted") @@ -105,8 +109,8 @@ func main() { fs.StringVar(&prefix, "prefix", "", "filters mail folders by displayName prefix") cobra.CheckErr(purgeCmd.MarkFlagRequired("prefix")) - if err := purgeCmd.Execute(); err != nil { - Info("Error: ", err.Error()) + if err := purgeCmd.ExecuteContext(ctx); err != nil { + Info(purgeCmd.Context(), "Error: ", err.Error()) os.Exit(1) } }