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>
246 lines
6.0 KiB
Go
246 lines
6.0 KiB
Go
package credentials
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/docker/cli/cli/config/types"
|
|
"github.com/docker/docker-credential-helpers/client"
|
|
"github.com/docker/docker-credential-helpers/credentials"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
)
|
|
|
|
const (
|
|
validServerAddress = "https://index.docker.io/v1"
|
|
validServerAddress2 = "https://example.com:5002"
|
|
invalidServerAddress = "https://foobar.example.com"
|
|
missingCredsAddress = "https://missing.docker.io/v1"
|
|
)
|
|
|
|
var errCommandExited = errors.New("exited 1")
|
|
|
|
// mockCommand simulates interactions between the docker client and a remote
|
|
// credentials helper.
|
|
// Unit tests inject this mocked command into the remote to control execution.
|
|
type mockCommand struct {
|
|
arg string
|
|
input io.Reader
|
|
}
|
|
|
|
// Output returns responses from the remote credentials helper.
|
|
// It mocks those responses based in the input in the mock.
|
|
func (m *mockCommand) Output() ([]byte, error) {
|
|
in, err := io.ReadAll(m.input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
inS := string(in)
|
|
|
|
switch m.arg {
|
|
case "erase":
|
|
switch inS {
|
|
case validServerAddress:
|
|
return nil, nil
|
|
default:
|
|
return []byte("program failed"), errCommandExited
|
|
}
|
|
case "get":
|
|
switch inS {
|
|
case validServerAddress:
|
|
return []byte(`{"Username": "foo", "Secret": "bar"}`), nil
|
|
case validServerAddress2:
|
|
return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil
|
|
case missingCredsAddress:
|
|
return []byte(credentials.NewErrCredentialsNotFound().Error()), errCommandExited
|
|
case invalidServerAddress:
|
|
return []byte("program failed"), errCommandExited
|
|
}
|
|
case "store":
|
|
var c credentials.Credentials
|
|
err := json.NewDecoder(strings.NewReader(inS)).Decode(&c)
|
|
if err != nil {
|
|
return []byte("program failed"), errCommandExited
|
|
}
|
|
switch c.ServerURL {
|
|
case validServerAddress:
|
|
return nil, nil
|
|
default:
|
|
return []byte("program failed"), errCommandExited
|
|
}
|
|
case "list":
|
|
return []byte(fmt.Sprintf(`{"%s": "%s", "%s": "%s"}`, validServerAddress, "foo", validServerAddress2, "<token>")), nil
|
|
}
|
|
|
|
return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errCommandExited
|
|
}
|
|
|
|
// Input sets the input to send to a remote credentials helper.
|
|
func (m *mockCommand) Input(in io.Reader) {
|
|
m.input = in
|
|
}
|
|
|
|
func mockCommandFn(args ...string) client.Program {
|
|
return &mockCommand{
|
|
arg: args[0],
|
|
}
|
|
}
|
|
|
|
func TestNativeStoreAddCredentials(t *testing.T) {
|
|
f := &fakeStore{configs: map[string]types.AuthConfig{}}
|
|
s := &nativeStore{
|
|
programFunc: mockCommandFn,
|
|
fileStore: NewFileStore(f),
|
|
}
|
|
auth := types.AuthConfig{
|
|
Username: "foo",
|
|
Password: "bar",
|
|
ServerAddress: validServerAddress,
|
|
}
|
|
err := s.Store(auth)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Len(f.GetAuthConfigs(), 1))
|
|
|
|
actual, ok := f.GetAuthConfigs()[validServerAddress]
|
|
assert.Check(t, ok)
|
|
expected := types.AuthConfig{
|
|
ServerAddress: auth.ServerAddress,
|
|
}
|
|
assert.Check(t, is.DeepEqual(expected, actual))
|
|
}
|
|
|
|
func TestNativeStoreAddInvalidCredentials(t *testing.T) {
|
|
f := &fakeStore{configs: map[string]types.AuthConfig{}}
|
|
s := &nativeStore{
|
|
programFunc: mockCommandFn,
|
|
fileStore: NewFileStore(f),
|
|
}
|
|
err := s.Store(types.AuthConfig{
|
|
Username: "foo",
|
|
Password: "bar",
|
|
ServerAddress: invalidServerAddress,
|
|
})
|
|
assert.ErrorContains(t, err, "program failed")
|
|
assert.Check(t, is.Len(f.GetAuthConfigs(), 0))
|
|
}
|
|
|
|
func TestNativeStoreGet(t *testing.T) {
|
|
f := &fakeStore{configs: map[string]types.AuthConfig{
|
|
validServerAddress: {
|
|
Username: "foo@example.com",
|
|
},
|
|
}}
|
|
s := &nativeStore{
|
|
programFunc: mockCommandFn,
|
|
fileStore: NewFileStore(f),
|
|
}
|
|
actual, err := s.Get(validServerAddress)
|
|
assert.NilError(t, err)
|
|
|
|
expected := types.AuthConfig{
|
|
Username: "foo",
|
|
Password: "bar",
|
|
ServerAddress: validServerAddress,
|
|
}
|
|
assert.Check(t, is.DeepEqual(expected, actual))
|
|
}
|
|
|
|
func TestNativeStoreGetIdentityToken(t *testing.T) {
|
|
f := &fakeStore{configs: map[string]types.AuthConfig{
|
|
validServerAddress2: {},
|
|
}}
|
|
|
|
s := &nativeStore{
|
|
programFunc: mockCommandFn,
|
|
fileStore: NewFileStore(f),
|
|
}
|
|
actual, err := s.Get(validServerAddress2)
|
|
assert.NilError(t, err)
|
|
|
|
expected := types.AuthConfig{
|
|
IdentityToken: "abcd1234",
|
|
ServerAddress: validServerAddress2,
|
|
}
|
|
assert.Check(t, is.DeepEqual(expected, actual))
|
|
}
|
|
|
|
func TestNativeStoreGetAll(t *testing.T) {
|
|
f := &fakeStore{configs: map[string]types.AuthConfig{
|
|
validServerAddress: {},
|
|
}}
|
|
|
|
s := &nativeStore{
|
|
programFunc: mockCommandFn,
|
|
fileStore: NewFileStore(f),
|
|
}
|
|
as, err := s.GetAll()
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Len(as, 2))
|
|
expected := types.AuthConfig{
|
|
Username: "foo",
|
|
Password: "bar",
|
|
ServerAddress: "https://index.docker.io/v1",
|
|
IdentityToken: "",
|
|
}
|
|
actual, ok := as[validServerAddress]
|
|
assert.Check(t, ok)
|
|
assert.Check(t, is.DeepEqual(expected, actual))
|
|
}
|
|
|
|
func TestNativeStoreGetMissingCredentials(t *testing.T) {
|
|
f := &fakeStore{configs: map[string]types.AuthConfig{
|
|
validServerAddress: {},
|
|
}}
|
|
|
|
s := &nativeStore{
|
|
programFunc: mockCommandFn,
|
|
fileStore: NewFileStore(f),
|
|
}
|
|
_, err := s.Get(missingCredsAddress)
|
|
assert.NilError(t, err)
|
|
}
|
|
|
|
func TestNativeStoreGetInvalidAddress(t *testing.T) {
|
|
f := &fakeStore{configs: map[string]types.AuthConfig{
|
|
validServerAddress: {},
|
|
}}
|
|
|
|
s := &nativeStore{
|
|
programFunc: mockCommandFn,
|
|
fileStore: NewFileStore(f),
|
|
}
|
|
_, err := s.Get(invalidServerAddress)
|
|
assert.ErrorContains(t, err, "program failed")
|
|
}
|
|
|
|
func TestNativeStoreErase(t *testing.T) {
|
|
f := &fakeStore{configs: map[string]types.AuthConfig{
|
|
validServerAddress: {},
|
|
}}
|
|
|
|
s := &nativeStore{
|
|
programFunc: mockCommandFn,
|
|
fileStore: NewFileStore(f),
|
|
}
|
|
err := s.Erase(validServerAddress)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Len(f.GetAuthConfigs(), 0))
|
|
}
|
|
|
|
func TestNativeStoreEraseInvalidAddress(t *testing.T) {
|
|
f := &fakeStore{configs: map[string]types.AuthConfig{
|
|
validServerAddress: {},
|
|
}}
|
|
|
|
s := &nativeStore{
|
|
programFunc: mockCommandFn,
|
|
fileStore: NewFileStore(f),
|
|
}
|
|
err := s.Erase(invalidServerAddress)
|
|
assert.ErrorContains(t, err, "program failed")
|
|
}
|