/* Copyright 2018 The Jetstack cert-manager contributors. 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 acmechallenges import ( "context" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" coretesting "k8s.io/client-go/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" ) // Present the challenge value with the given solver. func (f *fakeSolver) Present(ctx context.Context, issuer v1alpha1.GenericIssuer, ch *v1alpha1.Challenge) error { return f.fakePresent(ctx, issuer, ch) } // Check should return Error only if propagation check cannot be performed. // It MUST return `false, nil` if can contact all relevant services and all is // doing is waiting for propagation func (f *fakeSolver) Check(ch *v1alpha1.Challenge) (bool, error) { return f.fakeCheck(ch) } // CleanUp will remove challenge records for a given solver. // This may involve deleting resources in the Kubernetes API Server, or // communicating with other external components (e.g. DNS providers). func (f *fakeSolver) CleanUp(ctx context.Context, issuer v1alpha1.GenericIssuer, ch *v1alpha1.Challenge) error { return f.fakeCleanUp(ctx, issuer, ch) } type fakeSolver struct { fakePresent func(ctx context.Context, issuer v1alpha1.GenericIssuer, ch *v1alpha1.Challenge) error fakeCheck func(ch *v1alpha1.Challenge) (bool, error) fakeCleanUp func(ctx context.Context, issuer v1alpha1.GenericIssuer, ch *v1alpha1.Challenge) error } func TestSyncHappyPath(t *testing.T) { testIssuerHTTP01Enabled := &v1alpha1.Issuer{ Spec: v1alpha1.IssuerSpec{ IssuerConfig: v1alpha1.IssuerConfig{ ACME: &v1alpha1.ACMEIssuer{ HTTP01: &v1alpha1.ACMEIssuerHTTP01Config{}, }, }, }, } // build actual test fixtures testChallenge := &v1alpha1.Challenge{ ObjectMeta: metav1.ObjectMeta{Name: "testchal", Namespace: "default"}, Spec: v1alpha1.ChallengeSpec{ AuthzURL: "http://authzurl", URL: "http://chalurl", Type: "http-01", Token: "token", DNSName: "test.com", Key: "key", Config: v1alpha1.SolverConfig{ HTTP01: &v1alpha1.HTTP01SolverConfig{ Ingress: "", }, }, }, } testChallengePending := testChallenge.DeepCopy() testChallengePending.Status = v1alpha1.ChallengeStatus{ State: v1alpha1.Pending, } testChallengeInvalid := testChallengePending.DeepCopy() testChallengeInvalid.Status.State = v1alpha1.Invalid testChallengeValid := testChallengePending.DeepCopy() testChallengeValid.Status.State = v1alpha1.Valid testChallengeReady := testChallengePending.DeepCopy() testChallengeReady.Status.State = v1alpha1.Ready testChallengePendingPresented := testChallengePending.DeepCopy() testChallengePendingPresented.Status.Presented = true testChallengePendingPresented.Status.Reason = "Waiting for http-01 challenge propagation" testChallengeValidPresented := testChallengeValid.DeepCopy() testChallengeValidPresented.Status.Presented = true testChallengeValidPresented.Status.Reason = "Successfully authorized domain" testChallengeInvalidPresented := testChallengeInvalid.DeepCopy() testChallengeInvalidPresented.Status.Presented = true testChallengeInvalidPresented.Status.Reason = `Authorization status is "invalid" and not 'valid'` testACMEChallengePending := &acmeapi.Challenge{ URL: "http://chalurl", Status: acmeapi.StatusPending, Type: "http-01", Token: "token", } // shallow copy testACMEChallengeValid := &acmeapi.Challenge{} *testACMEChallengeValid = *testACMEChallengePending testACMEChallengeValid.Status = acmeapi.StatusValid // shallow copy testACMEChallengeReady := &acmeapi.Challenge{} *testACMEChallengeReady = *testACMEChallengePending testACMEChallengeReady.Status = acmeapi.StatusReady // shallow copy testACMEChallengeInvalid := &acmeapi.Challenge{} *testACMEChallengeInvalid = *testACMEChallengePending testACMEChallengeInvalid.Status = acmeapi.StatusInvalid testACMEAuthorizationPending := &acmeapi.Authorization{ URL: "http://authzurl", Status: acmeapi.StatusPending, Identifier: acmeapi.AuthzID{ Value: "test.com", }, Challenges: []*acmeapi.Challenge{ { URL: "http://chalurl", Type: "http-01", Token: "token", }, }, } // shallow copy testACMEAuthorizationValid := &acmeapi.Authorization{} *testACMEAuthorizationValid = *testACMEAuthorizationPending testACMEAuthorizationValid.Status = acmeapi.StatusValid // shallow copy testACMEAuthorizationReady := &acmeapi.Authorization{} *testACMEAuthorizationReady = *testACMEAuthorizationPending testACMEAuthorizationReady.Status = acmeapi.StatusReady // shallow copy testACMEAuthorizationInvalid := &acmeapi.Authorization{} *testACMEAuthorizationInvalid = *testACMEAuthorizationPending testACMEAuthorizationInvalid.Status = acmeapi.StatusInvalid tests := map[string]controllerFixture{ "update status if state is unknown": { Issuer: testIssuerHTTP01Enabled, Challenge: testChallenge, Builder: &testpkg.Builder{ CertManagerObjects: []runtime.Object{testChallenge}, ExpectedActions: []testpkg.Action{ testpkg.NewAction(coretesting.NewUpdateAction(v1alpha1.SchemeGroupVersion.WithResource("challenges"), testChallengePending.Namespace, testChallengePending)), }, }, Client: &acmecl.FakeACME{ FakeGetChallenge: func(ctx context.Context, url string) (*acmeapi.Challenge, error) { return testACMEChallengePending, nil }, }, CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) { }, Err: false, }, "call Present and update challenge status to presented": { Issuer: testIssuerHTTP01Enabled, Challenge: testChallengePending, HTTP01: &fakeSolver{ fakePresent: func(ctx context.Context, issuer v1alpha1.GenericIssuer, ch *v1alpha1.Challenge) error { return nil }, fakeCheck: func(ch *v1alpha1.Challenge) (bool, error) { return false, nil }, }, Builder: &testpkg.Builder{ CertManagerObjects: []runtime.Object{testChallengePending}, ExpectedActions: []testpkg.Action{ testpkg.NewAction(coretesting.NewUpdateAction(v1alpha1.SchemeGroupVersion.WithResource("challenges"), testChallengePendingPresented.Namespace, testChallengePendingPresented)), }, }, Client: &acmecl.FakeACME{}, CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) { }, Err: true, }, "accept the challenge if the self check is passing": { Issuer: testIssuerHTTP01Enabled, Challenge: testChallengePendingPresented, HTTP01: &fakeSolver{ fakeCheck: func(ch *v1alpha1.Challenge) (bool, error) { return true, nil }, fakeCleanUp: func(context.Context, v1alpha1.GenericIssuer, *v1alpha1.Challenge) error { return nil }, }, Builder: &testpkg.Builder{ CertManagerObjects: []runtime.Object{testChallengePendingPresented}, ExpectedActions: []testpkg.Action{ testpkg.NewAction(coretesting.NewUpdateAction(v1alpha1.SchemeGroupVersion.WithResource("challenges"), testChallengeValidPresented.Namespace, testChallengeValidPresented)), }, }, Client: &acmecl.FakeACME{ FakeAcceptChallenge: func(context.Context, *acmeapi.Challenge) (*acmeapi.Challenge, error) { // return something other than valid here so we can verify that // the challenge.status.state is set to the *authorizations* // status and not the challenges return testACMEChallengePending, nil }, FakeWaitAuthorization: func(context.Context, string) (*acmeapi.Authorization, error) { return testACMEAuthorizationValid, nil }, }, CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) { }, Err: false, }, "mark certificate as failed if accepting the authorization fails": { Issuer: testIssuerHTTP01Enabled, Challenge: testChallengePendingPresented, HTTP01: &fakeSolver{ fakeCheck: func(ch *v1alpha1.Challenge) (bool, error) { return true, nil }, fakeCleanUp: func(context.Context, v1alpha1.GenericIssuer, *v1alpha1.Challenge) error { return nil }, }, Builder: &testpkg.Builder{ CertManagerObjects: []runtime.Object{testChallengePendingPresented}, ExpectedActions: []testpkg.Action{ testpkg.NewAction(coretesting.NewUpdateAction(v1alpha1.SchemeGroupVersion.WithResource("challenges"), testChallengeInvalidPresented.Namespace, testChallengeInvalidPresented)), }, }, Client: &acmecl.FakeACME{ FakeAcceptChallenge: func(context.Context, *acmeapi.Challenge) (*acmeapi.Challenge, error) { // return something other than invalid here so we can verify that // the challenge.status.state is set to the *authorizations* // status and not the challenges return testACMEChallengePending, nil }, FakeWaitAuthorization: func(context.Context, string) (*acmeapi.Authorization, error) { return testACMEAuthorizationInvalid, nil }, }, CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) { }, Err: true, }, "do nothing if the challenge is valid": { Issuer: testIssuerHTTP01Enabled, Challenge: testChallengeValid, Builder: &testpkg.Builder{ CertManagerObjects: []runtime.Object{testChallengeValid}, ExpectedActions: []testpkg.Action{}, }, Client: &acmecl.FakeACME{}, CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) { }, Err: false, }, "do nothing if the challenge is failed": { Issuer: testIssuerHTTP01Enabled, Challenge: testChallengeInvalid, Builder: &testpkg.Builder{ CertManagerObjects: []runtime.Object{testChallengeInvalid}, 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{} } test.Setup(t) chalCopy := test.Challenge.DeepCopy() err := test.Controller.Sync(test.Ctx, chalCopy) 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, chalCopy, err) }) } }