add Account pattern to mimic storage (#221)

* 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.
This commit is contained in:
Keepers 2022-06-21 16:43:18 -06:00 committed by GitHub
parent 243343c0e9
commit 3a3303a817
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 332 additions and 1 deletions

View File

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

View File

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

59
src/pkg/account/m365.go Normal file
View File

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

View File

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

View File

@ -133,7 +133,7 @@ func (suite *S3CfgSuite) TestStorage_S3Config_InvalidCases() {
st, err := storage.NewStorage(storage.ProviderUnknown, goodS3Config) st, err := storage.NewStorage(storage.ProviderUnknown, goodS3Config)
assert.NoError(t, err) assert.NoError(t, err)
test.amend(st) test.amend(st)
_, err = st.CommonConfig() _, err = st.S3Config()
assert.Error(t, err) assert.Error(t, err)
}) })
} }