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.
This commit is contained in:
Keepers 2022-08-17 12:13:55 -06:00 committed by GitHub
parent 9049f3c2bf
commit a1b9f876ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 379 additions and 115 deletions

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -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() {

View File

@ -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

View File

@ -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)))
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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)
}
}