Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
312 changes: 312 additions & 0 deletions pkg/controller/controllercmd/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
package controllercmd

import (
"crypto/x509"
"encoding/pem"
"os"
"path/filepath"
"testing"
"time"

configv1 "github.com/openshift/api/config/v1"
operatorv1alpha1 "github.com/openshift/api/operator/v1alpha1"
)

func TestAddDefaultRotationToConfig_SelfSignedCertLifetime(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally I find it very helpful to add a docstring to each test explaining what is being tested and potentially why we need the test. Just the overall context, you know.

// Skip if service-serving-cert files exist, as we need to test the self-signed cert path
if hasServiceServingCerts("/var/run/secrets/serving-cert") {
t.Skip("skipping: service-serving-cert exists, cannot test self-signed cert generation")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid this, we should rather create a private method and test that:

func (c *ControllerCommandConfig) AddDefaultRotationToConfig(config *operatorv1alpha1.GenericOperatorConfig, configContent []byte) (map[string][]byte, []string, error) {
	return c.addDefaultRotationToConfigAtPath("/var/run/secrets/serving-cert", config, configContent)
}

func (c *ControllerCommandConfig) addDefaultRotationToConfigAtPath(certDir string, config *operatorv1alpha1.GenericOperatorConfig, configContent []byte) (map[string][]byte, []string, error) {
...
}

Then we can simply use a temp directory.


c := &ControllerCommandConfig{
componentName: "test-controller",
basicFlags: &ControllerFlags{},
}

config := &operatorv1alpha1.GenericOperatorConfig{
ServingInfo: configv1.HTTPServingInfo{},
}
config.ServingInfo.CertFile = ""
config.ServingInfo.KeyFile = ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hasn't this just been initialized to an empty object?


_, _, err := c.AddDefaultRotationToConfig(config, []byte{})
Comment thread
jubittajohn marked this conversation as resolved.
if err != nil {
t.Fatalf("AddDefaultRotationToConfig failed: %v", err)
}

if config.ServingInfo.CertFile == "" {
t.Fatal("expected CertFile to be set, got empty string")
}
if config.ServingInfo.KeyFile == "" {
t.Fatal("expected KeyFile to be set, got empty string")
}

certPEM, err := os.ReadFile(config.ServingInfo.CertFile)
if err != nil {
t.Fatalf("failed to read generated certificate: %v", err)
}

block, _ := pem.Decode(certPEM)
if block == nil {
t.Fatal("failed to parse certificate PEM")
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("failed to parse certificate: %v", err)
}

actualLifetime := cert.NotAfter.Sub(cert.NotBefore)
expectedLifetime := 30 * 24 * time.Hour

// Allow 1 minute tolerance
tolerance := 1 * time.Minute
lowerBound := expectedLifetime - tolerance
upperBound := expectedLifetime + tolerance

if actualLifetime < lowerBound || actualLifetime > upperBound {
t.Errorf("certificate lifetime is incorrect: expected %v (30 days), got %v", expectedLifetime, actualLifetime)
}

// Ensure the certificate is not already expired (would indicate nanosecond lifetime bug)
if time.Now().After(cert.NotAfter) {
t.Errorf("certificate is already expired - likely created with nanosecond lifetime (the bug)")
}

// Verify the certificate won't expire in the next 29 days
minValidUntil := time.Now().Add(29 * 24 * time.Hour)
if cert.NotAfter.Before(minValidUntil) {
t.Errorf("certificate expires too soon: expected valid for ~30 days, expires %v", cert.NotAfter)
}
}

func TestHasServiceServingCerts(t *testing.T) {
tests := []struct {
name string
setup func(string) error
expected bool
}{
{
name: "both cert and key exist",
setup: func(dir string) error {
if err := os.WriteFile(filepath.Join(dir, "tls.crt"), []byte("cert"), 0644); err != nil {
return err
}
return os.WriteFile(filepath.Join(dir, "tls.key"), []byte("key"), 0644)
},
expected: true,
},
{
name: "only cert exists",
setup: func(dir string) error {
return os.WriteFile(filepath.Join(dir, "tls.crt"), []byte("cert"), 0644)
},
expected: false,
},
{
name: "only key exists",
setup: func(dir string) error {
return os.WriteFile(filepath.Join(dir, "tls.key"), []byte("key"), 0644)
},
expected: false,
},
{
name: "neither exists",
setup: func(dir string) error { return nil },
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := t.TempDir()
if err := tt.setup(tmpDir); err != nil {
t.Fatalf("setup failed: %v", err)
}

result := hasServiceServingCerts(tmpDir)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly this function is weird. It returns true when the os.Stat calls fail. I don't find that acceptable.

if result != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, result)
}
})
}
}

