set up CLI for integration testing (#478)
Makes the necessary changes, including adding helper funcs, to bring the CLI up to an integration-testable state. The changes made in this commit should be sufficient for most other CLI tests. Includes a single test as verification.
This commit is contained in:
parent
d920589507
commit
342dd2e9f9
@ -126,7 +126,7 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s, acct, err := config.GetStorageAndAccount(true, nil)
|
s, acct, err := config.GetStorageAndAccount(ctx, true, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Only(err)
|
return Only(err)
|
||||||
}
|
}
|
||||||
@ -224,7 +224,7 @@ var exchangeListCmd = &cobra.Command{
|
|||||||
func listExchangeCmd(cmd *cobra.Command, args []string) error {
|
func listExchangeCmd(cmd *cobra.Command, args []string) error {
|
||||||
ctx := cmd.Context()
|
ctx := cmd.Context()
|
||||||
|
|
||||||
s, acct, err := config.GetStorageAndAccount(true, nil)
|
s, acct, err := config.GetStorageAndAccount(ctx, true, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Only(err)
|
return Only(err)
|
||||||
}
|
}
|
||||||
@ -285,7 +285,7 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s, acct, err := config.GetStorageAndAccount(true, nil)
|
s, acct, err := config.GetStorageAndAccount(ctx, true, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Only(err)
|
return Only(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
"github.com/alcionai/corso/cli/backup"
|
"github.com/alcionai/corso/cli/backup"
|
||||||
"github.com/alcionai/corso/cli/config"
|
"github.com/alcionai/corso/cli/config"
|
||||||
@ -22,27 +21,14 @@ var corsoCmd = &cobra.Command{
|
|||||||
Short: "Protect your Microsoft 365 data.",
|
Short: "Protect your Microsoft 365 data.",
|
||||||
Long: `Reliable, secure, and efficient data protection for Microsoft 365.`,
|
Long: `Reliable, secure, and efficient data protection for Microsoft 365.`,
|
||||||
RunE: handleCorsoCmd,
|
RunE: handleCorsoCmd,
|
||||||
|
PersistentPreRunE: config.InitFunc(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// the root-level flags
|
// the root-level flags
|
||||||
var (
|
var (
|
||||||
cfgFile string
|
|
||||||
version bool
|
version bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
cobra.OnInitialize(initConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initConfig() {
|
|
||||||
err := config.InitConfig(cfgFile)
|
|
||||||
cobra.CheckErr(err)
|
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err == nil {
|
|
||||||
print.Info("Using config file:", viper.ConfigFileUsed())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler for flat calls to `corso`.
|
// Handler for flat calls to `corso`.
|
||||||
// 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 {
|
||||||
@ -55,8 +41,10 @@ func handleCorsoCmd(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
// Handle builds and executes the cli processor.
|
// Handle builds and executes the cli processor.
|
||||||
func Handle() {
|
func Handle() {
|
||||||
|
ctx := config.Seed(context.Background())
|
||||||
|
|
||||||
corsoCmd.Flags().BoolP("version", "v", version, "current version info")
|
corsoCmd.Flags().BoolP("version", "v", version, "current version info")
|
||||||
corsoCmd.PersistentFlags().StringVar(&cfgFile, "config-file", "", "config file (default is $HOME/.corso)")
|
config.AddConfigFileFlag(corsoCmd)
|
||||||
print.SetRootCommand(corsoCmd)
|
print.SetRootCommand(corsoCmd)
|
||||||
print.AddOutputFlag(corsoCmd)
|
print.AddOutputFlag(corsoCmd)
|
||||||
|
|
||||||
@ -66,7 +54,7 @@ func Handle() {
|
|||||||
backup.AddCommands(corsoCmd)
|
backup.AddCommands(corsoCmd)
|
||||||
restore.AddCommands(corsoCmd)
|
restore.AddCommands(corsoCmd)
|
||||||
|
|
||||||
ctx, log := logger.Seed(context.Background())
|
ctx, log := logger.Seed(ctx)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = log.Sync() // flush all logs in the buffer
|
_ = log.Sync() // flush all logs in the buffer
|
||||||
}()
|
}()
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
. "github.com/alcionai/corso/cli/print"
|
||||||
"github.com/alcionai/corso/pkg/account"
|
"github.com/alcionai/corso/pkg/account"
|
||||||
"github.com/alcionai/corso/pkg/storage"
|
"github.com/alcionai/corso/pkg/storage"
|
||||||
)
|
)
|
||||||
@ -24,15 +28,43 @@ const (
|
|||||||
TenantIDKey = "tenantid"
|
TenantIDKey = "tenantid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitConfig(configFilePath string) error {
|
var configFilePath string
|
||||||
return initConfigWithViper(viper.GetViper(), configFilePath)
|
|
||||||
|
// adds the persistent flag --config-file to the provided command.
|
||||||
|
func AddConfigFileFlag(cmd *cobra.Command) {
|
||||||
|
fs := cmd.PersistentFlags()
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
Err("finding $HOME directory (default) for config file")
|
||||||
|
}
|
||||||
|
fs.StringVar(
|
||||||
|
&configFilePath,
|
||||||
|
"config-file",
|
||||||
|
filepath.Join(homeDir, ".corso.toml"),
|
||||||
|
"config file (default is $HOME/.corso)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// initConfigWithViper implements InitConfig, but takes in a viper
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
// Initialization & Storage
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// InitFunc provides a func that lazily initializes viper and
|
||||||
|
// verifies that the configuration was able to read a file.
|
||||||
|
func InitFunc() func(*cobra.Command, []string) error {
|
||||||
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
|
err := initWithViper(GetViper(cmd.Context()), configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return Read(cmd.Context())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initWithViper implements InitConfig, but takes in a viper
|
||||||
// struct for testing.
|
// struct for testing.
|
||||||
func initConfigWithViper(vpr *viper.Viper, configFilePath string) error {
|
func initWithViper(vpr *viper.Viper, configFP string) error {
|
||||||
// Configure default config file location
|
// Configure default config file location
|
||||||
if configFilePath == "" {
|
if configFP == "" {
|
||||||
// Find home directory.
|
// Find home directory.
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -46,14 +78,14 @@ func initConfigWithViper(vpr *viper.Viper, configFilePath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
vpr.SetConfigFile(configFilePath)
|
vpr.SetConfigFile(configFP)
|
||||||
// We also configure the path, type and filename
|
// We also configure the path, type and filename
|
||||||
// because `vpr.SafeWriteConfig` needs these set to
|
// because `vpr.SafeWriteConfig` needs these set to
|
||||||
// work correctly (it does not use the configured file)
|
// work correctly (it does not use the configured file)
|
||||||
vpr.AddConfigPath(path.Dir(configFilePath))
|
vpr.AddConfigPath(path.Dir(configFP))
|
||||||
|
|
||||||
fileName := path.Base(configFilePath)
|
fileName := path.Base(configFP)
|
||||||
ext := path.Ext(configFilePath)
|
ext := path.Ext(configFP)
|
||||||
if len(ext) == 0 {
|
if len(ext) == 0 {
|
||||||
return errors.New("config file requires an extension e.g. `toml`")
|
return errors.New("config file requires an extension e.g. `toml`")
|
||||||
}
|
}
|
||||||
@ -64,10 +96,53 @@ func initConfigWithViper(vpr *viper.Viper, configFilePath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type viperCtx struct{}
|
||||||
|
|
||||||
|
// Seed embeds a viper instance in the context.
|
||||||
|
func Seed(ctx context.Context) context.Context {
|
||||||
|
return SetViper(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a viper instance to the context.
|
||||||
|
// If vpr is nil, sets the default (global) viper.
|
||||||
|
func SetViper(ctx context.Context, vpr *viper.Viper) context.Context {
|
||||||
|
if vpr == nil {
|
||||||
|
vpr = viper.GetViper()
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, viperCtx{}, vpr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets a viper instance from the context.
|
||||||
|
// If no viper instance is found, returns the default
|
||||||
|
// (global) viper instance.
|
||||||
|
func GetViper(ctx context.Context) *viper.Viper {
|
||||||
|
vprIface := ctx.Value(viperCtx{})
|
||||||
|
vpr, ok := vprIface.(*viper.Viper)
|
||||||
|
if vpr == nil || !ok {
|
||||||
|
return viper.GetViper()
|
||||||
|
}
|
||||||
|
return vpr
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
// Reading & Writing the config
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Read reads the config from the viper instance in the context.
|
||||||
|
// Primarily used as a test-check to ensure the instance was
|
||||||
|
// set up properly.
|
||||||
|
func Read(ctx context.Context) error {
|
||||||
|
if err := viper.ReadInConfig(); err == nil {
|
||||||
|
Info("Using config file:", viper.ConfigFileUsed())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// WriteRepoConfig currently just persists corso config to the config file
|
// WriteRepoConfig currently just persists corso config to the config file
|
||||||
// It does not check for conflicts or existing data.
|
// It does not check for conflicts or existing data.
|
||||||
func WriteRepoConfig(s3Config storage.S3Config, m365Config account.M365Config) error {
|
func WriteRepoConfig(ctx context.Context, s3Config storage.S3Config, m365Config account.M365Config) error {
|
||||||
return writeRepoConfigWithViper(viper.GetViper(), s3Config, m365Config)
|
return writeRepoConfigWithViper(GetViper(ctx), s3Config, m365Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeRepoConfigWithViper implements WriteRepoConfig, but takes in a viper
|
// writeRepoConfigWithViper implements WriteRepoConfig, but takes in a viper
|
||||||
@ -94,8 +169,12 @@ func writeRepoConfigWithViper(vpr *viper.Viper, s3Config storage.S3Config, m365C
|
|||||||
|
|
||||||
// GetStorageAndAccount creates a storage and account instance by mediating all the possible
|
// GetStorageAndAccount creates a storage and account instance by mediating all the possible
|
||||||
// data sources (config file, env vars, flag overrides) and the config file.
|
// data sources (config file, env vars, flag overrides) and the config file.
|
||||||
func GetStorageAndAccount(readFromFile bool, overrides map[string]string) (storage.Storage, account.Account, error) {
|
func GetStorageAndAccount(
|
||||||
return getStorageAndAccountWithViper(viper.GetViper(), readFromFile, overrides)
|
ctx context.Context,
|
||||||
|
readFromFile bool,
|
||||||
|
overrides map[string]string,
|
||||||
|
) (storage.Storage, account.Account, error) {
|
||||||
|
return getStorageAndAccountWithViper(GetViper(ctx), readFromFile, overrides)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSorageAndAccountWithViper implements GetSorageAndAccount, but takes in a viper
|
// getSorageAndAccountWithViper implements GetSorageAndAccount, but takes in a viper
|
||||||
|
|||||||
@ -82,7 +82,7 @@ func (suite *ConfigSuite) TestWriteReadConfig() {
|
|||||||
|
|
||||||
// Configure viper to read test config file
|
// Configure viper to read test config file
|
||||||
testConfigFilePath := path.Join(t.TempDir(), "corso.toml")
|
testConfigFilePath := path.Join(t.TempDir(), "corso.toml")
|
||||||
require.NoError(t, initConfigWithViper(vpr, testConfigFilePath), "initializing repo config")
|
require.NoError(t, initWithViper(vpr, testConfigFilePath), "initializing repo config")
|
||||||
|
|
||||||
s3Cfg := storage.S3Config{Bucket: bkt}
|
s3Cfg := storage.S3Config{Bucket: bkt}
|
||||||
m365 := account.M365Config{TenantID: tid}
|
m365 := account.M365Config{TenantID: tid}
|
||||||
@ -112,7 +112,7 @@ func (suite *ConfigSuite) TestMustMatchConfig() {
|
|||||||
|
|
||||||
// Configure viper to read test config file
|
// Configure viper to read test config file
|
||||||
testConfigFilePath := path.Join(t.TempDir(), "corso.toml")
|
testConfigFilePath := path.Join(t.TempDir(), "corso.toml")
|
||||||
require.NoError(t, initConfigWithViper(vpr, testConfigFilePath), "initializing repo config")
|
require.NoError(t, initWithViper(vpr, testConfigFilePath), "initializing repo config")
|
||||||
|
|
||||||
s3Cfg := storage.S3Config{Bucket: bkt}
|
s3Cfg := storage.S3Config{Bucket: bkt}
|
||||||
m365 := account.M365Config{TenantID: tid}
|
m365 := account.M365Config{TenantID: tid}
|
||||||
@ -216,7 +216,7 @@ func (suite *ConfigIntegrationSuite) TestGetStorageAndAccount() {
|
|||||||
|
|
||||||
// Configure viper to read test config file
|
// Configure viper to read test config file
|
||||||
testConfigFilePath := path.Join(t.TempDir(), "corso.toml")
|
testConfigFilePath := path.Join(t.TempDir(), "corso.toml")
|
||||||
require.NoError(t, initConfigWithViper(vpr, testConfigFilePath), "initializing repo config")
|
require.NoError(t, initWithViper(vpr, testConfigFilePath), "initializing repo config")
|
||||||
|
|
||||||
s3Cfg := storage.S3Config{
|
s3Cfg := storage.S3Config{
|
||||||
Bucket: bkt,
|
Bucket: bkt,
|
||||||
|
|||||||
@ -24,7 +24,7 @@ func SetRootCommand(root *cobra.Command) {
|
|||||||
rootCmd = root
|
rootCmd = root
|
||||||
}
|
}
|
||||||
|
|
||||||
// adds the --output flag to the provided command.
|
// adds the persistent flag --output to the provided command.
|
||||||
func AddOutputFlag(parent *cobra.Command) {
|
func AddOutputFlag(parent *cobra.Command) {
|
||||||
fs := parent.PersistentFlags()
|
fs := parent.PersistentFlags()
|
||||||
fs.BoolVar(&outputAsJSON, "json", false, "output data in JSON format")
|
fs.BoolVar(&outputAsJSON, "json", false, "output data in JSON format")
|
||||||
@ -43,7 +43,23 @@ func Only(e error) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info prints the strings 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.
|
||||||
|
// Prepends the message with "Error: "
|
||||||
|
func Err(s ...any) {
|
||||||
|
err(rootCmd.ErrOrStderr(), s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// err is the testable core of Err()
|
||||||
|
func err(w io.Writer, s ...any) {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := append([]any{"Error: "}, s...)
|
||||||
|
fmt.Fprint(w, msg...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(s ...any) {
|
||||||
info(rootCmd.ErrOrStderr(), s...)
|
info(rootCmd.ErrOrStderr(), s...)
|
||||||
|
|||||||
@ -27,6 +27,15 @@ func (suite *PrintUnitSuite) TestOnly() {
|
|||||||
assert.True(t, c.SilenceUsage)
|
assert.True(t, c.SilenceUsage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *PrintUnitSuite) TestErr() {
|
||||||
|
t := suite.T()
|
||||||
|
var b bytes.Buffer
|
||||||
|
msg := "I have seen the fnords!"
|
||||||
|
err(&b, msg)
|
||||||
|
assert.Contains(t, b.String(), "Error: ")
|
||||||
|
assert.Contains(t, b.String(), msg)
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *PrintUnitSuite) TestInfo() {
|
func (suite *PrintUnitSuite) TestInfo() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
|
|||||||
@ -33,10 +33,11 @@ func addS3Commands(parent *cobra.Command) *cobra.Command {
|
|||||||
)
|
)
|
||||||
switch parent.Use {
|
switch parent.Use {
|
||||||
case initCommand:
|
case initCommand:
|
||||||
c, fs = utils.AddCommand(parent, s3InitCmd)
|
c, fs = utils.AddCommand(parent, s3InitCmd())
|
||||||
case connectCommand:
|
case connectCommand:
|
||||||
c, fs = utils.AddCommand(parent, s3ConnectCmd)
|
c, fs = utils.AddCommand(parent, s3ConnectCmd())
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.StringVar(&accessKey, "access-key", "", "Access key ID (replaces the AWS_ACCESS_KEY_ID env variable).")
|
fs.StringVar(&accessKey, "access-key", "", "Access key ID (replaces the AWS_ACCESS_KEY_ID env variable).")
|
||||||
fs.StringVar(&bucket, "bucket", "", "Name of the S3 bucket (required).")
|
fs.StringVar(&bucket, "bucket", "", "Name of the S3 bucket (required).")
|
||||||
cobra.CheckErr(c.MarkFlagRequired("bucket"))
|
cobra.CheckErr(c.MarkFlagRequired("bucket"))
|
||||||
@ -51,14 +52,20 @@ func addS3Commands(parent *cobra.Command) *cobra.Command {
|
|||||||
|
|
||||||
const s3ProviderCommand = "s3"
|
const s3ProviderCommand = "s3"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
// Init
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// `corso repo init s3 [<flag>...]`
|
// `corso repo init s3 [<flag>...]`
|
||||||
var s3InitCmd = &cobra.Command{
|
func s3InitCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
Use: s3ProviderCommand,
|
Use: s3ProviderCommand,
|
||||||
Short: "Initialize a S3 repository",
|
Short: "Initialize a S3 repository",
|
||||||
Long: `Bootstraps a new S3 repository and connects it to your m356 account.`,
|
Long: `Bootstraps a new S3 repository and connects it to your m356 account.`,
|
||||||
RunE: initS3Cmd,
|
RunE: initS3Cmd,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// initializes a s3 repo.
|
// initializes a s3 repo.
|
||||||
func initS3Cmd(cmd *cobra.Command, args []string) error {
|
func initS3Cmd(cmd *cobra.Command, args []string) error {
|
||||||
@ -69,7 +76,7 @@ func initS3Cmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s, a, err := config.GetStorageAndAccount(false, s3Overrides())
|
s, a, err := config.GetStorageAndAccount(ctx, false, s3Overrides())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Only(err)
|
return Only(err)
|
||||||
}
|
}
|
||||||
@ -102,20 +109,26 @@ func initS3Cmd(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
Infof("Initialized a S3 repository within bucket %s.\n", s3Cfg.Bucket)
|
Infof("Initialized a S3 repository within bucket %s.\n", s3Cfg.Bucket)
|
||||||
|
|
||||||
if err = config.WriteRepoConfig(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(errors.Wrap(err, "Failed to write repository configuration"))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
// Connect
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// `corso repo connect s3 [<flag>...]`
|
// `corso repo connect s3 [<flag>...]`
|
||||||
var s3ConnectCmd = &cobra.Command{
|
func s3ConnectCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
Use: s3ProviderCommand,
|
Use: s3ProviderCommand,
|
||||||
Short: "Connect to a S3 repository",
|
Short: "Connect to a S3 repository",
|
||||||
Long: `Ensures a connection to an existing S3 repository.`,
|
Long: `Ensures a connection to an existing S3 repository.`,
|
||||||
RunE: connectS3Cmd,
|
RunE: connectS3Cmd,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// connects to an existing s3 repo.
|
// connects to an existing s3 repo.
|
||||||
func connectS3Cmd(cmd *cobra.Command, args []string) error {
|
func connectS3Cmd(cmd *cobra.Command, args []string) error {
|
||||||
@ -126,7 +139,7 @@ func connectS3Cmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s, a, err := config.GetStorageAndAccount(true, s3Overrides())
|
s, a, err := config.GetStorageAndAccount(ctx, true, s3Overrides())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Only(err)
|
return Only(err)
|
||||||
}
|
}
|
||||||
@ -155,7 +168,7 @@ func connectS3Cmd(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
Infof("Connected to S3 bucket %s.\n", s3Cfg.Bucket)
|
Infof("Connected to S3 bucket %s.\n", s3Cfg.Bucket)
|
||||||
|
|
||||||
if err = config.WriteRepoConfig(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(errors.Wrap(err, "Failed to write repository configuration"))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
76
src/cli/repo/s3_integration_test.go
Normal file
76
src/cli/repo/s3_integration_test.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package repo_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/cli/backup"
|
||||||
|
"github.com/alcionai/corso/cli/config"
|
||||||
|
"github.com/alcionai/corso/cli/print"
|
||||||
|
"github.com/alcionai/corso/cli/repo"
|
||||||
|
"github.com/alcionai/corso/cli/restore"
|
||||||
|
"github.com/alcionai/corso/internal/tester"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
// Integration
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type S3IntegrationSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestS3IntegrationSuite(t *testing.T) {
|
||||||
|
if err := tester.RunOnAny(
|
||||||
|
tester.CorsoCITests,
|
||||||
|
tester.CorsoCLIRepoTests,
|
||||||
|
); err != nil {
|
||||||
|
t.Skip(err)
|
||||||
|
}
|
||||||
|
suite.Run(t, new(S3IntegrationSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *S3IntegrationSuite) SetupSuite() {
|
||||||
|
_, err := tester.GetRequiredEnvVars(
|
||||||
|
append(
|
||||||
|
tester.AWSStorageCredEnvs,
|
||||||
|
tester.M365AcctCredEnvs...,
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *S3IntegrationSuite) TestInitS3Cmd() {
|
||||||
|
ctx := tester.NewContext()
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
st, err := tester.NewPrefixedS3Storage(t)
|
||||||
|
require.NoError(t, err)
|
||||||
|
cfg, err := st.S3Config()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
vpr, configFP, err := tester.MakeTempTestConfigClone(t)
|
||||||
|
require.NoError(t, err)
|
||||||
|
ctx = config.SetViper(ctx, vpr)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cmd := tester.StubRootCmd(
|
||||||
|
"repo", "init", "s3",
|
||||||
|
"--config-file", configFP,
|
||||||
|
"--bucket", cfg.Bucket,
|
||||||
|
"--prefix", cfg.Prefix)
|
||||||
|
cmd.PersistentPostRunE = config.InitFunc()
|
||||||
|
|
||||||
|
// TODO: replace with Build() from in-flight PR #453
|
||||||
|
config.AddConfigFileFlag(cmd)
|
||||||
|
print.AddOutputFlag(cmd)
|
||||||
|
repo.AddCommands(cmd)
|
||||||
|
backup.AddCommands(cmd)
|
||||||
|
restore.AddCommands(cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
require.NoError(t, cmd.ExecuteContext(ctx))
|
||||||
|
}
|
||||||
@ -28,8 +28,8 @@ func (suite *S3Suite) TestAddS3Commands() {
|
|||||||
expectShort string
|
expectShort string
|
||||||
expectRunE func(*cobra.Command, []string) error
|
expectRunE func(*cobra.Command, []string) error
|
||||||
}{
|
}{
|
||||||
{"init s3", initCommand, expectUse, s3InitCmd.Short, initS3Cmd},
|
{"init s3", initCommand, expectUse, s3InitCmd().Short, initS3Cmd},
|
||||||
{"connect s3", connectCommand, expectUse, s3ConnectCmd.Short, connectS3Cmd},
|
{"connect s3", connectCommand, expectUse, s3ConnectCmd().Short, connectS3Cmd},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.T().Run(test.name, func(t *testing.T) {
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
|
|||||||
@ -105,7 +105,7 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s, a, err := config.GetStorageAndAccount(true, nil)
|
s, a, err := config.GetStorageAndAccount(ctx, true, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Only(err)
|
return Only(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,8 +46,8 @@ func HasNoFlagsAndShownHelp(cmd *cobra.Command) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddCommand adds the subCommand to the parent, and returns
|
// AddCommand adds a clone of the subCommand to the parent,
|
||||||
// both the subCommand and its pflags.
|
// and returns both the clone and its pflags.
|
||||||
func AddCommand(parent, c *cobra.Command) (*cobra.Command, *pflag.FlagSet) {
|
func AddCommand(parent, c *cobra.Command) (*cobra.Command, *pflag.FlagSet) {
|
||||||
parent.AddCommand(c)
|
parent.AddCommand(c)
|
||||||
return c, c.Flags()
|
return c, c.Flags()
|
||||||
|
|||||||
36
src/internal/tester/cli.go
Normal file
36
src/internal/tester/cli.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package tester
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/internal/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StubRootCmd builds a stub cobra command to be used as
|
||||||
|
// the root command for integration testing on the CLI
|
||||||
|
func StubRootCmd(args ...string) *cobra.Command {
|
||||||
|
id := uuid.NewString()
|
||||||
|
now := common.FormatTime(time.Now())
|
||||||
|
cmdArg := "testing-corso"
|
||||||
|
c := &cobra.Command{
|
||||||
|
Use: cmdArg,
|
||||||
|
Short: id,
|
||||||
|
Long: id + " - " + now,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
fmt.Fprintf(cmd.OutOrStdout(), "test command args: %+v", args)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.SetArgs(args)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContext() context.Context {
|
||||||
|
type stub struct{}
|
||||||
|
return context.WithValue(context.Background(), stub{}, stub{})
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@ -25,6 +26,7 @@ const (
|
|||||||
// test specific env vars
|
// test specific env vars
|
||||||
const (
|
const (
|
||||||
EnvCorsoM365TestUserID = "CORSO_M356_TEST_USER_ID"
|
EnvCorsoM365TestUserID = "CORSO_M356_TEST_USER_ID"
|
||||||
|
EnvCorsoTestConfigFilePath = "CORSO_TEST_CONFIG_FILE"
|
||||||
)
|
)
|
||||||
|
|
||||||
// global to hold the test config results.
|
// global to hold the test config results.
|
||||||
@ -42,10 +44,10 @@ func cloneTestConfig() map[string]string {
|
|||||||
return clone
|
return clone
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestViper() (*viper.Viper, error) {
|
func NewTestViper() (*viper.Viper, error) {
|
||||||
vpr := viper.New()
|
vpr := viper.New()
|
||||||
|
|
||||||
configFilePath := os.Getenv("CORSO_TEST_CONFIG_FILE")
|
configFilePath := os.Getenv(EnvCorsoTestConfigFilePath)
|
||||||
if len(configFilePath) == 0 {
|
if len(configFilePath) == 0 {
|
||||||
return vpr, nil
|
return vpr, nil
|
||||||
}
|
}
|
||||||
@ -74,7 +76,7 @@ func readTestConfig() (map[string]string, error) {
|
|||||||
return cloneTestConfig(), nil
|
return cloneTestConfig(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
vpr, err := newTestViper()
|
vpr, err := NewTestViper()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -93,11 +95,52 @@ func readTestConfig() (map[string]string, error) {
|
|||||||
fallbackTo(testEnv, testCfgPrefix, vpr.GetString(testCfgPrefix))
|
fallbackTo(testEnv, testCfgPrefix, vpr.GetString(testCfgPrefix))
|
||||||
fallbackTo(testEnv, testCfgTenantID, os.Getenv(account.TenantID), vpr.GetString(testCfgTenantID))
|
fallbackTo(testEnv, testCfgTenantID, os.Getenv(account.TenantID), vpr.GetString(testCfgTenantID))
|
||||||
fallbackTo(testEnv, testCfgUserID, os.Getenv(EnvCorsoM365TestUserID), vpr.GetString(testCfgTenantID), "lidiah@8qzvrj.onmicrosoft.com")
|
fallbackTo(testEnv, testCfgUserID, os.Getenv(EnvCorsoM365TestUserID), vpr.GetString(testCfgTenantID), "lidiah@8qzvrj.onmicrosoft.com")
|
||||||
|
testEnv[EnvCorsoTestConfigFilePath] = os.Getenv(EnvCorsoTestConfigFilePath)
|
||||||
|
|
||||||
testConfig = testEnv
|
testConfig = testEnv
|
||||||
return cloneTestConfig(), nil
|
return cloneTestConfig(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MakeTempTestConfigClone makes a copy of the test config file in a temp directory.
|
||||||
|
// This allows tests which interface with reading and writing to a config file
|
||||||
|
// (such as the CLI) to safely manipulate file contents without amending the user's
|
||||||
|
// original file.
|
||||||
|
//
|
||||||
|
// Returns a filepath string pointing to the location of the temp file.
|
||||||
|
func MakeTempTestConfigClone(t *testing.T) (*viper.Viper, string, error) {
|
||||||
|
cfg, err := readTestConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
fName := path.Base(os.Getenv(EnvCorsoTestConfigFilePath))
|
||||||
|
if len(fName) == 0 || fName == "." || fName == "/" {
|
||||||
|
fName = ".corso_test.toml"
|
||||||
|
}
|
||||||
|
tDir := t.TempDir()
|
||||||
|
tDirFp := path.Join(tDir, fName)
|
||||||
|
|
||||||
|
if _, err := os.Create(tDirFp); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
vpr := viper.New()
|
||||||
|
vpr.SetConfigFile(tDirFp)
|
||||||
|
vpr.AddConfigPath(tDir)
|
||||||
|
vpr.SetConfigType(path.Ext(fName))
|
||||||
|
vpr.SetConfigName(fName)
|
||||||
|
|
||||||
|
for k, v := range cfg {
|
||||||
|
vpr.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := vpr.WriteConfig(); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return vpr, tDirFp, nil
|
||||||
|
}
|
||||||
|
|
||||||
// writes the first non-zero valued string to the map at the key.
|
// writes the first non-zero valued string to the map at the key.
|
||||||
// fallback priority should match viper ordering (manually handled
|
// fallback priority should match viper ordering (manually handled
|
||||||
// here since viper fails to provide fallbacks on fileNotFoundErr):
|
// here since viper fails to provide fallbacks on fileNotFoundErr):
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
CorsoCITests = "CORSO_CI_TESTS"
|
CorsoCITests = "CORSO_CI_TESTS"
|
||||||
CorsoCLIConfigTests = "CORSO_CLI_CONFIG_TESTS"
|
CorsoCLIConfigTests = "CORSO_CLI_CONFIG_TESTS"
|
||||||
|
CorsoCLIRepoTests = "CORSO_CLI_REPO_TESTS"
|
||||||
CorsoGraphConnectorTests = "CORSO_GRAPH_CONNECTOR_TESTS"
|
CorsoGraphConnectorTests = "CORSO_GRAPH_CONNECTOR_TESTS"
|
||||||
CorsoKopiaWrapperTests = "CORSO_KOPIA_WRAPPER_TESTS"
|
CorsoKopiaWrapperTests = "CORSO_KOPIA_WRAPPER_TESTS"
|
||||||
CorsoModelStoreTests = "CORSO_MODEL_STORE_TESTS"
|
CorsoModelStoreTests = "CORSO_MODEL_STORE_TESTS"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user