From 3a3303a8175fa707a25e0252da751c86069f7a32 Mon Sep 17 00:00:00 2001 From: Keepers <104464746+ryanfkeepers@users.noreply.github.com> Date: Tue, 21 Jun 2022 16:43:18 -0600 Subject: [PATCH] add Account pattern to mimic storage (#221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add Account pattern to mimic storage Introduces /pkg/account, which produces a generic Account struct that can be configured from specific provider details (such as m365 credentials), and serialized back to those details as needed. Next steps include replacing repository.Account with the new account. After that, tenantID should get removed from the m365 credentials handling and placed into the Account configuration instead.  --- src/pkg/account/account.go | 53 ++++++++++++++ src/pkg/account/account_test.go | 97 +++++++++++++++++++++++++ src/pkg/account/m365.go | 59 +++++++++++++++ src/pkg/account/m365_test.go | 122 ++++++++++++++++++++++++++++++++ src/pkg/storage/s3_test.go | 2 +- 5 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 src/pkg/account/account.go create mode 100644 src/pkg/account/account_test.go create mode 100644 src/pkg/account/m365.go create mode 100644 src/pkg/account/m365_test.go diff --git a/src/pkg/account/account.go b/src/pkg/account/account.go new file mode 100644 index 000000000..faa00a240 --- /dev/null +++ b/src/pkg/account/account.go @@ -0,0 +1,53 @@ +package account + +import "errors" + +type accountProvider int + +//go:generate stringer -type=accountProvider -linecomment +const ( + ProviderUnknown accountProvider = iota // Unknown Provider + ProviderM365 // M365 +) + +// storage parsing errors +var ( + errMissingRequired = errors.New("missing required storage configuration") +) + +type ( + config map[string]string + configurer interface { + Config() (config, error) + } +) + +// Account defines an account provider, along with any credentials +// and identifiers requried to set up or communicate with that provider. +type Account struct { + Provider accountProvider + Config config +} + +// NewAccount aggregates all the supplied configurations into a single configuration +func NewAccount(p accountProvider, cfgs ...configurer) (Account, error) { + cs, err := unionConfigs(cfgs...) + return Account{ + Provider: p, + Config: cs, + }, err +} + +func unionConfigs(cfgs ...configurer) (config, error) { + union := config{} + for _, cfg := range cfgs { + c, err := cfg.Config() + if err != nil { + return nil, err + } + for k, v := range c { + union[k] = v + } + } + return union, nil +} diff --git a/src/pkg/account/account_test.go b/src/pkg/account/account_test.go new file mode 100644 index 000000000..ec1fcdeb4 --- /dev/null +++ b/src/pkg/account/account_test.go @@ -0,0 +1,97 @@ +package account + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type testConfig struct { + expect string + err error +} + +func (c testConfig) Config() (config, error) { + return config{"expect": c.expect}, c.err +} + +type AccountSuite struct { + suite.Suite +} + +func TestAccountSuite(t *testing.T) { + suite.Run(t, new(AccountSuite)) +} + +func (suite *AccountSuite) TestNewAccount() { + table := []struct { + name string + p accountProvider + c testConfig + errCheck assert.ErrorAssertionFunc + }{ + {"unknown no error", ProviderUnknown, testConfig{"configVal", nil}, assert.NoError}, + {"m365 no error", ProviderM365, testConfig{"configVal", nil}, assert.NoError}, + {"unknown w/ error", ProviderUnknown, testConfig{"configVal", assert.AnError}, assert.Error}, + {"m365 w/ error", ProviderM365, testConfig{"configVal", assert.AnError}, assert.Error}, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + s, err := NewAccount(test.p, test.c) + test.errCheck(t, err) + // remaining tests are dependent upon error-free state + if test.c.err != nil { + return + } + assert.Equalf(t, + test.p, + s.Provider, + "expected account provider [%s], got [%s]", test.p, s.Provider) + assert.Equalf(t, + test.c.expect, + s.Config["expect"], + "expected account config [%s], got [%s]", test.c.expect, s.Config["expect"]) + }) + } +} + +type fooConfig struct { + foo string + err error +} + +func (c fooConfig) Config() (config, error) { + return config{"foo": c.foo}, c.err +} + +func (suite *AccountSuite) TestUnionConfigs() { + table := []struct { + name string + tc testConfig + fc fooConfig + errCheck assert.ErrorAssertionFunc + }{ + {"no error", testConfig{"test", nil}, fooConfig{"foo", nil}, assert.NoError}, + {"tc error", testConfig{"test", assert.AnError}, fooConfig{"foo", nil}, assert.Error}, + {"fc error", testConfig{"test", nil}, fooConfig{"foo", assert.AnError}, assert.Error}, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + cs, err := unionConfigs(test.tc, test.fc) + test.errCheck(t, err) + // remaining tests depend on error-free state + if test.tc.err != nil || test.fc.err != nil { + return + } + assert.Equalf(t, + test.tc.expect, + cs["expect"], + "expected unioned config to have value [%s] at key [expect], got [%s]", test.tc.expect, cs["expect"]) + assert.Equalf(t, + test.fc.foo, + cs["foo"], + "expected unioned config to have value [%s] at key [foo], got [%s]", test.fc.foo, cs["foo"]) + }) + } +} diff --git a/src/pkg/account/m365.go b/src/pkg/account/m365.go new file mode 100644 index 000000000..bbcd2ff13 --- /dev/null +++ b/src/pkg/account/m365.go @@ -0,0 +1,59 @@ +package account + +import ( + "github.com/pkg/errors" + + "github.com/alcionai/corso/pkg/credentials" +) + +type M365Config struct { + credentials.M365 // requires: ClientID, ClientSecret, TenantID + + // (todo) TenantID string +} + +// config key consts +const ( + keyM365ClientID = "m365_clientID" + keyM365ClientSecret = "m365_clientSecret" + keyM365TenantID = "m365_tenantID" +) + +// config exported name consts +const ( +// (todo) TenantID = "TENANT_ID" +) + +func (c M365Config) Config() (config, error) { + cfg := config{ + keyM365ClientID: c.ClientID, + keyM365ClientSecret: c.ClientSecret, + keyM365TenantID: c.TenantID, + } + return cfg, c.validate() +} + +// M365Config retrieves the M365Config details from the Account config. +func (a Account) M365Config() (M365Config, error) { + c := M365Config{} + if len(a.Config) > 0 { + c.ClientID = a.Config[keyM365ClientID] + c.ClientSecret = a.Config[keyM365ClientSecret] + c.TenantID = a.Config[keyM365TenantID] + } + return c, c.validate() +} + +func (c M365Config) validate() error { + check := map[string]string{ + credentials.ClientID: c.ClientID, + credentials.ClientSecret: c.ClientSecret, + credentials.TenantID: c.TenantID, + } + for k, v := range check { + if len(v) == 0 { + return errors.Wrap(errMissingRequired, k) + } + } + return nil +} diff --git a/src/pkg/account/m365_test.go b/src/pkg/account/m365_test.go new file mode 100644 index 000000000..fee798bf2 --- /dev/null +++ b/src/pkg/account/m365_test.go @@ -0,0 +1,122 @@ +package account_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/pkg/account" + "github.com/alcionai/corso/pkg/credentials" +) + +type M365CfgSuite struct { + suite.Suite +} + +func TestM365CfgSuite(t *testing.T) { + suite.Run(t, new(M365CfgSuite)) +} + +var goodM365Config = account.M365Config{ + M365: credentials.M365{ + ClientID: "cid", + ClientSecret: "cs", + TenantID: "tid", + }, +} + +func (suite *M365CfgSuite) TestM365Config_Config() { + m365 := goodM365Config + c, err := m365.Config() + require.NoError(suite.T(), err) + + table := []struct { + key string + expect string + }{ + {"m365_clientID", m365.ClientID}, + {"m365_clientSecret", m365.ClientSecret}, + {"m365_tenantID", m365.TenantID}, + } + for _, test := range table { + assert.Equal(suite.T(), test.expect, c[test.key]) + } +} + +func (suite *M365CfgSuite) TestAccount_M365Config() { + t := suite.T() + + in := goodM365Config + a, err := account.NewAccount(account.ProviderM365, in) + require.NoError(t, err) + out, err := a.M365Config() + require.NoError(t, err) + + assert.Equal(t, in.ClientID, out.ClientID) + assert.Equal(t, in.ClientSecret, out.ClientSecret) + assert.Equal(t, in.TenantID, out.TenantID) +} + +func makeTestM365Cfg(cid, cs, tid string) account.M365Config { + return account.M365Config{ + M365: credentials.M365{ + ClientID: cid, + ClientSecret: cs, + TenantID: tid, + }, + } +} + +func (suite *M365CfgSuite) TestAccount_M365Config_InvalidCases() { + // missing required properties + table := []struct { + name string + cfg account.M365Config + }{ + {"missing client ID", makeTestM365Cfg("", "cs", "tid")}, + {"missing client secret", makeTestM365Cfg("cid", "", "tid")}, + {"missing tenant ID", makeTestM365Cfg("cid", "cs", "")}, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + _, err := account.NewAccount(account.ProviderUnknown, test.cfg) + assert.Error(t, err) + }) + } + + // required property not populated in account + table2 := []struct { + name string + amend func(account.Account) + }{ + { + "missing clientID", + func(a account.Account) { + a.Config["m365_clientID"] = "" + }, + }, + { + "missing client secret", + func(a account.Account) { + a.Config["m365_clientSecret"] = "" + }, + }, + { + "missing tenant id", + func(a account.Account) { + a.Config["m365_tenantID"] = "" + }, + }, + } + for _, test := range table2 { + suite.T().Run(test.name, func(t *testing.T) { + st, err := account.NewAccount(account.ProviderUnknown, goodM365Config) + assert.NoError(t, err) + test.amend(st) + _, err = st.M365Config() + assert.Error(t, err) + }) + } +} diff --git a/src/pkg/storage/s3_test.go b/src/pkg/storage/s3_test.go index 985fd2161..4adc25f20 100644 --- a/src/pkg/storage/s3_test.go +++ b/src/pkg/storage/s3_test.go @@ -133,7 +133,7 @@ func (suite *S3CfgSuite) TestStorage_S3Config_InvalidCases() { st, err := storage.NewStorage(storage.ProviderUnknown, goodS3Config) assert.NoError(t, err) test.amend(st) - _, err = st.CommonConfig() + _, err = st.S3Config() assert.Error(t, err) }) }