Relates to [cli@27b2797], which forked this type from the Moby API, and [moby@6cfff7e], which made the same change on the API side. The Email field was originally used to create a new Docker Hub account through the `docker login` command. The `docker login` command could be used both to log in to an existing account (providing only username and password), or to create a new account (providing desired username and password, and an e-mail address to use for the new account). This functionality was confusing, because it was implemented when Docker Hub was the only registry, but the same functionality could not be used for other registries. This functionality was removed in Docker 1.11 (API version 1.23) through [moby@aee260d], which also removed the Email field ([engine-api@9a9e468]) as it was no longer used. However, this caused issues when using a new CLI connecting with an old daemon, as the field would no longer be serialized, and the deprecation may not yet be picked up by custom registries, so [engine-api@167efc7] added the field back, deprecated it, and added an "omitempty". There was no official "deprecated" format yet at the time, so let's make sure the deprecation follows the proper format to make sure it gets noticed. [cli@27b2797]:27b2797f7d[moby@6cfff7e]:6cfff7e880[moby@aee260d]:aee260d4eb[engine-api@9a9e468]:9a9e468f50[engine-api@167efc7]:167efc72bbSigned-off-by: Sebastiaan van Stijn <github@gone.nl>
303 lines
7.8 KiB
Go
303 lines
7.8 KiB
Go
package credentials
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/docker/cli/cli/config/types"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
)
|
|
|
|
type fakeStore struct {
|
|
configs map[string]types.AuthConfig
|
|
saveFn func(*fakeStore) 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
|
|
}
|
|
|
|
func (f *fakeStore) GetAuthConfigs() map[string]types.AuthConfig {
|
|
return f.configs
|
|
}
|
|
|
|
func (*fakeStore) GetFilename() string {
|
|
return "no-config.json"
|
|
}
|
|
|
|
// TestFileStoreIdempotent verifies that the config-file isn't updated
|
|
// 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{
|
|
Username: "foo@example.com",
|
|
Auth: "super_secret_token",
|
|
ServerAddress: "https://example.com",
|
|
}
|
|
authTwo := types.AuthConfig{
|
|
Username: "bar@example.com",
|
|
Auth: "also_super_secret_token",
|
|
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) {
|
|
f := &fakeStore{configs: map[string]types.AuthConfig{}}
|
|
|
|
s := NewFileStore(f)
|
|
auth := types.AuthConfig{
|
|
Username: "foo@example.com",
|
|
Auth: "super_secret_token",
|
|
ServerAddress: "https://example.com",
|
|
}
|
|
err := s.Store(auth)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Len(f.GetAuthConfigs(), 1))
|
|
|
|
actual, ok := f.GetAuthConfigs()["https://example.com"]
|
|
assert.Check(t, ok)
|
|
assert.Check(t, is.DeepEqual(auth, actual))
|
|
}
|
|
|
|
func TestFileStoreGet(t *testing.T) {
|
|
f := &fakeStore{configs: map[string]types.AuthConfig{
|
|
"https://example.com": {
|
|
Username: "foo@example.com",
|
|
Auth: "super_secret_token",
|
|
ServerAddress: "https://example.com",
|
|
},
|
|
}}
|
|
|
|
s := NewFileStore(f)
|
|
a, err := s.Get("https://example.com")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a.Auth != "super_secret_token" {
|
|
t.Fatalf("expected auth `super_secret_token`, got %s", a.Auth)
|
|
}
|
|
if a.Username != "foo@example.com" {
|
|
t.Fatalf("expected username `foo@example.com`, got %s", a.Username)
|
|
}
|
|
}
|
|
|
|
func TestFileStoreGetAll(t *testing.T) {
|
|
s1 := "https://example.com"
|
|
s2 := "https://example2.example.com"
|
|
f := &fakeStore{configs: map[string]types.AuthConfig{
|
|
s1: {
|
|
Username: "foo@example.com",
|
|
Auth: "super_secret_token",
|
|
ServerAddress: "https://example.com",
|
|
},
|
|
s2: {
|
|
Username: "foo@example2.com",
|
|
Auth: "super_secret_token2",
|
|
ServerAddress: "https://example2.example.com",
|
|
},
|
|
}}
|
|
|
|
s := NewFileStore(f)
|
|
as, err := s.GetAll()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(as) != 2 {
|
|
t.Fatalf("wanted 2, got %d", len(as))
|
|
}
|
|
if as[s1].Auth != "super_secret_token" {
|
|
t.Fatalf("expected auth `super_secret_token`, got %s", as[s1].Auth)
|
|
}
|
|
if as[s1].Username != "foo@example.com" {
|
|
t.Fatalf("expected username `foo@example.com`, got %s", as[s1].Username)
|
|
}
|
|
if as[s2].Auth != "super_secret_token2" {
|
|
t.Fatalf("expected auth `super_secret_token2`, got %s", as[s2].Auth)
|
|
}
|
|
if as[s2].Username != "foo@example2.com" {
|
|
t.Fatalf("expected username `foo@example2.com`, got %s", as[s2].Username)
|
|
}
|
|
}
|
|
|
|
func TestFileStoreErase(t *testing.T) {
|
|
f := &fakeStore{configs: map[string]types.AuthConfig{
|
|
"https://example.com": {
|
|
Username: "foo@example.com",
|
|
Auth: "super_secret_token",
|
|
ServerAddress: "https://example.com",
|
|
},
|
|
}}
|
|
|
|
s := NewFileStore(f)
|
|
err := s.Erase("https://example.com")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// file store never returns errors, check that the auth config is empty
|
|
a, err := s.Get("https://example.com")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if a.Auth != "" {
|
|
t.Fatalf("expected empty auth token, got %s", a.Auth)
|
|
}
|
|
}
|
|
|
|
func TestConvertToHostname(t *testing.T) {
|
|
tests := []struct{ input, expected string }{
|
|
{
|
|
input: "127.0.0.1",
|
|
expected: "127.0.0.1",
|
|
},
|
|
{
|
|
input: "::1",
|
|
expected: "::1",
|
|
},
|
|
{
|
|
// FIXME(thaJeztah): this should be normalized to "::1" if there's no port (or vice-versa, as long as we're consistent)
|
|
input: "[::1]",
|
|
expected: "[::1]",
|
|
},
|
|
{
|
|
input: "example.com",
|
|
expected: "example.com",
|
|
},
|
|
{
|
|
input: "http://example.com",
|
|
expected: "example.com",
|
|
},
|
|
{
|
|
input: "https://example.com",
|
|
expected: "example.com",
|
|
},
|
|
{
|
|
input: "https://example.com/",
|
|
expected: "example.com",
|
|
},
|
|
{
|
|
input: "https://example.com/v2/",
|
|
expected: "example.com",
|
|
},
|
|
{
|
|
// FIXME(thaJeztah): should ConvertToHostname correctly handle this / fail on this?
|
|
input: "unix:///var/run/docker.sock",
|
|
expected: "unix:",
|
|
},
|
|
{
|
|
// FIXME(thaJeztah): should ConvertToHostname correctly handle this?
|
|
input: "ftp://example.com",
|
|
expected: "example.com",
|
|
},
|
|
// should support non-standard port in registry url
|
|
{
|
|
input: "127.0.0.1:6556",
|
|
expected: "127.0.0.1:6556",
|
|
},
|
|
{
|
|
// FIXME(thaJeztah): this should be normalized to "[::1]:6556"
|
|
input: "::1:6556",
|
|
expected: "::1:6556",
|
|
},
|
|
{
|
|
input: "[::1]:6556",
|
|
expected: "[::1]:6556",
|
|
},
|
|
{
|
|
input: "example.com:6555",
|
|
expected: "example.com:6555",
|
|
},
|
|
{
|
|
input: "https://127.0.0.1:6555/v2/",
|
|
expected: "127.0.0.1:6555",
|
|
},
|
|
{
|
|
input: "https://::1:6555/v2/",
|
|
expected: "[::1]:6555",
|
|
},
|
|
{
|
|
input: "https://[::1]:6555/v2/",
|
|
expected: "[::1]:6555",
|
|
},
|
|
{
|
|
input: "http://example.com:6555",
|
|
expected: "example.com:6555",
|
|
},
|
|
{
|
|
input: "https://example.com:6555",
|
|
expected: "example.com:6555",
|
|
},
|
|
{
|
|
input: "https://example.com:6555/v2/",
|
|
expected: "example.com:6555",
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.input, func(t *testing.T) {
|
|
actual := ConvertToHostname(tc.input)
|
|
assert.Equal(t, actual, tc.expected)
|
|
})
|
|
}
|
|
}
|