From eef9136e52251276e9c7c36616413afe7c05abfb Mon Sep 17 00:00:00 2001 From: Marcos Navarro Date: Thu, 19 Sep 2024 13:31:30 -0600 Subject: [PATCH] Credentials service instance has its own logger --- cmd/publisher/commands/credentials.go | 9 +- internal/accounts/account_list.go | 2 +- internal/accounts/provider_credentials.go | 11 +- internal/credentials/credentials.go | 42 +++- internal/credentials/credentials_test.go | 192 ++++++++++-------- internal/logging/logger.go | 6 + internal/services/api/delete_credentials.go | 2 +- .../services/api/delete_credentials_test.go | 3 +- internal/services/api/get_credential.go | 2 +- internal/services/api/get_credentials.go | 2 +- internal/services/api/post_credentials.go | 2 +- .../services/api/post_credentials_test.go | 2 +- 12 files changed, 168 insertions(+), 107 deletions(-) diff --git a/cmd/publisher/commands/credentials.go b/cmd/publisher/commands/credentials.go index 3ad1824aa..133519c39 100644 --- a/cmd/publisher/commands/credentials.go +++ b/cmd/publisher/commands/credentials.go @@ -8,6 +8,7 @@ import ( "github.com/posit-dev/publisher/internal/cli_types" "github.com/posit-dev/publisher/internal/credentials" + "github.com/posit-dev/publisher/internal/logging" ) type CredentialsCommand struct { @@ -24,7 +25,7 @@ type CreateCredentialCommand struct { } func (cmd *CreateCredentialCommand) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContext) error { - cs := credentials.CredentialsService{} + cs := credentials.NewCredentialsService(logging.NewDiscardLogger()) cred, err := cs.Set(cmd.Name, cmd.URL, cmd.ApiKey) if err != nil { return err @@ -44,7 +45,7 @@ type DeleteCredentialCommand struct { } func (cmd *DeleteCredentialCommand) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContext) error { - cs := credentials.CredentialsService{} + cs := credentials.NewCredentialsService(logging.NewDiscardLogger()) err := cs.Delete(cmd.GUID) if err != nil { return err @@ -59,7 +60,7 @@ type GetCredentialCommand struct { } func (cmd *GetCredentialCommand) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContext) error { - cs := credentials.CredentialsService{} + cs := credentials.NewCredentialsService(logging.NewDiscardLogger()) cred, err := cs.Get(cmd.GUID) if err != nil { return err @@ -78,7 +79,7 @@ type ListCredentialsCommand struct { } func (cmd *ListCredentialsCommand) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContext) error { - cs := credentials.CredentialsService{} + cs := credentials.NewCredentialsService(logging.NewDiscardLogger()) creds, err := cs.List() if err != nil { return err diff --git a/internal/accounts/account_list.go b/internal/accounts/account_list.go index b44f9bcbb..7ae6e598c 100644 --- a/internal/accounts/account_list.go +++ b/internal/accounts/account_list.go @@ -31,7 +31,7 @@ var _ AccountList = &defaultAccountList{} func NewAccountList(fs afero.Fs, log logging.Logger) *defaultAccountList { return &defaultAccountList{ providers: []AccountProvider{ - NewCredentialsProvider(), + NewCredentialsProvider(log), }, log: log, } diff --git a/internal/accounts/provider_credentials.go b/internal/accounts/provider_credentials.go index f59743df1..9dc22ae47 100644 --- a/internal/accounts/provider_credentials.go +++ b/internal/accounts/provider_credentials.go @@ -2,14 +2,19 @@ package accounts -import "github.com/posit-dev/publisher/internal/credentials" +import ( + "github.com/posit-dev/publisher/internal/credentials" + "github.com/posit-dev/publisher/internal/logging" +) type CredentialsProvider struct { cs credentials.CredentialsService } -func NewCredentialsProvider() *CredentialsProvider { - return &CredentialsProvider{cs: credentials.CredentialsService{}} +func NewCredentialsProvider(log logging.Logger) *CredentialsProvider { + return &CredentialsProvider{ + cs: credentials.NewCredentialsService(log), + } } func (p *CredentialsProvider) Load() ([]Account, error) { diff --git a/internal/credentials/credentials.go b/internal/credentials/credentials.go index 949087de1..bbce228b6 100644 --- a/internal/credentials/credentials.go +++ b/internal/credentials/credentials.go @@ -32,6 +32,7 @@ import ( "net/url" "github.com/google/uuid" + "github.com/posit-dev/publisher/internal/logging" "github.com/posit-dev/publisher/internal/util" "github.com/spf13/afero" "github.com/zalando/go-keyring" @@ -74,8 +75,21 @@ func (cr *CredentialRecord) ToCredential() (*Credential, error) { } } -type CredentialsService struct { +type CredentialsService interface { + FileCredentialRecordFactory() (*CredentialRecord, error) + Delete(guid string) error + Get(guid string) (*Credential, error) + List() ([]Credential, error) + Set(name string, url string, ak string) (*Credential, error) +} + +type credentialsService struct { afs afero.Fs + log logging.Logger +} + +func NewCredentialsService(log logging.Logger) *credentialsService { + return &credentialsService{log: log} } // FileCredentialRecordFactory creates a Credential based on the presence of the @@ -86,7 +100,7 @@ type fileCredential struct { Key string `toml:"key"` } -func (cs *CredentialsService) FileCredentialRecordFactory() (*CredentialRecord, error) { +func (cs *credentialsService) FileCredentialRecordFactory() (*CredentialRecord, error) { homeDir, err := util.UserHomeDir(cs.afs) if err != nil { return nil, err @@ -148,7 +162,7 @@ func (cs *CredentialsService) FileCredentialRecordFactory() (*CredentialRecord, return nil, nil } -func (cs *CredentialsService) checkForConflicts( +func (cs *credentialsService) checkForConflicts( table *map[string]CredentialRecord, c *Credential) error { // Check if Credential attributes (URL or name) are already used by another credential @@ -187,7 +201,7 @@ func (cs *CredentialsService) checkForConflicts( // Delete removes a Credential by its guid. // If lookup by guid fails, a NotFoundError is returned. -func (cs *CredentialsService) Delete(guid string) error { +func (cs *credentialsService) Delete(guid string) error { table, err := cs.load() if err != nil { @@ -196,6 +210,7 @@ func (cs *CredentialsService) Delete(guid string) error { _, exists := table[guid] if !exists { + cs.log.Debug("Credential does not exist", "credential", guid) return &NotFoundError{GUID: guid} } @@ -209,7 +224,7 @@ func (cs *CredentialsService) Delete(guid string) error { } // Get retrieves a Credential by its guid. -func (cs *CredentialsService) Get(guid string) (*Credential, error) { +func (cs *credentialsService) Get(guid string) (*Credential, error) { table, err := cs.load() if err != nil { return nil, err @@ -217,6 +232,7 @@ func (cs *CredentialsService) Get(guid string) (*Credential, error) { cr, exists := table[guid] if !exists { + cs.log.Debug("Credential does not exist", "credential", guid) return nil, &NotFoundError{GUID: guid} } @@ -224,7 +240,7 @@ func (cs *CredentialsService) Get(guid string) (*Credential, error) { } // List retrieves all Credentials -func (cs *CredentialsService) List() ([]Credential, error) { +func (cs *credentialsService) List() ([]Credential, error) { records, err := cs.load() if err != nil { return nil, err @@ -242,7 +258,7 @@ func (cs *CredentialsService) List() ([]Credential, error) { // Set creates a Credential. // A guid is assigned to the Credential using the UUIDv4 specification. -func (cs *CredentialsService) Set(name string, url string, ak string) (*Credential, error) { +func (cs *credentialsService) Set(name string, url string, ak string) (*Credential, error) { table, err := cs.load() if err != nil { return nil, err @@ -286,7 +302,7 @@ func (cs *CredentialsService) Set(name string, url string, ak string) (*Credenti } // Saves the CredentialTable, but removes Env Credentials first -func (cs *CredentialsService) save(table CredentialTable) error { +func (cs *credentialsService) save(table CredentialTable) error { // remove any environment variable credential from the table // before saving @@ -299,7 +315,7 @@ func (cs *CredentialsService) save(table CredentialTable) error { } // Saves the CredentialTable to keyring -func (cs *CredentialsService) saveToKeyRing(table CredentialTable) error { +func (cs *credentialsService) saveToKeyRing(table CredentialTable) error { data, err := json.Marshal(table) if err != nil { return fmt.Errorf("failed to serialize credentials: %v", err) @@ -314,24 +330,28 @@ func (cs *CredentialsService) saveToKeyRing(table CredentialTable) error { } // Loads the CredentialTable with keyring and env values -func (cs *CredentialsService) load() (CredentialTable, error) { +func (cs *credentialsService) load() (CredentialTable, error) { table, err := cs.loadFromKeyRing() if err != nil { + cs.log.Debug("Failed to load credentials from system keyring", "error", err.Error()) return nil, err } // insert a possible file-based credential before returning record, err := cs.FileCredentialRecordFactory() if err != nil { + cs.log.Debug("Failed to create file based credentials records instance", "error", err.Error()) return nil, err } if record != nil { c, err := record.ToCredential() if err != nil { + cs.log.Debug("Error building credentials from file record", "error", err.Error()) return nil, err } err = cs.checkForConflicts(&table, c) if err != nil { + cs.log.Debug("Conflict on credential record", "error", err.Error()) return nil, err } table[EnvVarGUID] = *record @@ -340,7 +360,7 @@ func (cs *CredentialsService) load() (CredentialTable, error) { } // Loads the CredentialTable from keyRing -func (cs *CredentialsService) loadFromKeyRing() (CredentialTable, error) { +func (cs *credentialsService) loadFromKeyRing() (CredentialTable, error) { ks := KeyringService{} data, err := ks.Get(ServiceName, "credentials") if err != nil { diff --git a/internal/credentials/credentials_test.go b/internal/credentials/credentials_test.go index 806645c06..fcff8f65d 100644 --- a/internal/credentials/credentials_test.go +++ b/internal/credentials/credentials_test.go @@ -6,9 +6,11 @@ import ( "testing" "github.com/pelletier/go-toml/v2" + "github.com/posit-dev/publisher/internal/logging/loggingtest" "github.com/posit-dev/publisher/internal/util" + "github.com/posit-dev/publisher/internal/util/utiltest" "github.com/spf13/afero" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" "github.com/zalando/go-keyring" ) @@ -26,7 +28,20 @@ var fileCred = Credential{ Name: "connect.localtest.me", } -func credentialsFilePath(afs afero.Fs) (util.AbsolutePath, error) { +type CredentialsTestSuite struct { + utiltest.Suite + log *loggingtest.MockLogger +} + +func TestCredentialsTestSuite(t *testing.T) { + suite.Run(t, new(CredentialsTestSuite)) +} + +func (s *CredentialsTestSuite) SetupTest() { + s.log = loggingtest.NewMockLogger() +} + +func (s *CredentialsTestSuite) credentialsFilePath(afs afero.Fs) (util.AbsolutePath, error) { homeDir, err := util.UserHomeDir(afs) if err != nil { return util.AbsolutePath{}, err @@ -34,12 +49,12 @@ func credentialsFilePath(afs afero.Fs) (util.AbsolutePath, error) { return homeDir.Join(".connect-credentials"), nil } -func createFileCredentials(t *testing.T, cs *CredentialsService, errorCheck bool) { - path, err := credentialsFilePath(cs.afs) - assert.NoError(t, err) +func (s *CredentialsTestSuite) createFileCredentials(cs *credentialsService, errorCheck bool) { + path, err := s.credentialsFilePath(cs.afs) + s.NoError(err) f, err := path.Create() - assert.NoError(t, err) + s.NoError(err) defer f.Close() enc := toml.NewEncoder(f) @@ -48,195 +63,210 @@ func createFileCredentials(t *testing.T, cs *CredentialsService, errorCheck bool Key: fileCred.ApiKey, } err = enc.Encode(cred) - assert.NoError(t, err) + s.NoError(err) if errorCheck { res, err := cs.Get(fileCred.GUID) - assert.NoError(t, err) + s.NoError(err) expected := Credential{ GUID: fileCred.GUID, Name: fileCred.Name, URL: fileCred.URL, ApiKey: fileCred.ApiKey, } - assert.Equal(t, res, &expected) + s.Equal(res, &expected) } } -func clearFileCredentials(t *testing.T, cs *CredentialsService) { - path, err := credentialsFilePath(cs.afs) - assert.NoError(t, err) +func (s *CredentialsTestSuite) clearFileCredentials(cs *credentialsService) { + path, err := s.credentialsFilePath(cs.afs) + s.NoError(err) _ = path.Remove() } -func TestSet(t *testing.T) { +func (s *CredentialsTestSuite) TestSet() { keyring.MockInit() - cs := CredentialsService{ + cs := credentialsService{ afs: afero.NewMemMapFs(), + log: s.log, } - clearFileCredentials(t, &cs) + s.clearFileCredentials(&cs) cred, err := cs.Set("example", "https://example.com", "12345") - assert.NoError(t, err) - assert.NotNil(t, cred.GUID) - assert.Equal(t, cred.Name, "example") - assert.Equal(t, cred.URL, "https://example.com") - assert.Equal(t, cred.ApiKey, "12345") + s.NoError(err) + s.NotNil(cred.GUID) + s.Equal(cred.Name, "example") + s.Equal(cred.URL, "https://example.com") + s.Equal(cred.ApiKey, "12345") } -func TestSetURLCollisionError(t *testing.T) { +func (s *CredentialsTestSuite) TestSetURLCollisionError() { keyring.MockInit() - cs := CredentialsService{ + cs := credentialsService{ afs: afero.NewMemMapFs(), + log: s.log, } - clearFileCredentials(t, &cs) + s.clearFileCredentials(&cs) _, err := cs.Set("example", "https://example.com", "12345") - assert.NoError(t, err) + s.NoError(err) _, err = cs.Set("example", "https://example.com", "12345") - assert.Error(t, err) - assert.IsType(t, &URLCollisionError{}, err) + s.Error(err) + s.IsType(&URLCollisionError{}, err) // validate collision with env var credentials - createFileCredentials(t, &cs, true) + s.createFileCredentials(&cs, true) _, err = cs.Set("unique", fileCred.URL, "12345") - assert.Error(t, err) - assert.IsType(t, &EnvURLCollisionError{}, err) + s.Error(err) + s.IsType(&EnvURLCollisionError{}, err) } -func TestGet(t *testing.T) { +func (s *CredentialsTestSuite) TestGet() { keyring.MockInit() - cs := CredentialsService{ + cs := credentialsService{ afs: afero.NewMemMapFs(), + log: s.log, } - clearFileCredentials(t, &cs) + s.clearFileCredentials(&cs) + testGuid := "5ede880a-acd8-4206-b9fa-7d788c42fbe4" // First test without any credentials in environment + s.log.On("Debug", "Credential does not exist", "credential", testGuid).Return() // error if missing - _, err := cs.Get("example") - assert.Error(t, err) + _, err := cs.Get(testGuid) + s.Error(err) + s.log.AssertExpectations(s.T()) // pass if exists cred, err := cs.Set("example", "https://example.com", "12345") - assert.NoError(t, err) + s.NoError(err) res, err := cs.Get(cred.GUID) - assert.NoError(t, err) - assert.Equal(t, res, cred) + s.NoError(err) + s.Equal(res, cred) // confirm environment credential not available + s.log.On("Debug", "Credential does not exist", "credential", fileCred.GUID).Return() _, err = cs.Get(fileCred.GUID) - assert.Error(t, err) + s.Error(err) + s.log.AssertExpectations(s.T()) // Test with credentials in environment - createFileCredentials(t, &cs, true) + s.createFileCredentials(&cs, true) // retest prior test res, err = cs.Get(cred.GUID) - assert.NoError(t, err) - assert.Equal(t, res, cred) + s.NoError(err) + s.Equal(res, cred) // request environment credentials res, err = cs.Get(fileCred.GUID) - assert.NoError(t, err) - assert.Equal(t, res, &fileCred) + s.NoError(err) + s.Equal(res, &fileCred) // Test for conflicts where credential was saved ahead of env variable - clearFileCredentials(t, &cs) + s.clearFileCredentials(&cs) cred, err = cs.Set("env", fileCred.URL, "12345") - assert.NoError(t, err) + s.NoError(err) _, err = cs.Get(cred.GUID) - assert.NoError(t, err) + s.NoError(err) - createFileCredentials(t, &cs, false) + s.log.On("Debug", "Conflict on credential record", "error", "CONNECT_SERVER URL value conflicts with existing credential (connect.localtest.me) URL: https://connect.localtest.me/rsc/dev-password-copy").Return() + s.createFileCredentials(&cs, false) _, err = cs.Get(cred.GUID) - assert.Error(t, err) - assert.IsType(t, &EnvURLCollisionError{}, err) + s.Error(err) + s.IsType(&EnvURLCollisionError{}, err) + s.log.AssertExpectations(s.T()) } -func TestNormalizedSet(t *testing.T) { +func (s *CredentialsTestSuite) TestNormalizedSet() { keyring.MockInit() - cs := CredentialsService{ + cs := credentialsService{ afs: afero.NewMemMapFs(), + log: s.log, } - clearFileCredentials(t, &cs) + s.clearFileCredentials(&cs) // pass if no change (already normalized) cred, err := cs.Set("example", "https://example.com", "12345") - assert.NoError(t, err) + s.NoError(err) res, err := cs.Get(cred.GUID) - assert.NoError(t, err) - assert.Equal(t, res.URL, cred.URL) + s.NoError(err) + s.Equal(res.URL, cred.URL) // pass if URL ends up normalized cred, err = cs.Set("example2", "https://example.com///another/seg/", "12345") - assert.NoError(t, err) - assert.NotEqual(t, cred.URL, "https://example.com///another/seg/") + s.NoError(err) + s.NotEqual(cred.URL, "https://example.com///another/seg/") res, err = cs.Get(cred.GUID) - assert.NoError(t, err) - assert.Equal(t, res.URL, "https://example.com/another/seg") - assert.Equal(t, cred.URL, res.URL) + s.NoError(err) + s.Equal(res.URL, "https://example.com/another/seg") + s.Equal(cred.URL, res.URL) } -func TestSetCollisions(t *testing.T) { +func (s *CredentialsTestSuite) TestSetCollisions() { keyring.MockInit() - cs := CredentialsService{ + cs := credentialsService{ afs: afero.NewMemMapFs(), + log: s.log, } // Add credentials into environment - createFileCredentials(t, &cs, true) + s.createFileCredentials(&cs, true) // add a non-environment credential _, err := cs.Set("example", "https://example.com", "12345") - assert.NoError(t, err) + s.NoError(err) // non-environment name collision _, err = cs.Set("example", "https://more_examples.com", "12345") - assert.Error(t, err) - assert.IsType(t, &NameCollisionError{}, err) + s.Error(err) + s.IsType(&NameCollisionError{}, err) // environment name collision _, err = cs.Set(fileCred.Name, "https://more_examples2.com", "12345") - assert.Error(t, err) - assert.IsType(t, &EnvNameCollisionError{}, err) + s.Error(err) + s.IsType(&EnvNameCollisionError{}, err) // non-environment URL collision _, err = cs.Set("another_example", "https://example.com", "12345") - assert.Error(t, err) - assert.IsType(t, &URLCollisionError{}, err) + s.Error(err) + s.IsType(&URLCollisionError{}, err) // environment URL collision _, err = cs.Set("one_more", fileCred.URL, "12345") - assert.Error(t, err) - assert.IsType(t, &EnvURLCollisionError{}, err) + s.Error(err) + s.IsType(&EnvURLCollisionError{}, err) } -func TestDelete(t *testing.T) { +func (s *CredentialsTestSuite) TestDelete() { keyring.MockInit() - cs := CredentialsService{ + cs := credentialsService{ afs: afero.NewMemMapFs(), + log: s.log, } - clearFileCredentials(t, &cs) + s.clearFileCredentials(&cs) cred, err := cs.Set("example", "https://example.com", "12345") - assert.NoError(t, err) + s.NoError(err) // no error if exists err = cs.Delete(cred.GUID) - assert.NoError(t, err) + s.NoError(err) // err if missing + s.log.On("Debug", "Credential does not exist", "credential", cred.GUID).Return() err = cs.Delete(cred.GUID) - assert.Error(t, err) + s.Error(err) + s.log.AssertExpectations(s.T()) // Add credentials into environment - createFileCredentials(t, &cs, true) + s.createFileCredentials(&cs, true) // err for our special GUID err = cs.Delete(fileCred.GUID) - assert.Error(t, err) - assert.IsType(t, &EnvURLDeleteError{}, err) + s.Error(err) + s.IsType(&EnvURLDeleteError{}, err) } diff --git a/internal/logging/logger.go b/internal/logging/logger.go index ab0e4d13c..a3dd8ad7e 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -3,6 +3,7 @@ package logging // Copyright (C) 2023 by Posit Software, PBC. import ( + "io" "log/slog" ) @@ -36,3 +37,8 @@ func FromStdLogger(log *slog.Logger) Logger { func (l logger) WithArgs(args ...any) Logger { return logger{l.BaseLogger.With(args...)} } + +func NewDiscardLogger() Logger { + discardLgr := slog.New(slog.NewJSONHandler(io.Discard, nil)) + return logger{discardLgr} +} diff --git a/internal/services/api/delete_credentials.go b/internal/services/api/delete_credentials.go index 20340c2f7..3020b9b0e 100644 --- a/internal/services/api/delete_credentials.go +++ b/internal/services/api/delete_credentials.go @@ -13,7 +13,7 @@ import ( func DeleteCredentialHandlerFunc(log logging.Logger) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { guid := mux.Vars(req)["guid"] - cs := credentials.CredentialsService{} + cs := credentials.NewCredentialsService(log) err := cs.Delete(guid) if err != nil { switch e := err.(type) { diff --git a/internal/services/api/delete_credentials_test.go b/internal/services/api/delete_credentials_test.go index db58bea8e..299a05866 100644 --- a/internal/services/api/delete_credentials_test.go +++ b/internal/services/api/delete_credentials_test.go @@ -34,8 +34,7 @@ func (s *DeleteCredentialsSuite) SetupTest() { } func (s *DeleteCredentialsSuite) Test204() { - - cs := credentials.CredentialsService{} + cs := credentials.NewCredentialsService(s.log) cred, err := cs.Set("example", "https://example.com", "12345") s.NoError(err) diff --git a/internal/services/api/get_credential.go b/internal/services/api/get_credential.go index e4ad3bd0f..fd95ff579 100644 --- a/internal/services/api/get_credential.go +++ b/internal/services/api/get_credential.go @@ -14,7 +14,7 @@ import ( func GetCredentialHandlerFunc(log logging.Logger) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { guid := mux.Vars(req)["guid"] - cs := credentials.CredentialsService{} + cs := credentials.NewCredentialsService(log) cred, err := cs.Get(guid) if err != nil { switch e := err.(type) { diff --git a/internal/services/api/get_credentials.go b/internal/services/api/get_credentials.go index 29da7a960..d10fc1787 100644 --- a/internal/services/api/get_credentials.go +++ b/internal/services/api/get_credentials.go @@ -12,7 +12,7 @@ import ( func GetCredentialsHandlerFunc(log logging.Logger) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { - cs := credentials.CredentialsService{} + cs := credentials.NewCredentialsService(log) creds, err := cs.List() if err != nil { InternalError(w, req, log, err) diff --git a/internal/services/api/post_credentials.go b/internal/services/api/post_credentials.go index 2cf4e87be..1bd9b28e6 100644 --- a/internal/services/api/post_credentials.go +++ b/internal/services/api/post_credentials.go @@ -30,7 +30,7 @@ func PostCredentialFuncHandler(log logging.Logger) http.HandlerFunc { return } - cs := credentials.CredentialsService{} + cs := credentials.NewCredentialsService(log) cred, err := cs.Set(body.Name, body.URL, body.ApiKey) if err != nil { if _, ok := err.(*credentials.URLCollisionError); ok { diff --git a/internal/services/api/post_credentials_test.go b/internal/services/api/post_credentials_test.go index ac2d29e7d..c6da6d176 100644 --- a/internal/services/api/post_credentials_test.go +++ b/internal/services/api/post_credentials_test.go @@ -60,7 +60,7 @@ func (s *PostCredentialTestSuite) Test409() { url := "http://example.com" ak := "12345" - cs := credentials.CredentialsService{} + cs := credentials.NewCredentialsService(s.log) _, err := cs.Set(name, url, ak) s.NoError(err)