Merge pull request #5569 from thaJeztah/27.x_backport_login_idempotent

This commit is contained in:
Laura Brehm 2024-10-22 16:18:18 +01:00 committed by GitHub
commit 968506fd0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 111 additions and 28 deletions

View File

@ -25,8 +25,13 @@ func NewFileStore(file store) Store {
return &fileStore{file: file} return &fileStore{file: file}
} }
// Erase removes the given credentials from the file store. // Erase removes the given credentials from the file store.This function is
// idempotent and does not update the file if credentials did not change.
func (c *fileStore) Erase(serverAddress string) error { func (c *fileStore) Erase(serverAddress string) error {
if _, exists := c.file.GetAuthConfigs()[serverAddress]; !exists {
// nothing to do; no credentials found for the given serverAddress
return nil
}
delete(c.file.GetAuthConfigs(), serverAddress) delete(c.file.GetAuthConfigs(), serverAddress)
return c.file.Save() return c.file.Save()
} }
@ -52,9 +57,14 @@ func (c *fileStore) GetAll() (map[string]types.AuthConfig, error) {
return c.file.GetAuthConfigs(), nil return c.file.GetAuthConfigs(), nil
} }
// Store saves the given credentials in the file store. // Store saves the given credentials in the file store. This function is
// idempotent and does not update the file if credentials did not change.
func (c *fileStore) Store(authConfig types.AuthConfig) error { func (c *fileStore) Store(authConfig types.AuthConfig) error {
authConfigs := c.file.GetAuthConfigs() authConfigs := c.file.GetAuthConfigs()
if oldAuthConfig, ok := authConfigs[authConfig.ServerAddress]; ok && oldAuthConfig == authConfig {
// Credentials didn't change, so skip updating the configuration file.
return nil
}
authConfigs[authConfig.ServerAddress] = authConfig authConfigs[authConfig.ServerAddress] = authConfig
return c.file.Save() return c.file.Save()
} }

View File

