Repository: replicatedhq/troubleshoot
Pull request #326 (2021-02-19 20:45:04)

Analyze TLS certificate


Patch:
diff --git a/examples/preflight/host/certificate.yaml b/examples/preflight/host/certificate.yaml
new file mode 100644
index 00000000..1c750813
--- /dev/null
+++ b/examples/preflight/host/certificate.yaml
@@ -0,0 +1,30 @@
+apiVersion: troubleshoot.sh/v1beta2
+kind: HostPreflight
+metadata:
+  name: certificate
+spec:
+  collectors:
+    - certificate:
+        certificatePath: /etc/ssl/corp.crt
+        keyPath: /etc/ssl/corp.key
+  analyzers:
+    - certificate:
+        outcomes:
+          - fail:
+              when: "key-pair-missing"
+              message: Certificate key pair not found in /etc/ssl
+          - fail:
+              when: "key-pair-switched"
+              message: Cert and key pair are switched
+          - fail:
+              when: "key-pair-encrypted"
+              message: Private key is encrypted
+          - fail:
+              when: "key-pair-mismatch"
+              message: Cert and key do not match
+          - fail:
+              when: "key-pair-invalid"
+              message: Certificate key pair is invalid
+          - pass:
+              when: "key-pair-valid"
+              message: Certificate key pair is valid
diff --git a/examples/preflight/host/sample.yaml b/examples/preflight/host/sample.yaml
index 102862f8..b7f5d330 100644
--- a/examples/preflight/host/sample.yaml
+++ b/examples/preflight/host/sample.yaml
@@ -5,6 +5,9 @@ metadata:
 spec:
   collectors:
     - blockDevices: {}
+    - certificate:
+        certificatePath: /etc/ssl/corp.crt
+        keyPath: /etc/ssl/corp.key
     - cpu: {}
     - diskUsage:
         collectorName: ephemeral
@@ -50,6 +53,26 @@ spec:
               message: Multiple available block devices
           - fail:
               message: No available block devices
+    - certificate:
+        outcomes:
+          - fail:
+              when: "key-pair-missing"
+              message: Certificate key pair not found in /etc/ssl
+          - fail:
+              when: "key-pair-switched"
+              message: Cert and key pair are switched
+          - fail:
+              when: "key-pair-encrypted"
+              message: Private key is encrypted
+          - fail:
+              when: "key-pair-mismatch"
+              message: Cert and key do not match
+          - fail:
+              when: "key-pair-invalid"
+              message: Certificate key pair is invalid
+          - pass:
+              when: "key-pair-valid"
+              message: Certificate key pair is valid
     - cpu:
         outcomes:
           - fail:
diff --git a/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go
index dc91fdaa..81acb94c 100644
--- a/pkg/analyze/analyzer.go
+++ b/pkg/analyze/analyzer.go
@@ -125,6 +125,13 @@ func HostAnalyze(hostAnalyzer *troubleshootv1beta2.HostAnalyze, getFile getColle
 		}
 		return []*AnalyzeResult{result}, nil
 	}
+	if hostAnalyzer.Certificate != nil {
+		result, err := analyzeHostCertificate(hostAnalyzer.Certificate, getFile)
+		if err != nil {
+			return nil, err
+		}
+		return []*AnalyzeResult{result}, nil
+	}
 
 	return nil, errors.New("invalid analyzer")
 }
