/*
Copyright 2020 The Kubernetes 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 validation

import (
	"crypto/ed25519"
	"crypto/rand"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"fmt"
	"reflect"
	"strings"
	"testing"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/apimachinery/pkg/util/validation/field"
	"k8s.io/client-go/util/certificate/csr"
	capi "k8s.io/kubernetes/pkg/apis/certificates"
	capiv1beta1 "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
	"k8s.io/kubernetes/pkg/apis/core"
	"k8s.io/utils/pointer"
)

var (
	validObjectMeta = metav1.ObjectMeta{Name: "testcsr"}
	validSignerName = "example.com/valid-name"
	validUsages     = []capi.KeyUsage{capi.UsageKeyEncipherment}
)

func TestValidateCertificateSigningRequestCreate(t *testing.T) {
	specPath := field.NewPath("spec")
	// maxLengthSignerName is a signerName that is of maximum length, utilising
	// the max length specifications defined in validation.go.
	// It is of the form <fqdn(253)>/<resource-namespace(63)>.<resource-name(253)>
	maxLengthFQDN := fmt.Sprintf("%s.%s.%s.%s", repeatString("a", 63), repeatString("a", 63), repeatString("a", 63), repeatString("a", 61))
	maxLengthSignerName := fmt.Sprintf("%s/%s.%s", maxLengthFQDN, repeatString("a", 63), repeatString("a", 253))
	tests := map[string]struct {
		csr  capi.CertificateSigningRequest
		gv   schema.GroupVersion
		errs field.ErrorList
	}{
		"CSR with empty request data should fail": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     validUsages,
					SignerName: validSignerName,
				},
			},
			errs: field.ErrorList{
				field.Invalid(specPath.Child("request"), []byte(nil), "PEM block type must be CERTIFICATE REQUEST"),
			},
		},
		"CSR with invalid request data should fail": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     validUsages,
					SignerName: validSignerName,
					Request:    []byte("invalid data"),
				},
			},
			errs: field.ErrorList{
				field.Invalid(specPath.Child("request"), []byte("invalid data"), "PEM block type must be CERTIFICATE REQUEST"),
			},
		},
		"CSR with no usages should fail": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					SignerName: validSignerName,
					Request:    newCSRPEM(t),
				},
			},
			errs: field.ErrorList{
				field.Required(specPath.Child("usages"), "usages must be provided"),
			},
		},
		"CSR with no signerName set should fail": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:  validUsages,
					Request: newCSRPEM(t),
				},
			},
			errs: field.ErrorList{
				field.Required(specPath.Child("signerName"), "signerName must be provided"),
			},
		},
		"signerName contains no '/'": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     validUsages,
					Request:    newCSRPEM(t),
					SignerName: "an-invalid-signer-name",
				},
			},
			errs: field.ErrorList{
				field.Invalid(specPath.Child("signerName"), "an-invalid-signer-name", "must be a fully qualified domain and path of the form 'example.com/signer-name'"),
			},
		},
		"signerName contains two '/'": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     validUsages,
					Request:    newCSRPEM(t),
					SignerName: "an-invalid-signer-name.com/something/else",
				},
			},
			errs: field.ErrorList{
				field.Invalid(specPath.Child("signerName"), "an-invalid-signer-name.com/something/else", "must be a fully qualified domain and path of the form 'example.com/signer-name'"),
			},
		},
		"signerName domain component is not fully qualified": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     validUsages,
					Request:    newCSRPEM(t),
					SignerName: "example/some-signer-name",
				},
			},
			errs: field.ErrorList{
				field.Invalid(specPath.Child("signerName"), "example", "should be a domain with at least two segments separated by dots"),
			},
		},
		"signerName path component is empty": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     validUsages,
					Request:    newCSRPEM(t),
					SignerName: "example.com/",
				},
			},
			errs: field.ErrorList{
				field.Invalid(specPath.Child("signerName"), "", `validating label "": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
			},
		},
		"signerName path component ends with a symbol": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     validUsages,
					Request:    newCSRPEM(t),
					SignerName: "example.com/something-",
				},
			},
			errs: field.ErrorList{
				field.Invalid(specPath.Child("signerName"), "something-", `validating label "something-": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
			},
		},
		"signerName path component is a symbol": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     validUsages,
					Request:    newCSRPEM(t),
					SignerName: "example.com/-",
				},
			},
			errs: field.ErrorList{
				field.Invalid(specPath.Child("signerName"), "-", `validating label "-": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
			},
		},
		"signerName path component contains no '.' but is valid": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     validUsages,
					Request:    newCSRPEM(t),
					SignerName: "example.com/some-signer-name",
				},
			},
		},
		"signerName with a total length greater than 571 characters should be rejected": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:  validUsages,
					Request: newCSRPEM(t),
					// this string is longer than the max signerName limit (635 chars)
					SignerName: maxLengthSignerName + ".toolong",
				},
			},
			errs: field.ErrorList{
				field.TooLong(specPath.Child("signerName"), maxLengthSignerName+".toolong", len(maxLengthSignerName)),
			},
		},
		"signerName with a fqdn greater than 253 characters should be rejected": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:  validUsages,
					Request: newCSRPEM(t),
					// this string is longer than the max signerName limit (635 chars)
					SignerName: fmt.Sprintf("%s.extra/valid-path", maxLengthFQDN),
				},
			},
			errs: field.ErrorList{
				field.TooLong(specPath.Child("signerName"), fmt.Sprintf("%s.extra", maxLengthFQDN), len(maxLengthFQDN)),
			},
		},
		"signerName can have a longer path if the domain component is less than the max length": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     validUsages,
					Request:    newCSRPEM(t),
					SignerName: fmt.Sprintf("abc.io/%s.%s", repeatString("a", 253), repeatString("a", 253)),
				},
			},
		},
		"signerName with a domain label greater than 63 characters will fail": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     validUsages,
					Request:    newCSRPEM(t),
					SignerName: fmt.Sprintf("%s.example.io/valid-path", repeatString("a", 66)),
				},
			},
			errs: field.ErrorList{
				field.Invalid(specPath.Child("signerName"), fmt.Sprintf("%s.example.io", repeatString("a", 66)), fmt.Sprintf(`validating label "%s": must be no more than 63 characters`, repeatString("a", 66))),
			},
		},
		"signerName of max length in format <fully-qualified-domain-name>/<resource-namespace>.<resource-name> is valid": {
			// ensure signerName is of the form domain.com/something and up to 571 characters.
			// This length and format is specified to accommodate signerNames like:
			// <fqdn>/<resource-namespace>.<resource-name>.
			// The max length of a FQDN is 253 characters (DNS1123Subdomain max length)
			// The max length of a namespace name is 63 characters (DNS1123Label max length)
			// The max length of a resource name is 253 characters (DNS1123Subdomain max length)
			// We then add an additional 2 characters to account for the one '.' and one '/'.
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     validUsages,
					Request:    newCSRPEM(t),
					SignerName: maxLengthSignerName,
				},
			},
		},
		"negative duration": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:            validUsages,
					Request:           newCSRPEM(t),
					SignerName:        validSignerName,
					ExpirationSeconds: pointer.Int32(-1),
				},
			},
			errs: field.ErrorList{
				field.Invalid(specPath.Child("expirationSeconds"), int32(-1), "may not specify a duration less than 600 seconds (10 minutes)"),
			},
		},
		"zero duration": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:            validUsages,
					Request:           newCSRPEM(t),
					SignerName:        validSignerName,
					ExpirationSeconds: pointer.Int32(0),
				},
			},
			errs: field.ErrorList{
				field.Invalid(specPath.Child("expirationSeconds"), int32(0), "may not specify a duration less than 600 seconds (10 minutes)"),
			},
		},
		"one duration": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:            validUsages,
					Request:           newCSRPEM(t),
					SignerName:        validSignerName,
					ExpirationSeconds: pointer.Int32(1),
				},
			},
			errs: field.ErrorList{
				field.Invalid(specPath.Child("expirationSeconds"), int32(1), "may not specify a duration less than 600 seconds (10 minutes)"),
			},
		},
		"too short duration": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:            validUsages,
					Request:           newCSRPEM(t),
					SignerName:        validSignerName,
					ExpirationSeconds: csr.DurationToExpirationSeconds(time.Minute),
				},
			},
			errs: field.ErrorList{
				field.Invalid(specPath.Child("expirationSeconds"), *csr.DurationToExpirationSeconds(time.Minute), "may not specify a duration less than 600 seconds (10 minutes)"),
			},
		},
		"valid duration": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:            validUsages,
					Request:           newCSRPEM(t),
					SignerName:        validSignerName,
					ExpirationSeconds: csr.DurationToExpirationSeconds(10 * time.Minute),
				},
			},
		},
		"missing usages": {
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     []capi.KeyUsage{},
					Request:    newCSRPEM(t),
					SignerName: validSignerName,
				},
			},
			errs: field.ErrorList{
				field.Required(specPath.Child("usages"), "usages must be provided"),
			},
		},
		"unknown and duplicate usages - v1beta1": {
			gv: schema.GroupVersion{Group: capi.SchemeGroupVersion.Group, Version: "v1beta1"},
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     []capi.KeyUsage{"unknown", "unknown"},
					Request:    newCSRPEM(t),
					SignerName: validSignerName,
				},
			},
		},
		"unknown and duplicate usages - v1": {
			gv: schema.GroupVersion{Group: capi.SchemeGroupVersion.Group, Version: "v1"},
			csr: capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: capi.CertificateSigningRequestSpec{
					Usages:     []capi.KeyUsage{"unknown", "unknown"},
					Request:    newCSRPEM(t),
					SignerName: validSignerName,
				},
			},
			errs: field.ErrorList{
				field.NotSupported(specPath.Child("usages").Index(0), capi.KeyUsage("unknown"), allValidUsages.List()),
				field.NotSupported(specPath.Child("usages").Index(1), capi.KeyUsage("unknown"), allValidUsages.List()),
				field.Duplicate(specPath.Child("usages").Index(1), capi.KeyUsage("unknown")),
			},
		},
	}
	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			el := ValidateCertificateSigningRequestCreate(&test.csr, test.gv)
			if !reflect.DeepEqual(el, test.errs) {
				t.Errorf("returned and expected errors did not match - expected\n%v\nbut got\n%v", test.errs.ToAggregate(), el.ToAggregate())
			}
		})
	}
}

func repeatString(s string, num int) string {
	l := make([]string, num)
	for i := 0; i < num; i++ {
		l[i] = s
	}
	return strings.Join(l, "")
}

func newCSRPEM(t *testing.T) []byte {
	template := &x509.CertificateRequest{
		Subject: pkix.Name{
			Organization: []string{"testing-org"},
		},
	}

	_, key, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatal(err)
	}

	csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, key)
	if err != nil {
		t.Fatal(err)
	}

	csrPemBlock := &pem.Block{
		Type:  "CERTIFICATE REQUEST",
		Bytes: csrDER,
	}

	p := pem.EncodeToMemory(csrPemBlock)
	if p == nil {
		t.Fatal("invalid pem block")
	}

	return p
}

func Test_getValidationOptions(t *testing.T) {
	tests := []struct {
		name    string
		version schema.GroupVersion
		newCSR  *capi.CertificateSigningRequest
		oldCSR  *capi.CertificateSigningRequest
		want    certificateValidationOptions
	}{
		{
			name:    "v1beta1 compatible create",
			version: capiv1beta1.SchemeGroupVersion,
			oldCSR:  nil,
			want: certificateValidationOptions{
				allowResettingCertificate:    true,
				allowBothApprovedAndDenied:   false,
				allowLegacySignerName:        true,
				allowDuplicateConditionTypes: true,
				allowEmptyConditionType:      true,
				allowArbitraryCertificate:    true,
				allowUnknownUsages:           true,
				allowDuplicateUsages:         true,
			},
		},
		{
			name:    "v1 strict create",
			version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"},
			oldCSR:  nil,
			want:    certificateValidationOptions{},
		},
		{
			name:    "v1beta1 compatible update",
			version: capiv1beta1.SchemeGroupVersion,
			oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved}, {Type: capi.CertificateDenied}},
			}},
			want: certificateValidationOptions{
				allowResettingCertificate:    true,
				allowBothApprovedAndDenied:   true, // existing object has both approved and denied
				allowLegacySignerName:        true,
				allowDuplicateConditionTypes: true,
				allowEmptyConditionType:      true,
				allowArbitraryCertificate:    true,
				allowUnknownUsages:           true,
				allowDuplicateUsages:         true,
			},
		},
		{
			name:    "v1 strict update",
			version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"},
			oldCSR:  &capi.CertificateSigningRequest{},
			want:    certificateValidationOptions{},
		},
		{
			name:    "v1 compatible update, approved+denied",
			version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"},
			oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved}, {Type: capi.CertificateDenied}},
			}},
			want: certificateValidationOptions{
				allowBothApprovedAndDenied: true,
			},
		},
		{
			name:    "v1 compatible update, legacy signerName",
			version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"},
			oldCSR:  &capi.CertificateSigningRequest{Spec: capi.CertificateSigningRequestSpec{SignerName: capi.LegacyUnknownSignerName}},
			want: certificateValidationOptions{
				allowLegacySignerName: true,
			},
		},
		{
			name:    "v1 compatible update, duplicate condition types",
			version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"},
			oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved}, {Type: capi.CertificateApproved}},
			}},
			want: certificateValidationOptions{
				allowDuplicateConditionTypes: true,
			},
		},
		{
			name:    "v1 compatible update, empty condition types",
			version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"},
			oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{}},
			}},
			want: certificateValidationOptions{
				allowEmptyConditionType: true,
			},
		},
		{
			name:    "v1 compatible update, no diff to certificate",
			version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"},
			newCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
				Certificate: validCertificate,
			}},
			oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
				Certificate: validCertificate,
			}},
			want: certificateValidationOptions{
				allowArbitraryCertificate: true,
			},
		},
		{
			name:    "v1 compatible update, existing invalid certificate",
			version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"},
			newCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
				Certificate: []byte(`new - no PEM blocks`),
			}},
			oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
				Certificate: []byte(`old - no PEM blocks`),
			}},
			want: certificateValidationOptions{
				allowArbitraryCertificate: true,
			},
		},
		{
			name:    "v1 compatible update, existing unknown usages",
			version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"},
			oldCSR:  &capi.CertificateSigningRequest{Spec: capi.CertificateSigningRequestSpec{Usages: []capi.KeyUsage{"unknown"}}},
			want: certificateValidationOptions{
				allowUnknownUsages: true,
			},
		},
		{
			name:    "v1 compatible update, existing duplicate usages",
			version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"},
			oldCSR:  &capi.CertificateSigningRequest{Spec: capi.CertificateSigningRequestSpec{Usages: []capi.KeyUsage{"any", "any"}}},
			want: certificateValidationOptions{
				allowDuplicateUsages: true,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := getValidationOptions(tt.version, tt.newCSR, tt.oldCSR); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("got  %#v\nwant %#v", got, tt.want)
			}
		})
	}
}

func TestValidateCertificateSigningRequestUpdate(t *testing.T) {
	validUpdateMeta := validObjectMeta
	validUpdateMeta.ResourceVersion = "1"

	validUpdateMetaWithFinalizers := validUpdateMeta
	validUpdateMetaWithFinalizers.Finalizers = []string{"foo"}

	validSpec := capi.CertificateSigningRequestSpec{
		Usages:     validUsages,
		Request:    newCSRPEM(t),
		SignerName: "example.com/something",
	}

	tests := []struct {
		name        string
		newCSR      *capi.CertificateSigningRequest
		oldCSR      *capi.CertificateSigningRequest
		versionErrs map[string][]string
	}{
		{
			name:   "no-op",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
		},
		{
			name:   "finalizer change with invalid status",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}},
		},
		{
			name: "add Approved condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
			}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
			versionErrs: map[string][]string{
				"v1":      {`status.conditions: Forbidden: updates may not add a condition of type "Approved"`},
				"v1beta1": {`status.conditions: Forbidden: updates may not add a condition of type "Approved"`},
			},
		},
		{
			name:   "remove Approved condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
			}},
			versionErrs: map[string][]string{
				"v1":      {`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`},
				"v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`},
			},
		},
		{
			name: "add Denied condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
			}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
			versionErrs: map[string][]string{
				"v1":      {`status.conditions: Forbidden: updates may not add a condition of type "Denied"`},
				"v1beta1": {`status.conditions: Forbidden: updates may not add a condition of type "Denied"`},
			},
		},
		{
			name:   "remove Denied condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
			}},
			versionErrs: map[string][]string{
				"v1":      {`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`},
				"v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`},
			},
		},
		{
			name: "add Failed condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
			}},
			oldCSR:      &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
			versionErrs: map[string][]string{},
		},
		{
			name:   "remove Failed condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
			}},
			versionErrs: map[string][]string{
				"v1":      {`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`},
				"v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`},
			},
		},
		{
			name: "set certificate",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Certificate: validCertificate,
			}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
			versionErrs: map[string][]string{
				"v1":      {`status.certificate: Forbidden: updates may not set certificate content`},
				"v1beta1": {`status.certificate: Forbidden: updates may not set certificate content`},
			},
		},
	}

	for _, tt := range tests {
		for _, version := range []string{"v1", "v1beta1"} {
			t.Run(tt.name+"_"+version, func(t *testing.T) {
				gotErrs := sets.NewString()
				for _, err := range ValidateCertificateSigningRequestUpdate(tt.newCSR, tt.oldCSR, schema.GroupVersion{Group: capi.GroupName, Version: version}) {
					gotErrs.Insert(err.Error())
				}
				wantErrs := sets.NewString(tt.versionErrs[version]...)
				for _, missing := range wantErrs.Difference(gotErrs).List() {
					t.Errorf("missing expected error: %s", missing)
				}
				for _, unexpected := range gotErrs.Difference(wantErrs).List() {
					t.Errorf("unexpected error: %s", unexpected)
				}
			})
		}
	}
}

func TestValidateCertificateSigningRequestStatusUpdate(t *testing.T) {
	validUpdateMeta := validObjectMeta
	validUpdateMeta.ResourceVersion = "1"

	validUpdateMetaWithFinalizers := validUpdateMeta
	validUpdateMetaWithFinalizers.Finalizers = []string{"foo"}

	validSpec := capi.CertificateSigningRequestSpec{
		Usages:     validUsages,
		Request:    newCSRPEM(t),
		SignerName: "example.com/something",
	}

	tests := []struct {
		name        string
		newCSR      *capi.CertificateSigningRequest
		oldCSR      *capi.CertificateSigningRequest
		versionErrs map[string][]string
	}{
		{
			name:   "no-op",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
		},
		{
			name:   "finalizer change with invalid status",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}},
		},
		{
			name: "finalizer change with duplicate and unknown usages",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: capi.CertificateSigningRequestSpec{
				Usages:     []capi.KeyUsage{"unknown", "unknown"},
				Request:    newCSRPEM(t),
				SignerName: validSignerName,
			}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: capi.CertificateSigningRequestSpec{
				Usages:     []capi.KeyUsage{"unknown", "unknown"},
				Request:    newCSRPEM(t),
				SignerName: validSignerName,
			}},
		},
		{
			name: "add Approved condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
			}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
			versionErrs: map[string][]string{
				"v1":      {`status.conditions: Forbidden: updates may not add a condition of type "Approved"`},
				"v1beta1": {`status.conditions: Forbidden: updates may not add a condition of type "Approved"`},
			},
		},
		{
			name:   "remove Approved condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
			}},
			versionErrs: map[string][]string{
				"v1":      {`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`},
				"v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`},
			},
		},
		{
			name: "add Denied condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
			}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
			versionErrs: map[string][]string{
				"v1":      {`status.conditions: Forbidden: updates may not add a condition of type "Denied"`},
				"v1beta1": {`status.conditions: Forbidden: updates may not add a condition of type "Denied"`},
			},
		},
		{
			name:   "remove Denied condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
			}},
			versionErrs: map[string][]string{
				"v1":      {`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`},
				"v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`},
			},
		},
		{
			name: "add Failed condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
			}},
			oldCSR:      &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
			versionErrs: map[string][]string{},
		},
		{
			name:   "remove Failed condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
			}},
			versionErrs: map[string][]string{
				"v1":      {`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`},
				"v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`},
			},
		},
		{
			name: "set valid certificate",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Certificate: validCertificate,
			}},
			oldCSR:      &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
			versionErrs: map[string][]string{},
		},
		{
			name: "set invalid certificate",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Certificate: invalidCertificateNoPEM,
			}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
			versionErrs: map[string][]string{
				"v1": {`status.certificate: Invalid value: "<certificate data>": must contain at least one CERTIFICATE PEM block`},
			},
		},
		{
			name: "reset certificate",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Certificate: invalidCertificateNonCertificatePEM,
			}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Certificate: invalidCertificateNoPEM,
			}},
			versionErrs: map[string][]string{
				"v1": {`status.certificate: Forbidden: updates may not modify existing certificate content`},
			},
		},
	}

	for _, tt := range tests {
		for _, version := range []string{"v1", "v1beta1"} {
			t.Run(tt.name+"_"+version, func(t *testing.T) {
				gotErrs := sets.NewString()
				for _, err := range ValidateCertificateSigningRequestStatusUpdate(tt.newCSR, tt.oldCSR, schema.GroupVersion{Group: capi.GroupName, Version: version}) {
					gotErrs.Insert(err.Error())
				}
				wantErrs := sets.NewString(tt.versionErrs[version]...)
				for _, missing := range wantErrs.Difference(gotErrs).List() {
					t.Errorf("missing expected error: %s", missing)
				}
				for _, unexpected := range gotErrs.Difference(wantErrs).List() {
					t.Errorf("unexpected error: %s", unexpected)
				}
			})
		}
	}
}

func TestValidateCertificateSigningRequestApprovalUpdate(t *testing.T) {
	validUpdateMeta := validObjectMeta
	validUpdateMeta.ResourceVersion = "1"

	validUpdateMetaWithFinalizers := validUpdateMeta
	validUpdateMetaWithFinalizers.Finalizers = []string{"foo"}

	validSpec := capi.CertificateSigningRequestSpec{
		Usages:     validUsages,
		Request:    newCSRPEM(t),
		SignerName: "example.com/something",
	}

	tests := []struct {
		name        string
		newCSR      *capi.CertificateSigningRequest
		oldCSR      *capi.CertificateSigningRequest
		versionErrs map[string][]string
	}{
		{
			name:   "no-op",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
		},
		{
			name:   "finalizer change with invalid certificate",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}},
		},
		{
			name: "add Approved condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
			}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
		},
		{
			name:   "remove Approved condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
			}},
			versionErrs: map[string][]string{
				"v1":      {`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`},
				"v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`},
			},
		},
		{
			name: "add Denied condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
			}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
		},
		{
			name:   "remove Denied condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
			}},
			versionErrs: map[string][]string{
				"v1":      {`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`},
				"v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`},
			},
		},
		{
			name: "add Failed condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
			}},
			oldCSR:      &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
			versionErrs: map[string][]string{},
		},
		{
			name:   "remove Failed condition",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
			}},
			versionErrs: map[string][]string{
				"v1":      {`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`},
				"v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`},
			},
		},
		{
			name: "set certificate",
			newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
				Certificate: validCertificate,
			}},
			oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
			versionErrs: map[string][]string{
				"v1":      {`status.certificate: Forbidden: updates may not set certificate content`},
				"v1beta1": {`status.certificate: Forbidden: updates may not set certificate content`},
			},
		},
	}

	for _, tt := range tests {
		for _, version := range []string{"v1", "v1beta1"} {
			t.Run(tt.name+"_"+version, func(t *testing.T) {
				gotErrs := sets.NewString()
				for _, err := range ValidateCertificateSigningRequestApprovalUpdate(tt.newCSR, tt.oldCSR, schema.GroupVersion{Group: capi.GroupName, Version: version}) {
					gotErrs.Insert(err.Error())
				}
				wantErrs := sets.NewString(tt.versionErrs[version]...)
				for _, missing := range wantErrs.Difference(gotErrs).List() {
					t.Errorf("missing expected error: %s", missing)
				}
				for _, unexpected := range gotErrs.Difference(wantErrs).List() {
					t.Errorf("unexpected error: %s", unexpected)
				}
			})
		}
	}
}

// Test_validateCertificateSigningRequestOptions verifies validation options are effective in tolerating specific aspects of CSRs
func Test_validateCertificateSigningRequestOptions(t *testing.T) {
	validSpec := capi.CertificateSigningRequestSpec{
		Usages:     validUsages,
		Request:    newCSRPEM(t),
		SignerName: "example.com/something",
	}

	tests := []struct {
		// testcase name
		name string

		// csr being validated
		csr *capi.CertificateSigningRequest

		// options that allow the csr to pass validation
		lenientOpts certificateValidationOptions

		// expected errors when validating strictly
		strictErrs []string
	}{
		// valid strict cases
		{
			name: "no status",
			csr:  &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec},
		},
		{
			name: "approved condition",
			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
				Status: capi.CertificateSigningRequestStatus{
					Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
				},
			},
		},
		{
			name: "denied condition",
			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
				Status: capi.CertificateSigningRequestStatus{
					Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
				},
			},
		},
		{
			name: "failed condition",
			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
				Status: capi.CertificateSigningRequestStatus{
					Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
				},
			},
		},
		{
			name: "approved+issued",
			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
				Status: capi.CertificateSigningRequestStatus{
					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
					Certificate: validCertificate,
				},
			},
		},

		// legacy signer
		{
			name: "legacy signer",
			csr: &capi.CertificateSigningRequest{
				ObjectMeta: validObjectMeta,
				Spec: func() capi.CertificateSigningRequestSpec {
					specCopy := validSpec
					specCopy.SignerName = capi.LegacyUnknownSignerName
					return specCopy
				}(),
			},
			lenientOpts: certificateValidationOptions{allowLegacySignerName: true},
			strictErrs:  []string{`spec.signerName: Invalid value: "kubernetes.io/legacy-unknown": the legacy signerName is not allowed via this API version`},
		},

		// invalid condition cases
		{
			name: "empty condition type",
			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
				Status: capi.CertificateSigningRequestStatus{
					Conditions: []capi.CertificateSigningRequestCondition{{Status: core.ConditionTrue}},
				},
			},
			lenientOpts: certificateValidationOptions{allowEmptyConditionType: true},
			strictErrs:  []string{`status.conditions[0].type: Required value`},
		},
		{
			name: "approved and denied",
			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
				Status: capi.CertificateSigningRequestStatus{
					Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}, {Type: capi.CertificateDenied, Status: core.ConditionTrue}},
				},
			},
			lenientOpts: certificateValidationOptions{allowBothApprovedAndDenied: true},
			strictErrs:  []string{`status.conditions[1].type: Invalid value: "Denied": Approved and Denied conditions are mutually exclusive`},
		},
		{
			name: "duplicate condition",
			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
				Status: capi.CertificateSigningRequestStatus{
					Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}, {Type: capi.CertificateApproved, Status: core.ConditionTrue}},
				},
			},
			lenientOpts: certificateValidationOptions{allowDuplicateConditionTypes: true},
			strictErrs:  []string{`status.conditions[1].type: Duplicate value: "Approved"`},
		},

		// invalid allowArbitraryCertificate cases
		{
			name: "status.certificate, no PEM",
			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
				Status: capi.CertificateSigningRequestStatus{
					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
					Certificate: invalidCertificateNoPEM,
				},
			},
			lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true},
			strictErrs:  []string{`status.certificate: Invalid value: "<certificate data>": must contain at least one CERTIFICATE PEM block`},
		},
		{
			name: "status.certificate, non-CERTIFICATE PEM",
			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
				Status: capi.CertificateSigningRequestStatus{
					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
					Certificate: invalidCertificateNonCertificatePEM,
				},
			},
			lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true},
			strictErrs:  []string{`status.certificate: Invalid value: "<certificate data>": only CERTIFICATE PEM blocks are allowed, found "CERTIFICATE1"`},
		},
		{
			name: "status.certificate, PEM headers",
			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
				Status: capi.CertificateSigningRequestStatus{
					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
					Certificate: invalidCertificatePEMHeaders,
				},
			},
			lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true},
			strictErrs:  []string{`status.certificate: Invalid value: "<certificate data>": no PEM block headers are permitted`},
		},
		{
			name: "status.certificate, non-base64 PEM",
			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
				Status: capi.CertificateSigningRequestStatus{
					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
					Certificate: invalidCertificateNonBase64PEM,
				},
			},
			lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true},
			strictErrs:  []string{`status.certificate: Invalid value: "<certificate data>": must contain at least one CERTIFICATE PEM block`},
		},
		{
			name: "status.certificate, empty PEM block",
			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
				Status: capi.CertificateSigningRequestStatus{
					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
					Certificate: invalidCertificateEmptyPEM,
				},
			},
			lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true},
			strictErrs:  []string{`status.certificate: Invalid value: "<certificate data>": found CERTIFICATE PEM block containing 0 certificates`},
		},
		{
			name: "status.certificate, non-ASN1 data",
			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
				Status: capi.CertificateSigningRequestStatus{
					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
					Certificate: invalidCertificateNonASN1Data,
				},
			},
			lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true},
			strictErrs:  []string{`status.certificate: Invalid value: "<certificate data>": asn1: structure error: sequence tag mismatch`},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// make sure the lenient options validate with no errors
			for _, err := range validateCertificateSigningRequest(tt.csr, tt.lenientOpts) {
				t.Errorf("unexpected error with lenient options: %s", err.Error())
			}

			// make sure the strict options produce the expected errors
			gotErrs := sets.NewString()
			for _, err := range validateCertificateSigningRequest(tt.csr, certificateValidationOptions{}) {
				gotErrs.Insert(err.Error())
			}
			wantErrs := sets.NewString(tt.strictErrs...)
			for _, missing := range wantErrs.Difference(gotErrs).List() {
				t.Errorf("missing expected strict error: %s", missing)
			}
			for _, unexpected := range gotErrs.Difference(wantErrs).List() {
				t.Errorf("unexpected strict error: %s", unexpected)
			}
		})
	}
}

var (
	validCertificate = []byte(`
Leading non-PEM content
-----BEGIN CERTIFICATE-----
MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw
GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx
MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb
KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC
BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q
a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5
MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps=
-----END CERTIFICATE-----
Intermediate non-PEM content
-----BEGIN CERTIFICATE-----
MIIBqDCCAU6gAwIBAgIUfqZtjoFgczZ+oQZbEC/BDSS2J6wwCgYIKoZIzj0EAwIw
EjEQMA4GA1UEAxMHUm9vdC1DQTAgFw0xNjEwMTEwNTA2MDBaGA8yMTE2MDkxNzA1
MDYwMFowGjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMFkwEwYHKoZIzj0CAQYI
KoZIzj0DAQcDQgAEyWHEMMCctJg8Xa5YWLqaCPbk3MjB+uvXac42JM9pj4k9jedD
kpUJRkWIPzgJI8Zk/3cSzluUTixP6JBSDKtwwaN4MHYwDgYDVR0PAQH/BAQDAgGm
MBMGA1UdJQQMMAoGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
FF+p0JcY31pz+mjNZnjv0Gum92vZMB8GA1UdIwQYMBaAFB7P6+i4/pfNjqZgJv/b
dgA7Fe4tMAoGCCqGSM49BAMCA0gAMEUCIQCTT1YWQZaAqfQ2oBxzOkJE2BqLFxhz
3smQlrZ5gCHddwIgcvT7puhYOzAgcvMn9+SZ1JOyZ7edODjshCVCRnuHK2c=
-----END CERTIFICATE-----
Trailing non-PEM content
`)

	invalidCertificateNoPEM = []byte(`no PEM content`)

	invalidCertificateNonCertificatePEM = []byte(`
Leading non-PEM content
-----BEGIN CERTIFICATE1-----
MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw
GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx
MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb
KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC
BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q
a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5
MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps=
-----END CERTIFICATE1-----
Trailing non-PEM content
`)

	invalidCertificatePEMHeaders = []byte(`
Leading non-PEM content
-----BEGIN CERTIFICATE-----
Some-Header: Some-Value
MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw
GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx
MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb
KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC
BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q
a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5
MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps=
-----END CERTIFICATE-----
Trailing non-PEM content
`)

	invalidCertificateNonBase64PEM = []byte(`
Leading non-PEM content
-----BEGIN CERTIFICATE-----
MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw
GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx
MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb
KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC
BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q
a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5
MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1d?????????
-----END CERTIFICATE-----
Trailing non-PEM content
`)

	invalidCertificateEmptyPEM = []byte(`
Leading non-PEM content
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
Trailing non-PEM content
`)

	// first character is invalid
	invalidCertificateNonASN1Data = []byte(`
Leading non-PEM content
-----BEGIN CERTIFICATE-----
MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw
GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx
MTYwOTE3MDUwNjAwWjAUNRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb
KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC
BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q
a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5
MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps=
-----END CERTIFICATE-----
Trailing non-PEM content
`)
)
