terraform-provider-stackitp.../stackit/internal/services/cdn/customdomain/resource_test.go
Politano df0f152158
feat(cdn): add custom certificate support (#983)
relates to STACKITCDN-1000
2025-09-18 19:27:22 +02:00

308 lines
9.2 KiB
Go

package cdn
import (
"context"
cryptoRand "crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"fmt"
"math/big"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/stackitcloud/stackit-sdk-go/services/cdn"
)
func TestMapFields(t *testing.T) {
// Redefine certificateTypes locally for testing, matching the updated schema
certificateTypes := map[string]attr.Type{
"version": types.Int64Type,
"certificate": types.StringType,
"private_key": types.StringType,
}
const dummyCert = "dummy-cert-pem"
const dummyKey = "dummy-key-pem"
emtpyErrorsList := types.ListValueMust(types.StringType, []attr.Value{})
// Expected object when a custom certificate is returned
certAttributes := map[string]attr.Value{
"version": types.Int64Value(3),
"certificate": types.StringValue(dummyCert),
"private_key": types.StringValue(dummyKey),
}
certificateObj, _ := types.ObjectValue(certificateTypes, certAttributes)
expectedModel := func(mods ...func(*CustomDomainModel)) *CustomDomainModel {
model := &CustomDomainModel{
ID: types.StringValue("test-project-id,test-distribution-id,https://testdomain.com"),
DistributionId: types.StringValue("test-distribution-id"),
ProjectId: types.StringValue("test-project-id"),
Status: types.StringValue("ACTIVE"),
Errors: emtpyErrorsList,
Certificate: types.ObjectUnknown(certificateTypes),
}
for _, mod := range mods {
mod(model)
}
return model
}
customType := "custom"
customVersion := int64(3)
getRespCustom := cdn.GetCustomDomainResponseGetCertificateAttributeType(&cdn.GetCustomDomainResponseCertificate{
GetCustomDomainCustomCertificate: &cdn.GetCustomDomainCustomCertificate{
Type: &customType,
Version: &customVersion,
},
})
managedType := "managed"
getRespManaged := cdn.GetCustomDomainResponseGetCertificateAttributeType(&cdn.GetCustomDomainResponseCertificate{
GetCustomDomainManagedCertificate: &cdn.GetCustomDomainManagedCertificate{
Type: &managedType,
},
})
customDomainFixture := func(mods ...func(*cdn.GetCustomDomainResponse)) *cdn.GetCustomDomainResponse {
distribution := &cdn.CustomDomain{
Errors: &[]cdn.StatusError{},
Name: cdn.PtrString("https://testdomain.com"),
Status: cdn.DOMAINSTATUS_ACTIVE.Ptr(),
}
customDomainResponse := &cdn.GetCustomDomainResponse{
CustomDomain: distribution,
Certificate: getRespCustom,
}
for _, mod := range mods {
mod(customDomainResponse)
}
return customDomainResponse
}
tests := map[string]struct {
Input *cdn.GetCustomDomainResponse
Certificate interface{}
Expected *CustomDomainModel
InitialModel *CustomDomainModel
IsValid bool
SkipInitialNil bool
}{
"happy_path_custom_cert": {
Expected: expectedModel(func(m *CustomDomainModel) {
m.Certificate = certificateObj
}),
Input: customDomainFixture(),
IsValid: true,
InitialModel: expectedModel(func(m *CustomDomainModel) {
m.Certificate = basetypes.NewObjectValueMust(certificateTypes, map[string]attr.Value{
"certificate": types.StringValue(dummyCert),
"private_key": types.StringValue(dummyKey),
"version": types.Int64Null(),
})
}),
},
"happy_path_managed_cert": {
Expected: expectedModel(func(m *CustomDomainModel) {
m.Certificate = types.ObjectNull(certificateTypes)
}),
Input: customDomainFixture(func(gcdr *cdn.GetCustomDomainResponse) {
gcdr.Certificate = getRespManaged
}),
IsValid: true,
InitialModel: expectedModel(func(m *CustomDomainModel) { m.Certificate = types.ObjectNull(certificateTypes) }),
},
"happy_path_status_error": {
Expected: expectedModel(func(m *CustomDomainModel) {
m.Status = types.StringValue("ERROR")
m.Certificate = certificateObj
}),
Input: customDomainFixture(func(d *cdn.GetCustomDomainResponse) {
d.CustomDomain.Status = cdn.DOMAINSTATUS_ERROR.Ptr()
}),
IsValid: true,
InitialModel: expectedModel(func(m *CustomDomainModel) {
m.Certificate = basetypes.NewObjectValueMust(certificateTypes, map[string]attr.Value{
"certificate": types.StringValue(dummyCert),
"private_key": types.StringValue(dummyKey),
"version": types.Int64Null(),
})
}),
},
"sad_path_custom_domain_nil": {
Expected: expectedModel(),
Input: nil,
IsValid: false,
InitialModel: &CustomDomainModel{},
},
"sad_path_name_missing": {
Expected: expectedModel(),
Input: customDomainFixture(func(d *cdn.GetCustomDomainResponse) {
d.CustomDomain.Name = nil
}),
IsValid: false,
InitialModel: &CustomDomainModel{},
},
}
for tn, tc := range tests {
t.Run(tn, func(t *testing.T) {
model := tc.InitialModel
model.DistributionId = tc.Expected.DistributionId
model.ProjectId = tc.Expected.ProjectId
err := mapCustomDomainResourceFields(tc.Input, model)
if err != nil && tc.IsValid {
t.Fatalf("Error mapping fields: %v", err)
}
if err == nil && !tc.IsValid {
t.Fatalf("Should have failed")
}
if tc.IsValid {
diff := cmp.Diff(tc.Expected, model)
if diff != "" {
t.Fatalf("Mapped model not as expected (-want +got):\n%s", diff)
}
}
})
}
}
func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) {
privateKey, err := rsa.GenerateKey(cryptoRand.Reader, 2048)
if err != nil {
t.Fatalf("failed to generate key: %s", err.Error())
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Issuer: pkix.Name{CommonName: organization},
Subject: pkix.Name{
Organization: []string{organization},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
cert, err = x509.CreateCertificate(
cryptoRand.Reader,
&template,
&template,
&privateKey.PublicKey,
privateKey,
)
if err != nil {
t.Fatalf("failed to generate cert: %s", err.Error())
}
return pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert,
}), pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
})
}
func TestToCertificatePayload(t *testing.T) {
organization := fmt.Sprintf("organization-%s", uuid.NewString())
cert, key := makeCertAndKey(t, organization)
certPEM := string(cert)
keyPEM := string(key)
certBase64 := base64.StdEncoding.EncodeToString(cert)
keyBase64 := base64.StdEncoding.EncodeToString(key)
tests := map[string]struct {
model *CustomDomainModel
expectedPayload *cdn.PutCustomDomainPayloadCertificate
expectErr bool
expectedErrMsg string
}{
"success_managed_when_certificate_block_is_nil": {
model: &CustomDomainModel{
Certificate: types.ObjectNull(certificateTypes),
},
expectedPayload: &cdn.PutCustomDomainPayloadCertificate{
PutCustomDomainManagedCertificate: cdn.NewPutCustomDomainManagedCertificate("managed"),
},
expectErr: false,
},
"success_custom_certificate": {
model: &CustomDomainModel{
Certificate: basetypes.NewObjectValueMust(
certificateTypes,
map[string]attr.Value{
"version": types.Int64Null(),
"certificate": types.StringValue(certPEM),
"private_key": types.StringValue(keyPEM),
},
),
},
expectedPayload: &cdn.PutCustomDomainPayloadCertificate{
PutCustomDomainCustomCertificate: cdn.NewPutCustomDomainCustomCertificate(certBase64, keyBase64, "custom"),
},
expectErr: false,
},
"fail_custom_missing_cert_value": {
model: &CustomDomainModel{
Certificate: basetypes.NewObjectValueMust(
certificateTypes,
map[string]attr.Value{
"version": types.Int64Null(),
"certificate": types.StringValue(""), // Empty certificate
"private_key": types.StringValue(keyPEM),
},
),
},
expectErr: true,
expectedErrMsg: "invalid certificate or private key. Please check if the string of the public certificate and private key in PEM format",
},
"success_managed_when_certificate_attributes_are_nil": {
model: &CustomDomainModel{
Certificate: basetypes.NewObjectValueMust(
certificateTypes,
map[string]attr.Value{
"version": types.Int64Null(),
"certificate": types.StringNull(),
"private_key": types.StringNull(),
},
),
},
expectErr: true,
expectedErrMsg: `"certificate" and "private_key" must be set`,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
payload, err := toCertificatePayload(context.Background(), tt.model)
if tt.expectErr {
if err == nil {
t.Fatalf("expected err, but got none")
}
if err.Error() != tt.expectedErrMsg {
t.Fatalf("expected err '%s', got '%s'", tt.expectedErrMsg, err.Error())
}
return // Test ends here for failing cases
}
if err != nil {
t.Fatalf("did not expect err, but got: %s", err.Error())
}
if diff := cmp.Diff(tt.expectedPayload, payload); diff != "" {
t.Errorf("payload mismatch (-want +got):\n%s", diff)
}
})
}
}