Skip to content

Commit

Permalink
Merge pull request #10 from dbsystel/develop
Browse files Browse the repository at this point in the history
Added an integration testing fixture
  • Loading branch information
Tanemahuta authored Jun 14, 2021
2 parents b5df518 + 21ef1fb commit 3489cf9
Show file tree
Hide file tree
Showing 7 changed files with 297 additions and 26 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ jobs:
run: go get github.com/ory/go-acc
- name: Run tests
run: go-acc ./pkg/...
- name: Run example tests
run: go-acc ./examples/...
- name: Codecov
uses: codecov/[email protected]
with:
Expand Down
7 changes: 7 additions & 0 deletions examples/invalid-pod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Pod
metadata:
name: cat
spec:
containers:
- name: cat
42 changes: 29 additions & 13 deletions examples/mutator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,41 @@ package examples_test
import (
"github.com/dbsystel/kewl/examples"
"github.com/dbsystel/kewl/pkg/mutation"
"github.com/dbsystel/kewl/pkg/webhook/integtest"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("PodMutator", func() {
sut := examples.PodMutator
It("should change a pod with name cat", func() {
Expect(mutation.Mutate(NewPod("cat"), nil, sut)).
To(BeEquivalentTo(NewPod("dog")))
Context("unit test", func() {
It("should change a pod with name cat", func() {
Expect(mutation.Mutate(NewPod("cat"), nil, sut)).
To(BeEquivalentTo(NewPod("dog")))
})
It("should not change a pod with name dog", func() {
Expect(mutation.Mutate(NewPod("dog"), nil, sut)).To(BeNil())
})
It("should change a pod label value cat", func() {
Expect(mutation.Mutate(NewPod("rabbit", "dog", "cat"), nil, sut)).
To(BeEquivalentTo(NewPod("rabbit", "dog", "dog")))
})
It("should not change a pod labels not cat", func() {
Expect(mutation.Mutate(NewPod("rabbit", "dog", "dog"), nil, sut)).
To(BeNil())
})
})
It("should not change a pod with name dog", func() {
Expect(mutation.Mutate(NewPod("dog"), nil, sut)).To(BeNil())
})
It("should change a pod label value cat", func() {
Expect(mutation.Mutate(NewPod("rabbit", "dog", "cat"), nil, sut)).
To(BeEquivalentTo(NewPod("rabbit", "dog", "dog")))
})
It("should not change a pod labels not cat", func() {
Expect(mutation.Mutate(NewPod("rabbit", "dog", "dog"), nil, sut)).
To(BeNil())
Context("integration test", func() {
it, err := integtest.NewMutation("test-namespace", sut)
Expect(err).NotTo(HaveOccurred())
It("should mutate the pod", func() {
response, err := it.InvokeFromFile("invalid-pod.yaml", "")
Expect(response, err).NotTo(BeNil())
Expect(response.PatchMaps()).To(ConsistOf(map[string]string{
"op": "replace",
"path": "/metadata/name",
"value": "dog",
}))
})
})
})
38 changes: 25 additions & 13 deletions examples/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,37 @@ package examples_test
import (
"github.com/dbsystel/kewl/examples"
"github.com/dbsystel/kewl/pkg/validation"
"github.com/dbsystel/kewl/pkg/webhook/integtest"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("PodValidator", func() {
sut := examples.PodValidator
It("should add an error for name cat", func() {
Expect(validation.Validate(NewPod("cat"), nil, sut)).
To(HaveLen(1))
Context("unit test", func() {
It("should add an error for name cat", func() {
Expect(validation.Validate(NewPod("cat"), nil, sut)).
To(HaveLen(1))
})
It("should not find errors for name dog", func() {
Expect(validation.Validate(NewPod("dog"), nil, sut)).To(BeEmpty())
})
It("should add an error for label value cat", func() {
Expect(validation.Validate(NewPod("rabbit", "dog", "cat"), nil, sut)).
To(HaveLen(1))
})
It("should add an error for label value dog", func() {
Expect(validation.Validate(NewPod("rabbit", "dog", "dog"), nil, sut)).
To(BeEmpty())
})
})
It("should not find errors for name dog", func() {
Expect(validation.Validate(NewPod("dog"), nil, sut)).To(BeEmpty())
})
It("should add an error for label value cat", func() {
Expect(validation.Validate(NewPod("rabbit", "dog", "cat"), nil, sut)).
To(HaveLen(1))
})
It("should add an error for label value dog", func() {
Expect(validation.Validate(NewPod("rabbit", "dog", "dog"), nil, sut)).
To(BeEmpty())
Context("integration test", func() {
it, err := integtest.NewValidation("test-namespace", sut)
Expect(err).NotTo(HaveOccurred())
It("should mutate the pod", func() {
response, err := it.InvokeFromFile("invalid-pod.yaml", "")
Expect(response, err).NotTo(BeNil())
Expect(response.Allowed).To(BeFalse())
})
})
})
21 changes: 21 additions & 0 deletions pkg/webhook/integtest/admission_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package integtest

import (
"encoding/json"

admissionv1 "k8s.io/api/admission/v1"
)

// AdmissionResponse is a decorator for admissionv1.AdmissionResponse
type AdmissionResponse struct {
admissionv1.AdmissionResponse
}

// PatchMaps returns the patches as maps
func (a *AdmissionResponse) PatchMaps() (result []map[string]string, err error) {
if a.PatchType == nil || len(a.Patch) == 0 {
return nil, nil
}
err = json.Unmarshal(a.Patch, &result)
return result, err
}
48 changes: 48 additions & 0 deletions pkg/webhook/integtest/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package integtest

import (
"reflect"

"github.com/dbsystel/kewl/pkg/mutation"
"github.com/dbsystel/kewl/pkg/validation"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
)

// Interface is the interface for an integration test fixture
type Interface interface {
Invoke(obj, oldObj runtime.Object) (*AdmissionResponse, error)
InvokeFromFile(objPath, oldObjPath string) (*AdmissionResponse, error)
}

// NewMutation creates a new Interface for mutation testing
func NewMutation(defaultNamespace string, mutators ...mutation.Mutator) (Interface, error) {
result, err := newIntegrationWithPath(defaultNamespace, "/mutate")
if err != nil {
return nil, err
}
for _, mutator := range mutators {
if err = result.sut.AddMutator(mutator); err != nil {
return nil, errors.Wrapf(err, "could not add mutator: %v", reflect.TypeOf(mutator))
}
// Note: error is handled in server already
_ = mutator.AddToScheme(result.scheme)
}
return result, nil
}

// NewValidation creates a new Interface for validation testing
func NewValidation(defaultNamespace string, validators ...validation.Validator) (Interface, error) {
result, err := newIntegrationWithPath(defaultNamespace, "/validate")
if err != nil {
return nil, err
}
for _, validator := range validators {
if err = result.sut.AddValidator(validator); err != nil {
return nil, errors.Wrapf(err, "could not add validator: %v", reflect.TypeOf(validators))
}
// Note: error is handled in server already
_ = validator.AddToScheme(result.scheme)
}
return result, err
}
165 changes: 165 additions & 0 deletions pkg/webhook/integtest/fixture.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package integtest

import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"reflect"

"github.com/dbsystel/kewl/pkg/httpext"
"github.com/dbsystel/kewl/pkg/webhook"
"github.com/go-logr/logr"
"github.com/pkg/errors"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/uuid"
)

var _ Interface = &fixture{}

type fixture struct {
sut *webhook.Server
scheme *runtime.Scheme
path string
defaultNamespace string
}

func newIntegrationWithPath(defaultNamespace, path string) (*fixture, error) {
cfg := &httpext.Config{}
server, err := webhook.NewServer(logr.DiscardLogger{}, cfg)
if err != nil {
return nil, errors.Wrap(err, "could not create servers")
}
return &fixture{sut: server, path: path, scheme: runtime.NewScheme(), defaultNamespace: defaultNamespace}, nil
}

func (i *fixture) Invoke(obj, oldObj runtime.Object) (*AdmissionResponse, error) {
if obj == nil && oldObj == nil {
return nil, errors.New("at least one object must be provided")
}
operation := i.operation(obj, oldObj)
var (
err error
marshalledObj []byte
marshalledOldObj []byte
)
if obj != nil {
if marshalledObj, err = json.Marshal(obj); err != nil {
return nil, errors.Wrapf(err, "could not marshal obj of type: %v", reflect.TypeOf(obj))
}
}
if oldObj != nil {
if marshalledOldObj, err = json.Marshal(oldObj); err != nil {
return nil, errors.Wrapf(err, "could not marshal obj of type: %v", reflect.TypeOf(oldObj))
}
}
review := &admissionv1.AdmissionReview{
TypeMeta: metav1.TypeMeta{Kind: "AdmissionReview", APIVersion: admissionv1.SchemeGroupVersion.Identifier()},
Request: &admissionv1.AdmissionRequest{
UID: uuid.NewUUID(),
Kind: i.kind(obj, oldObj),
Name: "test-review",
Namespace: i.namespace(obj, oldObj),
Operation: operation,
Object: runtime.RawExtension{Raw: marshalledObj},
OldObject: runtime.RawExtension{Raw: marshalledOldObj},
},
}
recorder := httptest.NewRecorder()
body, err := json.Marshal(review)
if err != nil {
return nil, errors.Wrap(err, "could not marshal the review")
}
request := httptest.NewRequest("POST", i.path, bytes.NewBuffer(body))
i.sut.Server.Server.Handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
return nil, errors.Errorf("review invocation response code invalid: %v", recorder.Code)
}
body, err = ioutil.ReadAll(recorder.Body)
if err != nil {
return nil, errors.Wrap(err, "could not read response body")
}
review = &admissionv1.AdmissionReview{}
if err = json.Unmarshal(body, &review); err != nil {
return nil, errors.Wrap(err, "could not unmarshal response body")
}
if review.Request != nil {
return nil, errors.Wrap(err, "review should not have the request set")
}
var result *AdmissionResponse
if review.Response != nil {
result = &AdmissionResponse{*review.Response}
}
return result, nil
}

func (i *fixture) InvokeFromFile(objPath, oldObjPath string) (*AdmissionResponse, error) {
obj, err := i.load(objPath)
if err != nil {
return nil, errors.Wrapf(err, "could not load object from path: %v", objPath)
}
oldObj, err := i.load(oldObjPath)
if err != nil {
return nil, errors.Wrapf(err, "could not load old object from path: %v", oldObjPath)
}
return i.Invoke(obj, oldObj)
}

func (i *fixture) load(path string) (runtime.Object, error) {
if len(path) == 0 {
return nil, nil
}
contents, err := ioutil.ReadFile(path)
if err != nil {
return nil, errors.Wrapf(err, "could not read from path: %v", path)
}
result, _, err := serializer.NewCodecFactory(i.scheme).UniversalDeserializer().Decode(contents, nil, nil)
return result, errors.Wrapf(err, "could not decode object from path: %v", path)
}

func (i *fixture) operation(obj, oldObj runtime.Object) admissionv1.Operation {
if obj == nil {
if oldObj == nil {
return ""
}
return admissionv1.Delete
}
if oldObj != nil {
return admissionv1.Update
}
return admissionv1.Create
}

func (i *fixture) kind(objs ...runtime.Object) (result metav1.GroupVersionKind) {
for _, candidate := range objs {
if candidate != nil {
result.Group, result.Version, result.Kind = i.gvk(candidate)
break
}
}
return result
}

func (i *fixture) gvk(obj runtime.Object) (string, string, string) {
gvk := obj.GetObjectKind().GroupVersionKind()
return gvk.Group, gvk.Version, gvk.Kind
}

func (i *fixture) namespace(objs ...runtime.Object) string {
for _, obj := range objs {
metaAcc, ok := obj.(metav1.ObjectMetaAccessor)
if !ok {
continue
}
objMeta := metaAcc.GetObjectMeta()
if objMeta == nil {
continue
}
return objMeta.GetNamespace()
}
return i.defaultNamespace
}

0 comments on commit 3489cf9

Please sign in to comment.