Files
deploy/util/git/creds_test.go
41536738c8 refactor(cd): hard-fork - rename CRD API group argoproj.io -> deploy.hanzo.ai (#1)
* feat(api): hard-fork CRD API group argoproj.io -> deploy.hanzo.ai

Hanzo CD hard-fork, phase 1.1. Single source of truth is
pkg/apis/application/register.go's Group constant (Application/AppProject/
ApplicationSet all derive their FullName from it); every other Go call-site
that hardcoded the literal group string is updated to match: the 3 generated
informer GVRs, the app-of-apps nested-Application detector in types.go, the
resource-tree Application-kind cache populator, the 3 NotFound error
constructors in server/application, and the applicationset-controller's
leader-election Lease name/ID (kept in lockstep across Go source + RBAC
resourceNames, or leader election would get RBAC-denied on startup).

CRD YAMLs (application/appproject/applicationset) and every RBAC apiGroups
rule that actually governs our 3 CRDs are repointed to deploy.hanzo.ai, across
both manifests/base/* (namespaced Roles) and manifests/cluster-rbac/* (cluster
scope). Two adjacent things that share the "argoproj.io" string but are NOT
ours are deliberately left untouched: gitops-engine's health-check dispatch
for Argo Workflows' `Workflow` kind, and argocd-server-clusterrole's second
apiGroups block granting read on Argo Workflows' `workflows` resource — both
belong to a separate, unrelated Argoproj-family project we are not forking.

Also untouched, per explicit scope: annotation/label keys (argocd.argoproj.io/*)
and finalizers (*.argoproj.io) that track already-applied live resources, and
argocd-cm.yaml's resource.customizations.ignoreResourceUpdates keys (a narrow
app-of-apps diffing nicety, not build- or RBAC-relevant).

CGO_ENABLED=0 go build ./... verified green.

* ci: wire canonical hanzo.yml + cicd.yml (Hanzo CD image build)

Phase 1.3. Follows the same one-way pattern every hanzo/lux/zoo repo uses:
hanzo.yml declares the image (root Dockerfile -> ghcr.io/hanzoai/deploy) and a
fast go-build sanity test (the multi-stage Dockerfile build is the slow, real
gate); cicd.yml is the ~10-line trigger shim importing hanzoai/ci's reusable
build workflow on the arc pool, secrets from KMS only.

No `deploy:` block yet, matching the hanzoai/cms build-only-first-cutover
pattern: this repo's own live hanzo-cd namespace runs the whole Hanzo fleet's
GitOps, so the prod image/CRD swap is a deliberate separate action (see the
[HELD] universe PR), never an automatic rollout on merge. Every PR/push still
builds and pushes an immutable ghcr.io/hanzoai/deploy:sha-<7>-amd64 image.

* feat(ui): rebrand Argo CD -> Hanzo CD (title, favicon, logo, sidebar)

Phase 1.2. User-facing "Argo CD"/"ArgoCD" text replaced with "Hanzo CD"
across the login page, help modal, settings screens, version-info panel,
sync-options warning, fullscreen-logs title, app.tsx page titles, and
index.html's <title>/noscript text (dropped the now-wrong upstream CLI
doc link rather than invent a hanzo.ai URL that doesn't exist).

Logo/favicon assets replaced with the canonical Hanzo mark (never
hand-drawn, copied from ~/work/hanzo/logo/dist): the two real usages of
Argo's logo collapse onto one Hanzo glyph (mono + white variants) at the
same 5 call sites (login icon + footer, login.scss $logoPath/
$logoNegativePath, sidebar text-logo), preserving the existing CSS
invert() filters so both the light login panel and dark sidebar still
render correctly. Old argologo.svg/argo_o.svg/argo.png deleted (no
longer referenced). favicon-{16x16,32x32}.png added at
ui/src/assets/favicon/ (index.html's links to that path were previously
dead - the directory didn't exist in this checkout). package.json name
argo-cd-ui -> hanzo-cd-ui.

Left alone, deliberately: the two Kubernetes resource-kind icon sets
under ui/src/assets/images/resources/ (unrelated to branding), and
upstream doc/project links (argoproj.io, argo-cd.readthedocs.io) since
we have no equivalent hanzo.ai docs to point to yet - except the login
footer logo's outbound link, which now points to hanzo.ai instead of
argoproj.io (a Hanzo-branded mark linking to the upstream project it
was forked from read as an obvious leftover).

* feat(module): rename Go module github.com/argoproj/argo-cd/v3 -> github.com/hanzoai/deploy

Phase 2. No version suffix on the new path (org policy: never publish our
own Go modules above v1.x.x / with a v2+ suffix) — this fork starts a fresh
v0/v1 module identity rather than carrying the v3 the upstream fork point
had reached.

Mechanical, symmetric rewrite: 704 .go files (import statements only; two
disjoint substitutions applied specific-first so the nested gitops-engine
module path never double-matches), plus go.mod (module line, the
gitops-engine require+replace pair, version reset to v0.0.0-... since a
suffix-less path can't carry a v3 semver), gitops-engine/go.mod, Makefile
(ldflags PACKAGE + 2 TEST_MODULE filters), .golangci.yaml, .goreleaser.yaml,
.mockery.yaml (+ gitops-engine's), Procfile, the codegen/proto generation
scripts (hack/generate-proto.sh, update-codegen.sh, update-openapi.sh), and
20 .proto files' go_package options. Also one OpenTelemetry tracer name
string in gitops-engine/pkg/sync/sync_context.go (a cosmestic label, not an
import, but worth keeping consistent).

CGO_ENABLED=0 go build ./... verified green for both the root module and
the nested gitops-engine module; go vet clean on both; go mod tidy run
(network available) to reconcile go.sum, dependency set unchanged.

Left alone, deliberately: doc-only mentions (docs/snyk/*.html scan reports,
gitops-engine/README.md), a comment-only reference in
.github/workflows/release.yaml, and one Renovate bot config line — none are
build- or codegen-load-bearing. Also left alone: comments citing upstream
GitHub issue URLs (github.com/argoproj/argo-cd/issues/NNNNN) and a handful
of test fixtures using "https://github.com/argoproj/argo-cd.git" as a
representative example repo URL — both are accurate historical/test-data
references, not stale self-references.

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-29 18:27:58 -07:00

1114 lines
38 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package git
import (
_ "embed"
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"os"
"path"
"regexp"
"slices"
"strings"
"testing"
"time"
"github.com/google/uuid"
gocache "github.com/patrickmn/go-cache"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
argoio "github.com/hanzoai/deploy/gitops-engine/pkg/utils/io"
"github.com/hanzoai/deploy/util/cert"
utilio "github.com/hanzoai/deploy/util/io"
"github.com/hanzoai/deploy/util/workloadidentity"
"github.com/hanzoai/deploy/util/workloadidentity/mocks"
)
type cred struct {
username string
password string
}
type memoryCredsStore struct {
creds map[string]cred
}
func (s *memoryCredsStore) Add(username string, password string) string {
id := uuid.New().String()
s.creds[id] = cred{
username: username,
password: password,
}
return id
}
func (s *memoryCredsStore) Remove(id string) {
delete(s.creds, id)
}
func (s *memoryCredsStore) Environ(_ string) []string {
return nil
}
func TestHTTPSCreds_Environ_no_cert_cleanup(t *testing.T) {
store := &memoryCredsStore{creds: make(map[string]cred)}
creds := NewHTTPSCreds("", "", "", "", "", true, store, false)
closer, _, err := creds.Environ()
require.NoError(t, err)
credsLenBefore := len(store.creds)
utilio.Close(closer)
assert.Len(t, store.creds, credsLenBefore-1)
}
func TestHTTPSCreds_Environ_insecure_true(t *testing.T) {
creds := NewHTTPSCreds("", "", "", "", "", true, &NoopCredsStore{}, false)
closer, env, err := creds.Environ()
t.Cleanup(func() {
utilio.Close(closer)
})
require.NoError(t, err)
found := slices.Contains(env, "GIT_SSL_NO_VERIFY=true")
assert.True(t, found)
}
func TestHTTPSCreds_Environ_insecure_false(t *testing.T) {
creds := NewHTTPSCreds("", "", "", "", "", false, &NoopCredsStore{}, false)
closer, env, err := creds.Environ()
t.Cleanup(func() {
utilio.Close(closer)
})
require.NoError(t, err)
found := slices.Contains(env, "GIT_SSL_NO_VERIFY=true")
assert.False(t, found)
}
func TestHTTPSCreds_Environ_forceBasicAuth(t *testing.T) {
t.Run("Enabled and credentials set", func(t *testing.T) {
store := &memoryCredsStore{creds: make(map[string]cred)}
creds := NewHTTPSCreds("username", "password", "", "", "", false, store, true)
closer, env, err := creds.Environ()
require.NoError(t, err)
defer closer.Close()
var header string
for _, envVar := range env {
if strings.HasPrefix(envVar, forceBasicAuthHeaderEnv+"=") {
header = envVar[len(forceBasicAuthHeaderEnv)+1:]
}
if header != "" {
break
}
}
b64enc := base64.StdEncoding.EncodeToString([]byte("username:password"))
assert.Equal(t, "Authorization: Basic "+b64enc, header)
})
t.Run("Enabled but credentials not set", func(t *testing.T) {
store := &memoryCredsStore{creds: make(map[string]cred)}
creds := NewHTTPSCreds("", "", "", "", "", false, store, true)
closer, env, err := creds.Environ()
require.NoError(t, err)
defer closer.Close()
var header string
for _, envVar := range env {
if strings.HasPrefix(envVar, forceBasicAuthHeaderEnv+"=") {
header = envVar[len(forceBasicAuthHeaderEnv)+1:]
}
if header != "" {
break
}
}
assert.Empty(t, header)
})
t.Run("Disabled with credentials set", func(t *testing.T) {
store := &memoryCredsStore{creds: make(map[string]cred)}
creds := NewHTTPSCreds("username", "password", "", "", "", false, store, false)
closer, env, err := creds.Environ()
require.NoError(t, err)
defer closer.Close()
var header string
for _, envVar := range env {
if strings.HasPrefix(envVar, forceBasicAuthHeaderEnv+"=") {
header = envVar[len(forceBasicAuthHeaderEnv)+1:]
}
if header != "" {
break
}
}
assert.Empty(t, header)
})
t.Run("Disabled with credentials not set", func(t *testing.T) {
store := &memoryCredsStore{creds: make(map[string]cred)}
creds := NewHTTPSCreds("", "", "", "", "", false, store, false)
closer, env, err := creds.Environ()
require.NoError(t, err)
defer closer.Close()
var header string
for _, envVar := range env {
if strings.HasPrefix(envVar, forceBasicAuthHeaderEnv+"=") {
header = envVar[len(forceBasicAuthHeaderEnv)+1:]
}
if header != "" {
break
}
}
assert.Empty(t, header)
})
}
func TestHTTPSCreds_Environ_bearerTokenAuth(t *testing.T) {
t.Run("Enabled and credentials set", func(t *testing.T) {
store := &memoryCredsStore{creds: make(map[string]cred)}
creds := NewHTTPSCreds("", "", "token", "", "", false, store, false)
closer, env, err := creds.Environ()
require.NoError(t, err)
defer closer.Close()
var header string
for _, envVar := range env {
if strings.HasPrefix(envVar, bearerAuthHeaderEnv+"=") {
header = envVar[len(bearerAuthHeaderEnv)+1:]
}
if header != "" {
break
}
}
assert.Equal(t, "Authorization: Bearer token", header)
})
}
func TestHTTPSCreds_Environ_clientCert(t *testing.T) {
store := &memoryCredsStore{creds: make(map[string]cred)}
creds := NewHTTPSCreds("", "", "", "clientCertData", "clientCertKey", false, store, false)
closer, env, err := creds.Environ()
require.NoError(t, err)
var cert, key string
for _, envVar := range env {
if strings.HasPrefix(envVar, "GIT_SSL_CERT=") {
cert = envVar[13:]
} else if strings.HasPrefix(envVar, "GIT_SSL_KEY=") {
key = envVar[12:]
}
if cert != "" && key != "" {
break
}
}
assert.NotEmpty(t, cert)
assert.NotEmpty(t, key)
certBytes, err := os.ReadFile(cert)
require.NoError(t, err)
assert.Equal(t, "clientCertData", string(certBytes))
keyBytes, err := os.ReadFile(key)
assert.Equal(t, "clientCertKey", string(keyBytes))
require.NoError(t, err)
utilio.Close(closer)
_, err = os.Stat(cert)
require.ErrorIs(t, err, os.ErrNotExist)
_, err = os.Stat(key)
require.ErrorIs(t, err, os.ErrNotExist)
}
func Test_SSHCreds_Environ(t *testing.T) {
for _, insecureIgnoreHostKey := range []bool{false, true} {
tempDir := t.TempDir()
caFile := path.Join(tempDir, "caFile")
err := os.WriteFile(caFile, []byte(""), os.FileMode(0o600))
require.NoError(t, err)
creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, "")
closer, env, err := creds.Environ()
require.NoError(t, err)
require.Len(t, env, 2)
assert.Equal(t, fmt.Sprintf("GIT_SSL_CAINFO=%s/caFile", tempDir), env[0], "CAINFO env var must be set")
assert.True(t, strings.HasPrefix(env[1], "GIT_SSH_COMMAND="))
if insecureIgnoreHostKey {
assert.Contains(t, env[1], "-o StrictHostKeyChecking=no")
assert.Contains(t, env[1], "-o UserKnownHostsFile=/dev/null")
} else {
assert.Contains(t, env[1], "-o StrictHostKeyChecking=yes")
hostsPath := cert.GetSSHKnownHostsDataPath()
assert.Contains(t, env[1], "-o UserKnownHostsFile="+hostsPath)
}
envRegex := regexp.MustCompile("-i ([^ ]+)")
assert.Regexp(t, envRegex, env[1])
privateKeyFile := envRegex.FindStringSubmatch(env[1])[1]
assert.FileExists(t, privateKeyFile)
utilio.Close(closer)
assert.NoFileExists(t, privateKeyFile)
}
}
func Test_SSHCreds_Environ_WithProxy(t *testing.T) {
for _, insecureIgnoreHostKey := range []bool{false, true} {
tempDir := t.TempDir()
caFile := path.Join(tempDir, "caFile")
err := os.WriteFile(caFile, []byte(""), os.FileMode(0o600))
require.NoError(t, err)
creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, "socks5://127.0.0.1:1080")
closer, env, err := creds.Environ()
require.NoError(t, err)
require.Len(t, env, 2)
assert.Equal(t, fmt.Sprintf("GIT_SSL_CAINFO=%s/caFile", tempDir), env[0], "CAINFO env var must be set")
assert.True(t, strings.HasPrefix(env[1], "GIT_SSH_COMMAND="))
if insecureIgnoreHostKey {
assert.Contains(t, env[1], "-o StrictHostKeyChecking=no")
assert.Contains(t, env[1], "-o UserKnownHostsFile=/dev/null")
} else {
assert.Contains(t, env[1], "-o StrictHostKeyChecking=yes")
hostsPath := cert.GetSSHKnownHostsDataPath()
assert.Contains(t, env[1], "-o UserKnownHostsFile="+hostsPath)
}
assert.Contains(t, env[1], "-o ProxyCommand='connect-proxy -S 127.0.0.1:1080 -5 %h %p'")
envRegex := regexp.MustCompile("-i ([^ ]+)")
assert.Regexp(t, envRegex, env[1])
privateKeyFile := envRegex.FindStringSubmatch(env[1])[1]
assert.FileExists(t, privateKeyFile)
utilio.Close(closer)
assert.NoFileExists(t, privateKeyFile)
}
}
func Test_SSHCreds_Environ_WithProxyUserNamePassword(t *testing.T) {
for _, insecureIgnoreHostKey := range []bool{false, true} {
tempDir := t.TempDir()
caFile := path.Join(tempDir, "caFile")
err := os.WriteFile(caFile, []byte(""), os.FileMode(0o600))
require.NoError(t, err)
creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, "socks5://user:[email protected]:1080")
closer, env, err := creds.Environ()
require.NoError(t, err)
require.Len(t, env, 4)
assert.Equal(t, fmt.Sprintf("GIT_SSL_CAINFO=%s/caFile", tempDir), env[0], "CAINFO env var must be set")
assert.True(t, strings.HasPrefix(env[1], "GIT_SSH_COMMAND="))
assert.Equal(t, "SOCKS5_USER=user", env[2], "SOCKS5 user env var must be set")
assert.Equal(t, "SOCKS5_PASSWD=password", env[3], "SOCKS5 password env var must be set")
if insecureIgnoreHostKey {
assert.Contains(t, env[1], "-o StrictHostKeyChecking=no")
assert.Contains(t, env[1], "-o UserKnownHostsFile=/dev/null")
} else {
assert.Contains(t, env[1], "-o StrictHostKeyChecking=yes")
hostsPath := cert.GetSSHKnownHostsDataPath()
assert.Contains(t, env[1], "-o UserKnownHostsFile="+hostsPath)
}
assert.Contains(t, env[1], "-o ProxyCommand='connect-proxy -S 127.0.0.1:1080 -5 %h %p'")
envRegex := regexp.MustCompile("-i ([^ ]+)")
assert.Regexp(t, envRegex, env[1])
privateKeyFile := envRegex.FindStringSubmatch(env[1])[1]
assert.FileExists(t, privateKeyFile)
utilio.Close(closer)
assert.NoFileExists(t, privateKeyFile)
}
}
func Test_SSHCreds_Environ_TempFileCleanupOnInvalidProxyURL(t *testing.T) {
// Previously, if the proxy URL was invalid, a temporary file would be left in /dev/shm. This ensures the file is cleaned up in this case.
// argoio.TempDir will be /dev/shm or "" (on an OS without /dev/shm).
// In this case os.CreateTemp(), which is used by creds.Environ(),
// will use os.TempDir for the temporary directory.
// Reproducing this logic here:
argoioTempDir := argoio.TempDir
if argoioTempDir == "" {
argoioTempDir = os.TempDir()
}
// countDev returns the number of files in the temporary directory
countFilesInDevShm := func() int {
entries, err := os.ReadDir(argoioTempDir)
require.NoError(t, err)
return len(entries)
}
for _, insecureIgnoreHostKey := range []bool{false, true} {
tempDir := t.TempDir()
caFile := path.Join(tempDir, "caFile")
err := os.WriteFile(caFile, []byte(""), os.FileMode(0o600))
require.NoError(t, err)
creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, ":invalid-proxy-url")
filesInDevShmBeforeInvocation := countFilesInDevShm()
_, _, err = creds.Environ()
require.Error(t, err)
filesInDevShmAfterInvocation := countFilesInDevShm()
assert.Equal(t, filesInDevShmBeforeInvocation, filesInDevShmAfterInvocation, "no temporary files should leak if the proxy url cannot be parsed")
}
}
const gcpServiceAccountKeyJSON = `{
"type": "service_account",
"project_id": "my-google-project",
"private_key_id": "REDACTED",
"private_key": "-----BEGIN PRIVATE KEY-----\nREDACTED\n-----END PRIVATE KEY-----\n",
"client_email": "[email protected]",
"client_id": "REDACTED",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/argocd-service-account%40my-google-project.iam.gserviceaccount.com"
}`
const invalidJSON = `{
"type": "service_account",
"project_id": "my-google-project",
`
func TestNewGoogleCloudCreds(t *testing.T) {
store := &memoryCredsStore{creds: make(map[string]cred)}
googleCloudCreds := NewGoogleCloudCreds(gcpServiceAccountKeyJSON, store)
assert.NotNil(t, googleCloudCreds)
}
func TestNewGoogleCloudCreds_invalidJSON(t *testing.T) {
store := &memoryCredsStore{creds: make(map[string]cred)}
googleCloudCreds := NewGoogleCloudCreds(invalidJSON, store)
assert.Nil(t, googleCloudCreds.creds)
token, err := googleCloudCreds.getAccessToken()
assert.Empty(t, token)
require.Error(t, err)
username, err := googleCloudCreds.getUsername()
assert.Empty(t, username)
require.Error(t, err)
closer, envStringSlice, err := googleCloudCreds.Environ()
assert.Equal(t, NopCloser{}, closer)
assert.Equal(t, []string(nil), envStringSlice)
require.Error(t, err)
}
func TestGoogleCloudCreds_Environ_cleanup(t *testing.T) {
store := &memoryCredsStore{creds: make(map[string]cred)}
staticToken := &oauth2.Token{AccessToken: "token"}
googleCloudCreds := GoogleCloudCreds{&google.Credentials{
ProjectID: "my-google-project",
TokenSource: oauth2.StaticTokenSource(staticToken),
JSON: []byte(gcpServiceAccountKeyJSON),
}, store}
closer, _, err := googleCloudCreds.Environ()
require.NoError(t, err)
credsLenBefore := len(store.creds)
utilio.Close(closer)
assert.Len(t, store.creds, credsLenBefore-1)
}
func TestAzureWorkloadIdentityCreds_Environ(t *testing.T) {
resetAzureTokenCache()
store := &memoryCredsStore{creds: make(map[string]cred)}
workloadIdentityMock := &mocks.TokenProvider{}
workloadIdentityMock.EXPECT().GetToken(azureDevopsEntraResourceId).Return(&workloadidentity.Token{AccessToken: "accessToken", ExpiresOn: time.Now().Add(time.Minute)}, nil).Maybe()
creds := AzureWorkloadIdentityCreds{store, workloadIdentityMock}
_, env, err := creds.Environ()
require.NoError(t, err)
assert.Len(t, store.creds, 1)
for _, value := range store.creds {
assert.Empty(t, value.username)
assert.Equal(t, "accessToken", value.password)
}
require.Len(t, env, 1)
assert.Equal(t, "ARGOCD_GIT_BEARER_AUTH_HEADER=Authorization: Bearer accessToken", env[0], "ARGOCD_GIT_BEARER_AUTH_HEADER env var must be set")
}
func TestAzureWorkloadIdentityCreds_Environ_cleanup(t *testing.T) {
resetAzureTokenCache()
store := &memoryCredsStore{creds: make(map[string]cred)}
workloadIdentityMock := &mocks.TokenProvider{}
workloadIdentityMock.EXPECT().GetToken(azureDevopsEntraResourceId).Return(&workloadidentity.Token{AccessToken: "accessToken", ExpiresOn: time.Now().Add(time.Minute)}, nil).Maybe()
creds := AzureWorkloadIdentityCreds{store, workloadIdentityMock}
closer, _, err := creds.Environ()
require.NoError(t, err)
credsLenBefore := len(store.creds)
utilio.Close(closer)
assert.Len(t, store.creds, credsLenBefore-1)
}
func TestAzureWorkloadIdentityCreds_GetUserInfo(t *testing.T) {
resetAzureTokenCache()
store := &memoryCredsStore{creds: make(map[string]cred)}
workloadIdentityMock := &mocks.TokenProvider{}
workloadIdentityMock.EXPECT().GetToken(azureDevopsEntraResourceId).Return(&workloadidentity.Token{AccessToken: "accessToken", ExpiresOn: time.Now().Add(time.Minute)}, nil).Maybe()
creds := AzureWorkloadIdentityCreds{store, workloadIdentityMock}
user, email, err := creds.GetUserInfo(t.Context())
require.NoError(t, err)
assert.Equal(t, workloadidentity.EmptyGuid, user)
assert.Empty(t, email)
}
func TestGetHelmCredsShouldReturnHelmCredsIfAzureWorkloadIdentityNotSpecified(t *testing.T) {
var creds Creds = NewAzureWorkloadIdentityCreds(NoopCredsStore{}, new(mocks.TokenProvider))
_, ok := creds.(AzureWorkloadIdentityCreds)
require.Truef(t, ok, "expected HelmCreds but got %T", creds)
}
func TestAzureWorkloadIdentityCreds_FetchNewTokenIfExistingIsExpired(t *testing.T) {
resetAzureTokenCache()
store := &memoryCredsStore{creds: make(map[string]cred)}
workloadIdentityMock := &mocks.TokenProvider{}
workloadIdentityMock.EXPECT().GetToken(azureDevopsEntraResourceId).
Return(&workloadidentity.Token{AccessToken: "firstToken", ExpiresOn: time.Now().Add(time.Minute)}, nil).Once()
workloadIdentityMock.EXPECT().GetToken(azureDevopsEntraResourceId).
Return(&workloadidentity.Token{AccessToken: "secondToken"}, nil).Once()
creds := AzureWorkloadIdentityCreds{store, workloadIdentityMock}
token, err := creds.GetAzureDevOpsAccessToken()
require.NoError(t, err)
assert.Equal(t, "firstToken", token)
time.Sleep(5 * time.Second)
token, err = creds.GetAzureDevOpsAccessToken()
require.NoError(t, err)
assert.Equal(t, "secondToken", token)
}
func TestAzureWorkloadIdentityCreds_ReuseTokenIfExistingIsNotExpired(t *testing.T) {
resetAzureTokenCache()
store := &memoryCredsStore{creds: make(map[string]cred)}
workloadIdentityMock := &mocks.TokenProvider{}
firstToken := &workloadidentity.Token{AccessToken: "firstToken", ExpiresOn: time.Now().Add(6 * time.Minute)}
secondToken := &workloadidentity.Token{AccessToken: "secondToken"}
workloadIdentityMock.EXPECT().GetToken(azureDevopsEntraResourceId).Return(firstToken, nil).Once()
workloadIdentityMock.EXPECT().GetToken(azureDevopsEntraResourceId).Return(secondToken, nil).Once()
creds := AzureWorkloadIdentityCreds{store, workloadIdentityMock}
token, err := creds.GetAzureDevOpsAccessToken()
require.NoError(t, err)
assert.Equal(t, "firstToken", token)
time.Sleep(5 * time.Second)
token, err = creds.GetAzureDevOpsAccessToken()
require.NoError(t, err)
assert.Equal(t, "firstToken", token)
}
func resetAzureTokenCache() {
azureTokenCache = gocache.New(gocache.NoExpiration, 0)
}
const fakeGitHubAppPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA2KHkp2fe0ReHqAt9BimWEec2ryWZIyg9jvB3BdP3mzFf0bOt
WlHm1FAETFxH4h5jYASUaaWEwRNNyGlT1GhTp+jOMC4xhOSb5/SnI2dt2EITkudQ
FKsSFUdJAndqOzkjrP2pL4fi4b7JWhuLDO36ufAP4l2m3tnAseGSSTIccWvzLFFU
s3wsHOHxOcJGCP1Z7rizxl6mTKYL/Z+GHqN17OJslDf901uPXsUeDCYL2iigGPhD
Ao6k8POsfbpqLG7poCDTK50FLnS5qEocjxt+J4ZjBEWTU/DOFXWYstzfbhm8OZPQ
pSSEiBCxpg+zjtkQfCyZxXB5RQ84CY78fXOI9QIDAQABAoIBAG8jL0FLIp62qZvm
uO9ualUo/37/lP7aaCpq50UQJ9lwjS3yNh8+IWQO4QWj2iUBXg4mi1Vf2ymKk78b
eixgkXp1D0Lcj/8ToYBwnUami04FKDGXhhf0Y8SS27vuM4vKlqjrQd7modkangYi
V0X82UKHDD8fuLpfkGIxzXDLypfMzjMuVpSntnWaf2YX3VR/0/66yEp9GejftF2k
wqhGoWM6r68pN5XuCqWd5PRluSoDy/o4BAFMhYCSfp9PjgZE8aoeWHgYzlZ3gUyn
r+HaDDNWbibhobXk/9h8lwAJ6KCZ5RZ+HFfh0HuwIxmocT9OCFgy/S0g1p+o3m9K
VNd5AMkCgYEA5fbS5UK7FBzuLoLgr1hktmbLJhpt8y8IPHNABHcUdE+O4/1xTQNf
pMUwkKjGG1MtrGjLOIoMGURKKn8lR1GMZueOTSKY0+mAWUGvSzl6vwtJwvJruT8M
otEO03o0tPnRKGxbFjqxkp2b6iqJ8MxCRZ3lSidc4mdi7PHzv9lwgvsCgYEA8Siq
7weCri9N6y+tIdORAXgRzcW54BmJyqB147c72RvbMacb6rN28KXpM3qnRXyp3Llb
yh81TW3FH10GqrjATws7BK8lP9kkAw0Z/7kNiS1NgH3pUbO+5H2kAa/6QW35nzRe
Jw2lyfYGWqYO4hYXH14ML1kjgS1hgd3XHOQ64M8CgYAKcjDYSzS2UC4dnMJaFLjW
dErsGy09a7iDDnUs/r/GHMsP3jZkWi/hCzgOiiwdl6SufUAl/FdaWnjH/2iRGco3
7nLPXC/3CFdVNp+g2iaSQRADtAFis9N+HeL/hkCYq/RtUqa8lsP0NgacF3yWnKCy
Ct8chDc67ZlXzBHXeCgdOwKBgHHGFPbWXUHeUW1+vbiyvrupsQSanznp8oclMtkv
Dk48hSokw9fzuU6Jh77gw9/Vk7HtxS9Tj+squZA1bDrJFPl1u+9WzkUUJZhG6xgp
bwhj1iejv5rrKUlVOTYOlwudXeJNa4oTNz9UEeVcaLMjZt9GmIsSC90a0uDZD26z
AlAjAoGAEoqm2DcNN7SrH6aVFzj1EVOrNsHYiXj/yefspeiEmf27PSAslP+uF820
SDpz4h+Bov5qTKkzcxuu1QWtA4M0K8Iy6IYLwb83DZEm1OsAf4i0pODz21PY/I+O
VHzjB10oYgaInHZgMUdyb6F571UdiYSB6a/IlZ3ngj5touy3VIM=
-----END RSA PRIVATE KEY-----`
func TestDiscoverGitHubAppInstallationID(t *testing.T) {
t.Run("returns cached installation ID", func(t *testing.T) {
// Setup: prepopulate cache
org := "test-org"
appId := int64(12345)
domain := "github.com"
expectedId := int64(98765)
// Clean up at both start and end to ensure test isolation
cacheKey := fmt.Sprintf("%s:%s:%d", strings.ToLower(org), domain, appId)
githubInstallationIdCache.Set(cacheKey, expectedId, gocache.NoExpiration)
// Ensure cleanup even if test fails
t.Cleanup(func() {
githubInstallationIdCache.Delete(cacheKey)
})
// Execute
ctx := t.Context()
actualId, err := DiscoverGitHubAppInstallationID(ctx, appId, "fake-key", "", org)
// Assert
require.NoError(t, err)
assert.Equal(t, expectedId, actualId)
})
t.Run("discovers installation ID from GitHub API", func(t *testing.T) {
// Setup: mock GitHub API server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// GitHub Enterprise expects paths like /api/v3/app/installations
// go-github's WithEnterpriseURLs adds this prefix automatically
if strings.HasSuffix(r.URL.Path, "/app/installations") {
w.WriteHeader(http.StatusOK)
//nolint:errcheck
json.NewEncoder(w).Encode([]map[string]any{
{"id": 98765, "account": map[string]any{"login": "test-org"}},
})
return
}
// Return 404 for any other path
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
// Clean up cache entry for this test on completion
t.Cleanup(func() {
// Extract domain from server URL for proper cache key
domain, _ := domainFromBaseURL(server.URL)
cacheKey := fmt.Sprintf("%s:%s:%d", strings.ToLower("test-org"), domain, 12345)
githubInstallationIdCache.Delete(cacheKey)
})
// Execute & Assert
ctx := t.Context()
// Pass the mock server URL as the enterpriseBaseURL so the GitHub client uses it
// Note: The mock server will have a different domain (e.g., 127.0.0.1) than the first test (github.com),
// so there's no cache collision between the two subtests.
actualId, err := DiscoverGitHubAppInstallationID(ctx, 12345, fakeGitHubAppPrivateKey, server.URL, "test-org")
require.NoError(t, err)
assert.Equal(t, int64(98765), actualId)
})
t.Run("returns correct installation ID when app is installed on multiple orgs", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/app/installations") {
w.WriteHeader(http.StatusOK)
//nolint:errcheck
json.NewEncoder(w).Encode([]map[string]any{
{"id": 11111, "account": map[string]any{"login": "org-alpha"}},
{"id": 22222, "account": map[string]any{"login": "target-org"}},
{"id": 33333, "account": map[string]any{"login": "org-gamma"}},
})
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
t.Cleanup(func() {
domain, _ := domainFromBaseURL(server.URL)
for _, org := range []string{"org-alpha", "target-org", "org-gamma"} {
githubInstallationIdCache.Delete(fmt.Sprintf("%s:%s:%d", org, domain, 12345))
}
})
ctx := t.Context()
actualId, err := DiscoverGitHubAppInstallationID(ctx, 12345, fakeGitHubAppPrivateKey, server.URL, "target-org")
require.NoError(t, err)
assert.Equal(t, int64(22222), actualId, "should return the installation ID for the requested org, not the last one in the list")
})
}
func TestExtractOrgFromRepoURL(t *testing.T) {
tests := []struct {
name string
repoURL string
expected string
expectError bool
}{
{"HTTPS URL", "https://github.com/argoproj/argo-cd", "argoproj", false},
{"HTTPS URL with .git", "https://github.com/argoproj/argo-cd.git", "argoproj", false},
{"HTTPS URL with port", "https://github.com:443/argoproj/argo-cd.git", "argoproj", false},
{"SSH URL", "[email protected]:argoproj/argo-cd.git", "argoproj", false},
{"SSH URL without .git", "[email protected]:argoproj/argo-cd", "argoproj", false},
{"SSH URL with ssh:// prefix", "ssh://[email protected]:argoproj/argo-cd.git", "argoproj", false},
{"SSH URL with port", "ssh://[email protected]:22/argoproj/argo-cd.git", "argoproj", false},
{"GitHub Enterprise HTTPS", "https://github.example.com/myorg/myrepo.git", "myorg", false},
{"GitHub Enterprise SSH", "[email protected]:myorg/myrepo.git", "myorg", false},
{"Case insensitive", "https://github.com/ArgoPROJ/argo-cd", "argoproj", false}, // Test case sensitivity
{"Invalid URL", "not-a-url", "", true},
{"Empty string", "", "", true},
{"URL without org/repo", "https://github.com", "", true},
{"URL with only org", "https://github.com/argoproj", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := ExtractOrgFromRepoURL(tt.repoURL)
if tt.expectError {
require.Error(t, err)
assert.Empty(t, actual)
} else {
require.NoError(t, err)
assert.Equal(t, tt.expected, actual)
}
})
}
}
// ---------------------------------------------------------------------------
// GitHub App based authentication
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// 1. TLS Errors
// ---------------------------------------------------------------------------
func TestGitHubAppGetAccessToken_TLSErrors(t *testing.T) {
t.Run("self-signed certificate rejected when insecure=false", func(t *testing.T) {
t.Cleanup(func() {
githubAppTokenCache.Flush()
})
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, githubTokenResponse("ghs_tls"))
}))
defer server.Close()
creds := newTestGitHubAppCreds(12345, 67890, fakeGitHubAppPrivateKey, server.URL,
func(c *GitHubAppCreds) { c.insecure = false })
token, err := creds.getAccessToken()
require.Error(t, err)
require.Empty(t, token)
errLower := strings.ToLower(err.Error())
require.True(t,
strings.Contains(errLower, "failed to verify certificate") &&
strings.Contains(errLower, "certificate signed by unknown authority") &&
strings.Contains(errLower, "x509"),
"error should indicate TLS/certificate verification failure, got: %s", err.Error())
})
t.Run("TLS handshake timeout - server never completes handshake", func(t *testing.T) {
if testing.Short() {
t.Skip("skipping slow TLS handshake timeout test (~15s)")
}
t.Cleanup(func() {
githubAppTokenCache.Flush()
})
// Create a TCP listener that accepts but never sends TLS handshake data.
lcfg := &net.ListenConfig{}
ln, err := lcfg.Listen(t.Context(), "tcp", "127.0.0.1:0")
require.NoError(t, err)
defer ln.Close()
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
// Hold the connection open; never respond with TLS ServerHello.
go func(c net.Conn) {
defer c.Close()
buf := make([]byte, 1024)
for {
if _, err := c.Read(buf); err != nil {
return
}
}
}(conn)
}
}()
creds := newTestGitHubAppCreds(12345, 67890, fakeGitHubAppPrivateKey,
"https://"+ln.Addr().String(),
func(c *GitHubAppCreds) { c.insecure = true })
start := time.Now()
token, tokenErr := creds.getAccessToken()
elapsed := time.Since(start)
require.Error(t, tokenErr)
require.Empty(t, token)
errLower := strings.ToLower(tokenErr.Error())
require.True(t,
strings.Contains(errLower, "timeout") ||
strings.Contains(errLower, "tls handshake"),
"error should indicate timeout during TLS handshake, got: %s", tokenErr.Error())
require.Less(t, elapsed, 20*time.Second,
"should not hang indefinitely")
})
}
// ---------------------------------------------------------------------------
// 2. Invalid Private key
// ---------------------------------------------------------------------------
func TestGitHubAppGetAccessToken_InvalidPrivateKey(t *testing.T) {
t.Parallel()
tests := []struct {
name string
privateKey string
wantErrContains []string
}{
{
name: "empty private key",
privateKey: "",
wantErrContains: []string{"private key"},
},
{
name: "random garbage as private key",
privateKey: "this-is-not-a-valid-key",
wantErrContains: []string{"private key"},
},
{
name: "truncated PEM block",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIE\n-----END RSA PRIVATE KEY-----",
wantErrContains: []string{"private key"},
},
{
name: "PEM with wrong type marker",
privateKey: "-----BEGIN CERTIFICATE-----\nMIIE\n-----END CERTIFICATE-----",
wantErrContains: []string{"private key"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, githubTokenResponse("ghs_unused"))
}))
defer server.Close()
creds := newTestGitHubAppCreds(12345, 67890, tt.privateKey, server.URL)
token, err := creds.getAccessToken()
require.Error(t, err, "should fail with invalid private key")
require.Empty(t, token)
for _, want := range tt.wantErrContains {
require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(want),
"error should contain %q for diagnostics", want)
}
})
}
}
// ---------------------------------------------------------------------------
// 3. TCP Connection Errors
// ---------------------------------------------------------------------------
func TestGitHubAppGetAccessToken_TCPConnectionErrors(t *testing.T) {
t.Parallel()
t.Cleanup(func() {
githubAppTokenCache.Flush()
})
t.Run("connection refused - no server listening", func(t *testing.T) {
t.Parallel()
// Grab a port then close it so nothing is listening.
lcfg := &net.ListenConfig{}
ln, err := lcfg.Listen(t.Context(), "tcp", "127.0.0.1:0")
require.NoError(t, err)
addr := ln.Addr().String()
ln.Close()
creds := newTestGitHubAppCreds(12345, 67890, fakeGitHubAppPrivateKey, "http://"+addr)
token, tokenErr := creds.getAccessToken()
require.Error(t, tokenErr)
require.Empty(t, token)
require.Contains(t, strings.ToLower(tokenErr.Error()), "connection refused",
"error should clearly indicate the connection was refused")
})
t.Run("connection reset by peer", func(t *testing.T) {
t.Parallel()
// Accept then immediately RST-close the connection.
lcfg := &net.ListenConfig{}
ln, err := lcfg.Listen(t.Context(), "tcp", "127.0.0.1:0")
require.NoError(t, err)
defer ln.Close()
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
if tcp, ok := conn.(*net.TCPConn); ok {
err := tcp.SetLinger(0) // trigger RST
require.NoError(t, err)
}
conn.Close()
}
}()
creds := newTestGitHubAppCreds(12345, 67890, fakeGitHubAppPrivateKey, "http://"+ln.Addr().String())
token, tokenErr := creds.getAccessToken()
require.Error(t, tokenErr)
require.Empty(t, token)
errLower := strings.ToLower(tokenErr.Error())
require.True(t,
strings.Contains(errLower, "connection reset") ||
strings.Contains(errLower, "eof") ||
strings.Contains(errLower, "broken pipe"),
"error should indicate connection reset, got: %s", tokenErr.Error())
})
t.Run("DNS resolution failure", func(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping DNS failure test (~15s due to hardcoded context timeout)")
}
creds := newTestGitHubAppCreds(12345, 67890, fakeGitHubAppPrivateKey,
"http://this-host-does-not-exist-anywhere.invalid:8080")
token, err := creds.getAccessToken()
require.Error(t, err)
require.Empty(t, token)
errLower := strings.ToLower(err.Error())
require.True(t,
strings.Contains(errLower, "no such host") ||
strings.Contains(errLower, "dns") ||
strings.Contains(errLower, "lookup"),
"error should indicate DNS resolution failure, got: %s", err.Error())
})
}
// ---------------------------------------------------------------------------
// 4. Timeout and Latency
// ---------------------------------------------------------------------------
func TestGitHubAppGetAccessToken_CustomTimeout(t *testing.T) {
// These tests mutate gitClientTimeout so they must NOT run in parallel
// with each other.
// Initialize the cache key
org := "test-org"
domain := "github.com"
appID := int64(12345)
cacheKey := fmt.Sprintf("%s:%s:%d", strings.ToLower(org), domain, appID)
githubInstallationIdCache.Delete(cacheKey)
t.Cleanup(func() {
githubInstallationIdCache.Delete(cacheKey)
// Reset gitClientTimeout back to default value
gitClientTimeout = 15 * time.Second
})
t.Run("gitClientTimeout must be honoured for token requests as well", func(t *testing.T) {
gitClientTimeout = 1 * time.Second // Very short — should cause timeout but won't
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/access_tokens") {
time.Sleep(3 * time.Second) // 3× the gitClientTimeout
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, githubTokenResponse("ghs_despite_timeout"))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
creds := newTestGitHubAppCreds(12345, 67890, fakeGitHubAppPrivateKey, server.URL)
token, err := creds.getAccessToken()
// This SHOULD fail with a timeout error
require.Error(t, err,
"gitClientTimeout (1s) should cause timeout for a 3s response, but "+
"it does not because only the transport is passed to ghinstallation")
if err == nil {
require.Equal(t, "ghs_despite_timeout", token,
"token was returned despite delay exceeding gitClientTimeout")
}
})
t.Run("slow response within timeout succeeds", func(t *testing.T) {
githubInstallationIdCache.Delete(cacheKey)
gitClientTimeout = 5 * time.Second
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/access_tokens") {
time.Sleep(1 * time.Second) // within 5s timeout
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, githubTokenResponse("ghs_slow_ok"))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
creds := newTestGitHubAppCreds(12345, 67890, fakeGitHubAppPrivateKey, server.URL)
token, err := creds.getAccessToken()
require.NoError(t, err, "should succeed when response arrives within timeout")
require.Equal(t, "ghs_slow_ok", token)
})
}
// ---------------------------------------------------------------------------
// 5. Dialer Default Timeout (30 seconds)
// ---------------------------------------------------------------------------
func TestGitHubAppGetAccessToken_DialerDefaultTimeout(t *testing.T) {
t.Run("dialer 30s timeout governs when connecting to unreachable host", func(t *testing.T) {
if testing.Short() {
t.Skip("skipping slow dialer timeout test (~30s)")
}
githubAppTokenCache.Flush()
t.Cleanup(func() {
githubAppTokenCache.Flush()
gitClientTimeout = 15 * time.Second
})
// Set a timeout value higher than the default dial context timeout 30s
gitClientTimeout = 35 * time.Second
// Use the transport directly (as ghinstallation does internally)
// without any context deadline, isolating the dialer's 30s timeout.
const unreachableAddr = "192.0.2.1:12345"
creds := newTestGitHubAppCreds(12345, 67890, fakeGitHubAppPrivateKey, "http://"+unreachableAddr)
token, err := creds.getAccessToken()
require.Empty(t, token)
require.Error(t, err)
require.Contains(t, err.Error(), fmt.Sprintf("dial tcp %s: i/o timeout", unreachableAddr))
})
t.Run("successful connection completes well within dialer 30s timeout", func(t *testing.T) {
githubAppTokenCache.Flush()
t.Cleanup(func() {
githubAppTokenCache.Flush()
gitClientTimeout = 15 * time.Second
})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/access_tokens") {
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, githubTokenResponse("ghs_dialer_ok"))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
creds := newTestGitHubAppCreds(12345, 67890, fakeGitHubAppPrivateKey, server.URL)
start := time.Now()
token, err := creds.getAccessToken()
elapsed := time.Since(start)
require.NoError(t, err)
require.Equal(t, "ghs_dialer_ok", token)
require.Less(t, elapsed, 5*time.Second,
"reachable server should connect well within dialer's 30s timeout")
})
}
// newTestGitHubAppCreds creates GitHubAppCreds pointed at a test server.
// By default insecure=true so plain-HTTP test servers work without TLS issues.
func newTestGitHubAppCreds(appID, installID int64, privateKey, baseURL string, opts ...func(*GitHubAppCreds)) GitHubAppCreds {
creds := GitHubAppCreds{
appID: appID,
appInstallId: installID,
privateKey: privateKey,
baseURL: baseURL,
insecure: true,
store: &NoopCredsStore{},
repoURL: "https://github.com/test-org/test-repo.git",
}
for _, opt := range opts {
opt(&creds)
}
return creds
}
// githubTokenResponse returns a JSON body that ghinstallation accepts as a
// valid access-token response.
func githubTokenResponse(token string) string {
b, _ := json.Marshal(map[string]string{
"token": token,
"expires_at": time.Now().Add(1 * time.Hour).Format(time.RFC3339),
})
return string(b)
}
//go:embed testdata/gcpExternalAccountKey.json
var gcpExternalAccountKeyJSON string
//go:embed testdata/gcpImpersonatedServiceAccountKey.json
var gcpImpersonatedServiceAccountKeyJSON string
//go:embed testdata/gcpAuthorizedUser.json
var gcpAuthorizedUserJSON string
func TestCredentialTypeFromJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expected google.CredentialsType
}{
{
name: "service_account maps to google.ServiceAccount",
input: gcpServiceAccountKeyJSON,
expected: google.ServiceAccount,
},
{
name: "external_account maps to google.ExternalAccount",
input: gcpExternalAccountKeyJSON,
expected: google.ExternalAccount,
},
{
name: "impersonated_service_account maps to google.ImpersonatedServiceAccount",
input: gcpImpersonatedServiceAccountKeyJSON,
expected: google.ImpersonatedServiceAccount,
},
{
name: "authorized_user maps to google.UserCredentials",
input: gcpAuthorizedUserJSON,
expected: google.AuthorizedUser,
},
{
name: "unknown type defaults to google.ServiceAccount",
input: `{"type": "some_future_unknown_type"}`,
expected: google.ServiceAccount,
},
{
name: "missing type field defaults to google.ServiceAccount",
input: `{"project_id": "my-project"}`,
expected: google.ServiceAccount,
},
{
name: "invalid JSON defaults to google.ServiceAccount",
input: `{not valid json`,
expected: google.ServiceAccount,
},
{
name: "empty string defaults to google.ServiceAccount",
input: ``,
expected: google.ServiceAccount,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := credentialTypeFromJSON([]byte(tt.input))
assert.Equal(t, tt.expected, actual)
})
}
}