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:
parent
9049f3c2bf
commit
a1b9f876ae
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)))
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
100
src/cli/restore/exchange_integration_test.go
Normal file
100
src/cli/restore/exchange_integration_test.go
Normal 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))
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user