Skip to content

Commit

Permalink
test(e2e): add e2e tests for certificate resource (#945)
Browse files Browse the repository at this point in the history
  • Loading branch information
phm07 authored Jan 3, 2025
1 parent 2798902 commit ee2aed7
Show file tree
Hide file tree
Showing 5 changed files with 300 additions and 5 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ jobs:

- name: Run tests
run: go test -tags e2e -coverpkg=./... -coverprofile=coverage.txt -v -race ./test/e2e
env:
# Domain must be available in the account running the tests. This domain is
# available in the account running the public integration tests.
CERT_DOMAIN: hc-integrations-test.de

- name: Upload coverage reports to Codecov
if: >
Expand Down
208 changes: 208 additions & 0 deletions test/e2e/certificate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
//go:build e2e

package e2e

import (
"context"
"fmt"
"os"
"path"
"strconv"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/hetznercloud/hcloud-go/v2/hcloud"
)

const fingerprintRegex = `[0-9A-F]{2}(:[0-9A-F]{2}){31}`

func TestCertificate(t *testing.T) {
t.Parallel()

t.Run("uploaded", func(t *testing.T) {
tmpDir := t.TempDir()
notBefore := time.Now()
notAfter := notBefore.Add(365 * 24 * time.Hour)
certPath, keyPath := path.Join(tmpDir, "cert.pem"), path.Join(tmpDir, "key.pem")
err := generateCertificate(certPath, keyPath, notBefore, notAfter)
require.NoError(t, err)

certName := withSuffix("test-certificate-uploaded")
certID, err := createCertificate(t, certName, hcloud.CertificateTypeUploaded, "--cert-file", certPath, "--key-file", keyPath)
require.NoError(t, err)

runCertificateTestSuite(t, certName, certID, hcloud.CertificateTypeUploaded, "example.com")
})

t.Run("managed", func(t *testing.T) {
certDomain := os.Getenv("CERT_DOMAIN")
if certDomain == "" {
t.Skip("Skipping because CERT_DOMAIN is not set")
}

// random subdomain
certDomain = fmt.Sprintf("%s.%s", randomHex(4), certDomain)

certName := withSuffix("test-certificate-managed")
certID, err := createCertificate(t, certName, hcloud.CertificateTypeManaged, "--type", "managed", "--domain", certDomain)
require.NoError(t, err)

runCertificateTestSuite(t, certName, certID, hcloud.CertificateTypeManaged, certDomain)
})
}

func runCertificateTestSuite(t *testing.T, certName string, certID int64, certType hcloud.CertificateType, domainName string) {
t.Helper()

t.Run("add-label", func(t *testing.T) {
t.Run("non-existing", func(t *testing.T) {
out, err := runCommand(t, "certificate", "add-label", "non-existing-certificate", "foo=bar")
require.EqualError(t, err, "certificate not found: non-existing-certificate")
assert.Empty(t, out)
})

t.Run("1", func(t *testing.T) {
out, err := runCommand(t, "certificate", "add-label", strconv.FormatInt(certID, 10), "foo=bar")
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("Label(s) foo added to certificate %d\n", certID), out)
})

t.Run("2", func(t *testing.T) {
out, err := runCommand(t, "certificate", "add-label", strconv.FormatInt(certID, 10), "baz=qux")
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("Label(s) baz added to certificate %d\n", certID), out)
})
})

t.Run("update-name", func(t *testing.T) {
certName = withSuffix("new-test-certificate-" + string(certType))

out, err := runCommand(t, "certificate", "update", strconv.FormatInt(certID, 10), "--name", certName)
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("certificate %d updated\n", certID), out)
})

t.Run("remove-label", func(t *testing.T) {
out, err := runCommand(t, "certificate", "remove-label", certName, "baz")
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("Label(s) baz removed from certificate %d\n", certID), out)
})

t.Run("list", func(t *testing.T) {
out, err := runCommand(t, "certificate", "list", "-o=columns=id,name,labels,type,created,"+
"not_valid_before,not_valid_after,domain_names,fingerprint,issuance_status,renewal_status,age")
require.NoError(t, err)

labels := []string{"foo=bar"}
if certType == hcloud.CertificateTypeManaged {
labels = append([]string{"HC-Use-Staging-CA=true"}, labels...)
}

assert.Regexp(t,
NewRegex().Start().
SeparatedByWhitespace(
"ID", "NAME", "LABELS", "TYPE", "CREATED", "NOT VALID BEFORE", "NOT VALID AFTER",
"DOMAIN NAMES", "FINGERPRINT", "ISSUANCE STATUS", "RENEWAL STATUS", "AGE",
).Newline().
Lit(strconv.FormatInt(certID, 10)).Whitespace().
Lit(certName).Whitespace().
Lit(strings.Join(labels, ", ")).Whitespace().
Lit(string(certType)).Whitespace().
UnixDate().Whitespace().
UnixDate().Whitespace().
UnixDate().Whitespace().
Lit(domainName).Whitespace().
Raw(fingerprintRegex).Whitespace().
OneOf("completed", "n/a").Whitespace().
Lit("n/a").Whitespace().
Age().Newline().
End(),
out,
)
})