diff --git a/pkg/analyze/host_certificate.go b/pkg/analyze/host_certificate.go
new file mode 100644
index 00000000..a31a79be
--- /dev/null
+++ b/pkg/analyze/host_certificate.go
@@ -0,0 +1,59 @@
+package analyzer
+
+import (
+	"path/filepath"
+
+	"github.com/pkg/errors"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+)
+
+func analyzeHostCertificate(hostAnalyzer *troubleshootv1beta2.CertificateAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
+	collectorName := hostAnalyzer.CollectorName
+	if collectorName == "" {
+		collectorName = "certificate"
+	}
+	name := filepath.Join("certificate", collectorName+".json")
+	contents, err := getCollectedFileContents(name)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get collected file")
+	}
+	status := string(contents)
+
+	result := AnalyzeResult{}
+
+	title := hostAnalyzer.CheckName
+	if title == "" {
+		title = "Certificate Key Pair"
+	}
+	result.Title = title
+
+	for _, outcome := range hostAnalyzer.Outcomes {
+		if outcome.Fail != nil {
+			if outcome.Fail.When == "" || outcome.Fail.When == status {
+				result.IsFail = true
+				result.Message = outcome.Fail.Message
+				result.URI = outcome.Fail.URI
+
+				return &result, nil
+			}
+		} else if outcome.Warn != nil {
+			if outcome.Warn.When == "" || outcome.Warn.When == status {
+				result.IsWarn = true
+				result.Message = outcome.Warn.Message
+				result.URI = outcome.Warn.URI
+
+				return &result, nil
+			}
+		} else if outcome.Pass != nil {
+			if outcome.Pass.When == "" || outcome.Pass.When == status {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return &result, nil
+			}
+		}
+	}
+
+	return &result, nil
+}
diff --git a/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go
index aebed833..5cc25c80 100644
--- a/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go
+++ b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go
@@ -67,6 +67,12 @@ type FilesystemPerformanceAnalyze struct {
 	Outcomes      []*Outcome `json:"outcomes" yaml:"outcomes"`
 }
 
