Skip to content

Commit 5f5b5ff

Browse files
committed
test: add unit tests for UserController and user finalizer logic with Zitadel integration
1 parent 1238f41 commit 5f5b5ff

File tree

1 file changed

+134
-0
lines changed

1 file changed

+134
-0
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package controller
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"sync/atomic"
8+
"time"
9+
10+
ginkgo "github.com/onsi/ginkgo/v2"
11+
gomega "github.com/onsi/gomega"
12+
13+
iammiloapiscomv1alpha1 "go.miloapis.com/milo/pkg/apis/iam/v1alpha1"
14+
"k8s.io/apimachinery/pkg/runtime"
15+
"k8s.io/apimachinery/pkg/types"
16+
ctrl "sigs.k8s.io/controller-runtime"
17+
"sigs.k8s.io/controller-runtime/pkg/client"
18+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
19+
"sigs.k8s.io/controller-runtime/pkg/finalizer"
20+
21+
"go.miloapis.com/auth-provider-zitadel/internal/zitadel"
22+
"golang.org/x/oauth2"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
)
25+
26+
var _ = ginkgo.Describe("UserController", func() {
27+
var (
28+
scheme *runtime.Scheme
29+
k8sClient client.Client
30+
ctx context.Context
31+
)
32+
33+
ginkgo.BeforeEach(func() {
34+
ctx = context.TODO()
35+
scheme = runtime.NewScheme()
36+
gomega.Expect(iammiloapiscomv1alpha1.AddToScheme(scheme)).To(gomega.Succeed())
37+
k8sClient = fake.NewClientBuilder().WithScheme(scheme).Build()
38+
})
39+
40+
ginkgo.Context("Reconcile", func() {
41+
ginkgo.It("should ignore requests for missing User resources", func() {
42+
r := &UserController{
43+
Client: k8sClient,
44+
Finalizers: finalizer.NewFinalizers(),
45+
}
46+
47+
req := ctrl.Request{NamespacedName: types.NamespacedName{Name: "ghost"}}
48+
res, err := r.Reconcile(ctx, req)
49+
gomega.Expect(err).ToNot(gomega.HaveOccurred())
50+
gomega.Expect(res.RequeueAfter).To(gomega.Equal(time.Duration(0)))
51+
})
52+
})
53+
54+
ginkgo.Context("userFinalizer", func() {
55+
ginkgo.It("should skip deletion when user not found in Zitadel", func() {
56+
var getCalls, deleteCalls int32
57+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
58+
if r.Method == http.MethodGet {
59+
atomic.AddInt32(&getCalls, 1)
60+
http.NotFound(w, r)
61+
return
62+
}
63+
ginkgo.GinkgoT().Errorf("unexpected method %s", r.Method)
64+
}))
65+
defer ts.Close()
66+
67+
client := zitadel.NewClientWithTokenSource(ts.URL, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "test", TokenType: "Bearer"}))
68+
f := &userFinalizer{Zitadel: client}
69+
70+
user := &iammiloapiscomv1alpha1.User{ObjectMeta: metav1.ObjectMeta{Name: "john"}}
71+
res, err := f.Finalize(ctx, user)
72+
gomega.Expect(err).ToNot(gomega.HaveOccurred())
73+
gomega.Expect(res.Updated).To(gomega.BeFalse())
74+
gomega.Expect(getCalls).To(gomega.BeNumerically(">=", 1))
75+
gomega.Expect(deleteCalls).To(gomega.BeZero())
76+
})
77+
78+
ginkgo.It("should delete user when present in Zitadel", func() {
79+
var deleteCalls int32
80+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
81+
if r.Method == http.MethodGet {
82+
w.Header().Set("Content-Type", "application/json")
83+
w.WriteHeader(http.StatusOK)
84+
_, _ = w.Write([]byte(`{"user": {"userId": "john"}, "details": {}}`))
85+
return
86+
}
87+
if r.Method == http.MethodDelete {
88+
atomic.AddInt32(&deleteCalls, 1)
89+
w.WriteHeader(http.StatusOK)
90+
return
91+
}
92+
ginkgo.GinkgoT().Errorf("unexpected method %s", r.Method)
93+
}))
94+
defer ts.Close()
95+
96+
client := zitadel.NewClientWithTokenSource(ts.URL, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "test", TokenType: "Bearer"}))
97+
f := &userFinalizer{Zitadel: client}
98+
user := &iammiloapiscomv1alpha1.User{ObjectMeta: metav1.ObjectMeta{Name: "john"}}
99+
100+
res, err := f.Finalize(ctx, user)
101+
gomega.Expect(err).ToNot(gomega.HaveOccurred())
102+
gomega.Expect(res.Updated).To(gomega.BeFalse())
103+
gomega.Expect(deleteCalls).To(gomega.Equal(int32(1)))
104+
})
105+
})
106+
107+
ginkgo.Context("Finalizer registration", func() {
108+
ginkgo.It("should add the user finalizer on first reconcile", func() {
109+
// Arrange: create a User without finalizers in the fake cluster
110+
user := &iammiloapiscomv1alpha1.User{
111+
ObjectMeta: metav1.ObjectMeta{Name: "alice"},
112+
}
113+
gomega.Expect(k8sClient.Create(ctx, user)).To(gomega.Succeed())
114+
115+
// Setup controller with proper finalizer registration (no Zitadel interaction needed)
116+
finalizers := finalizer.NewFinalizers()
117+
gomega.Expect(finalizers.Register(userFinalizerKey, &userFinalizer{})).To(gomega.Succeed())
118+
119+
r := &UserController{
120+
Client: k8sClient,
121+
Finalizers: finalizers,
122+
}
123+
124+
// Act: reconcile the user
125+
_, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Name: "alice"}})
126+
gomega.Expect(err).ToNot(gomega.HaveOccurred())
127+
128+
// Assert: the user object now contains the finalizer key
129+
updated := &iammiloapiscomv1alpha1.User{}
130+
gomega.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "alice"}, updated)).To(gomega.Succeed())
131+
gomega.Expect(updated.GetFinalizers()).To(gomega.ContainElement(userFinalizerKey))
132+
})
133+
})
134+
})

0 commit comments

Comments
 (0)