t.Run("describe", func(t *testing.T) {
out, err := runCommand(t, "certificate", "describe", strconv.FormatInt(certID, 10))
require.NoError(t, err)

regex := NewRegex().Start().
Lit("ID:").Whitespace().Int().Newline().
Lit("Name:").Whitespace().Lit(certName).Newline().
Lit("Type:").Whitespace().Lit(string(certType)).Newline().
Lit("Fingerprint:").Whitespace().Raw(fingerprintRegex).Newline().
Lit("Created:").Whitespace().UnixDate().Lit(" (").HumanizeTime().Lit(")").Newline().
Lit("Not valid before:").Whitespace().UnixDate().Lit(" (").HumanizeTime().Lit(")").Newline().
Lit("Not valid after:").Whitespace().UnixDate().Lit(" (").HumanizeTime().Lit(")").Newline()

if certType == hcloud.CertificateTypeManaged {
regex = regex.
Lit("Status:").Newline().
Lit(" Issuance:").Whitespace().Lit("completed").Newline().
Lit(" Renewal:").Whitespace().Lit("unavailable").Newline()
}

regex = regex.
Lit("Domain names:").Newline().
Lit(" - ").Lit(domainName).Newline().
Lit("Labels:").Newline()

if certType == hcloud.CertificateTypeManaged {
regex = regex.Lit(" HC-Use-Staging-CA:").Whitespace().Lit("true").Newline()
}

regex = regex.
Lit(" foo:").Whitespace().Lit("bar").Newline().
Lit("Used By:").Newline().
Lit(" Certificate unused").Newline().
End()

assert.Regexp(t, regex, out)
})

t.Run("retry", func(t *testing.T) {
out, err := runCommand(t, "certificate", "retry", strconv.FormatInt(certID, 10))
assert.Empty(t, out)
require.Error(t, err)
assert.Regexp(t, `certificate not retryable \(unsupported_error, [0-9a-f]+\)`, err.Error())
})

t.Run("delete", func(t *testing.T) {
out, err := runCommand(t, "certificate", "delete", strconv.FormatInt(certID, 10))
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("certificate %d deleted\n", certID), out)
})
}

func createCertificate(t *testing.T, name string, certificateType hcloud.CertificateType, args ...string) (int64, error) {
t.Helper()
t.Cleanup(func() {
_, _ = client.Certificate.Delete(context.Background(), &hcloud.Certificate{Name: name})
})

if certificateType == hcloud.CertificateTypeManaged {
args = append([]string{"--label", "HC-Use-Staging-CA=true"}, args...)
}

out, err := runCommand(t, append([]string{"certificate", "create", "--name", name, "--type", string(certificateType)}, args...)...)
if err != nil {
return 0, err
}

if !assert.Regexp(t, `^Certificate [0-9]+ created\n$`, out) {
return 0, fmt.Errorf("invalid response: %s", out)
}

id, err := strconv.ParseInt(out[12:len(out)-9], 10, 64)
if err != nil {
return 0, err
}

t.Cleanup(func() {
_, _ = client.Certificate.Delete(context.Background(), &hcloud.Certificate{ID: id})
})
return id, nil
}
80 changes: 80 additions & 0 deletions test/e2e/certificate_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//go:build e2e

package e2e

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"time"
)

// Adapted from https://go.dev/src/crypto/tls/generate_cert.go

func generateCertificate(certFile, keyFile string, notBefore, notAfter time.Time) error {

priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}

serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return err
}

template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Acme Co"},
},
NotBefore: notBefore,
NotAfter: notAfter,

KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,

DNSNames: []string{
"example.com",
},
}

derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return err
}

certOut, err := os.Create(certFile)
if err != nil {
return err
}
defer func() {
_ = certOut.Close()
}()
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return err
}

keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer func() {
_ = keyOut.Close()
}()
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return err
}
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
return err
}

return nil
}
11 changes: 7 additions & 4 deletions test/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,15 @@ func runCommand(t *testing.T, args ...string) (string, error) {
return buf.String(), err
}

func withSuffix(s string) string {
b := make([]byte, 4)
func randomHex(n int) string {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
suffix := hex.EncodeToString(b)
return fmt.Sprintf("%s-%s", s, suffix)
return hex.EncodeToString(b)
}

func withSuffix(s string) string {
return fmt.Sprintf("%s-%s", s, randomHex(4))
}
2 changes: 1 addition & 1 deletion test/e2e/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (r RegexBuilder) Age() RegexBuilder {
}

func (r RegexBuilder) HumanizeTime() RegexBuilder {
return r.OneOf(`now`, `[0-9]+ (?:seconds?|minutes?|hours?|days?|months?|years?) ago`)
return r.OneOf(`now`, `[0-9]+ (?:seconds?|minutes?|hours?|days?|months?|years?) (ago|from now)`)
}

func (r RegexBuilder) Whitespace() RegexBuilder {
Expand Down

0 comments on commit ee2aed7

Please sign in to comment.