<commit_before>/*
Copyright 2020 The cert-manager Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package vault

import (
	"bytes"
	"crypto"
	"crypto/rsa"
	"crypto/x509"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	vault "github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/sdk/helper/certutil"
	"github.com/hashicorp/vault/sdk/helper/jsonutil"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	clientcorev1 "k8s.io/client-go/listers/core/v1"

	vaultfake "github.com/cert-manager/cert-manager/internal/vault/fake"
	cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	v1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
	"github.com/cert-manager/cert-manager/pkg/util/pki"
	"github.com/cert-manager/cert-manager/test/unit/gen"
	"github.com/cert-manager/cert-manager/test/unit/listers"
)

const (
	testLeafCertificate = `-----BEGIN CER<KEY>
-----END CERTIFICATE-----
`

	testIntermediateCa = `-----BEGIN CERTIFICATE-----
<KEY>
-----END CERTIFICATE-----
`
	testRootCa = `-----BEGIN CERTIFICATE-----
<KEY>IBAgIU<KEY>
-----END CERTIFICATE-----
`
)

func generateRSAPrivateKey(t *testing.T) *rsa.PrivateKey {
	pk, err := pki.GenerateRSAPrivateKey(2048)
	if err != nil {
		t.Errorf("failed to generate private key: %v", err)
		t.FailNow()
	}
	return pk
}

func generateCSR(t *testing.T, secretKey crypto.Signer) []byte {
	csr, err := gen.CSRWithSigner(secretKey,
		gen.SetCSRCommonName("test"),
	)
	if err != nil {
		t.Fatal(err)
	}

	return csr
}

type testSignT struct {
	issuer     *cmapi.Issuer
	fakeLister *listers.FakeSecretLister
	fakeClient *vaultfake.Client

	csrPEM       []byte
	expectedErr  error
	expectedCert string
	expectedCA   string
}

func signedCertificateSecret(issuingCaPEM string, caPEM ...string) *certutil.Secret {
	secret := &certutil.Secret{
		Data: map[string]interface{}{
			"certificate": testLeafCertificate,
		},
	}

	secret.Data["issuing_ca"] = issuingCaPEM

	// Vault returns ca_chain only when a certificate chain is set along with a CA certificate to Vault PKI mount
	// See https://github.com/hashicorp/vault/blob/v1.5.0/builtin/logical/pki/path_issue_sign.go#L256
	// See https://github.com/hashicorp/vault/blob/v1.5.5/sdk/helper/certutil/types.go#L627
	if len(caPEM) > 0 {
		chain := []string{issuingCaPEM}
		chain = append(chain, caPEM...)
		secret.Data["ca_chain"] = chain
	}

	return secret
}

func bundlePEM(issuingCaPEM string, caPEM ...string) ([]byte, error) {
	secret := signedCertificateSecret(issuingCaPEM, caPEM...)
	return jsonutil.EncodeJSON(&secret)
}

func TestSign(t *testing.T) {
	privatekey := generateRSAPrivateKey(t)
	csrPEM := generateCSR(t, privatekey)

	bundleData, err := bundlePEM(testIntermediateCa)
	if err != nil {
		t.Errorf("failed to encode bundle for testing: %s", err)
		t.FailNow()
	}

	rootBundleData, err := bundlePEM(testIntermediateCa, testRootCa)
	if err != nil {
		t.Errorf("failed to encode root bundle for testing: %s", err)
		t.FailNow()
	}

	tests := map[string]testSignT{
		"a garbage csr should return err": {
			csrPEM:       []byte("a bad csr"),
			expectedErr:  errors.New("failed to decode CSR for signing: error decoding certificate request PEM block"),
			expectedCert: "",
			expectedCA:   "",
		},

		"a good csr but failed request should error": {
			csrPEM: csrPEM,
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{}),
			),
			fakeClient:   vaultfake.NewFakeClient().WithRawRequest(nil, errors.New("request failed")),
			expectedErr:  errors.New("failed to sign certificate by vault: request failed"),
			expectedCert: "",
			expectedCA:   "",
		},

		"a good csr and good response with no root should return a certificate with the intermediate in the chain and as the CA": {
			csrPEM: csrPEM,
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{}),
			),
			fakeClient: vaultfake.NewFakeClient().WithRawRequest(&vault.Response{
				Response: &http.Response{
					Body: io.NopCloser(bytes.NewReader(bundleData))},
			}, nil),
			expectedErr:  nil,
			expectedCert: testLeafCertificate + testIntermediateCa,
			expectedCA:   testIntermediateCa,
		},

		"a good csr and good response with a root should return a certificate without the root in the chain but with the root as the CA": {
			csrPEM: csrPEM,
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{}),
			),
			fakeClient: vaultfake.NewFakeClient().WithRawRequest(&vault.Response{
				Response: &http.Response{
					Body: io.NopCloser(bytes.NewReader(rootBundleData))},
			}, nil),
			expectedErr:  nil,
			expectedCert: testLeafCertificate + testIntermediateCa,
			expectedCA:   testRootCa,
		},

		"vault issuer with namespace specified": {
			csrPEM: csrPEM,
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{Namespace: "test"}),
			),
			fakeClient: vaultfake.NewFakeClient().WithRawRequest(&vault.Response{
				Response: &http.Response{
					Body: io.NopCloser(bytes.NewReader(bundleData))},
			}, nil),
			expectedErr:  nil,
			expectedCert: testLeafCertificate + testIntermediateCa,
			expectedCA:   testIntermediateCa,
		},
	}

	for name, test := range tests {
		v := &Vault{
			namespace:     "test-namespace",
			secretsLister: test.fakeLister,
			issuer:        test.issuer,
			client:        test.fakeClient,
		}

		cert, ca, err := v.Sign(test.csrPEM, time.Minute)
		if ((test.expectedErr == nil) != (err == nil)) &&
			test.expectedErr != nil &&
			test.expectedErr.Error() != err.Error() {
			t.Errorf("%s: unexpected error, exp=%v got=%v",
				name, test.expectedErr, err)
		}

		if (test.expectedCert == "" || string(cert) == "") && test.expectedCert != string(cert) {
			t.Errorf("unexpected certificate in response bundle, exp=%s got=%s",
				test.expectedCert, cert)
		} else if test.expectedCert != string(cert) {
			parsedBundle, err := certutil.ParsePEMBundle(string(cert))
			if err != nil {
				t.Errorf("%s: failed to decode bundle: %s", name, err)
			}
			bundle, err := parsedBundle.ToCertBundle()
			if err != nil {
				t.Errorf("%s: failed to convert bundle: %s", name, err)
			}
			if test.expectedCert != bundle.Certificate {
				t.Errorf("%s: unexpected certificate in response bundle, exp=%s got=%s",
					name, test.expectedCert, cert)
			}
		}

		if test.expectedCA != string(ca) {
			t.Errorf("unexpected ca in response bundle, exp=%s got=%s; %s",
				test.expectedCA, ca, name)
		}
	}
}

type testExtractCertificatesFromVaultCertT struct {
	secret       *certutil.Secret
	expectedCert string
	expectedCA   string
}

func TestExtractCertificatesFromVaultCertificateSecret(t *testing.T) {
	tests := map[string]testExtractCertificatesFromVaultCertT{
		"when a Vault engine is a root CA": {
			secret:       signedCertificateSecret(testIntermediateCa),
			expectedCert: testLeafCertificate + testIntermediateCa,
			expectedCA:   testIntermediateCa,
		},
		"when a Vault engine is an intermediate CA, and its parent is a root CA": {
			secret:       signedCertificateSecret(testIntermediateCa, testRootCa),
			expectedCert: testLeafCertificate + testIntermediateCa,
			expectedCA:   testRootCa,
		},
		"when a Vault engine is an intermediate CA, and its parent is a intermediate CA": {
			secret:       signedCertificateSecret(testIntermediateCa, testIntermediateCa, testRootCa),
			expectedCert: testLeafCertificate + testIntermediateCa,
			expectedCA:   testRootCa,
		},
	}

	for name, test := range tests {
		cert, ca, err := extractCertificatesFromVaultCertificateSecret(test.secret)

		if err != nil {
			t.Errorf("%s: failed to extract certificate: %s", name, err)
		}
		if test.expectedCert != string(cert) {
			t.Errorf("%s: unexpected leaf certificate, exp=%q, got=%q", name, test.expectedCert, cert)
		}
		if test.expectedCA != string(ca) {
			t.Errorf("%s: unexpected root certificate, exp=%q, got=%q", name, test.expectedCA, cert)
		}
	}
}

type testSetTokenT struct {
	expectedToken string
	expectedErr   error

	issuer     *cmapi.Issuer
	fakeLister *listers.FakeSecretLister
	fakeClient *vaultfake.Client
}

func TestSetToken(t *testing.T) {
	tokenSecret := &corev1.Secret{
		Data: map[string][]byte{
			"my-token-key": []byte("my-secret-token"),
		},
	}

	appRoleSecret := &corev1.Secret{
		Data: map[string][]byte{
			"my-role-key": []byte("my-secret-role-token"),
		},
	}

	kubeAuthSecret := &corev1.Secret{
		Data: map[string][]byte{
			"my-kube-key": []byte("my-secret-kube-token"),
		},
	}
	tests := map[string]testSetTokenT{
		"if neither token secret ref, app role secret ref, or kube auth then not found then error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth:     cmapi.VaultAuth{},
				}),
			),
			fakeLister:    listers.FakeSecretListerFrom(listers.NewFakeSecretLister()),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "",
			expectedErr: errors.New(
				"error initializing Vault client: tokenSecretRef, appRoleSecretRef, or Kubernetes auth role not set",
			),
		},

		"if token secret ref is set but secret doesn't exist should error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						TokenSecretRef: &cmmeta.SecretKeySelector{
							LocalObjectReference: cmmeta.LocalObjectReference{
								Name: "secret-ref-name",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(nil, errors.New("secret does not exists")),
			),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "",
			expectedErr:   errors.New("secret does not exists"),
		},

		"if token secret ref set, return client using token stored": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						TokenSecretRef: &cmmeta.SecretKeySelector{
							LocalObjectReference: cmmeta.LocalObjectReference{
								Name: "secret-ref-name",
							},
							Key: "my-token-key",
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(tokenSecret, nil),
			),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "my-<PASSWORD>-token",
			expectedErr:   nil,
		},

		"if app role set but secret token not but vault fails to return token, error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						AppRole: &cmapi.VaultAppRole{
							RoleId: "my-role-id",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-role-key",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(nil, errors.New("secret not found")),
			),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "",
			expectedErr:   errors.New("secret not found"),
		},

		"if app role secret ref set, return client using token stored": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						AppRole: &cmapi.VaultAppRole{
							RoleId: "my-role-id",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-role-key",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(appRoleSecret, nil),
			),
			fakeClient: vaultfake.NewFakeClient().WithRawRequest(&vault.Response{
				Response: &http.Response{
					Body: io.NopCloser(
						strings.NewReader(
							`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":null,"warnings":null,"data":{"id":"my-roleapp-token"}}`),
					),
				},
			}, nil),
			expectedToken: "<PASSWORD>",
			expectedErr:   nil,
		},

		"if kubernetes role auth set but reference secret doesn't exist return error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						Kubernetes: &cmapi.VaultKubernetesAuth{
							Role: "kube-vault-role",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-kube-key",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(nil, errors.New("secret does not exists")),
			),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "",
			expectedErr:   errors.New("error reading Kubernetes service account token from secret-ref-name: secret does not exists"),
		},

		"if kubernetes role auth set but reference secret doesn't contain data at key error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						Kubernetes: &cmapi.VaultKubernetesAuth{
							Role: "kube-vault-role",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-kube-key",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(&corev1.Secret{}, nil),
			),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "",
			expectedErr:   errors.New(`error reading Kubernetes service account token from secret-ref-name: no data for "my-kube-key" in secret 'test-namespace/secret-ref-name'`),
		},

		"if kubernetes role auth set but errors with a raw request should error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						Kubernetes: &cmapi.VaultKubernetesAuth{
							Role: "kube-vault-role",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-kube-key",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(kubeAuthSecret, nil),
			),
			fakeClient:    vaultfake.NewFakeClient().WithRawRequest(nil, errors.New("raw request error")),
			expectedToken: "",
			expectedErr:   errors.New("error reading Kubernetes service account token from secret-ref-name: error calling Vault server: raw request error"),
		},

		"foo": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						Kubernetes: &cmapi.VaultKubernetesAuth{
							Role: "kube-vault-role",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-kube-key",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(kubeAuthSecret, nil),
			),
			fakeClient: vaultfake.NewFakeClient().WithRawRequest(&vault.Response{
				Response: &http.Response{
					Body: io.NopCloser(
						strings.NewReader(
							`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":null,"warnings":null,"data":{"id":"my-token"}}`),
					),
				},
			}, nil),
			expectedToken: "my-token",
			expectedErr:   nil,
		},

		"if app role secret ref and token secret set, take preference on token secret": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						AppRole: &cmapi.VaultAppRole{
							RoleId: "my-role-id",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-role-key",
							},
						},
						TokenSecretRef: &cmmeta.SecretKeySelector{
							LocalObjectReference: cmmeta.LocalObjectReference{
								Name: "secret-ref-name",
							},
							Key: "my-token-key",
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(tokenSecret, nil),
			),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "my-secret-token",
			expectedErr:   nil,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			v := &Vault{
				namespace:     "test-namespace",
				secretsLister: test.fakeLister,
				issuer:        test.issuer,
			}

			err := v.setToken(test.fakeClient)
			if ((test.expectedErr == nil) != (err == nil)) &&
				test.expectedErr != nil &&
				test.expectedErr.Error() != err.Error() {
				t.Errorf("unexpected error, exp=%v got=%v",
					test.expectedErr, err)
			}

			if test.fakeClient.Token() != test.expectedToken {
				t.Errorf("got unexpected client token, exp=%s got=%s",
					test.expectedToken, test.fakeClient.Token())
			}
		})
	}
}

type testAppRoleRefT struct {
	expectedRoleID   string
	expectedSecretID string
	expectedErr      error

	appRole *cmapi.VaultAppRole

	fakeLister *listers.FakeSecretLister
}

func TestAppRoleRef(t *testing.T) {
	errSecretGet := errors.New("no secret found")

	basicAppRoleRef := &cmapi.VaultAppRole{
		RoleId: "my-role-id",
	}

	tests := map[string]testAppRoleRefT{
		"failing to get secret should error": {
			appRole: basicAppRoleRef,
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(nil, errSecretGet),
			),
			expectedRoleID:   "",
			expectedSecretID: "",
			expectedErr:      errSecretGet,
		},

		"no data in key should fail": {
			appRole: &cmapi.VaultAppRole{
				RoleId: "",
				SecretRef: cmmeta.SecretKeySelector{
					LocalObjectReference: cmmeta.LocalObjectReference{
						Name: "secret-name",
					},
					Key: "my-key",
				},
			},
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(
					&corev1.Secret{
						Data: map[string][]byte{
							"foo": []byte("bar"),
						},
					}, nil),
			),
			expectedRoleID:   "",
			expectedSecretID: "",
			expectedErr:      errors.New(`no data for "my-key" in secret 'test-namespace/secret-name'`),
		},

		"should return roleID and secretID with trimmed space": {
			appRole: &cmapi.VaultAppRole{
				RoleId: "    my-role-id  ",
				SecretRef: cmmeta.SecretKeySelector{
					LocalObjectReference: cmmeta.LocalObjectReference{
						Name: "secret-name",
					},
					Key: "my-key",
				},
			},
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(
					&corev1.Secret{
						Data: map[string][]byte{
							"foo":    []byte("bar"),
							"my-key": []byte("    my-key-data   "),
						},
					}, nil),
			),
			expectedRoleID:   "my-role-id",
			expectedSecretID: "my-key-data",
			expectedErr:      nil,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			v := &Vault{
				namespace:     "test-namespace",
				secretsLister: test.fakeLister,
				issuer:        nil,
			}

			roleID, secretID, err := v.appRoleRef(test.appRole)
			if ((test.expectedErr == nil) != (err == nil)) &&
				test.expectedErr != nil &&
				test.expectedErr.Error() != err.Error() {
				t.Errorf("unexpected error, exp=%v got=%v",
					test.expectedErr, err)
			}

			if test.expectedRoleID != roleID {
				t.Errorf("got unexpected roleID, exp=%s got=%s",
					test.expectedRoleID, roleID)
			}

			if test.expectedSecretID != secretID {
				t.Errorf("got unexpected secretID, exp=%s got=%s",
					test.expectedSecretID, secretID)
			}
		})
	}
}

type testTokenRefT struct {
	expectedToken string
	expectedErr   error

	key string

	fakeLister *listers.FakeSecretLister
}

func TestTokenRef(t *testing.T) {
	errSecretGet := errors.New("no secret found")

	testName, testNamespace := "test-name", "test-namespace"

	tests := map[string]testTokenRefT{
		"failing to get secret should error": {
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(nil, errSecretGet),
			),
			key:           "a-key",
			expectedToken: "",
			expectedErr:   errSecretGet,
		},

		"if no vault at key exists then error": {
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(
					&corev1.Secret{
						Data: map[string][]byte{
							"foo": []byte("bar"),
						},
					}, nil),
			),

			key:           "a-key",
			expectedToken: "",
			expectedErr: fmt.Errorf(`no data for "a-key" in secret '%s/%s'`,
				testName, testNamespace),
		},
		"if value exists at key then return with whitespace trimmed": {
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(
					&corev1.Secret{
						Data: map[string][]byte{
							"foo":   []byte("bar"),
							"a-key": []byte(" my-token              "),
						},
					}, nil),
			),

			key:           "a-key",
			expectedToken: "my-token",
		},
		"if no key is given then it should default to 'token'": {
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(
					&corev1.Secret{
						Data: map[string][]byte{
							"foo":   []byte("bar"),
							"token": []byte(" <PASSWORD>              "),
						},
					}, nil),
			),

			key:           "",
			expectedToken: "<PASSWORD>",
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			v := &Vault{
				namespace:     "test-namespace",
				secretsLister: test.fakeLister,
				issuer:        nil,
			}

			token, err := v.tokenRef("test-name", "test-namespace", test.key)
			if ((test.expectedErr == nil) != (err == nil)) &&
				test.expectedErr != nil &&
				test.expectedErr.Error() != err.Error() {
				t.Errorf("unexpected error, exp=%v got=%v",
					test.expectedErr, err)
			}

			if test.expectedToken != token {
				t.Errorf("got unexpected token, exp=%s got=%s",
					test.expectedToken, token)
			}
		})
	}
}

type testNewConfigT struct {
	expectedErr error
	issuer      *cmapi.Issuer
	checkFunc   func(cfg *vault.Config, err error) error

	fakeLister *listers.FakeSecretLister
}

func TestNewConfig(t *testing.T) {
	caBundleSecretRefFakeSecretLister := func(namespace, secret, key, cert string) *listers.FakeSecretLister {
		return listers.FakeSecretListerFrom(listers.NewFakeSecretLister(), func(f *listers.FakeSecretLister) {
			f.SecretsFn = func(namespace string) clientcorev1.SecretNamespaceLister {
				return listers.FakeSecretNamespaceListerFrom(listers.NewFakeSecretNamespaceLister(), func(fn *listers.FakeSecretNamespaceLister) {
					fn.GetFn = func(name string) (*corev1.Secret, error) {
						if name == secret && namespace == namespace {
							return &corev1.Secret{
								Data: map[string][]byte{
									key: []byte(cert),
								}}, nil
						}
						return nil, errors.New("unexpected secret name or namespace passed to FakeSecretLister")
					}
				})
			}
		})
	}
	tests := map[string]testNewConfigT{
		"no CA bundle set in issuer should return nil": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: nil,
				}),
			),
			expectedErr: nil,
		},

		"a bad cert bundle should error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte("a bad cert bundle"),
				}),
			),
			expectedErr: errors.New("no Vault CA bundles loaded, check bundle contents"),
		},

		"a good cert bundle should be added to the config": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
				}),
			),
			expectedErr: nil,
			checkFunc: func(cfg *vault.Config, error error) error {
				testCA := x509.NewCertPool()
				testCA.AppendCertsFromPEM([]byte(testLeafCertificate))
				subs := cfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs.Subjects()

				err := fmt.Errorf("got unexpected root CAs in config, exp=%s got=%s",
					testCA.Subjects(), subs)
				if len(subs) != len(testCA.Subjects()) {
					return err
				}
				for i := range subs {
					if !bytes.Equal(subs[i], testCA.Subjects()[i]) {
						return err
					}
				}

				return nil
			},
		},

		"a good bundle from a caBundleSecretRef should be added to the config": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundleSecretRef: &cmmeta.SecretKeySelector{
						Key: "my-bundle.crt",
						LocalObjectReference: cmmeta.LocalObjectReference{
							Name: "bundle",
						},
					},
				},
				)),
			checkFunc: func(cfg *vault.Config, error error) error {
				if error != nil {
					return error
				}

				testCA := x509.NewCertPool()
				testCA.AppendCertsFromPEM([]byte(testLeafCertificate))
				subs := cfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs.Subjects()

				err := fmt.Errorf("got unexpected root CAs in config, exp=%s got=%s",
					testCA.Subjects(), subs)
				if len(subs) != len(testCA.Subjects()) {
					return err
				}
				for i := range subs {
					if !bytes.Equal(subs[i], testCA.Subjects()[i]) {
						return err
					}
				}

				return nil
			},
			fakeLister: caBundleSecretRefFakeSecretLister("test-namespace", "bundle", "my-bundle.crt", testLeafCertificate),
		},
		"a good bundle from a caBundleSecretRef with default key should be added to the config": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundleSecretRef: &cmmeta.SecretKeySelector{
						LocalObjectReference: cmmeta.LocalObjectReference{
							Name: "bundle",
						},
					},
				},
				)),
			checkFunc: func(cfg *vault.Config, error error) error {
				if error != nil {
					return error
				}

				testCA := x509.NewCertPool()
				testCA.AppendCertsFromPEM([]byte(testLeafCertificate))
				subs := cfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs.Subjects()

				err := fmt.Errorf("got unexpected root CAs in config, exp=%s got=%s",
					testCA.Subjects(), subs)
				if len(subs) != len(testCA.Subjects()) {
					return err
				}
				for i := range subs {
					if !bytes.Equal(subs[i], testCA.Subjects()[i]) {
						return err
					}
				}

				return nil
			},
			fakeLister: caBundleSecretRefFakeSecretLister("test-namespace", "bundle", "ca.crt", testLeafCertificate),
		},
		"a bad bundle from a caBundleSecretRef should error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundleSecretRef: &cmmeta.SecretKeySelector{
						Key: "my-bundle.crt",
						LocalObjectReference: cmmeta.LocalObjectReference{
							Name: "bundle",
						},
					},
				},
				)),
			expectedErr: errors.New("no Vault CA bundles loaded, check bundle contents"),
			fakeLister:  caBundleSecretRefFakeSecretLister("test-namespace", "bundle", "my-bundle.crt", "not a valid certificate"),
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			v := &Vault{
				namespace:     "test-namespace",
				secretsLister: test.fakeLister,
				issuer:        test.issuer,
			}

			cfg, err := v.newConfig()
			if test.expectedErr != nil && err != nil && test.expectedErr.Error() != err.Error() {
				t.Errorf("unexpected error, exp=%v got=%v", test.expectedErr, err)
			}

			if test.checkFunc != nil {
				if err := test.checkFunc(cfg, err); err != nil {
					t.Errorf("check function failed: %s", err)
				}
			}
		})
	}
}

type requestTokenWithAppRoleRefT struct {
	client  Client
	appRole *cmapi.VaultAppRole

	fakeLister *listers.FakeSecretLister

	expectedToken string
	expectedErr   error
}

func TestRequestTokenWithAppRoleRef(t *testing.T) {
	basicAppRoleRef := &cmapi.VaultAppRole{
		RoleId: "test-role-id",
		SecretRef: cmmeta.SecretKeySelector{
			LocalObjectReference: cmmeta.LocalObjectReference{
				Name: "test-secret",
			},
			Key: "my-key",
		},
	}

	basicSecretLister := listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
		listers.SetFakeSecretNamespaceListerGet(
			&corev1.Secret{
				Data: map[string][]byte{
					"my-key": []byte("my-key-data"),
				},
			}, nil),
	)

	tests := map[string]requestTokenWithAppRoleRefT{
		"a secret reference that does not exist should error": {
			client:  vaultfake.NewFakeClient(),
			appRole: basicAppRoleRef,
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(nil, errors.New("secret not found")),
			),

			expectedToken: "",
			expectedErr:   errors.New("secret not found"),
		},
		"if a raw request fails then error": {
			client:     vaultfake.NewFakeClient().WithRawRequest(nil, errors.New("request failed")),
			appRole:    basicAppRoleRef,
			fakeLister: basicSecretLister,

			expectedToken: "",
			expectedErr:   errors.New("error logging in to Vault server: request failed"),
		},
		"no id in the JSON response should return no token": {
			client: vaultfake.NewFakeClient().WithRawRequest(
				&vault.Response{
					Response: &http.Response{
						Body: io.NopCloser(
							strings.NewReader(
								`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":null,"warnings":null,"data":{}}`),
						),
					},
				}, nil,
			),
			appRole:    basicAppRoleRef,
			fakeLister: basicSecretLister,

			expectedToken: "",
			expectedErr:   errors.New("no token returned"),
		},
		"an id in the JSON response should return that token": {
			client: vaultfake.NewFakeClient().WithRawRequest(
				&vault.Response{
					Response: &http.Response{
						Body: io.NopCloser(
							strings.NewReader(
								`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":null,"warnings":null,"data":{"id":"my-token"}}`),
						),
					},
				}, nil,
			),
			appRole:    basicAppRoleRef,
			fakeLister: basicSecretLister,

			expectedToken: "<PASSWORD>",
			expectedErr:   nil,
		},
		"a client_token present should take president over id": {
			client: vaultfake.NewFakeClient().WithRawRequest(
				&vault.Response{
					Response: &http.Response{
						Body: io.NopCloser(
							strings.NewReader(
								`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":null,"warnings":null,"data":{"id":"my-token"},"auth":{"client_token":"my-<PASSWORD>"}}`),
						),
					},
				}, nil,
			),
			appRole:    basicAppRoleRef,
			fakeLister: basicSecretLister,

			expectedToken: "<PASSWORD>",
			expectedErr:   nil,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			v := &Vault{
				namespace:     "test-namespace",
				secretsLister: test.fakeLister,
				issuer: gen.Issuer("vault-issuer",
					gen.SetIssuerNamespace("namespace"),
				),
			}

			token, err := v.requestTokenWithAppRoleRef(test.client, test.appRole)
			if ((test.expectedErr == nil) != (err == nil)) &&
				test.expectedErr != nil &&
				test.expectedErr.Error() != err.Error() {
				t.Errorf("unexpected error, exp=%v got=%v",
					test.expectedErr, err)
			}

			if test.expectedToken != token {
				t.Errorf("got unexpected token, exp=%s got=%s",
					test.expectedToken, token)
			}
		})
	}
}

// TestNewWithVaultNamespaces demonstrates that New initializes two Vault
// clients, one with a namespace and one without a namespace which is used for
// interacting with root-only APIs.
func TestNewWithVaultNamespaces(t *testing.T) {
	type testCase struct {
		name    string
		vaultNS string
	}

	tests := []testCase{
		{
			name:    "without-namespace",
			vaultNS: "",
		},
		{
			name:    "with-namespace",
			vaultNS: "vault-ns-1",
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			c, err := New(
				"k8s-ns1",
				listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
					listers.SetFakeSecretNamespaceListerGet(
						&corev1.Secret{
							Data: map[string][]byte{
								"key1": []byte("not-used"),
							},
						}, nil),
				),
				&cmapi.Issuer{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "issuer1",
						Namespace: "k8s-ns1",
					},
					Spec: v1.IssuerSpec{
						IssuerConfig: v1.IssuerConfig{
							Vault: &v1.VaultIssuer{
								Namespace: tc.vaultNS,
								Auth: cmapi.VaultAuth{
									TokenSecretRef: &cmmeta.SecretKeySelector{
										LocalObjectReference: cmmeta.LocalObjectReference{
											Name: "secret1",
										},
										Key: "key1",
									},
								},
							},
						},
					},
				})
			require.NoError(t, err)
			assert.Equal(t, tc.vaultNS, c.(*Vault).client.(*vault.Client).Namespace(),
				"The vault client should have the namespace provided in the Issuer recource")
			assert.Equal(t, "", c.(*Vault).clientSys.(*vault.Client).Namespace(),
				"The vault sys client should never have a namespace")
		})
	}
}

// TestIsVaultInitiatedAndUnsealedIntegration demonstrates that it interacts only with the
// sys/health endpoint and that it supplies the Vault token but not a Vault namespace header.
func TestIsVaultInitiatedAndUnsealedIntegration(t *testing.T) {

	const vaultToken = "<PASSWORD>"

	mux := http.NewServeMux()
	mux.HandleFunc("/v1/sys/health", func(response http.ResponseWriter, request *http.Request) {
		assert.Empty(t, request.Header.Values("X-Vault-Namespace"), "Unexpected Vault namespace header for root-only API path")
		assert.Equal(t, vaultToken, request.Header.Get("X-Vault-Token"), "Expected the Vault token for root-only API path")
	})
	server := httptest.NewServer(mux)
	defer server.Close()

	v, err := New(
		"k8s-ns1",
		listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
			listers.SetFakeSecretNamespaceListerGet(
				&corev1.Secret{
					Data: map[string][]byte{
						"key1": []byte(vaultToken),
					},
				}, nil),
		),
		&cmapi.Issuer{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "issuer1",
				Namespace: "k8s-ns1",
			},
			Spec: v1.IssuerSpec{
				IssuerConfig: v1.IssuerConfig{
					Vault: &v1.VaultIssuer{
						Server:    server.URL,
						Namespace: "ns1",
						Auth: cmapi.VaultAuth{
							TokenSecretRef: &cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret1",
								},
								Key: "key1",
							},
						},
					},
				},
			},
		})
	require.NoError(t, err)

	err = v.IsVaultInitializedAndUnsealed()
	require.NoError(t, err)
}<commit_msg>Test that the Sign function *does* use the Vault namespace<commit_after>/*
Copyright 2020 The cert-manager Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package vault

import (
	"bytes"
	"crypto"
	"crypto/rsa"
	"crypto/x509"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	vault "github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/sdk/helper/certutil"
	"github.com/hashicorp/vault/sdk/helper/jsonutil"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	clientcorev1 "k8s.io/client-go/listers/core/v1"

	vaultfake "github.com/cert-manager/cert-manager/internal/vault/fake"
	cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	v1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
	"github.com/cert-manager/cert-manager/pkg/util/pki"
	"github.com/cert-manager/cert-manager/test/unit/gen"
	"github.com/cert-manager/cert-manager/test/unit/listers"
)

const (
	testLeafCertificate = `-----BEGIN CERTIFICATE-----
<KEY>
-----END CERTIFICATE-----
`

	testIntermediateCa = `-----BEGIN CERTIFICATE-----
<KEY>
-----END CERTIFICATE-----
`
	testRootCa = `-----BEGIN CERTIFICATE-----
<KEY>
-----END CERTIFICATE-----
`
)

func generateRSAPrivateKey(t *testing.T) *rsa.PrivateKey {
	pk, err := pki.GenerateRSAPrivateKey(2048)
	if err != nil {
		t.Errorf("failed to generate private key: %v", err)
		t.FailNow()
	}
	return pk
}

func generateCSR(t *testing.T, secretKey crypto.Signer) []byte {
	csr, err := gen.CSRWithSigner(secretKey,
		gen.SetCSRCommonName("test"),
	)
	if err != nil {
		t.Fatal(err)
	}

	return csr
}

type testSignT struct {
	issuer     *cmapi.Issuer
	fakeLister *listers.FakeSecretLister
	fakeClient *vaultfake.Client

	csrPEM       []byte
	expectedErr  error
	expectedCert string
	expectedCA   string
}

func signedCertificateSecret(issuingCaPEM string, caPEM ...string) *certutil.Secret {
	secret := &certutil.Secret{
		Data: map[string]interface{}{
			"certificate": testLeafCertificate,
		},
	}

	secret.Data["issuing_ca"] = issuingCaPEM

	// Vault returns ca_chain only when a certificate chain is set along with a CA certificate to Vault PKI mount
	// See https://github.com/hashicorp/vault/blob/v1.5.0/builtin/logical/pki/path_issue_sign.go#L256
	// See https://github.com/hashicorp/vault/blob/v1.5.5/sdk/helper/certutil/types.go#L627
	if len(caPEM) > 0 {
		chain := []string{issuingCaPEM}
		chain = append(chain, caPEM...)
		secret.Data["ca_chain"] = chain
	}

	return secret
}

func bundlePEM(issuingCaPEM string, caPEM ...string) ([]byte, error) {
	secret := signedCertificateSecret(issuingCaPEM, caPEM...)
	return jsonutil.EncodeJSON(&secret)
}

func TestSign(t *testing.T) {
	privatekey := generateRSAPrivateKey(t)
	csrPEM := generateCSR(t, privatekey)

	bundleData, err := bundlePEM(testIntermediateCa)
	if err != nil {
		t.Errorf("failed to encode bundle for testing: %s", err)
		t.FailNow()
	}

	rootBundleData, err := bundlePEM(testIntermediateCa, testRootCa)
	if err != nil {
		t.Errorf("failed to encode root bundle for testing: %s", err)
		t.FailNow()
	}

	tests := map[string]testSignT{
		"a garbage csr should return err": {
			csrPEM:       []byte("a bad csr"),
			expectedErr:  errors.New("failed to decode CSR for signing: error decoding certificate request PEM block"),
			expectedCert: "",
			expectedCA:   "",
		},

		"a good csr but failed request should error": {
			csrPEM: csrPEM,
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{}),
			),
			fakeClient:   vaultfake.NewFakeClient().WithRawRequest(nil, errors.New("request failed")),
			expectedErr:  errors.New("failed to sign certificate by vault: request failed"),
			expectedCert: "",
			expectedCA:   "",
		},

		"a good csr and good response with no root should return a certificate with the intermediate in the chain and as the CA": {
			csrPEM: csrPEM,
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{}),
			),
			fakeClient: vaultfake.NewFakeClient().WithRawRequest(&vault.Response{
				Response: &http.Response{
					Body: io.NopCloser(bytes.NewReader(bundleData))},
			}, nil),
			expectedErr:  nil,
			expectedCert: testLeafCertificate + testIntermediateCa,
			expectedCA:   testIntermediateCa,
		},

		"a good csr and good response with a root should return a certificate without the root in the chain but with the root as the CA": {
			csrPEM: csrPEM,
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{}),
			),
			fakeClient: vaultfake.NewFakeClient().WithRawRequest(&vault.Response{
				Response: &http.Response{
					Body: io.NopCloser(bytes.NewReader(rootBundleData))},
			}, nil),
			expectedErr:  nil,
			expectedCert: testLeafCertificate + testIntermediateCa,
			expectedCA:   testRootCa,
		},

		"vault issuer with namespace specified": {
			csrPEM: csrPEM,
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{Namespace: "test"}),
			),
			fakeClient: vaultfake.NewFakeClient().WithRawRequest(&vault.Response{
				Response: &http.Response{
					Body: io.NopCloser(bytes.NewReader(bundleData))},
			}, nil),
			expectedErr:  nil,
			expectedCert: testLeafCertificate + testIntermediateCa,
			expectedCA:   testIntermediateCa,
		},
	}

	for name, test := range tests {
		v := &Vault{
			namespace:     "test-namespace",
			secretsLister: test.fakeLister,
			issuer:        test.issuer,
			client:        test.fakeClient,
		}

		cert, ca, err := v.Sign(test.csrPEM, time.Minute)
		if ((test.expectedErr == nil) != (err == nil)) &&
			test.expectedErr != nil &&
			test.expectedErr.Error() != err.Error() {
			t.Errorf("%s: unexpected error, exp=%v got=%v",
				name, test.expectedErr, err)
		}

		if (test.expectedCert == "" || string(cert) == "") && test.expectedCert != string(cert) {
			t.Errorf("unexpected certificate in response bundle, exp=%s got=%s",
				test.expectedCert, cert)
		} else if test.expectedCert != string(cert) {
			parsedBundle, err := certutil.ParsePEMBundle(string(cert))
			if err != nil {
				t.Errorf("%s: failed to decode bundle: %s", name, err)
			}
			bundle, err := parsedBundle.ToCertBundle()
			if err != nil {
				t.Errorf("%s: failed to convert bundle: %s", name, err)
			}
			if test.expectedCert != bundle.Certificate {
				t.Errorf("%s: unexpected certificate in response bundle, exp=%s got=%s",
					name, test.expectedCert, cert)
			}
		}

		if test.expectedCA != string(ca) {
			t.Errorf("unexpected ca in response bundle, exp=%s got=%s; %s",
				test.expectedCA, ca, name)
		}
	}
}

type testExtractCertificatesFromVaultCertT struct {
	secret       *certutil.Secret
	expectedCert string
	expectedCA   string
}

func TestExtractCertificatesFromVaultCertificateSecret(t *testing.T) {
	tests := map[string]testExtractCertificatesFromVaultCertT{
		"when a Vault engine is a root CA": {
			secret:       signedCertificateSecret(testIntermediateCa),
			expectedCert: testLeafCertificate + testIntermediateCa,
			expectedCA:   testIntermediateCa,
		},
		"when a Vault engine is an intermediate CA, and its parent is a root CA": {
			secret:       signedCertificateSecret(testIntermediateCa, testRootCa),
			expectedCert: testLeafCertificate + testIntermediateCa,
			expectedCA:   testRootCa,
		},
		"when a Vault engine is an intermediate CA, and its parent is a intermediate CA": {
			secret:       signedCertificateSecret(testIntermediateCa, testIntermediateCa, testRootCa),
			expectedCert: testLeafCertificate + testIntermediateCa,
			expectedCA:   testRootCa,
		},
	}

	for name, test := range tests {
		cert, ca, err := extractCertificatesFromVaultCertificateSecret(test.secret)

		if err != nil {
			t.Errorf("%s: failed to extract certificate: %s", name, err)
		}
		if test.expectedCert != string(cert) {
			t.Errorf("%s: unexpected leaf certificate, exp=%q, got=%q", name, test.expectedCert, cert)
		}
		if test.expectedCA != string(ca) {
			t.Errorf("%s: unexpected root certificate, exp=%q, got=%q", name, test.expectedCA, cert)
		}
	}
}

type testSetTokenT struct {
	expectedToken string
	expectedErr   error

	issuer     *cmapi.Issuer
	fakeLister *listers.FakeSecretLister
	fakeClient *vaultfake.Client
}

func TestSetToken(t *testing.T) {
	tokenSecret := &corev1.Secret{
		Data: map[string][]byte{
			"my-token-key": []byte("my-secret-token"),
		},
	}

	appRoleSecret := &corev1.Secret{
		Data: map[string][]byte{
			"my-role-key": []byte("my-secret-role-token"),
		},
	}

	kubeAuthSecret := &corev1.Secret{
		Data: map[string][]byte{
			"my-kube-key": []byte("my-secret-kube-token"),
		},
	}
	tests := map[string]testSetTokenT{
		"if neither token secret ref, app role secret ref, or kube auth then not found then error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth:     cmapi.VaultAuth{},
				}),
			),
			fakeLister:    listers.FakeSecretListerFrom(listers.NewFakeSecretLister()),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "",
			expectedErr: errors.New(
				"error initializing Vault client: tokenSecretRef, appRoleSecretRef, or Kubernetes auth role not set",
			),
		},

		"if token secret ref is set but secret doesn't exist should error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						TokenSecretRef: &cmmeta.SecretKeySelector{
							LocalObjectReference: cmmeta.LocalObjectReference{
								Name: "secret-ref-name",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(nil, errors.New("secret does not exists")),
			),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "",
			expectedErr:   errors.New("secret does not exists"),
		},

		"if token secret ref set, return client using token stored": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						TokenSecretRef: &cmmeta.SecretKeySelector{
							LocalObjectReference: cmmeta.LocalObjectReference{
								Name: "secret-ref-name",
							},
							Key: "my-token-key",
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(tokenSecret, nil),
			),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "<PASSWORD>",
			expectedErr:   nil,
		},

		"if app role set but secret token not but vault fails to return token, error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						AppRole: &cmapi.VaultAppRole{
							RoleId: "my-role-id",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-role-key",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(nil, errors.New("secret not found")),
			),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "",
			expectedErr:   errors.New("secret not found"),
		},

		"if app role secret ref set, return client using token stored": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						AppRole: &cmapi.VaultAppRole{
							RoleId: "my-role-id",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-role-key",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(appRoleSecret, nil),
			),
			fakeClient: vaultfake.NewFakeClient().WithRawRequest(&vault.Response{
				Response: &http.Response{
					Body: io.NopCloser(
						strings.NewReader(
							`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":null,"warnings":null,"data":{"id":"my-roleapp-token"}}`),
					),
				},
			}, nil),
			expectedToken: "my-roleapp-token",
			expectedErr:   nil,
		},

		"if kubernetes role auth set but reference secret doesn't exist return error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						Kubernetes: &cmapi.VaultKubernetesAuth{
							Role: "kube-vault-role",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-kube-key",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(nil, errors.New("secret does not exists")),
			),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "",
			expectedErr:   errors.New("error reading Kubernetes service account token from secret-ref-name: secret does not exists"),
		},

		"if kubernetes role auth set but reference secret doesn't contain data at key error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						Kubernetes: &cmapi.VaultKubernetesAuth{
							Role: "kube-vault-role",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-kube-key",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(&corev1.Secret{}, nil),
			),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "",
			expectedErr:   errors.New(`error reading Kubernetes service account token from secret-ref-name: no data for "my-kube-key" in secret 'test-namespace/secret-ref-name'`),
		},

		"if kubernetes role auth set but errors with a raw request should error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						Kubernetes: &cmapi.VaultKubernetesAuth{
							Role: "kube-vault-role",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-kube-key",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(kubeAuthSecret, nil),
			),
			fakeClient:    vaultfake.NewFakeClient().WithRawRequest(nil, errors.New("raw request error")),
			expectedToken: "",
			expectedErr:   errors.New("error reading Kubernetes service account token from secret-ref-name: error calling Vault server: raw request error"),
		},

		"foo": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						Kubernetes: &cmapi.VaultKubernetesAuth{
							Role: "kube-vault-role",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-kube-key",
							},
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(kubeAuthSecret, nil),
			),
			fakeClient: vaultfake.NewFakeClient().WithRawRequest(&vault.Response{
				Response: &http.Response{
					Body: io.NopCloser(
						strings.NewReader(
							`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":null,"warnings":null,"data":{"id":"my-token"}}`),
					),
				},
			}, nil),
			expectedToken: "my-token",
			expectedErr:   nil,
		},

		"if app role secret ref and token secret set, take preference on token secret": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
					Auth: cmapi.VaultAuth{
						AppRole: &cmapi.VaultAppRole{
							RoleId: "my-role-id",
							SecretRef: cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret-ref-name",
								},
								Key: "my-role-key",
							},
						},
						TokenSecretRef: &cmmeta.SecretKeySelector{
							LocalObjectReference: cmmeta.LocalObjectReference{
								Name: "secret-ref-name",
							},
							Key: "my-token-key",
						},
					},
				}),
			),
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(tokenSecret, nil),
			),
			fakeClient:    vaultfake.NewFakeClient(),
			expectedToken: "<PASSWORD>",
			expectedErr:   nil,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			v := &Vault{
				namespace:     "test-namespace",
				secretsLister: test.fakeLister,
				issuer:        test.issuer,
			}

			err := v.setToken(test.fakeClient)
			if ((test.expectedErr == nil) != (err == nil)) &&
				test.expectedErr != nil &&
				test.expectedErr.Error() != err.Error() {
				t.Errorf("unexpected error, exp=%v got=%v",
					test.expectedErr, err)
			}

			if test.fakeClient.Token() != test.expectedToken {
				t.Errorf("got unexpected client token, exp=%s got=%s",
					test.expectedToken, test.fakeClient.Token())
			}
		})
	}
}

type testAppRoleRefT struct {
	expectedRoleID   string
	expectedSecretID string
	expectedErr      error

	appRole *cmapi.VaultAppRole

	fakeLister *listers.FakeSecretLister
}

func TestAppRoleRef(t *testing.T) {
	errSecretGet := errors.New("no secret found")

	basicAppRoleRef := &cmapi.VaultAppRole{
		RoleId: "my-role-id",
	}

	tests := map[string]testAppRoleRefT{
		"failing to get secret should error": {
			appRole: basicAppRoleRef,
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(nil, errSecretGet),
			),
			expectedRoleID:   "",
			expectedSecretID: "",
			expectedErr:      errSecretGet,
		},

		"no data in key should fail": {
			appRole: &cmapi.VaultAppRole{
				RoleId: "",
				SecretRef: cmmeta.SecretKeySelector{
					LocalObjectReference: cmmeta.LocalObjectReference{
						Name: "secret-name",
					},
					Key: "my-key",
				},
			},
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(
					&corev1.Secret{
						Data: map[string][]byte{
							"foo": []byte("bar"),
						},
					}, nil),
			),
			expectedRoleID:   "",
			expectedSecretID: "",
			expectedErr:      errors.New(`no data for "my-key" in secret 'test-namespace/secret-name'`),
		},

		"should return roleID and secretID with trimmed space": {
			appRole: &cmapi.VaultAppRole{
				RoleId: "    my-role-id  ",
				SecretRef: cmmeta.SecretKeySelector{
					LocalObjectReference: cmmeta.LocalObjectReference{
						Name: "secret-name",
					},
					Key: "my-key",
				},
			},
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(
					&corev1.Secret{
						Data: map[string][]byte{
							"foo":    []byte("bar"),
							"my-key": []byte("    my-key-data   "),
						},
					}, nil),
			),
			expectedRoleID:   "my-role-id",
			expectedSecretID: "my-key-data",
			expectedErr:      nil,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			v := &Vault{
				namespace:     "test-namespace",
				secretsLister: test.fakeLister,
				issuer:        nil,
			}

			roleID, secretID, err := v.appRoleRef(test.appRole)
			if ((test.expectedErr == nil) != (err == nil)) &&
				test.expectedErr != nil &&
				test.expectedErr.Error() != err.Error() {
				t.Errorf("unexpected error, exp=%v got=%v",
					test.expectedErr, err)
			}

			if test.expectedRoleID != roleID {
				t.Errorf("got unexpected roleID, exp=%s got=%s",
					test.expectedRoleID, roleID)
			}

			if test.expectedSecretID != secretID {
				t.Errorf("got unexpected secretID, exp=%s got=%s",
					test.expectedSecretID, secretID)
			}
		})
	}
}

type testTokenRefT struct {
	expectedToken string
	expectedErr   error

	key string

	fakeLister *listers.FakeSecretLister
}

func TestTokenRef(t *testing.T) {
	errSecretGet := errors.New("no secret found")

	testName, testNamespace := "test-name", "test-namespace"

	tests := map[string]testTokenRefT{
		"failing to get secret should error": {
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(nil, errSecretGet),
			),
			key:           "a-key",
			expectedToken: "",
			expectedErr:   errSecretGet,
		},

		"if no vault at key exists then error": {
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(
					&corev1.Secret{
						Data: map[string][]byte{
							"foo": []byte("bar"),
						},
					}, nil),
			),

			key:           "a-key",
			expectedToken: "",
			expectedErr: fmt.Errorf(`no data for "a-key" in secret '%s/%s'`,
				testName, testNamespace),
		},
		"if value exists at key then return with whitespace trimmed": {
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(
					&corev1.Secret{
						Data: map[string][]byte{
							"foo":   []byte("bar"),
							"a-key": []byte(" my-token              "),
						},
					}, nil),
			),

			key:           "a-key",
			expectedToken: "<PASSWORD>",
		},
		"if no key is given then it should default to 'token'": {
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(
					&corev1.Secret{
						Data: map[string][]byte{
							"foo":   []byte("bar"),
							"token": []byte(" <PASSWORD>              "),
						},
					}, nil),
			),

			key:           "",
			expectedToken: "<PASSWORD>",
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			v := &Vault{
				namespace:     "test-namespace",
				secretsLister: test.fakeLister,
				issuer:        nil,
			}

			token, err := v.tokenRef("test-name", "test-namespace", test.key)
			if ((test.expectedErr == nil) != (err == nil)) &&
				test.expectedErr != nil &&
				test.expectedErr.Error() != err.Error() {
				t.Errorf("unexpected error, exp=%v got=%v",
					test.expectedErr, err)
			}

			if test.expectedToken != token {
				t.Errorf("got unexpected token, exp=%s got=%s",
					test.expectedToken, token)
			}
		})
	}
}

type testNewConfigT struct {
	expectedErr error
	issuer      *cmapi.Issuer
	checkFunc   func(cfg *vault.Config, err error) error

	fakeLister *listers.FakeSecretLister
}

func TestNewConfig(t *testing.T) {
	caBundleSecretRefFakeSecretLister := func(namespace, secret, key, cert string) *listers.FakeSecretLister {
		return listers.FakeSecretListerFrom(listers.NewFakeSecretLister(), func(f *listers.FakeSecretLister) {
			f.SecretsFn = func(namespace string) clientcorev1.SecretNamespaceLister {
				return listers.FakeSecretNamespaceListerFrom(listers.NewFakeSecretNamespaceLister(), func(fn *listers.FakeSecretNamespaceLister) {
					fn.GetFn = func(name string) (*corev1.Secret, error) {
						if name == secret && namespace == namespace {
							return &corev1.Secret{
								Data: map[string][]byte{
									key: []byte(cert),
								}}, nil
						}
						return nil, errors.New("unexpected secret name or namespace passed to FakeSecretLister")
					}
				})
			}
		})
	}
	tests := map[string]testNewConfigT{
		"no CA bundle set in issuer should return nil": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: nil,
				}),
			),
			expectedErr: nil,
		},

		"a bad cert bundle should error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte("a bad cert bundle"),
				}),
			),
			expectedErr: errors.New("no Vault CA bundles loaded, check bundle contents"),
		},

		"a good cert bundle should be added to the config": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundle: []byte(testLeafCertificate),
				}),
			),
			expectedErr: nil,
			checkFunc: func(cfg *vault.Config, error error) error {
				testCA := x509.NewCertPool()
				testCA.AppendCertsFromPEM([]byte(testLeafCertificate))
				subs := cfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs.Subjects()

				err := fmt.Errorf("got unexpected root CAs in config, exp=%s got=%s",
					testCA.Subjects(), subs)
				if len(subs) != len(testCA.Subjects()) {
					return err
				}
				for i := range subs {
					if !bytes.Equal(subs[i], testCA.Subjects()[i]) {
						return err
					}
				}

				return nil
			},
		},

		"a good bundle from a caBundleSecretRef should be added to the config": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundleSecretRef: &cmmeta.SecretKeySelector{
						Key: "my-bundle.crt",
						LocalObjectReference: cmmeta.LocalObjectReference{
							Name: "bundle",
						},
					},
				},
				)),
			checkFunc: func(cfg *vault.Config, error error) error {
				if error != nil {
					return error
				}

				testCA := x509.NewCertPool()
				testCA.AppendCertsFromPEM([]byte(testLeafCertificate))
				subs := cfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs.Subjects()

				err := fmt.Errorf("got unexpected root CAs in config, exp=%s got=%s",
					testCA.Subjects(), subs)
				if len(subs) != len(testCA.Subjects()) {
					return err
				}
				for i := range subs {
					if !bytes.Equal(subs[i], testCA.Subjects()[i]) {
						return err
					}
				}

				return nil
			},
			fakeLister: caBundleSecretRefFakeSecretLister("test-namespace", "bundle", "my-bundle.crt", testLeafCertificate),
		},
		"a good bundle from a caBundleSecretRef with default key should be added to the config": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundleSecretRef: &cmmeta.SecretKeySelector{
						LocalObjectReference: cmmeta.LocalObjectReference{
							Name: "bundle",
						},
					},
				},
				)),
			checkFunc: func(cfg *vault.Config, error error) error {
				if error != nil {
					return error
				}

				testCA := x509.NewCertPool()
				testCA.AppendCertsFromPEM([]byte(testLeafCertificate))
				subs := cfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs.Subjects()

				err := fmt.Errorf("got unexpected root CAs in config, exp=%s got=%s",
					testCA.Subjects(), subs)
				if len(subs) != len(testCA.Subjects()) {
					return err
				}
				for i := range subs {
					if !bytes.Equal(subs[i], testCA.Subjects()[i]) {
						return err
					}
				}

				return nil
			},
			fakeLister: caBundleSecretRefFakeSecretLister("test-namespace", "bundle", "ca.crt", testLeafCertificate),
		},
		"a bad bundle from a caBundleSecretRef should error": {
			issuer: gen.Issuer("vault-issuer",
				gen.SetIssuerVault(cmapi.VaultIssuer{
					CABundleSecretRef: &cmmeta.SecretKeySelector{
						Key: "my-bundle.crt",
						LocalObjectReference: cmmeta.LocalObjectReference{
							Name: "bundle",
						},
					},
				},
				)),
			expectedErr: errors.New("no Vault CA bundles loaded, check bundle contents"),
			fakeLister:  caBundleSecretRefFakeSecretLister("test-namespace", "bundle", "my-bundle.crt", "not a valid certificate"),
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			v := &Vault{
				namespace:     "test-namespace",
				secretsLister: test.fakeLister,
				issuer:        test.issuer,
			}

			cfg, err := v.newConfig()
			if test.expectedErr != nil && err != nil && test.expectedErr.Error() != err.Error() {
				t.Errorf("unexpected error, exp=%v got=%v", test.expectedErr, err)
			}

			if test.checkFunc != nil {
				if err := test.checkFunc(cfg, err); err != nil {
					t.Errorf("check function failed: %s", err)
				}
			}
		})
	}
}

type requestTokenWithAppRoleRefT struct {
	client  Client
	appRole *cmapi.VaultAppRole

	fakeLister *listers.FakeSecretLister

	expectedToken string
	expectedErr   error
}

func TestRequestTokenWithAppRoleRef(t *testing.T) {
	basicAppRoleRef := &cmapi.VaultAppRole{
		RoleId: "test-role-id",
		SecretRef: cmmeta.SecretKeySelector{
			LocalObjectReference: cmmeta.LocalObjectReference{
				Name: "test-secret",
			},
			Key: "my-key",
		},
	}

	basicSecretLister := listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
		listers.SetFakeSecretNamespaceListerGet(
			&corev1.Secret{
				Data: map[string][]byte{
					"my-key": []byte("my-key-data"),
				},
			}, nil),
	)

	tests := map[string]requestTokenWithAppRoleRefT{
		"a secret reference that does not exist should error": {
			client:  vaultfake.NewFakeClient(),
			appRole: basicAppRoleRef,
			fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
				listers.SetFakeSecretNamespaceListerGet(nil, errors.New("secret not found")),
			),

			expectedToken: "",
			expectedErr:   errors.New("secret not found"),
		},
		"if a raw request fails then error": {
			client:     vaultfake.NewFakeClient().WithRawRequest(nil, errors.New("request failed")),
			appRole:    basicAppRoleRef,
			fakeLister: basicSecretLister,

			expectedToken: "",
			expectedErr:   errors.New("error logging in to Vault server: request failed"),
		},
		"no id in the JSON response should return no token": {
			client: vaultfake.NewFakeClient().WithRawRequest(
				&vault.Response{
					Response: &http.Response{
						Body: io.NopCloser(
							strings.NewReader(
								`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":null,"warnings":null,"data":{}}`),
						),
					},
				}, nil,
			),
			appRole:    basicAppRoleRef,
			fakeLister: basicSecretLister,

			expectedToken: "",
			expectedErr:   errors.New("no token returned"),
		},
		"an id in the JSON response should return that token": {
			client: vaultfake.NewFakeClient().WithRawRequest(
				&vault.Response{
					Response: &http.Response{
						Body: io.NopCloser(
							strings.NewReader(
								`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":null,"warnings":null,"data":{"id":"my-token"}}`),
						),
					},
				}, nil,
			),
			appRole:    basicAppRoleRef,
			fakeLister: basicSecretLister,

			expectedToken: "my-<PASSWORD>",
			expectedErr:   nil,
		},
		"a client_token present should take president over id": {
			client: vaultfake.NewFakeClient().WithRawRequest(
				&vault.Response{
					Response: &http.Response{
						Body: io.NopCloser(
							strings.NewReader(
								`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":null,"warnings":null,"data":{"id":"my-token"},"auth":{"client_token":"<PASSWORD>"}}`),
						),
					},
				}, nil,
			),
			appRole:    basicAppRoleRef,
			fakeLister: basicSecretLister,

			expectedToken: "<PASSWORD>",
			expectedErr:   nil,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			v := &Vault{
				namespace:     "test-namespace",
				secretsLister: test.fakeLister,
				issuer: gen.Issuer("vault-issuer",
					gen.SetIssuerNamespace("namespace"),
				),
			}

			token, err := v.requestTokenWithAppRoleRef(test.client, test.appRole)
			if ((test.expectedErr == nil) != (err == nil)) &&
				test.expectedErr != nil &&
				test.expectedErr.Error() != err.Error() {
				t.Errorf("unexpected error, exp=%v got=%v",
					test.expectedErr, err)
			}

			if test.expectedToken != token {
				t.Errorf("got unexpected token, exp=%s got=%s",
					test.expectedToken, token)
			}
		})
	}
}

// TestNewWithVaultNamespaces demonstrates that New initializes two Vault
// clients, one with a namespace and one without a namespace which is used for
// interacting with root-only APIs.
func TestNewWithVaultNamespaces(t *testing.T) {
	type testCase struct {
		name    string
		vaultNS string
	}

	tests := []testCase{
		{
			name:    "without-namespace",
			vaultNS: "",
		},
		{
			name:    "with-namespace",
			vaultNS: "vault-ns-1",
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			c, err := New(
				"k8s-ns1",
				listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
					listers.SetFakeSecretNamespaceListerGet(
						&corev1.Secret{
							Data: map[string][]byte{
								"key1": []byte("not-used"),
							},
						}, nil),
				),
				&cmapi.Issuer{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "issuer1",
						Namespace: "k8s-ns1",
					},
					Spec: v1.IssuerSpec{
						IssuerConfig: v1.IssuerConfig{
							Vault: &v1.VaultIssuer{
								Namespace: tc.vaultNS,
								Auth: cmapi.VaultAuth{
									TokenSecretRef: &cmmeta.SecretKeySelector{
										LocalObjectReference: cmmeta.LocalObjectReference{
											Name: "secret1",
										},
										Key: "key1",
									},
								},
							},
						},
					},
				})
			require.NoError(t, err)
			assert.Equal(t, tc.vaultNS, c.(*Vault).client.(*vault.Client).Namespace(),
				"The vault client should have the namespace provided in the Issuer recource")
			assert.Equal(t, "", c.(*Vault).clientSys.(*vault.Client).Namespace(),
				"The vault sys client should never have a namespace")
		})
	}
}

// TestIsVaultInitiatedAndUnsealedIntegration demonstrates that it interacts only with the
// sys/health endpoint and that it supplies the Vault token but not a Vault namespace header.
func TestIsVaultInitiatedAndUnsealedIntegration(t *testing.T) {

	const vaultToken = "<PASSWORD>"

	mux := http.NewServeMux()
	mux.HandleFunc("/v1/sys/health", func(response http.ResponseWriter, request *http.Request) {
		assert.Empty(t, request.Header.Values("X-Vault-Namespace"), "Unexpected Vault namespace header for root-only API path")
		assert.Equal(t, vaultToken, request.Header.Get("X-Vault-Token"), "Expected the Vault token for root-only API path")
	})
	server := httptest.NewServer(mux)
	defer server.Close()

	v, err := New(
		"k8s-ns1",
		listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
			listers.SetFakeSecretNamespaceListerGet(
				&corev1.Secret{
					Data: map[string][]byte{
						"key1": []byte(vaultToken),
					},
				}, nil),
		),
		&cmapi.Issuer{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "issuer1",
				Namespace: "k8s-ns1",
			},
			Spec: v1.IssuerSpec{
				IssuerConfig: v1.IssuerConfig{
					Vault: &v1.VaultIssuer{
						Server:    server.URL,
						Namespace: "ns1",
						Auth: cmapi.VaultAuth{
							TokenSecretRef: &cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret1",
								},
								Key: "key1",
							},
						},
					},
				},
			},
		})
	require.NoError(t, err)

	err = v.IsVaultInitializedAndUnsealed()
	require.NoError(t, err)
}

// TestSignIntegration demonstrates that it interacts only with the API endpoint
// path supplied in the Issuer resource and that it supplies the Vault namespace
// and token to that endpoint.
func TestSignIntegration(t *testing.T) {
	const (
		vaultToken     = "<PASSWORD>"
		vaultNamespace = "vault-ns-1"
		vaultPath      = "my_pki_mount/sign/my-role-name"
	)

	privatekey := generateRSAPrivateKey(t)
	csrPEM := generateCSR(t, privatekey)

	rootBundleData, err := bundlePEM(testIntermediateCa, testRootCa)
	require.NoError(t, err)

	mux := http.NewServeMux()
	mux.HandleFunc(fmt.Sprintf("/v1/%s", vaultPath), func(response http.ResponseWriter, request *http.Request) {
		assert.Equal(t, vaultNamespace, request.Header.Get("X-Vault-Namespace"), "Expected Vault namespace header for namespaced API path")
		assert.Equal(t, vaultToken, request.Header.Get("X-Vault-Token"), "Expected the Vault token for root-only API path")
		_, err := response.Write(rootBundleData)
		require.NoError(t, err)
	})
	server := httptest.NewServer(mux)
	defer server.Close()

	v, err := New(
		"k8s-ns1",
		listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
			listers.SetFakeSecretNamespaceListerGet(
				&corev1.Secret{
					Data: map[string][]byte{
						"key1": []byte(vaultToken),
					},
				}, nil),
		),
		&cmapi.Issuer{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "issuer1",
				Namespace: "k8s-ns1",
			},
			Spec: v1.IssuerSpec{
				IssuerConfig: v1.IssuerConfig{
					Vault: &v1.VaultIssuer{
						Server:    server.URL,
						Path:      vaultPath,
						Namespace: vaultNamespace,
						Auth: cmapi.VaultAuth{
							TokenSecretRef: &cmmeta.SecretKeySelector{
								LocalObjectReference: cmmeta.LocalObjectReference{
									Name: "secret1",
								},
								Key: "key1",
							},
						},
					},
				},
			},
		})
	require.NoError(t, err)

	certPEM, caPEM, err := v.Sign(csrPEM, time.Hour)
	require.NoError(t, err)
	require.NotEmpty(t, certPEM)
	require.NotEmpty(t, caPEM)
}
{'fraction_non_alphanumeric': 0.13752910578847752, 'fraction_numerical': 0.0028458993178011685, 'mean_word_length': 3.380554727110051, 'pattern_counts': {'":': 185, '<': 28, '<?xml version=': 0, '>': 30, 'https://': 4, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5421632', 'n_tokens_mistral': 30605, 'n_tokens_neox': 24588, 'n_words': 4959}