func TestAddDefaultRotationToConfig(t *testing.T) {
tests := []struct {
name string
configFile string
existingCertFile string
existingKeyFile string
expectGenerated bool

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be really nice to add docs to the fields so that it's obvious what setting that field does.

expectObservedLen int
}{
{
name: "generates self-signed cert when none specified",
expectGenerated: true,
expectObservedLen: 2, // service cert paths
},
{
name: "does not generate when certs already specified",
existingCertFile: "/existing/tls.crt",
existingKeyFile: "/existing/tls.key",
expectGenerated: false,
expectObservedLen: 2,
},
{
name: "observes config file when provided",
configFile: "test-config.yaml",
expectGenerated: true,
expectObservedLen: 3, // service cert paths + config file
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := t.TempDir()
var configFile string
if tt.configFile != "" {
configFile = filepath.Join(tmpDir, tt.configFile)
if err := os.WriteFile(configFile, []byte("test-content"), 0644); err != nil {
t.Fatalf("failed to create config file: %v", err)
}
}

c := &ControllerCommandConfig{
componentName: "test-controller",
basicFlags: &ControllerFlags{
ConfigFile: configFile,
},
}

config := &operatorv1alpha1.GenericOperatorConfig{
ServingInfo: configv1.HTTPServingInfo{},
}
config.ServingInfo.CertFile = tt.existingCertFile
config.ServingInfo.KeyFile = tt.existingKeyFile

startingContent, observedFiles, err := c.AddDefaultRotationToConfig(config, []byte("test-content"))
if err != nil {
t.Fatalf("AddDefaultRotationToConfig failed: %v", err)
}

if tt.expectGenerated {
if config.ServingInfo.CertFile == "" {
t.Error("expected CertFile to be generated")
}
if config.ServingInfo.KeyFile == "" {
t.Error("expected KeyFile to be generated")
}
} else {
if config.ServingInfo.CertFile != tt.existingCertFile {
t.Errorf("expected CertFile to remain %q, got %q", tt.existingCertFile, config.ServingInfo.CertFile)
}
if config.ServingInfo.KeyFile != tt.existingKeyFile {
t.Errorf("expected KeyFile to remain %q, got %q", tt.existingKeyFile, config.ServingInfo.KeyFile)
}
}

if len(observedFiles) != tt.expectObservedLen {
t.Errorf("expected %d observed files, got %d: %v", tt.expectObservedLen, len(observedFiles), observedFiles)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this whole test a bit confusing. We are setting config.ServingInfo.CertFile = tt.existingCertFile and then we check on return that it equals the same value. Not sure what that is supposed to test. I would be happier if we could clearly state expected inputs and outputs in the table and skip deducing them. These are the params, these are the objects returned. Clear and simple to read and understand. Checking observedFile len is not very telling.


// Config file should be in observed files if provided
if tt.configFile != "" {
found := false
for _, f := range observedFiles {
if f == configFile {
found = true
break
}
}
if !found {
t.Errorf("expected config file %q in observed files", configFile)
}

// Config file should be in starting content
if _, ok := startingContent[configFile]; !ok {
t.Error("expected config file in starting content")
}
}
})
}
}

func TestConfig(t *testing.T) {
tests := []struct {
name string
configContent string
expectError bool
expectNilUnstr bool
validateConfig func(*testing.T, *operatorv1alpha1.GenericOperatorConfig)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, pls add comments.

}{
{
name: "no config file",
expectNilUnstr: true,
validateConfig: func(t *testing.T, config *operatorv1alpha1.GenericOperatorConfig) {
if config == nil {
t.Error("expected config to be initialized")
}
},
},
{
name: "valid config file",
configContent: `
apiVersion: operator.openshift.io/v1alpha1
kind: GenericOperatorConfig
servingInfo:
bindAddress: https://0.0.0.0:8443
leaderElection:
leaseDuration: 90s
`,
expectNilUnstr: false,
validateConfig: func(t *testing.T, config *operatorv1alpha1.GenericOperatorConfig) {
if config.ServingInfo.BindAddress != "https://0.0.0.0:8443" {
t.Errorf("expected BindAddress to be parsed correctly, got %q", config.ServingInfo.BindAddress)
}
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &ControllerCommandConfig{
componentName: "test",
basicFlags: &ControllerFlags{},
}

if tt.configContent != "" {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.yaml")
if err := os.WriteFile(configFile, []byte(tt.configContent), 0644); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
c.basicFlags.ConfigFile = configFile
}

unstructured, config, content, err := c.Config()

if tt.expectError && err == nil {
t.Error("expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("unexpected error: %v", err)
}

if tt.expectNilUnstr && unstructured != nil {
t.Error("expected unstructured to be nil")
}
if !tt.expectNilUnstr && unstructured == nil {
t.Error("expected unstructured to be non-nil")
}

if tt.validateConfig != nil {
tt.validateConfig(t, config)
}

if tt.configContent != "" && content == nil {
t.Error("expected content to be set when config file exists")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have something more declarative rather then this bunch of ifs pls?

})
}
}