@ -10,9 +10,15 @@ import (
type fakeStore struct { type fakeStore struct {
configs map[string]types.AuthConfig configs map[string]types.AuthConfig
saveFn func(*fakeStore) error
} }
func (f *fakeStore) Save() error { func (f *fakeStore) Save() error {
if f.saveFn != nil {
// Pass a reference to the fakeStore itself in case saveFn
// wants to access it.
return f.saveFn(f)
}
return nil return nil
} }
@ -21,15 +27,82 @@ func (f *fakeStore) GetAuthConfigs() map[string]types.AuthConfig {
} }
func (f *fakeStore) GetFilename() string { func (f *fakeStore) GetFilename() string {
return "/tmp/docker-fakestore" return "no-config.json"
} }
func newStore(auths map[string]types.AuthConfig) store { // TestFileStoreIdempotent verifies that the config-file isn't updated
return &fakeStore{configs: auths} // if nothing changed.
func TestFileStoreIdempotent(t *testing.T) {
var saveCount, expectedSaveCount int
s := NewFileStore(&fakeStore{
configs: map[string]types.AuthConfig{},
saveFn: func(*fakeStore) error {
saveCount++
return nil
},
})
authOne := types.AuthConfig{
Auth: "super_secret_token",
Email: "foo@example.com",
ServerAddress: "https://example.com",
}
authTwo := types.AuthConfig{
Auth: "also_super_secret_token",
Email: "bar@example.com",
ServerAddress: "https://other.example.com",
}
expectedSaveCount = 1
t.Run("store new credentials", func(t *testing.T) {
assert.NilError(t, s.Store(authOne))
retrievedAuth, err := s.Get(authOne.ServerAddress)
assert.NilError(t, err)
assert.Check(t, is.Equal(retrievedAuth, authOne))
assert.Check(t, is.Equal(saveCount, expectedSaveCount))
})
t.Run("store same credentials is a no-op", func(t *testing.T) {
assert.NilError(t, s.Store(authOne))
retrievedAuth, err := s.Get(authOne.ServerAddress)
assert.NilError(t, err)
assert.Check(t, is.Equal(retrievedAuth, authOne))
assert.Check(t, is.Equal(saveCount, expectedSaveCount), "should not have saved if nothing changed")
})
t.Run("store other credentials", func(t *testing.T) {
expectedSaveCount++
assert.NilError(t, s.Store(authTwo))
retrievedAuth, err := s.Get(authTwo.ServerAddress)
assert.NilError(t, err)
assert.Check(t, is.Equal(retrievedAuth, authTwo))
assert.Check(t, is.Equal(saveCount, expectedSaveCount))
})
t.Run("erase credentials", func(t *testing.T) {
expectedSaveCount++
assert.NilError(t, s.Erase(authOne.ServerAddress))
retrievedAuth, err := s.Get(authOne.ServerAddress)
assert.NilError(t, err)
assert.Check(t, is.Equal(retrievedAuth, types.AuthConfig{}))
assert.Check(t, is.Equal(saveCount, expectedSaveCount))
})
t.Run("erase non-existing credentials is a no-op", func(t *testing.T) {
assert.NilError(t, s.Erase(authOne.ServerAddress))
retrievedAuth, err := s.Get(authOne.ServerAddress)
assert.NilError(t, err)
assert.Check(t, is.Equal(retrievedAuth, types.AuthConfig{}))
assert.Check(t, is.Equal(saveCount, expectedSaveCount), "should not have saved if nothing changed")
})
t.Run("erase other credentials", func(t *testing.T) {
expectedSaveCount++
assert.NilError(t, s.Erase(authTwo.ServerAddress))
retrievedAuth, err := s.Get(authTwo.ServerAddress)
assert.NilError(t, err)
assert.Check(t, is.Equal(retrievedAuth, types.AuthConfig{}))
assert.Check(t, is.Equal(saveCount, expectedSaveCount))
})
} }
func TestFileStoreAddCredentials(t *testing.T) { func TestFileStoreAddCredentials(t *testing.T) {
f := newStore(make(map[string]types.AuthConfig)) f := &fakeStore{configs: map[string]types.AuthConfig{}}
s := NewFileStore(f) s := NewFileStore(f)
auth := types.AuthConfig{ auth := types.AuthConfig{
@ -47,13 +120,13 @@ func TestFileStoreAddCredentials(t *testing.T) {
} }
func TestFileStoreGet(t *testing.T) { func TestFileStoreGet(t *testing.T) {
f := newStore(map[string]types.AuthConfig{ f := &fakeStore{configs: map[string]types.AuthConfig{
"https://example.com": { "https://example.com": {
Auth: "super_secret_token", Auth: "super_secret_token",
Email: "foo@example.com", Email: "foo@example.com",
ServerAddress: "https://example.com", ServerAddress: "https://example.com",
}, },
}) }}
s := NewFileStore(f) s := NewFileStore(f)
a, err := s.Get("https://example.com") a, err := s.Get("https://example.com")
@ -71,7 +144,7 @@ func TestFileStoreGet(t *testing.T) {
func TestFileStoreGetAll(t *testing.T) { func TestFileStoreGetAll(t *testing.T) {
s1 := "https://example.com" s1 := "https://example.com"
s2 := "https://example2.example.com" s2 := "https://example2.example.com"
f := newStore(map[string]types.AuthConfig{ f := &fakeStore{configs: map[string]types.AuthConfig{
s1: { s1: {
Auth: "super_secret_token", Auth: "super_secret_token",
Email: "foo@example.com", Email: "foo@example.com",
@ -82,7 +155,7 @@ func TestFileStoreGetAll(t *testing.T) {
Email: "foo@example2.com", Email: "foo@example2.com",
ServerAddress: "https://example2.example.com", ServerAddress: "https://example2.example.com",
}, },
}) }}
s := NewFileStore(f) s := NewFileStore(f)
as, err := s.GetAll() as, err := s.GetAll()
@ -107,13 +180,13 @@ func TestFileStoreGetAll(t *testing.T) {
} }
func TestFileStoreErase(t *testing.T) { func TestFileStoreErase(t *testing.T) {
f := newStore(map[string]types.AuthConfig{ f := &fakeStore{configs: map[string]types.AuthConfig{
"https://example.com": { "https://example.com": {
Auth: "super_secret_token", Auth: "super_secret_token",
Email: "foo@example.com", Email: "foo@example.com",
ServerAddress: "https://example.com", ServerAddress: "https://example.com",
}, },
}) }}
s := NewFileStore(f) s := NewFileStore(f)
err := s.Erase("https://example.com") err := s.Erase("https://example.com")

View File

@ -91,7 +91,7 @@ func mockCommandFn(args ...string) client.Program {
} }
func TestNativeStoreAddCredentials(t *testing.T) { func TestNativeStoreAddCredentials(t *testing.T) {
f := newStore(make(map[string]types.AuthConfig)) f := &fakeStore{configs: map[string]types.AuthConfig{}}
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
fileStore: NewFileStore(f), fileStore: NewFileStore(f),
@ -116,7 +116,7 @@ func TestNativeStoreAddCredentials(t *testing.T) {
} }
func TestNativeStoreAddInvalidCredentials(t *testing.T) { func TestNativeStoreAddInvalidCredentials(t *testing.T) {
f := newStore(make(map[string]types.AuthConfig)) f := &fakeStore{configs: map[string]types.AuthConfig{}}
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
fileStore: NewFileStore(f), fileStore: NewFileStore(f),
@ -132,11 +132,11 @@ func TestNativeStoreAddInvalidCredentials(t *testing.T) {
} }
func TestNativeStoreGet(t *testing.T) { func TestNativeStoreGet(t *testing.T) {
f := newStore(map[string]types.AuthConfig{ f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress: { validServerAddress: {
Email: "foo@example.com", Email: "foo@example.com",
}, },
}) }}
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
fileStore: NewFileStore(f), fileStore: NewFileStore(f),
@ -154,11 +154,11 @@ func TestNativeStoreGet(t *testing.T) {
} }
func TestNativeStoreGetIdentityToken(t *testing.T) { func TestNativeStoreGetIdentityToken(t *testing.T) {
f := newStore(map[string]types.AuthConfig{ f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress2: { validServerAddress2: {
Email: "foo@example2.com", Email: "foo@example2.com",
}, },
}) }}
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
@ -176,11 +176,11 @@ func TestNativeStoreGetIdentityToken(t *testing.T) {
} }
func TestNativeStoreGetAll(t *testing.T) { func TestNativeStoreGetAll(t *testing.T) {
f := newStore(map[string]types.AuthConfig{ f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress: { validServerAddress: {
Email: "foo@example.com", Email: "foo@example.com",
}, },
}) }}
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
@ -217,11 +217,11 @@ func TestNativeStoreGetAll(t *testing.T) {
} }
func TestNativeStoreGetMissingCredentials(t *testing.T) { func TestNativeStoreGetMissingCredentials(t *testing.T) {
f := newStore(map[string]types.AuthConfig{ f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress: { validServerAddress: {
Email: "foo@example.com", Email: "foo@example.com",
}, },
}) }}
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
@ -232,11 +232,11 @@ func TestNativeStoreGetMissingCredentials(t *testing.T) {
} }
func TestNativeStoreGetInvalidAddress(t *testing.T) { func TestNativeStoreGetInvalidAddress(t *testing.T) {
f := newStore(map[string]types.AuthConfig{ f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress: { validServerAddress: {
Email: "foo@example.com", Email: "foo@example.com",
}, },
}) }}
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
@ -247,11 +247,11 @@ func TestNativeStoreGetInvalidAddress(t *testing.T) {
} }
func TestNativeStoreErase(t *testing.T) { func TestNativeStoreErase(t *testing.T) {
f := newStore(map[string]types.AuthConfig{ f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress: { validServerAddress: {
Email: "foo@example.com", Email: "foo@example.com",
}, },
}) }}
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
@ -263,11 +263,11 @@ func TestNativeStoreErase(t *testing.T) {
} }
func TestNativeStoreEraseInvalidAddress(t *testing.T) { func TestNativeStoreEraseInvalidAddress(t *testing.T) {
f := newStore(map[string]types.AuthConfig{ f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress: { validServerAddress: {
Email: "foo@example.com", Email: "foo@example.com",
}, },
}) }}
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,