+type CertificateAnalyze struct {
+	AnalyzeMeta   `json:",inline" yaml:",inline"`
+	CollectorName string     `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
+	Outcomes      []*Outcome `json:"outcomes" yaml:"outcomes"`
+}
+
 type HostAnalyze struct {
 	CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"`
 	//
@@ -90,4 +96,6 @@ type HostAnalyze struct {
 	IPV4Interfaces *IPV4InterfacesAnalyze `json:"ipv4Interfaces" yaml:"ipv4Interfaces"`
 
 	FilesystemPerformance *FilesystemPerformanceAnalyze `json:"filesystemPerformance" yaml:"filesystemPerformance"`
+
+	Certificate *CertificateAnalyze `json:"certificate" yaml:"certificate"`
 }
diff --git a/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go
index 013b6c3f..b681b83a 100644
--- a/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go
+++ b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go
@@ -82,6 +82,12 @@ type FilesystemPerformance struct {
 	Datasync           bool   `json:"datasync,omitempty"`
 }
 
+type Certificate struct {
+	HostCollectorMeta `json:",inline" yaml:",inline"`
+	CertificatePath   string `json:"certificatePath" yaml:"certificatepath"`
+	KeyPath           string `json:"keyPath" yaml:"keyPath"`
+}
+
 type HostCollect struct {
 	CPU                   *CPU                   `json:"cpu,omitempty" yaml:"cpu,omitempty"`
 	Memory                *Memory                `json:"memory,omitempty" yaml:"memory,omitempty"`
@@ -96,6 +102,7 @@ type HostCollect struct {
 	BlockDevices          *HostBlockDevices      `json:"blockDevices,omitempty" yaml:"blockDevices,omitempty"`
 	TCPConnect            *TCPConnect            `json:"tcpConnect,omitempty" yaml:"tcpConnect,omitempty"`
 	FilesystemPerformance *FilesystemPerformance `json:"filesystemPerformance" yaml:"filesystemPerformance"`
+	Certificate           *Certificate           `json:"certificate" yaml:"certificate" yaml:"certificate"`
 }
 
 func (c *HostCollect) GetName() string {
diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
index 44250298..e4dbed22 100644
--- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
+++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
@@ -400,6 +400,49 @@ func (in *CephStatusAnalyze) DeepCopy() *CephStatusAnalyze {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Certificate) DeepCopyInto(out *Certificate) {
+	*out = *in
+	out.HostCollectorMeta = in.HostCollectorMeta
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Certificate.
+func (in *Certificate) DeepCopy() *Certificate {
+	if in == nil {
+		return nil
+	}
+	out := new(Certificate)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CertificateAnalyze) DeepCopyInto(out *CertificateAnalyze) {
+	*out = *in
+	out.AnalyzeMeta = in.AnalyzeMeta
+	if in.Outcomes != nil {
+		in, out := &in.Outcomes, &out.Outcomes
+		*out = make([]*Outcome, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(Outcome)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAnalyze.
+func (in *CertificateAnalyze) DeepCopy() *CertificateAnalyze {
+	if in == nil {
+		return nil
+	}
+	out := new(CertificateAnalyze)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClusterInfo) DeepCopyInto(out *ClusterInfo) {
 	*out = *in
@@ -1230,6 +1273,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
 		*out = new(FilesystemPerformanceAnalyze)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Certificate != nil {
+		in, out := &in.Certificate, &out.Certificate
+		*out = new(CertificateAnalyze)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze.
@@ -1326,6 +1374,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
 		*out = new(FilesystemPerformance)
 		**out = **in
 	}
+	if in.Certificate != nil {
+		in, out := &in.Certificate, &out.Certificate
+		*out = new(Certificate)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect.
diff --git a/pkg/collect/host_certificate.go b/pkg/collect/host_certificate.go
new file mode 100644
index 00000000..16ffa639
--- /dev/null
+++ b/pkg/collect/host_certificate.go
@@ -0,0 +1,59 @@
+package collect
+
+import (
+	"bytes"
+	"crypto/tls"
+	"io/ioutil"
+	"path/filepath"
+	"strings"
+)
+
+const KeyPairMissing = "key-pair-missing"
+const KeyPairSwitched = "key-pair-switched"
+const KeyPairEncrypted = "key-pair-encrypted"
+const KeyPairMismatch = "key-pair-mismatch"
+const KeyPairInvalid = "key-pair-invalid"
+const KeyPairValid = "key-pair-valid"
+
+func HostCertificate(c *HostCollector) (map[string][]byte, error) {
+	var result = KeyPairValid
+
+	_, err := tls.LoadX509KeyPair(c.Collect.Certificate.CertificatePath, c.Collect.Certificate.KeyPath)
+	if err != nil {
+		if strings.Contains(err.Error(), "no such file") {
+			result = KeyPairMissing
+		} else if strings.Contains(err.Error(), "PEM inputs may have been switched") {
+			result = KeyPairSwitched
+		} else if strings.Contains(err.Error(), "found a certificate rather than a key") {
+			result = KeyPairSwitched
+		} else if strings.Contains(err.Error(), "private key does not match public key") {
+			result = KeyPairMismatch
+		} else if strings.Contains(err.Error(), "failed to parse private key") {
+			if encrypted, _ := isEncryptedKey(c.Collect.Certificate.KeyPath); encrypted {
+				result = KeyPairEncrypted
+			} else {
+				result = KeyPairInvalid
+			}
+		} else {
+			result = KeyPairInvalid
+		}
+	}
+
+	collectorName := c.Collect.Certificate.CollectorName
+	if collectorName == "" {
+		collectorName = "certificate"
+	}
+	name := filepath.Join("certificate", collectorName+".json")
+
+	return map[string][]byte{
+		name: []byte(result),
+	}, nil
+}
+
+func isEncryptedKey(filename string) (bool, error) {
+	data, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return false, err
+	}
+	return bytes.Contains(data, []byte("ENCRYPTED")), nil
+}
diff --git a/pkg/collect/host_collector.go b/pkg/collect/host_collector.go
index 6fddc9b5..a28fb0e8 100644
--- a/pkg/collect/host_collector.go
+++ b/pkg/collect/host_collector.go
@@ -42,6 +42,8 @@ func (c *HostCollector) RunCollectorSync() (result map[string][]byte, err error)
 		result, err = HostIPV4Interfaces(c)
 	} else if c.Collect.FilesystemPerformance != nil {
 		result, err = HostFilesystemPerformance(c)
+	} else if c.Collect.Certificate != nil {
+		result, err = HostCertificate(c)
 	} else {
 		err = errors.New("no spec found to run")
 		return


Test patch:
diff --git a/pkg/analyze/host_certificate_test.go b/pkg/analyze/host_certificate_test.go
new file mode 100644
index 00000000..d7a00144
--- /dev/null
+++ b/pkg/analyze/host_certificate_test.go
@@ -0,0 +1,333 @@
+package analyzer
+
+import (
+	"testing"
+
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/collect"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestAnalyzeCertificate(t *testing.T) {
+	tests := []struct {
+		name         string
+		status       string
+		hostAnalyzer *troubleshootv1beta2.CertificateAnalyze
+		result       *AnalyzeResult
+		expectErr    bool
+	}{
+		{
+			name:   "key-pair-valid",
+			status: collect.KeyPairValid,
+			hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-missing",
+							Message: "Certificate key pair not found",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-switched",
+							Message: "Public and private keys switched",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-encrypted",
+							Message: "Private key is encrypted",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-mismatch",
+							Message: "Public and private keys don't match",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-invalid",
+							Message: "Certificate key pair is invalid",
+						},
+					},
+					{
+						Pass: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-valid",
+							Message: "Certificate key pair is valid",
+						},
+					},
+				},
+			},
+			result: &AnalyzeResult{
+				Title:   "Certificate Key Pair",
+				IsPass:  true,
+				Message: "Certificate key pair is valid",
+			},
+		},
+		{
+			name:   "key-pair-invalid",
+			status: collect.KeyPairInvalid,
+			hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-missing",
+							Message: "Certificate key pair not found",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-switched",
+							Message: "Public and private keys switched",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-encrypted",
+							Message: "Private key is encrypted",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-mismatch",
+							Message: "Public and private keys don't match",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-invalid",
+							Message: "Certificate key pair is invalid",
+						},
+					},
+					{
+						Pass: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-valid",
+							Message: "Certificate key pair is valid",
+						},
+					},
+				},
+			},
+			result: &AnalyzeResult{
+				Title:   "Certificate Key Pair",
+				IsFail:  true,
+				Message: "Certificate key pair is invalid",
+			},
+		},
+		{
+			name:   "key-pair-missing",
+			status: collect.KeyPairMissing,
+			hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-missing",
+							Message: "Certificate key pair not found",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-switched",
+							Message: "Public and private keys switched",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-encrypted",
+							Message: "Private key is encrypted",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-mismatch",
+							Message: "Public and private keys don't match",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-invalid",
+							Message: "Certificate key pair is invalid",
+						},
+					},
+					{
+						Pass: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-valid",
+							Message: "Certificate key pair is valid",
+						},
+					},
+				},
+			},
+			result: &AnalyzeResult{
+				Title:   "Certificate Key Pair",
+				IsFail:  true,
+				Message: "Certificate key pair not found",
+			},
+		},
+		{
+			name:   "key-pair-switched",
+			status: collect.KeyPairSwitched,
+			hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-missing",
+							Message: "Certificate key pair not found",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-switched",
+							Message: "Public and private keys switched",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-encrypted",
+							Message: "Private key is encrypted",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-mismatch",
+							Message: "Public and private keys don't match",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-invalid",
+							Message: "Certificate key pair is invalid",
+						},
+					},
+					{
+						Pass: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-valid",
+							Message: "Certificate key pair is valid",
+						},
+					},
+				},
+			},
+			result: &AnalyzeResult{
+				Title:   "Certificate Key Pair",
+				IsFail:  true,
+				Message: "Public and private keys switched",
+			},
+		},
+		{
+			name:   "key-pair-encrypted",
+			status: collect.KeyPairEncrypted,
+			hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-missing",
+							Message: "Certificate key pair not found",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-switched",
+							Message: "Public and private keys switched",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-encrypted",
+							Message: "Private key is encrypted",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-mismatch",
+							Message: "Public and private keys don't match",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-invalid",
+							Message: "Certificate key pair is invalid",
+						},
+					},
+					{
+						Pass: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-valid",
+							Message: "Certificate key pair is valid",
+						},
+					},
+				},
+			},
+			result: &AnalyzeResult{
+				Title:   "Certificate Key Pair",
+				IsFail:  true,
+				Message: "Private key is encrypted",
+			},
+		},
+		{
+			name:   "key-pair-mismatch",
+			status: collect.KeyPairMismatch,
+			hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-missing",
+							Message: "Certificate key pair not found",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-switched",
+							Message: "Public and private keys switched",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-encrypted",
+							Message: "Private key is mismatch",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-mismatch",
+							Message: "Public and private keys don't match",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-invalid",
+							Message: "Certificate key pair is invalid",
+						},
+					},
+					{
+						Pass: &troubleshootv1beta2.SingleOutcome{
+							When:    "key-pair-valid",
+							Message: "Certificate key pair is valid",
+						},
+					},
+				},
+			},
+			result: &AnalyzeResult{
+				Title:   "Certificate Key Pair",
+				IsFail:  true,
+				Message: "Public and private keys don't match",
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := require.New(t)
+
+			getCollectedFileContents := func(filename string) ([]byte, error) {
+				return []byte(test.status), nil
+			}
+
+			result, err := analyzeHostCertificate(test.hostAnalyzer, getCollectedFileContents)
+			if test.expectErr {
+				req.Error(err)
+			} else {
+				req.NoError(err)
+			}
+
+			assert.Equal(t, test.result, result)
+		})
+	}
+}