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) s, acct, err := config.GetStorageAndAccount(ctx, true, nil)
if err != nil { if err != nil {
return Only(err) return Only(ctx, err)
} }
m365, err := acct.M365Config() m365, err := acct.M365Config()
if err != nil { 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( logger.Ctx(ctx).Debugw(
@ -187,7 +187,7 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error {
r, err := repository.Connect(ctx, acct, s) r, err := repository.Connect(ctx, acct, s)
if err != nil { 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) 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()) bo, err := r.NewBackup(ctx, sel, options.Control())
if err != nil { 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) err = bo.Run(ctx)
if err != nil { 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) bu, err := r.Backup(ctx, bo.Results.BackupID)
if err != nil { 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 return nil
} }
@ -272,12 +272,12 @@ func listExchangeCmd(cmd *cobra.Command, args []string) error {
s, acct, err := config.GetStorageAndAccount(ctx, true, nil) s, acct, err := config.GetStorageAndAccount(ctx, true, nil)
if err != nil { if err != nil {
return Only(err) return Only(ctx, err)
} }
m365, err := acct.M365Config() m365, err := acct.M365Config()
if err != nil { 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( logger.Ctx(ctx).Debugw(
@ -286,16 +286,16 @@ func listExchangeCmd(cmd *cobra.Command, args []string) error {
r, err := repository.Connect(ctx, acct, s) r, err := repository.Connect(ctx, acct, s)
if err != nil { 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) defer utils.CloseRepo(ctx, r)
rps, err := r.Backups(ctx) rps, err := r.Backups(ctx)
if err != nil { 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 return nil
} }
@ -335,12 +335,12 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error {
s, acct, err := config.GetStorageAndAccount(ctx, true, nil) s, acct, err := config.GetStorageAndAccount(ctx, true, nil)
if err != nil { if err != nil {
return Only(err) return Only(ctx, err)
} }
m365, err := acct.M365Config() m365, err := acct.M365Config()
if err != nil { 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( logger.Ctx(ctx).Debugw(
@ -349,13 +349,13 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error {
r, err := repository.Connect(ctx, acct, s) r, err := repository.Connect(ctx, acct, s)
if err != nil { 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) defer utils.CloseRepo(ctx, r)
d, _, err := r.BackupDetails(ctx, backupID) d, _, err := r.BackupDetails(ctx, backupID)
if err != nil { 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() sel := selectors.NewExchangeRestore()
@ -381,10 +381,10 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error {
ds := sel.Reduce(d) ds := sel.Reduce(d)
if len(ds.Entries) == 0 { 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 return nil
} }

View File

@ -1,22 +1,43 @@
package backup_test package backup_test
import ( import (
"fmt"
"strings"
"testing" "testing"
"time"
"github.com/spf13/viper"
"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/cli" "github.com/alcionai/corso/cli"
"github.com/alcionai/corso/cli/config" "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/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/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 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( if err := tester.RunOnAny(
tester.CorsoCITests, tester.CorsoCITests,
tester.CorsoCLITests, tester.CorsoCLITests,
@ -24,26 +45,21 @@ func TestExchangeIntegrationSuite(t *testing.T) {
); err != nil { ); err != nil {
t.Skip(err) t.Skip(err)
} }
suite.Run(t, new(ExchangeIntegrationSuite)) suite.Run(t, new(BackupExchangeIntegrationSuite))
} }
func (suite *ExchangeIntegrationSuite) SetupSuite() { func (suite *BackupExchangeIntegrationSuite) SetupSuite() {
_, err := tester.GetRequiredEnvVars(
append(
tester.AWSStorageCredEnvs,
tester.M365AcctCredEnvs...,
)...,
)
require.NoError(suite.T(), err)
}
func (suite *ExchangeIntegrationSuite) TestExchangeBackupCmd() {
ctx := tester.NewContext()
t := suite.T() t := suite.T()
_, err := tester.GetRequiredEnvSls(
tester.AWSStorageCredEnvs,
tester.M365AcctCredEnvs)
require.NoError(t, err)
acct := tester.NewM365Account(t) // prepare common details
st := tester.NewPrefixedS3Storage(t) suite.acct = tester.NewM365Account(t)
cfg, err := st.S3Config() suite.st = tester.NewPrefixedS3Storage(t)
cfg, err := suite.st.S3Config()
require.NoError(t, err) require.NoError(t, err)
force := map[string]string{ force := map[string]string{
@ -51,25 +67,155 @@ func (suite *ExchangeIntegrationSuite) TestExchangeBackupCmd() {
tester.TestCfgStorageProvider: "S3", tester.TestCfgStorageProvider: "S3",
tester.TestCfgPrefix: cfg.Prefix, tester.TestCfgPrefix: cfg.Prefix,
} }
vpr, configFP, err := tester.MakeTempTestConfigClone(t, force) suite.vpr, suite.cfgFP, err = tester.MakeTempTestConfigClone(t, force)
require.NoError(t, err) 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 // init the repo first
_, err = repository.Initialize(ctx, acct, st) suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st)
require.NoError(t, err) 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( cmd := tester.StubRootCmd(
"backup", "create", "exchange", "backup", "create", "exchange",
"--config-file", configFP, "--config-file", suite.cfgFP,
"--user", m365UserID, "--user", suite.m365UserID,
"--data", "email", "--data", "email")
)
cli.BuildCommandTree(cmd) cli.BuildCommandTree(cmd)
var recorder strings.Builder
cmd.SetOut(&recorder)
ctx = print.SetRootCmd(ctx, cmd)
// run the command // run the command
require.NoError(t, cmd.ExecuteContext(ctx)) 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`. // Produces the same output as `corso --help`.
func handleCorsoCmd(cmd *cobra.Command, args []string) error { func handleCorsoCmd(cmd *cobra.Command, args []string) error {
if version { if version {
print.Infof("Corso\nversion:\tpre-alpha\n") print.Infof(cmd.Context(), "Corso\nversion:\tpre-alpha\n")
return nil return nil
} }
return cmd.Help() return cmd.Help()
@ -73,9 +73,9 @@ func BuildCommandTree(cmd *cobra.Command) {
// Handle builds and executes the cli processor. // Handle builds and executes the cli processor.
func Handle() { func Handle() {
ctx := config.Seed(context.Background()) ctx := config.Seed(context.Background())
ctx = print.SetRootCmd(ctx, corsoCmd)
BuildCommandTree(corsoCmd) BuildCommandTree(corsoCmd)
print.SetRootCommand(corsoCmd)
ctx, log := logger.Seed(ctx) ctx, log := logger.Seed(ctx)
defer func() { defer func() {

View File

@ -35,7 +35,7 @@ func AddConfigFileFlag(cmd *cobra.Command) {
fs := cmd.PersistentFlags() fs := cmd.PersistentFlags()
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
Err("finding $HOME directory (default) for config file") Err(cmd.Context(), "finding $HOME directory (default) for config file")
} }
fs.StringVar( fs.StringVar(
&configFilePath, &configFilePath,
@ -133,7 +133,7 @@ func GetViper(ctx context.Context) *viper.Viper {
// set up properly. // set up properly.
func Read(ctx context.Context) error { func Read(ctx context.Context) error {
if err := viper.ReadInConfig(); err == nil { if err := viper.ReadInConfig(); err == nil {
Info("Using config file:", viper.ConfigFileUsed()) Info(ctx, "Using config file:", viper.ConfigFileUsed())
return err return err
} }
return nil return nil

View File

@ -1,6 +1,7 @@
package print package print
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -18,10 +19,24 @@ var (
outputAsJSONDebug bool outputAsJSONDebug bool
) )
var rootCmd = &cobra.Command{} type rootCmdCtx struct{}
func SetRootCommand(root *cobra.Command) { // Adds a root cobra command to the context.
rootCmd = root // 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. // 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 // Only tells the CLI to only display this error, preventing the usage
// (ie, help) menu from displaying as well. // (ie, help) menu from displaying as well.
func Only(e error) error { func Only(ctx context.Context, e error) error {
rootCmd.SilenceUsage = true getRootCmd(ctx).SilenceUsage = true
return e return e
} }
// Err prints the params to cobra's error writer (stdErr by default) // Err prints the params to cobra's error writer (stdErr by default)
// if s is nil, prints nothing. // if s is nil, prints nothing.
// Prepends the message with "Error: " // Prepends the message with "Error: "
func Err(s ...any) { func Err(ctx context.Context, s ...any) {
err(rootCmd.ErrOrStderr(), s...) err(getRootCmd(ctx).ErrOrStderr(), s...)
} }
// err is the testable core of Err() // 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) // Info prints the params to cobra's error writer (stdErr by default)
// if s is nil, prints nothing. // if s is nil, prints nothing.
func Info(s ...any) { func Info(ctx context.Context, s ...any) {
info(rootCmd.ErrOrStderr(), s...) info(getRootCmd(ctx).ErrOrStderr(), s...)
} }
// info is the testable core of Info() // 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) // Info prints the formatted strings to cobra's error writer (stdErr by default)
// if t is empty, prints nothing. // if t is empty, prints nothing.
func Infof(t string, s ...any) { func Infof(ctx context.Context, t string, s ...any) {
infof(rootCmd.ErrOrStderr(), t, s...) infof(getRootCmd(ctx).ErrOrStderr(), t, s...)
} }
// infof is the testable core of Infof() // infof is the testable core of Infof()
@ -105,25 +120,25 @@ type Printable interface {
} }
//revive:disable:redefines-builtin-id //revive:disable:redefines-builtin-id
func print(p Printable) { func print(w io.Writer, p Printable) {
if outputAsJSON || outputAsJSONDebug { if outputAsJSON || outputAsJSONDebug {
outputJSON(p, outputAsJSONDebug) outputJSON(w, p, outputAsJSONDebug)
return return
} }
outputTable([]Printable{p}) outputTable(w, []Printable{p})
} }
// printAll prints the slice of printable items, // printAll prints the slice of printable items,
// according to the caller's requested format. // according to the caller's requested format.
func printAll(ps []Printable) { func printAll(w io.Writer, ps []Printable) {
if len(ps) == 0 { if len(ps) == 0 {
return return
} }
if outputAsJSON || outputAsJSONDebug { if outputAsJSON || outputAsJSONDebug {
outputJSONArr(ps, outputAsJSONDebug) outputJSONArr(w, ps, outputAsJSONDebug)
return return
} }
outputTable(ps) outputTable(w, ps)
} }
// ------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------
@ -131,26 +146,26 @@ func printAll(ps []Printable) {
// ------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------
// Prints the backup to the terminal with stdout. // Prints the backup to the terminal with stdout.
func OutputBackup(b backup.Backup) { func OutputBackup(ctx context.Context, b backup.Backup) {
print(b) print(getRootCmd(ctx).OutOrStdout(), b)
} }
// Prints the backups to the terminal with stdout. // Prints the backups to the terminal with stdout.
func OutputBackups(bs []backup.Backup) { func OutputBackups(ctx context.Context, bs []backup.Backup) {
ps := []Printable{} ps := []Printable{}
for _, b := range bs { for _, b := range bs {
ps = append(ps, b) ps = append(ps, b)
} }
printAll(ps) printAll(getRootCmd(ctx).OutOrStdout(), ps)
} }
// Prints the entries to the terminal with stdout. // Prints the entries to the terminal with stdout.
func OutputEntries(des []details.DetailsEntry) { func OutputEntries(ctx context.Context, des []details.DetailsEntry) {
ps := []Printable{} ps := []Printable{}
for _, de := range des { for _, de := range des {
ps = append(ps, de) 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 // 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{ t := table.Table{
Headers: ps[0].Headers(), Headers: ps[0].Headers(),
Rows: [][]string{}, Rows: [][]string{},
@ -167,7 +182,7 @@ func outputTable(ps []Printable) {
t.Rows = append(t.Rows, p.Values()) t.Rows = append(t.Rows, p.Values())
} }
_ = t.WriteTable( _ = t.WriteTable(
rootCmd.OutOrStdout(), w,
&table.Config{ &table.Config{
ShowIndex: false, ShowIndex: false,
Color: false, Color: false,
@ -179,15 +194,15 @@ func outputTable(ps []Printable) {
// JSON // JSON
// ------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------
func outputJSON(p Printable, debug bool) { func outputJSON(w io.Writer, p Printable, debug bool) {
if debug { if debug {
printJSON(p) printJSON(w, p)
return 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)) sl := make([]any, 0, len(ps))
for _, p := range ps { for _, p := range ps {
if debug { if debug {
@ -196,17 +211,15 @@ func outputJSONArr(ps []Printable, debug bool) {
sl = append(sl, p.MinimumPrintable()) sl = append(sl, p.MinimumPrintable())
} }
} }
printJSON(sl) printJSON(w, sl)
} }
// output to stdout the list of printable structs as json. // 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) bs, err := json.Marshal(a)
if err != nil { 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 return
} }
fmt.Fprintln( fmt.Fprintln(w, string(pretty.Pretty(bs)))
rootCmd.OutOrStdout(),
string(pretty.Pretty(bs)))
} }

View File

@ -7,6 +7,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/internal/tester"
) )
type PrintUnitSuite struct { type PrintUnitSuite struct {
@ -18,12 +20,11 @@ func TestPrintUnitSuite(t *testing.T) {
} }
func (suite *PrintUnitSuite) TestOnly() { func (suite *PrintUnitSuite) TestOnly() {
ctx := tester.NewContext()
t := suite.T() t := suite.T()
c := &cobra.Command{} c := &cobra.Command{}
oldRoot := rootCmd ctx = SetRootCmd(ctx, c)
defer SetRootCommand(oldRoot) assert.NoError(t, Only(ctx, nil))
SetRootCommand(c)
assert.NoError(t, Only(nil))
assert.True(t, c.SilenceUsage) 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()) s, a, err := config.GetStorageAndAccount(ctx, false, s3Overrides())
if err != nil { if err != nil {
return Only(err) return Only(ctx, err)
} }
s3Cfg, err := s.S3Config() s3Cfg, err := s.S3Config()
if err != nil { if err != nil {
return Only(errors.Wrap(err, "Retrieving s3 configuration")) return Only(ctx, errors.Wrap(err, "Retrieving s3 configuration"))
} }
m365, err := a.M365Config() m365, err := a.M365Config()
if err != nil { 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( log.Debugw(
@ -103,14 +103,14 @@ func initS3Cmd(cmd *cobra.Command, args []string) error {
if succeedIfExists && kopia.IsRepoAlreadyExistsError(err) { if succeedIfExists && kopia.IsRepoAlreadyExistsError(err) {
return nil 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) 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 { 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 return nil
} }
@ -141,15 +141,15 @@ func connectS3Cmd(cmd *cobra.Command, args []string) error {
s, a, err := config.GetStorageAndAccount(ctx, true, s3Overrides()) s, a, err := config.GetStorageAndAccount(ctx, true, s3Overrides())
if err != nil { if err != nil {
return Only(err) return Only(ctx, err)
} }
s3Cfg, err := s.S3Config() s3Cfg, err := s.S3Config()
if err != nil { if err != nil {
return Only(errors.Wrap(err, "Retrieving s3 configuration")) return Only(ctx, errors.Wrap(err, "Retrieving s3 configuration"))
} }
m365, err := a.M365Config() m365, err := a.M365Config()
if err != nil { 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( log.Debugw(
@ -162,14 +162,14 @@ func connectS3Cmd(cmd *cobra.Command, args []string) error {
r, err := repository.Connect(ctx, a, s) r, err := repository.Connect(ctx, a, s)
if err != nil { 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) 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 { 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 return nil
} }

View File

@ -125,12 +125,12 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error {
s, a, err := config.GetStorageAndAccount(ctx, true, nil) s, a, err := config.GetStorageAndAccount(ctx, true, nil)
if err != nil { if err != nil {
return Only(err) return Only(ctx, err)
} }
m365, err := a.M365Config() m365, err := a.M365Config()
if err != nil { 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( logger.Ctx(ctx).Debugw(
@ -142,7 +142,7 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error {
r, err := repository.Connect(ctx, a, s) r, err := repository.Connect(ctx, a, s)
if err != nil { 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) 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()) ro, err := r.NewRestore(ctx, backupID, sel.Selector, options.Control())
if err != nil { 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 { 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 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 package main
import ( import (
"context"
"os" "os"
"time" "time"
@ -30,6 +31,8 @@ var (
) )
func doFolderPurge(cmd *cobra.Command, args []string) error { func doFolderPurge(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if utils.HasNoFlagsAndShownHelp(cmd) { if utils.HasNoFlagsAndShownHelp(cmd) {
return nil return nil
} }
@ -41,19 +44,19 @@ func doFolderPurge(cmd *cobra.Command, args []string) error {
} }
acct, err := account.NewAccount(account.ProviderM365, m365Cfg) acct, err := account.NewAccount(account.ProviderM365, m365Cfg)
if err != nil { 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 // build a graph connector
gc, err := connector.NewGraphConnector(acct) gc, err := connector.NewGraphConnector(acct)
if err != nil { 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 // get them folders
mfs, err := exchange.GetAllMailFolders(gc.Service(), user, prefix) mfs, err := exchange.GetAllMailFolders(gc.Service(), user, prefix)
if err != nil { if err != nil {
return Only(errors.Wrap(err, "retrieving mail folders")) return Only(ctx, errors.Wrap(err, "retrieving mail folders"))
} }
// format the time input // format the time input
@ -61,7 +64,7 @@ func doFolderPurge(cmd *cobra.Command, args []string) error {
if len(before) > 0 { if len(before) > 0 {
beforeTime, err = common.ParseTime(before) beforeTime, err = common.ParseTime(before)
if err != nil { 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) stLen := len(common.SimpleDateTimeFormat)
@ -76,7 +79,7 @@ func doFolderPurge(cmd *cobra.Command, args []string) error {
dnSuff := mf.DisplayName[dnLen-stLen:] dnSuff := mf.DisplayName[dnLen-stLen:]
dnTime, err := common.ParseTime(dnSuff) dnTime, err := common.ParseTime(dnSuff)
if err != nil { 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 continue
} }
delete = dnTime.Before(beforeTime) delete = dnTime.Before(beforeTime)
@ -86,10 +89,10 @@ func doFolderPurge(cmd *cobra.Command, args []string) error {
continue continue
} }
Info("Deleting folder: ", mf.DisplayName) Info(ctx, "Deleting folder: ", mf.DisplayName)
err = exchange.DeleteMailFolder(gc.Service(), user, mf.ID) err = exchange.DeleteMailFolder(gc.Service(), user, mf.ID)
if err != nil { 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() { func main() {
ctx := SetRootCmd(context.Background(), purgeCmd)
fs := purgeCmd.Flags() fs := purgeCmd.Flags()
fs.StringVar(&before, "before", "", "folders older than this date are deleted. (default: now in UTC)") 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") 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") fs.StringVar(&prefix, "prefix", "", "filters mail folders by displayName prefix")
cobra.CheckErr(purgeCmd.MarkFlagRequired("prefix")) cobra.CheckErr(purgeCmd.MarkFlagRequired("prefix"))
if err := purgeCmd.Execute(); err != nil { if err := purgeCmd.ExecuteContext(ctx); err != nil {
Info("Error: ", err.Error()) Info(purgeCmd.Context(), "Error: ", err.Error())
os.Exit(1) os.Exit(1)
} }
} }