Skip to content

Commit f42dc03

Browse files
author
Vadim Rutkovsky
committed
Add unit tests for cert_controller Sync
1 parent 30a0caa commit f42dc03

File tree

1 file changed

+313
-0
lines changed

1 file changed

+313
-0
lines changed
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
package csr
2+
3+
import (
4+
"context"
5+
"crypto/x509/pkix"
6+
"errors"
7+
"strings"
8+
"testing"
9+
"time"
10+
11+
"github.com/openshift/api/annotations"
12+
"github.com/openshift/library-go/pkg/operator/certrotation"
13+
"github.com/openshift/library-go/pkg/operator/csr/csrtestinghelpers"
14+
"github.com/openshift/library-go/pkg/operator/events"
15+
"github.com/stretchr/testify/require"
16+
17+
certificates "k8s.io/api/certificates/v1"
18+
corev1 "k8s.io/api/core/v1"
19+
apierrors "k8s.io/apimachinery/pkg/api/errors"
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
"k8s.io/apimachinery/pkg/runtime"
22+
"k8s.io/apimachinery/pkg/runtime/schema"
23+
"k8s.io/apimachinery/pkg/types"
24+
"k8s.io/apimachinery/pkg/watch"
25+
certificatesv1 "k8s.io/client-go/applyconfigurations/certificates/v1"
26+
"k8s.io/client-go/informers"
27+
"k8s.io/client-go/kubernetes/fake"
28+
clienttesting "k8s.io/client-go/testing"
29+
"k8s.io/client-go/util/workqueue"
30+
clocktesting "k8s.io/utils/clock/testing"
31+
)
32+
33+
const (
34+
testControllerNamespace = "test-ns"
35+
testControllerSecretName = "test-secret"
36+
testControllerCSRName = "test-csr"
37+
)
38+
39+
func TestReset(t *testing.T) {
40+
ctrl, _ := newTestControllerWithClient()
41+
ctrl.csrName = "test-csr"
42+
ctrl.keyData = []byte("test-key")
43+
44+
ctrl.reset()
45+
46+
if ctrl.csrName != "" {
47+
t.Errorf("expected csrName to be empty, got %q", ctrl.csrName)
48+
}
49+
if ctrl.keyData != nil {
50+
t.Errorf("expected keyData to be nil, got %v", ctrl.keyData)
51+
}
52+
}
53+
54+
func TestControllerSync(t *testing.T) {
55+
testCert := csrtestinghelpers.NewTestCert("test", time.Hour)
56+
57+
tests := []struct {
58+
name string
59+
ctrlPrepare func(*clientCertificateController)
60+
fakeClientPrepare func(*fake.Clientset)
61+
errorExpected bool
62+
errorContains string
63+
validateCtrl func(*testing.T, *clientCertificateController, error)
64+
validateSecret func(*testing.T, *corev1.Secret, error)
65+
}{
66+
{
67+
name: "secret not found",
68+
fakeClientPrepare: func(fakeClient *fake.Clientset) {
69+
fakeClient.PrependReactor("get", "secrets", func(action clienttesting.Action) (bool, runtime.Object, error) {
70+
return true, nil, apierrors.NewNotFound(schema.GroupResource{Resource: "secrets"}, testControllerSecretName)
71+
})
72+
},
73+
validateSecret: func(t *testing.T, secret *corev1.Secret, err error) {
74+
require.Equal(t, err, apierrors.NewNotFound(schema.GroupResource{Resource: "secrets"}, testControllerSecretName), "error message")
75+
},
76+
},
77+
{
78+
name: "secret get error",
79+
fakeClientPrepare: func(fakeClient *fake.Clientset) {
80+
fakeClient.PrependReactor("get", "secrets", func(action clienttesting.Action) (bool, runtime.Object, error) {
81+
return true, nil, errors.New("api error")
82+
})
83+
},
84+
errorExpected: true,
85+
errorContains: "api error",
86+
validateSecret: func(t *testing.T, secret *corev1.Secret, err error) {
87+
require.Equal(t, err, errors.New("api error"), "error message")
88+
},
89+
},
90+
{
91+
name: "secret exists",
92+
fakeClientPrepare: func(fakeClient *fake.Clientset) {
93+
secret := &corev1.Secret{
94+
ObjectMeta: metav1.ObjectMeta{
95+
Name: testControllerSecretName,
96+
Namespace: testControllerNamespace,
97+
},
98+
Data: map[string][]byte{
99+
TLSCertFile: []byte("some-cert-data"),
100+
},
101+
}
102+
fakeClient.PrependReactor("get", "secrets", func(action clienttesting.Action) (bool, runtime.Object, error) {
103+
return true, secret, nil
104+
})
105+
},
106+
validateSecret: func(t *testing.T, secret *corev1.Secret, err error) {
107+
require.NoError(t, err)
108+
require.NotNil(t, secret)
109+
require.NotNil(t, secret.Annotations)
110+
require.Equal(t, "test-component", secret.Annotations[annotations.OpenShiftComponent], "unexpected component")
111+
},
112+
},
113+
{
114+
name: "secret with metadata update",
115+
ctrlPrepare: func(ctrl *clientCertificateController) {
116+
ctrl.AdditionalAnnotations = certrotation.AdditionalAnnotations{
117+
JiraComponent: "test-component",
118+
}
119+
},
120+
fakeClientPrepare: func(fakeClient *fake.Clientset) {
121+
secret := &corev1.Secret{
122+
ObjectMeta: metav1.ObjectMeta{
123+
Name: testControllerSecretName,
124+
Namespace: testControllerNamespace,
125+
},
126+
Data: map[string][]byte{
127+
TLSCertFile: []byte("some-cert-data"),
128+
},
129+
}
130+
fakeClient.PrependReactor("get", "secrets", func(action clienttesting.Action) (bool, runtime.Object, error) {
131+
return true, secret, nil
132+
})
133+
fakeClient.PrependReactor("update", "secrets", func(action clienttesting.Action) (bool, runtime.Object, error) {
134+
return true, secret, nil
135+
})
136+
},
137+
validateSecret: func(t *testing.T, secret *corev1.Secret, err error) {
138+
require.NoError(t, err)
139+
require.NotNil(t, secret)
140+
require.NotNil(t, secret.Annotations)
141+
require.Equal(t, "test-component", secret.Annotations[annotations.OpenShiftComponent], "unexpected component")
142+
},
143+
},
144+
{
145+
name: "pending csr",
146+
ctrlPrepare: func(ctrl *clientCertificateController) {
147+
ctrl.csrName = testControllerCSRName
148+
ctrl.keyData = []byte("pending-key")
149+
150+
// Mock approved CSR with certificate
151+
approvedCSR := csrtestinghelpers.NewApprovedCSR(csrtestinghelpers.CSRHolder{Name: testControllerCSRName})
152+
approvedCSR.Status.Certificate = testCert.Cert
153+
ctrl.hubCSRClient = &fakeCSRClient{
154+
csr: approvedCSR,
155+
}
156+
ctrl.keyData = testCert.Key
157+
},
158+
validateSecret: func(t *testing.T, secret *corev1.Secret, err error) {
159+
require.NoError(t, err)
160+
require.NotNil(t, secret)
161+
require.NotNil(t, secret.Annotations)
162+
require.Equal(t, "test-component", secret.Annotations[annotations.OpenShiftComponent], "unexpected component")
163+
require.NotNil(t, secret.Data)
164+
require.NotNil(t, secret.Data[corev1.TLSCertKey])
165+
require.Equal(t, testCert.Cert, secret.Data[corev1.TLSCertKey], "unexpected certificate")
166+
require.Equal(t, testCert.Key, secret.Data[corev1.TLSPrivateKeyKey], "unexpected private key")
167+
},
168+
},
169+
}
170+
171+
for _, tt := range tests {
172+
t.Run(tt.name, func(t *testing.T) {
173+
ctrl, fakeClient := newTestControllerWithClient()
174+
175+
if tt.ctrlPrepare != nil {
176+
tt.ctrlPrepare(ctrl)
177+
}
178+
179+
if tt.fakeClientPrepare != nil {
180+
tt.fakeClientPrepare(fakeClient)
181+
}
182+
183+
ctx := context.Background()
184+
syncCtx := &testSyncContext{}
185+
err := ctrl.sync(ctx, syncCtx)
186+
187+
if tt.errorExpected && err == nil {
188+
t.Error("expected error, got nil")
189+
}
190+
if !tt.errorExpected && err != nil && tt.validateCtrl == nil {
191+
t.Errorf("unexpected error: %v", err)
192+
}
193+
if tt.errorContains != "" && (err == nil || !strings.Contains(err.Error(), tt.errorContains)) {
194+
t.Errorf("expected error to contain %q, got %q", tt.errorContains, err)
195+
}
196+
197+
if tt.validateCtrl != nil {
198+
tt.validateCtrl(t, ctrl, err)
199+
}
200+
201+
if tt.validateSecret != nil {
202+
secret, err := ctrl.spokeCoreClient.Secrets(testControllerNamespace).Get(ctx, testControllerSecretName, metav1.GetOptions{})
203+
tt.validateSecret(t, secret, err)
204+
}
205+
})
206+
}
207+
}
208+
209+
// Test helper functions and types
210+
func newTestController(client *fake.Clientset) *clientCertificateController {
211+
clientCertOption := ClientCertOption{
212+
SecretNamespace: testControllerNamespace,
213+
SecretName: testControllerSecretName,
214+
AdditonalSecretData: map[string][]byte{"test": []byte("data")},
215+
AdditionalAnnotations: certrotation.AdditionalAnnotations{
216+
JiraComponent: "test-component",
217+
},
218+
}
219+
csrOption := CSROption{
220+
ObjectMeta: metav1.ObjectMeta{
221+
GenerateName: "test-csr-",
222+
},
223+
Subject: &pkix.Name{CommonName: "test"},
224+
SignerName: certificates.KubeAPIServerClientSignerName,
225+
DNSNames: []string{"localhost"},
226+
EventFilterFunc: func(obj interface{}) bool { return true },
227+
}
228+
informerFactory := informers.NewSharedInformerFactory(client, 0)
229+
return &clientCertificateController{
230+
clientCertOption,
231+
csrOption,
232+
informerFactory.Certificates().V1().CertificateSigningRequests().Lister(),
233+
client.CertificatesV1().CertificateSigningRequests(),
234+
client.CoreV1(),
235+
"test-controller",
236+
"",
237+
[]byte{},
238+
}
239+
}
240+
241+
func newTestControllerWithClient() (*clientCertificateController, *fake.Clientset) {
242+
client := fake.NewSimpleClientset()
243+
ctrl := newTestController(client)
244+
return ctrl, client
245+
}
246+
247+
type testSyncContext struct{}
248+
249+
func (t *testSyncContext) Queue() workqueue.RateLimitingInterface { return nil }
250+
func (t *testSyncContext) QueueKey() string { return "test-key" }
251+
func (t *testSyncContext) Recorder() events.Recorder {
252+
return events.NewInMemoryRecorder("test", clocktesting.NewFakeClock(time.Now()))
253+
}
254+
255+
type fakeCSRClient struct {
256+
csr *certificates.CertificateSigningRequest
257+
err error
258+
}
259+
260+
func (f *fakeCSRClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*certificates.CertificateSigningRequest, error) {
261+
if f.err != nil {
262+
return nil, f.err
263+
}
264+
return f.csr, nil
265+
}
266+
267+
func (f *fakeCSRClient) Create(ctx context.Context, csr *certificates.CertificateSigningRequest, opts metav1.CreateOptions) (*certificates.CertificateSigningRequest, error) {
268+
if f.err != nil {
269+
return nil, f.err
270+
}
271+
csr.Name = "test-csr"
272+
return csr, nil
273+
}
274+
275+
func (f *fakeCSRClient) Update(ctx context.Context, csr *certificates.CertificateSigningRequest, opts metav1.UpdateOptions) (*certificates.CertificateSigningRequest, error) {
276+
panic("not implemented")
277+
}
278+
279+
func (f *fakeCSRClient) UpdateStatus(ctx context.Context, csr *certificates.CertificateSigningRequest, opts metav1.UpdateOptions) (*certificates.CertificateSigningRequest, error) {
280+
panic("not implemented")
281+
}
282+
283+
func (f *fakeCSRClient) UpdateApproval(ctx context.Context, certificateSigningRequestName string, certificateSigningRequest *certificates.CertificateSigningRequest, opts metav1.UpdateOptions) (*certificates.CertificateSigningRequest, error) {
284+
panic("not implemented")
285+
}
286+
287+
func (f *fakeCSRClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
288+
panic("not implemented")
289+
}
290+
291+
func (f *fakeCSRClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
292+
panic("not implemented")
293+
}
294+
295+
func (f *fakeCSRClient) List(ctx context.Context, opts metav1.ListOptions) (*certificates.CertificateSigningRequestList, error) {
296+
panic("not implemented")
297+
}
298+
299+
func (f *fakeCSRClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
300+
panic("not implemented")
301+
}
302+
303+
func (f *fakeCSRClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*certificates.CertificateSigningRequest, error) {
304+
panic("not implemented")
305+
}
306+
307+
func (f *fakeCSRClient) Apply(ctx context.Context, certificateSigningRequest *certificatesv1.CertificateSigningRequestApplyConfiguration, opts metav1.ApplyOptions) (*certificates.CertificateSigningRequest, error) {
308+
panic("not implemented")
309+
}
310+
311+
func (f *fakeCSRClient) ApplyStatus(ctx context.Context, certificateSigningRequest *certificatesv1.CertificateSigningRequestApplyConfiguration, opts metav1.ApplyOptions) (*certificates.CertificateSigningRequest, error) {
312+
panic("not implemented")
313+
}

0 commit comments

Comments
 (0)