cert-manager/pkg/controller/acmeorders/sync_test.go
James Munnelly d323a1df0d Add unit tests for acmeorders and acmechallenges
Signed-off-by: James Munnelly <james@munnelly.eu>
2018-10-12 12:40:39 +01:00

446 lines
14 KiB
Go

package acmeorders
import (
"context"
"reflect"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/diff"
coretesting "k8s.io/client-go/testing"
fakeclock "k8s.io/utils/clock/testing"
acmecl "github.com/jetstack/cert-manager/pkg/acme/client"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
testpkg "github.com/jetstack/cert-manager/pkg/controller/test"
acmeapi "github.com/jetstack/cert-manager/third_party/crypto/acme"
)
func TestSyncHappyPath(t *testing.T) {
nowTime := time.Now()
nowMetaTime := metav1.NewTime(nowTime)
fixedClock := fakeclock.NewFakeClock(nowTime)
testIssuerHTTP01Enabled := &v1alpha1.Issuer{
Spec: v1alpha1.IssuerSpec{
IssuerConfig: v1alpha1.IssuerConfig{
ACME: &v1alpha1.ACMEIssuer{
HTTP01: &v1alpha1.ACMEIssuerHTTP01Config{},
},
},
},
}
// build actual test fixtures
testOrder := &v1alpha1.Order{
ObjectMeta: metav1.ObjectMeta{Name: "testorder", Namespace: "default"},
Spec: v1alpha1.OrderSpec{
CommonName: "test.com",
Config: []v1alpha1.DomainSolverConfig{
{
Domains: []string{"test.com"},
SolverConfig: v1alpha1.SolverConfig{HTTP01: &v1alpha1.HTTP01SolverConfig{}},
},
},
},
}
testOrderPending := testOrder.DeepCopy()
testOrderPending.Status = v1alpha1.OrderStatus{
State: v1alpha1.Pending,
URL: "http://testurl.com/abcde",
FinalizeURL: "http://testurl.com/abcde/finalize",
Challenges: []v1alpha1.ChallengeSpec{
{
AuthzURL: "http://authzurl",
Type: "http-01",
Token: "token",
DNSName: "test.com",
Key: "key",
Config: v1alpha1.SolverConfig{
HTTP01: &v1alpha1.HTTP01SolverConfig{
Ingress: "",
},
},
},
},
}
testOrderInvalid := testOrderPending.DeepCopy()
testOrderInvalid.Status.State = v1alpha1.Invalid
testOrderInvalid.Status.FailureTime = &nowMetaTime
testOrderValid := testOrderPending.DeepCopy()
testOrderValid.Status.State = v1alpha1.Valid
testOrderReady := testOrderPending.DeepCopy()
testOrderReady.Status.State = v1alpha1.Ready
testAuthorizationChallenge := buildChallenge(0, testOrderPending, testOrderPending.Status.Challenges[0])
testAuthorizationChallengeValid := testAuthorizationChallenge.DeepCopy()
testAuthorizationChallengeValid.Status.State = v1alpha1.Valid
testAuthorizationChallengeInvalid := testAuthorizationChallenge.DeepCopy()
testAuthorizationChallengeInvalid.Status.State = v1alpha1.Invalid
testACMEAuthorizationPending := &acmeapi.Authorization{
URL: "http://authzurl",
Status: acmeapi.StatusPending,
Identifier: acmeapi.AuthzID{
Value: "test.com",
},
Challenges: []*acmeapi.Challenge{
{
Type: "http-01",
Token: "token",
},
},
}
testACMEOrderPending := &acmeapi.Order{
URL: testOrderPending.Status.URL,
Identifiers: []acmeapi.AuthzID{
{
Type: "dns",
Value: "test.com",
},
},
FinalizeURL: testOrderPending.Status.FinalizeURL,
Authorizations: []string{"http://authzurl"},
Status: acmeapi.StatusPending,
}
// shallow copy
testACMEOrderValid := &acmeapi.Order{}
*testACMEOrderValid = *testACMEOrderPending
testACMEOrderValid.Status = acmeapi.StatusValid
// shallow copy
testACMEOrderReady := &acmeapi.Order{}
*testACMEOrderReady = *testACMEOrderPending
testACMEOrderReady.Status = acmeapi.StatusReady
// shallow copy
testACMEOrderInvalid := &acmeapi.Order{}
*testACMEOrderInvalid = *testACMEOrderPending
testACMEOrderInvalid.Status = acmeapi.StatusInvalid
tests := map[string]controllerFixture{
"create a new order with the acme server, set the order url on the status resource and return nil to avoid cache timing issues": {
Issuer: testIssuerHTTP01Enabled,
Order: testOrder,
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testOrder},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateAction(v1alpha1.SchemeGroupVersion.WithResource("orders"), testOrderPending.Namespace, testOrderPending)),
},
},
Client: &acmecl.FakeACME{
FakeCreateOrder: func(ctx context.Context, o *acmeapi.Order) (*acmeapi.Order, error) {
return testACMEOrderPending, nil
},
FakeGetAuthorization: func(ctx context.Context, url string) (*acmeapi.Authorization, error) {
// TODO: assert url = "http://authzurl"
return testACMEAuthorizationPending, nil
},
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"create a challenge resource for the test.com dnsName on the order": {
Issuer: testIssuerHTTP01Enabled,
Order: testOrderPending,
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testOrder},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewCreateAction(v1alpha1.SchemeGroupVersion.WithResource("challenges"), testAuthorizationChallenge.Namespace, testAuthorizationChallenge)),
},
},
Client: &acmecl.FakeACME{},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"do nothing if the challenge for test.com is still pending": {
Issuer: testIssuerHTTP01Enabled,
Order: testOrderPending,
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testOrder, testAuthorizationChallenge},
ExpectedActions: []testpkg.Action{},
},
Client: &acmecl.FakeACME{},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"call GetOrder and update the order state to 'ready' if all challenges are 'valid'": {
Order: testOrderPending,
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testOrderPending, testAuthorizationChallengeValid},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateAction(v1alpha1.SchemeGroupVersion.WithResource("orders"), testOrderReady.Namespace, testOrderReady)),
},
},
Client: &acmecl.FakeACME{
FakeGetOrder: func(_ context.Context, url string) (*acmeapi.Order, error) {
return testACMEOrderReady, nil
},
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"call FinalizeOrder and update the order state to 'valid' if finalize succeeds": {
Order: testOrderReady,
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testOrderValid, testAuthorizationChallengeValid},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateAction(v1alpha1.SchemeGroupVersion.WithResource("orders"), testOrderValid.Namespace, testOrderValid)),
},
},
Client: &acmecl.FakeACME{
FakeGetOrder: func(_ context.Context, url string) (*acmeapi.Order, error) {
return testACMEOrderValid, nil
},
FakeFinalizeOrder: func(_ context.Context, url string, csr []byte) ([][]byte, error) {
// Order controller does not currently use the 'der' field, so
// for now we can return nil, nil.
return nil, nil
},
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"call GetOrder and update the order state if the challenge is 'failed'": {
Order: testOrderPending,
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testOrderPending, testAuthorizationChallengeInvalid},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateAction(v1alpha1.SchemeGroupVersion.WithResource("orders"), testOrderInvalid.Namespace, testOrderInvalid)),
},
},
Client: &acmecl.FakeACME{
FakeGetOrder: func(_ context.Context, url string) (*acmeapi.Order, error) {
return testACMEOrderInvalid, nil
},
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"should leave the order state as-is if the challenge is marked invalid but the acme order is pending": {
Order: testOrderPending,
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testOrderPending, testAuthorizationChallengeInvalid},
ExpectedActions: []testpkg.Action{},
},
Client: &acmecl.FakeACME{
FakeGetOrder: func(_ context.Context, url string) (*acmeapi.Order, error) {
return testACMEOrderPending, nil
},
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"do nothing if the order is valid": {
Issuer: testIssuerHTTP01Enabled,
Order: testOrderValid,
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testOrderValid},
ExpectedActions: []testpkg.Action{},
},
Client: &acmecl.FakeACME{},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"do nothing if the order is failed": {
Issuer: testIssuerHTTP01Enabled,
Order: testOrderInvalid,
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testOrderInvalid},
ExpectedActions: []testpkg.Action{},
},
Client: &acmecl.FakeACME{},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
if test.Builder == nil {
test.Builder = &testpkg.Builder{}
}
if test.Clock == nil {
test.Clock = fixedClock
}
test.Setup(t)
orderCopy := test.Order.DeepCopy()
err := test.Controller.Sync(test.Ctx, orderCopy)
if err != nil && !test.Err {
t.Errorf("Expected function to not error, but got: %v", err)
}
if err == nil && test.Err {
t.Errorf("Expected function to get an error, but got: %v", err)
}
test.Finish(t, orderCopy, err)
})
}
}
func TestSolverConfigurationForAuthorization(t *testing.T) {
type testT struct {
cfg []v1alpha1.DomainSolverConfig
authz *acmeapi.Authorization
expectedCfg *v1alpha1.SolverConfig
expectedErr bool
}
tests := map[string]testT{
"correctly selects normal domain": testT{
cfg: []v1alpha1.DomainSolverConfig{
{
Domains: []string{"example.com"},
SolverConfig: v1alpha1.SolverConfig{
DNS01: &v1alpha1.DNS01SolverConfig{
Provider: "correctdns",
},
},
},
},
authz: &acmeapi.Authorization{
Identifier: acmeapi.AuthzID{
Value: "example.com",
},
},
expectedCfg: &v1alpha1.SolverConfig{
DNS01: &v1alpha1.DNS01SolverConfig{
Provider: "correctdns",
},
},
},
"correctly selects normal domain with multiple domains configured": testT{
cfg: []v1alpha1.DomainSolverConfig{
{
Domains: []string{"notexample.com", "example.com"},
SolverConfig: v1alpha1.SolverConfig{
DNS01: &v1alpha1.DNS01SolverConfig{
Provider: "correctdns",
},
},
},
},
authz: &acmeapi.Authorization{
Identifier: acmeapi.AuthzID{
Value: "example.com",
},
},
expectedCfg: &v1alpha1.SolverConfig{
DNS01: &v1alpha1.DNS01SolverConfig{
Provider: "correctdns",
},
},
},
"correctly selects normal domain with multiple domains configured separately": testT{
cfg: []v1alpha1.DomainSolverConfig{
{
Domains: []string{"example.com"},
SolverConfig: v1alpha1.SolverConfig{
DNS01: &v1alpha1.DNS01SolverConfig{
Provider: "correctdns",
},
},
},
{
Domains: []string{"notexample.com"},
SolverConfig: v1alpha1.SolverConfig{
DNS01: &v1alpha1.DNS01SolverConfig{
Provider: "incorrectdns",
},
},
},
},
authz: &acmeapi.Authorization{
Identifier: acmeapi.AuthzID{
Value: "example.com",
},
},
expectedCfg: &v1alpha1.SolverConfig{
DNS01: &v1alpha1.DNS01SolverConfig{
Provider: "correctdns",
},
},
},
"correctly selects configuration for wildcard domain": testT{
cfg: []v1alpha1.DomainSolverConfig{
{
Domains: []string{"example.com"},
SolverConfig: v1alpha1.SolverConfig{
DNS01: &v1alpha1.DNS01SolverConfig{
Provider: "incorrectdns",
},
},
},
{
Domains: []string{"*.example.com"},
SolverConfig: v1alpha1.SolverConfig{
DNS01: &v1alpha1.DNS01SolverConfig{
Provider: "correctdns",
},
},
},
},
authz: &acmeapi.Authorization{
Wildcard: true,
Identifier: acmeapi.AuthzID{
// identifiers for wildcards do not include the *. prefix and
// instead set the Wildcard field on the Authz object
Value: "example.com",
},
},
expectedCfg: &v1alpha1.SolverConfig{
DNS01: &v1alpha1.DNS01SolverConfig{
Provider: "correctdns",
},
},
},
"returns an error when configuration for the domain is not found": testT{
cfg: []v1alpha1.DomainSolverConfig{
{
Domains: []string{"notexample.com"},
SolverConfig: v1alpha1.SolverConfig{
DNS01: &v1alpha1.DNS01SolverConfig{
Provider: "incorrectdns",
},
},
},
},
authz: &acmeapi.Authorization{
Identifier: acmeapi.AuthzID{
Value: "example.com",
},
},
expectedErr: true,
},
}
for n, test := range tests {
t.Run(n, func(t *testing.T) {
actualCfg, err := solverConfigurationForAuthorization(test.cfg, test.authz)
if err != nil && !test.expectedErr {
t.Errorf("Expected to return non-nil error, but got %v", err)
return
}
if err == nil && test.expectedErr {
t.Errorf("Expected error, but got none")
return
}
if !reflect.DeepEqual(test.expectedCfg, actualCfg) {
t.Errorf("Expected did not equal actual: %v", diff.ObjectDiff(test.expectedCfg, actualCfg))
}
})
}
}