diff --git a/pkg/auth/credential_test.go b/pkg/auth/credential_test.go new file mode 100644 index 000000000..2d6b9b5e0 --- /dev/null +++ b/pkg/auth/credential_test.go @@ -0,0 +1,82 @@ +package auth + +import ( + "fmt" + "testing" + + "github.com/notaryproject/notation/pkg/config" +) + +const ( + errMsg = "error message" + validStore = "pass" +) + +func TestLoadConfig_LoadNotationConfigFailed(t *testing.T) { + loadOrDefault = func() (*config.File, error) { + return nil, fmt.Errorf(errMsg) + } + _, err := LoadConfig() + if err == nil || err.Error() != errMsg { + t.Fatalf("Didn't get the expected error, but got: %v", err) + } +} + +func TestLoadConfig_NotationConfigContainsAuth(t *testing.T) { + loadOrDefault = func() (*config.File, error) { + return &config.File{ + CredentialsStore: validStore, + }, nil + } + file, err := LoadConfig() + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if file == nil || file.CredentialsStore != validStore { + t.Fatalf("Should contain auth") + } +} + +func TestLoadConfig_LoadDockerConfigFailed(t *testing.T) { + loadOrDefault = func() (*config.File, error) { + return nil, nil + } + loadDockerConfig = func() (*config.DockerConfigFile, error) { + return nil, fmt.Errorf(errMsg) + } + _, err := LoadConfig() + if err == nil || err.Error() != errMsg { + t.Fatalf("Didn't get the expected error, but got: %v", err) + } +} + +func TestLoadConfig_DockerConfigContainsAuth(t *testing.T) { + loadOrDefault = func() (*config.File, error) { + return nil, nil + } + loadDockerConfig = func() (*config.DockerConfigFile, error) { + return &config.DockerConfigFile{ + CredentialsStore: validStore, + }, nil + } + file, err := LoadConfig() + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if file == nil || file.CredentialsStore != validStore { + t.Fatalf("Should contain auth") + } +} + +func TestLoadConfig_DockerConfigEmptyAuth(t *testing.T) { + loadOrDefault = func() (*config.File, error) { + return nil, nil + } + loadDockerConfig = func() (*config.DockerConfigFile, error) { + return &config.DockerConfigFile{}, nil + } + _, err := LoadConfig() + if err == nil { + t.Fatalf("expect error but got nil") + } +} diff --git a/pkg/auth/native_store_test.go b/pkg/auth/native_store_test.go new file mode 100644 index 000000000..daeab16b3 --- /dev/null +++ b/pkg/auth/native_store_test.go @@ -0,0 +1,199 @@ +package auth + +import ( + "encoding/json" + "fmt" + "io" + "strings" + "testing" + + "github.com/docker/docker-credential-helpers/client" + "github.com/docker/docker-credential-helpers/credentials" + "github.com/notaryproject/notation/pkg/config" + "oras.land/oras-go/v2/registry/remote/auth" +) + +const ( + validServerAddress = "https://index.docker.io/v1" + validServerAddress2 = "https://example.com:5002" + invalidServerAddress = "https://foobar.example.com" + missingCredsAddress = "https://missing.docker.io/v1" + Username = "Username" + Secret = "Secret" + validUsername = "username" + validPassword = "password" + validIdentityToken = "identityToken" + validHelper = "helper" +) + +var ( + errCommandExited = fmt.Errorf("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": "username", "Secret": "password"}`), nil + case invalidServerAddress: + return []byte("program failed"), errCommandExited + case validServerAddress2: + return []byte(`{"Username": "", "Secret": "identityToken"}`), nil + } + 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, validServerAddress2: + return nil, nil + default: + return []byte("program failed"), errCommandExited + } + } + + 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 TestNativeStore_StoreGetErase(t *testing.T) { + creds := auth.Credential{ + Username: validUsername, + Password: validPassword, + } + s := &nativeAuthStore{ + programFunc: mockCommandFn, + } + + // store creds + err := s.Store(validServerAddress, creds) + if err != nil { + t.Fatalf("unexpected error: %v", err.Error()) + } + + // get creds + fetchedCreds, err := s.Get(validServerAddress) + if err != nil { + t.Fatalf("unexpected error: %v", err.Error()) + } + if fetchedCreds != creds { + t.Fatalf("expected %+v, got %+v", creds, fetchedCreds) + } + + // erase creds + err = s.Erase(validServerAddress) + if err != nil { + t.Fatalf("unexpected error: %v", err.Error()) + } + fetchedCreds, err = s.Get(validServerAddress) + if err != nil { + t.Fatalf("unexpected error: %v", err.Error()) + } + if fetchedCreds == auth.EmptyCredential { + t.Fatalf("expect empty conf, but got: %+v", fetchedCreds) + } +} + +func TestNativeStore_StoreIdentityToken(t *testing.T) { + creds := auth.Credential{ + RefreshToken: validIdentityToken, + } + s := &nativeAuthStore{ + programFunc: mockCommandFn, + } + + // store creds + err := s.Store(validServerAddress, creds) + if err != nil { + t.Fatalf("unexpected error: %v", err.Error()) + } + + // get creds + fetchedCreds, err := s.Get(validServerAddress2) + if err != nil { + t.Fatalf("unexpected error: %v", err.Error()) + } + if fetchedCreds.RefreshToken != creds.RefreshToken { + t.Fatalf("expected %+v, got %+v", creds, fetchedCreds) + } +} + +func TestNativeStore_FailedGet(t *testing.T) { + s := &nativeAuthStore{ + programFunc: mockCommandFn, + } + _, err := s.Get(invalidServerAddress) + if err == nil { + t.Fatalf("expect error, got nil") + } +} + +func TestNativeStore_GetCredentialsStore_LoadConfigFailed(t *testing.T) { + loadConfig = func() (*config.File, error) { + return nil, fmt.Errorf("loadConfig err") + } + _, err := GetCredentialsStore(validServerAddress) + if err == nil { + t.Fatalf("expect error, got nil") + } +} + +func TestNativeStore_GetCredentialsStore_NoHelperSet(t *testing.T) { + loadConfig = func() (*config.File, error) { + return &config.File{}, nil + } + _, err := GetCredentialsStore(validServerAddress) + if err == nil || err.Error() != "could not get the configured credentials store for registry: "+validServerAddress { + t.Fatalf("Didn't get the expected error, but got: %v", err) + } +} + +func TestNativeStore_GetCredentialsStore_HelperSet(t *testing.T) { + loadConfig = func() (*config.File, error) { + return &config.File{ + CredentialHelpers: map[string]string{ + validServerAddress: validHelper, + }, + }, nil + } + _, err := GetCredentialsStore(validServerAddress) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/pkg/config/docker_config_test.go b/pkg/config/docker_config_test.go new file mode 100644 index 000000000..825b8cf22 --- /dev/null +++ b/pkg/config/docker_config_test.go @@ -0,0 +1,79 @@ +package config + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +const ( + validJson = `{ + "credHelpers": { + "localhost:5000": "pass" + }, + "credsStore": "pass" + }` + invalidJson = `{` +) + +func TestLoadDockerConfig_noErrors(t *testing.T) { + // Create temp directory + dockerConfigDir := t.TempDir() + t.Setenv("DOCKER_CONFIG", dockerConfigDir) + + // Create config.json + f, err := os.Create(filepath.Join(dockerConfigDir, dockerConfigFileName)) + if err != nil { + t.Fatalf("Failed to mock docker config, err: %v", err) + } + defer f.Close() + data := []byte(validJson) + if _, err := f.Write(data); err != nil { + t.Fatalf("Failed to mock docker config, err: %v", err) + } + + // Load docker config + config, err := LoadDockerConfig() + if err != nil { + t.Fatalf("Unexpected error loading config.json: %v", err) + } + if config.CredentialsStore != "pass" { + t.Fatalf("Expected credentials store to be 'pass', but got %v", config.CredentialsStore) + } +} + +func TestLoadDockerConfig_noConfigFile(t *testing.T) { + // Create temp directory + dockerConfigDir := t.TempDir() + t.Setenv("DOCKER_CONFIG", dockerConfigDir) + + // Load docker config + _, err := LoadDockerConfig() + if err == nil { + t.Fatalf("Expected error not returned") + } +} + +func TestLoadDockerConfig_invalidConfigFile(t *testing.T) { + // Create temp directory + dockerConfigDir := t.TempDir() + t.Setenv("DOCKER_CONFIG", dockerConfigDir) + + // Create config.json + f, err := os.Create(filepath.Join(dockerConfigDir, dockerConfigFileName)) + if err != nil { + t.Fatalf("Failed to mock docker config, err: %v", err) + } + defer f.Close() + data := []byte(invalidJson) + if _, err := f.Write(data); err != nil { + t.Fatalf("Failed to mock docker config, err: %v", err) + } + + // Load docker config + _, err = LoadDockerConfig() + if err == nil || !strings.HasSuffix(err.Error(), "unexpected EOF") { + t.Fatalf("Expected error not returned") + } +}