Skip to content

Commit a9d93a8

Browse files
committed
Add unit tests for pkg/controller/controllercmd/cmd.go
Signed-off-by: jubittajohn <jujohn@redhat.com>
1 parent d21ba6f commit a9d93a8

1 file changed

Lines changed: 307 additions & 0 deletions

File tree

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
package controllercmd
2+
3+
import (
4+
"crypto/x509"
5+
"encoding/pem"
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
"time"
10+
11+
configv1 "github.com/openshift/api/config/v1"
12+
operatorv1alpha1 "github.com/openshift/api/operator/v1alpha1"
13+
)
14+
15+
func TestAddDefaultRotationToConfig_SelfSignedCertLifetime(t *testing.T) {
16+
c := &ControllerCommandConfig{
17+
componentName: "test-controller",
18+
basicFlags: &ControllerFlags{},
19+
}
20+
21+
config := &operatorv1alpha1.GenericOperatorConfig{
22+
ServingInfo: configv1.HTTPServingInfo{},
23+
}
24+
config.ServingInfo.CertFile = ""
25+
config.ServingInfo.KeyFile = ""
26+
27+
_, _, err := c.AddDefaultRotationToConfig(config, []byte{})
28+
if err != nil {
29+
t.Fatalf("AddDefaultRotationToConfig failed: %v", err)
30+
}
31+
32+
if config.ServingInfo.CertFile == "" {
33+
t.Fatal("expected CertFile to be set, got empty string")
34+
}
35+
if config.ServingInfo.KeyFile == "" {
36+
t.Fatal("expected KeyFile to be set, got empty string")
37+
}
38+
39+
certPEM, err := os.ReadFile(config.ServingInfo.CertFile)
40+
if err != nil {
41+
t.Fatalf("failed to read generated certificate: %v", err)
42+
}
43+
44+
block, _ := pem.Decode(certPEM)
45+
if block == nil {
46+
t.Fatal("failed to parse certificate PEM")
47+
}
48+
49+
cert, err := x509.ParseCertificate(block.Bytes)
50+
if err != nil {
51+
t.Fatalf("failed to parse certificate: %v", err)
52+
}
53+
54+
actualLifetime := cert.NotAfter.Sub(cert.NotBefore)
55+
expectedLifetime := 30 * 24 * time.Hour
56+
57+
// Allow 1 minute tolerance
58+
tolerance := 1 * time.Minute
59+
lowerBound := expectedLifetime - tolerance
60+
upperBound := expectedLifetime + tolerance
61+
62+
if actualLifetime < lowerBound || actualLifetime > upperBound {
63+
t.Errorf("certificate lifetime is incorrect: expected %v (30 days), got %v", expectedLifetime, actualLifetime)
64+
}
65+
66+
// Ensure the certificate is not already expired (would indicate nanosecond lifetime bug)
67+
if time.Now().After(cert.NotAfter) {
68+
t.Errorf("certificate is already expired - likely created with nanosecond lifetime (the bug)")
69+
}
70+
71+
// Verify the certificate won't expire in the next 29 days
72+
minValidUntil := time.Now().Add(29 * 24 * time.Hour)
73+
if cert.NotAfter.Before(minValidUntil) {
74+
t.Errorf("certificate expires too soon: expected valid for ~30 days, expires %v", cert.NotAfter)
75+
}
76+
}
77+
78+
func TestHasServiceServingCerts(t *testing.T) {
79+
tests := []struct {
80+
name string
81+
setup func(string) error
82+
expected bool
83+
}{
84+
{
85+
name: "both cert and key exist",
86+
setup: func(dir string) error {
87+
if err := os.WriteFile(filepath.Join(dir, "tls.crt"), []byte("cert"), 0644); err != nil {
88+
return err
89+
}
90+
return os.WriteFile(filepath.Join(dir, "tls.key"), []byte("key"), 0644)
91+
},
92+
expected: true,
93+
},
94+
{
95+
name: "only cert exists",
96+
setup: func(dir string) error {
97+
return os.WriteFile(filepath.Join(dir, "tls.crt"), []byte("cert"), 0644)
98+
},
99+
expected: false,
100+
},
101+
{
102+
name: "only key exists",
103+
setup: func(dir string) error {
104+
return os.WriteFile(filepath.Join(dir, "tls.key"), []byte("key"), 0644)
105+
},
106+
expected: false,
107+
},
108+
{
109+
name: "neither exists",
110+
setup: func(dir string) error { return nil },
111+
expected: false,
112+
},
113+
}
114+
115+
for _, tt := range tests {
116+
t.Run(tt.name, func(t *testing.T) {
117+
tmpDir := t.TempDir()
118+
if err := tt.setup(tmpDir); err != nil {
119+
t.Fatalf("setup failed: %v", err)
120+
}
121+
122+
result := hasServiceServingCerts(tmpDir)
123+
if result != tt.expected {
124+
t.Errorf("expected %v, got %v", tt.expected, result)
125+
}
126+
})
127+
}
128+
}
129+
130+
func TestAddDefaultRotationToConfig(t *testing.T) {
131+
tests := []struct {
132+
name string
133+
configFile string
134+
existingCertFile string
135+
existingKeyFile string
136+
expectGenerated bool
137+
expectObservedLen int
138+
}{
139+
{
140+
name: "generates self-signed cert when none specified",
141+
expectGenerated: true,
142+
expectObservedLen: 2, // service cert paths
143+
},
144+
{
145+
name: "does not generate when certs already specified",
146+
existingCertFile: "/existing/tls.crt",
147+
existingKeyFile: "/existing/tls.key",
148+
expectGenerated: false,
149+
expectObservedLen: 2,
150+
},
151+
{
152+
name: "observes config file when provided",
153+
configFile: "test-config.yaml",
154+
expectGenerated: true,
155+
expectObservedLen: 3, // service cert paths + config file
156+
},
157+
}
158+
159+
for _, tt := range tests {
160+
t.Run(tt.name, func(t *testing.T) {
161+
tmpDir := t.TempDir()
162+
var configFile string
163+
if tt.configFile != "" {
164+
configFile = filepath.Join(tmpDir, tt.configFile)
165+
if err := os.WriteFile(configFile, []byte("test-content"), 0644); err != nil {
166+
t.Fatalf("failed to create config file: %v", err)
167+
}
168+
}
169+
170+
c := &ControllerCommandConfig{
171+
componentName: "test-controller",
172+
basicFlags: &ControllerFlags{
173+
ConfigFile: configFile,
174+
},
175+
}
176+
177+
config := &operatorv1alpha1.GenericOperatorConfig{
178+
ServingInfo: configv1.HTTPServingInfo{},
179+
}
180+
config.ServingInfo.CertFile = tt.existingCertFile
181+
config.ServingInfo.KeyFile = tt.existingKeyFile
182+
183+
startingContent, observedFiles, err := c.AddDefaultRotationToConfig(config, []byte("test-content"))
184+
if err != nil {
185+
t.Fatalf("AddDefaultRotationToConfig failed: %v", err)
186+
}
187+
188+
if tt.expectGenerated {
189+
if config.ServingInfo.CertFile == "" {
190+
t.Error("expected CertFile to be generated")
191+
}
192+
if config.ServingInfo.KeyFile == "" {
193+
t.Error("expected KeyFile to be generated")
194+
}
195+
} else {
196+
if config.ServingInfo.CertFile != tt.existingCertFile {
197+
t.Errorf("expected CertFile to remain %q, got %q", tt.existingCertFile, config.ServingInfo.CertFile)
198+
}
199+
if config.ServingInfo.KeyFile != tt.existingKeyFile {
200+
t.Errorf("expected KeyFile to remain %q, got %q", tt.existingKeyFile, config.ServingInfo.KeyFile)
201+
}
202+
}
203+
204+
if len(observedFiles) != tt.expectObservedLen {
205+
t.Errorf("expected %d observed files, got %d: %v", tt.expectObservedLen, len(observedFiles), observedFiles)
206+
}
207+
208+
// Config file should be in observed files if provided
209+
if tt.configFile != "" {
210+
found := false
211+
for _, f := range observedFiles {
212+
if f == configFile {
213+
found = true
214+
break
215+
}
216+
}
217+
if !found {
218+
t.Errorf("expected config file %q in observed files", configFile)
219+
}
220+
221+
// Config file should be in starting content
222+
if _, ok := startingContent[configFile]; !ok {
223+
t.Error("expected config file in starting content")
224+
}
225+
}
226+
})
227+
}
228+
}
229+
230+
func TestConfig(t *testing.T) {
231+
tests := []struct {
232+
name string
233+
configContent string
234+
expectError bool
235+
expectNilUnstr bool
236+
validateConfig func(*testing.T, *operatorv1alpha1.GenericOperatorConfig)
237+
}{
238+
{
239+
name: "no config file",
240+
expectNilUnstr: true,
241+
validateConfig: func(t *testing.T, config *operatorv1alpha1.GenericOperatorConfig) {
242+
if config == nil {
243+
t.Error("expected config to be initialized")
244+
}
245+
},
246+
},
247+
{
248+
name: "valid config file",
249+
configContent: `
250+
apiVersion: operator.openshift.io/v1alpha1
251+
kind: GenericOperatorConfig
252+
servingInfo:
253+
bindAddress: https://0.0.0.0:8443
254+
leaderElection:
255+
leaseDuration: 90s
256+
`,
257+
expectNilUnstr: false,
258+
validateConfig: func(t *testing.T, config *operatorv1alpha1.GenericOperatorConfig) {
259+
if config.ServingInfo.BindAddress != "https://0.0.0.0:8443" {
260+
t.Errorf("expected BindAddress to be parsed correctly, got %q", config.ServingInfo.BindAddress)
261+
}
262+
},
263+
},
264+
}
265+
266+
for _, tt := range tests {
267+
t.Run(tt.name, func(t *testing.T) {
268+
c := &ControllerCommandConfig{
269+
componentName: "test",
270+
basicFlags: &ControllerFlags{},
271+
}
272+
273+
if tt.configContent != "" {
274+
tmpDir := t.TempDir()
275+
configFile := filepath.Join(tmpDir, "config.yaml")
276+
if err := os.WriteFile(configFile, []byte(tt.configContent), 0644); err != nil {
277+
t.Fatalf("failed to write config file: %v", err)
278+
}
279+
c.basicFlags.ConfigFile = configFile
280+
}
281+
282+
unstructured, config, content, err := c.Config()
283+
284+
if tt.expectError && err == nil {
285+
t.Error("expected error but got none")
286+
}
287+
if !tt.expectError && err != nil {
288+
t.Errorf("unexpected error: %v", err)
289+
}
290+
291+
if tt.expectNilUnstr && unstructured != nil {
292+
t.Error("expected unstructured to be nil")
293+
}
294+
if !tt.expectNilUnstr && unstructured == nil {
295+
t.Error("expected unstructured to be non-nil")
296+
}
297+
298+
if tt.validateConfig != nil {
299+
tt.validateConfig(t, config)
300+
}
301+
302+
if tt.configContent != "" && content == nil {
303+
t.Error("expected content to be set when config file exists")
304+
}
305+
})
306+
}
307+
}

0 commit comments

Comments
 (0)