Skip to content

Commit 8a0a9c8

Browse files
test: add Portal envtest coverage
Signed-off-by: Jintao Zhang <[email protected]>
1 parent 84cf660 commit 8a0a9c8

1 file changed

Lines changed: 263 additions & 0 deletions

File tree

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package envtest
2+
3+
import (
4+
"context"
5+
"sync/atomic"
6+
"testing"
7+
8+
sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components"
9+
sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations"
10+
sdkkonnecterrs "github.com/Kong/sdk-konnect-go/models/sdkerrors"
11+
"github.com/stretchr/testify/mock"
12+
"github.com/stretchr/testify/require"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
apiwatch "k8s.io/apimachinery/pkg/watch"
15+
"sigs.k8s.io/controller-runtime/pkg/client"
16+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
17+
18+
konnectv1alpha1 "github.com/kong/kong-operator/v2/api/konnect/v1alpha1"
19+
konnectv1alpha2 "github.com/kong/kong-operator/v2/api/konnect/v1alpha2"
20+
"github.com/kong/kong-operator/v2/controller/konnect"
21+
"github.com/kong/kong-operator/v2/controller/konnect/ops"
22+
"github.com/kong/kong-operator/v2/modules/manager/logging"
23+
"github.com/kong/kong-operator/v2/modules/manager/scheme"
24+
"github.com/kong/kong-operator/v2/test/helpers/deploy"
25+
"github.com/kong/kong-operator/v2/test/mocks/metricsmocks"
26+
"github.com/kong/kong-operator/v2/test/mocks/sdkmocks"
27+
)
28+
29+
func TestPortal(t *testing.T) {
30+
t.Parallel()
31+
ctx, cancel := Context(t, t.Context())
32+
defer cancel()
33+
cfg, ns := Setup(t, ctx, scheme.Get(), WithInstallGatewayCRDs(true))
34+
35+
t.Log("Setting up the manager with reconcilers")
36+
mgr, logs := NewManager(t, ctx, cfg, scheme.Get())
37+
factory := sdkmocks.NewMockSDKFactory(t)
38+
sdk := factory.SDK
39+
StartReconcilers(ctx, t, mgr, logs,
40+
konnect.NewKonnectEntityReconciler(factory, logging.DevelopmentMode, mgr.GetClient(),
41+
konnect.WithKonnectEntitySyncPeriod[konnectv1alpha1.Portal](konnectInfiniteSyncTime),
42+
konnect.WithMetricRecorder[konnectv1alpha1.Portal](&metricsmocks.MockRecorder{}),
43+
),
44+
)
45+
46+
t.Log("Setting up clients")
47+
cl, err := client.NewWithWatch(mgr.GetConfig(), client.Options{
48+
Scheme: scheme.Get(),
49+
})
50+
require.NoError(t, err)
51+
clientNamespaced := client.NewNamespacedClient(mgr.GetClient(), ns.Name)
52+
53+
t.Log("Creating KonnectAPIAuthConfiguration")
54+
apiAuth := deploy.KonnectAPIAuthConfigurationWithProgrammed(t, ctx, clientNamespaced)
55+
56+
t.Run("should create, update and delete Portal successfully", func(t *testing.T) {
57+
const (
58+
portalID = "portal-12345"
59+
portalName = "dev-portal"
60+
initialDisplayName = "Developer Portal"
61+
updatedDisplayName = "Updated Developer Portal"
62+
)
63+
initialDescription := "Portal created from envtest"
64+
var createCalls, updateCalls, deleteCalls atomic.Int32
65+
66+
watchI, err := cl.Watch(ctx, &konnectv1alpha1.PortalList{}, client.InNamespace(ns.Name))
67+
require.NoError(t, err)
68+
t.Cleanup(func() { watchI.Stop() })
69+
w := watch[*konnectv1alpha1.Portal]{w: watchI}
70+
71+
portal := testPortal(apiAuth, portalName, initialDisplayName, initialDescription)
72+
sdk.PortalsSDK.EXPECT().
73+
CreatePortal(mock.Anything, mock.MatchedBy(func(req sdkkonnectcomp.CreatePortal) bool {
74+
return req.Name == portalName &&
75+
req.DisplayName != nil && *req.DisplayName == initialDisplayName &&
76+
req.Description != nil && *req.Description == initialDescription &&
77+
req.Labels != nil &&
78+
req.Labels["team"] != nil && *req.Labels["team"] == "platform" &&
79+
req.Labels[ops.KubernetesUIDLabelKey] != nil && *req.Labels[ops.KubernetesUIDLabelKey] != ""
80+
})).
81+
RunAndReturn(func(_ context.Context, _ sdkkonnectcomp.CreatePortal, _ ...sdkkonnectops.Option) (*sdkkonnectops.CreatePortalResponse, error) {
82+
createCalls.Add(1)
83+
return &sdkkonnectops.CreatePortalResponse{
84+
PortalResponse: &sdkkonnectcomp.PortalResponse{
85+
ID: portalID,
86+
},
87+
}, nil
88+
}).
89+
Maybe()
90+
91+
t.Log("Creating Portal")
92+
require.NoError(t, clientNamespaced.Create(ctx, portal))
93+
94+
t.Log("Waiting for Portal to be programmed")
95+
watchFor(t, ctx, w, apiwatch.Modified,
96+
assertsAnd(
97+
objectMatchesName(portal),
98+
objectMatchesKonnectID[*konnectv1alpha1.Portal](portalID),
99+
objectHasConditionProgrammedSetToTrue[*konnectv1alpha1.Portal](),
100+
func(p *konnectv1alpha1.Portal) bool {
101+
return controllerutil.ContainsFinalizer(p, konnect.KonnectCleanupFinalizer)
102+
},
103+
),
104+
"Portal didn't get Programmed status condition, Konnect ID, or cleanup finalizer",
105+
)
106+
107+
require.Eventually(t, func() bool {
108+
return createCalls.Load() > 0
109+
}, waitTime, tickTime)
110+
111+
t.Log("Setting up SDK expectations on Portal update")
112+
portalToPatch := portal.DeepCopy()
113+
portalToPatch.Spec.APISpec.DisplayName = updatedDisplayName
114+
sdk.PortalsSDK.EXPECT().
115+
UpdatePortal(mock.Anything, portalID, mock.MatchedBy(func(req sdkkonnectcomp.UpdatePortal) bool {
116+
return req.DisplayName != nil && *req.DisplayName == updatedDisplayName &&
117+
req.Labels != nil &&
118+
req.Labels["team"] != nil && *req.Labels["team"] == "platform" &&
119+
req.Labels[ops.KubernetesUIDLabelKey] != nil && *req.Labels[ops.KubernetesUIDLabelKey] != ""
120+
})).
121+
RunAndReturn(func(_ context.Context, _ string, _ sdkkonnectcomp.UpdatePortal, _ ...sdkkonnectops.Option) (*sdkkonnectops.UpdatePortalResponse, error) {
122+
updateCalls.Add(1)
123+
return &sdkkonnectops.UpdatePortalResponse{}, nil
124+
}).
125+
Maybe()
126+
127+
t.Log("Patching Portal")
128+
require.NoError(t, clientNamespaced.Patch(ctx, portalToPatch, client.MergeFrom(portal)))
129+
130+
t.Log("Waiting for Portal to be patched")
131+
watchFor(t, ctx, w, apiwatch.Modified,
132+
assertsAnd(
133+
objectMatchesName(portal),
134+
objectMatchesKonnectID[*konnectv1alpha1.Portal](portalID),
135+
objectHasConditionProgrammedSetToTrue[*konnectv1alpha1.Portal](),
136+
func(p *konnectv1alpha1.Portal) bool {
137+
return p.Spec.APISpec.DisplayName == updatedDisplayName
138+
},
139+
),
140+
"Portal didn't get patched",
141+
)
142+
143+
require.Eventually(t, func() bool {
144+
return updateCalls.Load() > 0
145+
}, waitTime, tickTime)
146+
147+
t.Log("Setting up SDK expectations on Portal deletion")
148+
sdk.PortalsSDK.EXPECT().
149+
DeletePortal(mock.Anything, portalID, (*sdkkonnectops.DeletePortalQueryParamForce)(nil)).
150+
RunAndReturn(func(_ context.Context, _ string, _ *sdkkonnectops.DeletePortalQueryParamForce, _ ...sdkkonnectops.Option) (*sdkkonnectops.DeletePortalResponse, error) {
151+
deleteCalls.Add(1)
152+
return &sdkkonnectops.DeletePortalResponse{}, nil
153+
}).
154+
Maybe()
155+
156+
t.Log("Deleting Portal")
157+
require.NoError(t, clientNamespaced.Delete(ctx, portal))
158+
require.Eventually(t, func() bool {
159+
err := clientNamespaced.Get(ctx, client.ObjectKeyFromObject(portal), &konnectv1alpha1.Portal{})
160+
return client.IgnoreNotFound(err) == nil && err != nil
161+
}, waitTime, tickTime)
162+
require.Eventually(t, func() bool {
163+
return deleteCalls.Load() > 0
164+
}, waitTime, tickTime)
165+
})
166+
167+
t.Run("should create Portal successfully on conflict when Portal with matching uid tag exists", func(t *testing.T) {
168+
const (
169+
portalID = "portal-conflict-id"
170+
portalName = "conflict-portal"
171+
displayName = "Conflict Portal"
172+
description = "Portal resolved through UID tag lookup"
173+
)
174+
var createCalls, listCalls atomic.Int32
175+
176+
watchI, err := cl.Watch(ctx, &konnectv1alpha1.PortalList{}, client.InNamespace(ns.Name))
177+
require.NoError(t, err)
178+
t.Cleanup(func() { watchI.Stop() })
179+
w := watch[*konnectv1alpha1.Portal]{w: watchI}
180+
181+
portal := testPortal(apiAuth, portalName, displayName, description)
182+
sdk.PortalsSDK.EXPECT().
183+
CreatePortal(mock.Anything, mock.MatchedBy(func(req sdkkonnectcomp.CreatePortal) bool {
184+
return req.Name == portalName
185+
})).
186+
RunAndReturn(func(_ context.Context, _ sdkkonnectcomp.CreatePortal, _ ...sdkkonnectops.Option) (*sdkkonnectops.CreatePortalResponse, error) {
187+
createCalls.Add(1)
188+
return nil, &sdkkonnecterrs.SDKError{
189+
StatusCode: 400,
190+
Body: ErrBodyDataConstraintError,
191+
}
192+
}).
193+
Maybe()
194+
195+
sdk.PortalsSDK.EXPECT().
196+
ListPortals(mock.Anything, sdkkonnectops.ListPortalsRequest{}).
197+
RunAndReturn(func(_ context.Context, _ sdkkonnectops.ListPortalsRequest, _ ...sdkkonnectops.Option) (*sdkkonnectops.ListPortalsResponse, error) {
198+
listCalls.Add(1)
199+
return &sdkkonnectops.ListPortalsResponse{
200+
ListPortalsResponse: &sdkkonnectcomp.ListPortalsResponse{
201+
Data: []sdkkonnectcomp.ListPortalsResponsePortal{
202+
{
203+
ID: portalID,
204+
Labels: map[string]string{
205+
ops.KubernetesUIDLabelKey: string(portal.GetUID()),
206+
},
207+
},
208+
},
209+
},
210+
}, nil
211+
}).
212+
Maybe()
213+
214+
t.Log("Creating Portal")
215+
require.NoError(t, clientNamespaced.Create(ctx, portal))
216+
217+
t.Log("Waiting for Portal to be programmed after UID conflict lookup")
218+
watchFor(t, ctx, w, apiwatch.Modified,
219+
assertsAnd(
220+
objectMatchesName(portal),
221+
objectMatchesKonnectID[*konnectv1alpha1.Portal](portalID),
222+
objectHasConditionProgrammedSetToTrue[*konnectv1alpha1.Portal](),
223+
),
224+
"Portal didn't get Programmed status condition or Konnect ID after conflict resolution",
225+
)
226+
227+
require.Eventually(t, func() bool {
228+
return createCalls.Load() > 0
229+
}, waitTime, tickTime)
230+
require.Eventually(t, func() bool {
231+
return listCalls.Load() > 0
232+
}, waitTime, tickTime)
233+
})
234+
}
235+
236+
func testPortal(
237+
apiAuth *konnectv1alpha1.KonnectAPIAuthConfiguration,
238+
name,
239+
displayName,
240+
description string,
241+
) *konnectv1alpha1.Portal {
242+
return &konnectv1alpha1.Portal{
243+
ObjectMeta: metav1.ObjectMeta{
244+
Name: name,
245+
},
246+
Spec: konnectv1alpha1.PortalSpec{
247+
KonnectConfiguration: konnectv1alpha2.KonnectConfiguration{
248+
APIAuthConfigurationRef: konnectv1alpha2.KonnectAPIAuthConfigurationRef{
249+
Name: apiAuth.Name,
250+
},
251+
},
252+
APISpec: konnectv1alpha1.PortalAPISpec{
253+
AuthenticationEnabled: "Enabled",
254+
Description: &description,
255+
DisplayName: displayName,
256+
Labels: konnectv1alpha1.LabelsUpdate{
257+
"team": "platform",
258+
},
259+
Name: name,
260+
},
261+
},
262+
}
263+
}

0 commit comments

Comments
 (0)