Skip to content

Commit 97efc4c

Browse files
committed
move the tests to seperate file
Signed-off-by: Karol Szwaj <[email protected]> On-behalf-of: @SAP [email protected]
1 parent 7d0955d commit 97efc4c

File tree

2 files changed

+368
-288
lines changed

2 files changed

+368
-288
lines changed

internal/sync/syncer_related_test.go

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
/*
2+
Copyright 2025 The KCP Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package sync
18+
19+
import (
20+
"bytes"
21+
"context"
22+
"testing"
23+
24+
"github.com/kcp-dev/logicalcluster/v3"
25+
"go.uber.org/zap"
26+
27+
dummyv1alpha1 "github.com/kcp-dev/api-syncagent/internal/sync/apis/dummy/v1alpha1"
28+
"github.com/kcp-dev/api-syncagent/internal/test/diff"
29+
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
30+
31+
corev1 "k8s.io/api/core/v1"
32+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
33+
apierrors "k8s.io/apimachinery/pkg/api/errors"
34+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
35+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
36+
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
37+
"sigs.k8s.io/controller-runtime/pkg/kontext"
38+
)
39+
40+
func TestSyncerProcessingRelatedResources(t *testing.T) {
41+
const stateNamespace = "kcp-system"
42+
43+
type testcase struct {
44+
name string
45+
remoteAPIGroup string
46+
localCRD *apiextensionsv1.CustomResourceDefinition
47+
pubRes *syncagentv1alpha1.PublishedResource
48+
remoteObject *unstructured.Unstructured
49+
localObject *unstructured.Unstructured
50+
existingState string
51+
performRequeues bool
52+
expectedRemoteObject *unstructured.Unstructured
53+
expectedLocalObject *unstructured.Unstructured
54+
expectedState string
55+
customVerification func(t *testing.T, requeue bool, processErr error, finalRemoteObject *unstructured.Unstructured, finalLocalObject *unstructured.Unstructured, testcase testcase)
56+
}
57+
58+
clusterName := logicalcluster.Name("testcluster")
59+
60+
remoteThingPR := &syncagentv1alpha1.PublishedResource{
61+
Spec: syncagentv1alpha1.PublishedResourceSpec{
62+
Resource: syncagentv1alpha1.SourceResourceDescriptor{
63+
APIGroup: dummyv1alpha1.GroupName,
64+
Version: dummyv1alpha1.GroupVersion,
65+
Kind: "NamespacedThing",
66+
},
67+
Projection: &syncagentv1alpha1.ResourceProjection{
68+
Kind: "RemoteThing",
69+
},
70+
// include explicit naming rules to be independent of possible changes to the defaults
71+
Naming: &syncagentv1alpha1.ResourceNaming{
72+
Name: "$remoteClusterName-$remoteName", // Things are Cluster-scoped
73+
},
74+
Related: []syncagentv1alpha1.RelatedResourceSpec{
75+
{
76+
Identifier: "mandatory-credentials",
77+
Origin: "kcp",
78+
Kind: "Secret",
79+
Reference: syncagentv1alpha1.RelatedResourceReference{
80+
Name: syncagentv1alpha1.ResourceLocator{
81+
Path: "metadata.name",
82+
Regex: &syncagentv1alpha1.RegexResourceLocator{
83+
Replacement: "mandatory-credentials",
84+
},
85+
},
86+
},
87+
},
88+
{
89+
Identifier: "optional-secret",
90+
Origin: "service",
91+
Kind: "Secret",
92+
Reference: syncagentv1alpha1.RelatedResourceReference{
93+
Name: syncagentv1alpha1.ResourceLocator{
94+
Path: "metadata.name",
95+
Regex: &syncagentv1alpha1.RegexResourceLocator{
96+
Replacement: "optional-credentials",
97+
},
98+
},
99+
},
100+
Optional: true,
101+
},
102+
},
103+
},
104+
}
105+
106+
testcases := []testcase{
107+
{
108+
name: "optional related resource does not exist",
109+
remoteAPIGroup: "remote.example.corp",
110+
localCRD: loadCRD("things"),
111+
pubRes: remoteThingPR,
112+
performRequeues: true,
113+
114+
remoteObject: newUnstructured(&dummyv1alpha1.NamespacedThing{
115+
ObjectMeta: metav1.ObjectMeta{
116+
Name: "my-test-thing",
117+
Namespace: stateNamespace,
118+
},
119+
Spec: dummyv1alpha1.ThingSpec{
120+
Username: "Colonel Mustard",
121+
},
122+
}, withGroupKind("remote.example.corp", "RemoteThing")),
123+
localObject: newUnstructured(&dummyv1alpha1.NamespacedThing{
124+
ObjectMeta: metav1.ObjectMeta{
125+
Name: "testcluster-my-test-thing",
126+
Namespace: stateNamespace,
127+
Labels: map[string]string{
128+
agentNameLabel: "textor-the-doctor",
129+
remoteObjectClusterLabel: "testcluster",
130+
remoteObjectNameHashLabel: "c346c8ceb5d104cc783d09b95e8ea7032c190948",
131+
},
132+
Annotations: map[string]string{
133+
remoteObjectNameAnnotation: "my-test-thing",
134+
remoteObjectNamespaceAnnotation: stateNamespace,
135+
},
136+
},
137+
Spec: dummyv1alpha1.ThingSpec{
138+
Username: "Colonel Mustard",
139+
},
140+
}),
141+
existingState: "",
142+
143+
expectedRemoteObject: newUnstructured(&dummyv1alpha1.NamespacedThing{
144+
ObjectMeta: metav1.ObjectMeta{
145+
Name: "my-test-thing",
146+
Namespace: stateNamespace,
147+
Finalizers: []string{
148+
deletionFinalizer,
149+
},
150+
},
151+
Spec: dummyv1alpha1.ThingSpec{
152+
Username: "Colonel Mustard",
153+
},
154+
}, withGroupKind("remote.example.corp", "RemoteThing")),
155+
expectedLocalObject: newUnstructured(&dummyv1alpha1.NamespacedThing{
156+
ObjectMeta: metav1.ObjectMeta{
157+
Name: "testcluster-my-test-thing",
158+
Namespace: stateNamespace,
159+
Labels: map[string]string{
160+
agentNameLabel: "textor-the-doctor",
161+
remoteObjectClusterLabel: "testcluster",
162+
remoteObjectNameHashLabel: "c346c8ceb5d104cc783d09b95e8ea7032c190948",
163+
},
164+
Annotations: map[string]string{
165+
remoteObjectNameAnnotation: "my-test-thing",
166+
remoteObjectNamespaceAnnotation: stateNamespace,
167+
},
168+
},
169+
Spec: dummyv1alpha1.ThingSpec{
170+
Username: "Colonel Mustard",
171+
},
172+
}),
173+
expectedState: `{"apiVersion":"remote.example.corp/v1alpha1","kind":"RemoteThing","metadata":{"name":"my-test-thing","namespace":"kcp-system"},"spec":{"username":"Colonel Mustard"}}`,
174+
},
175+
{
176+
name: "mandatory related resource does not exist",
177+
remoteAPIGroup: "remote.example.corp",
178+
localCRD: loadCRD("things"),
179+
pubRes: remoteThingPR,
180+
performRequeues: true,
181+
182+
remoteObject: newUnstructured(&dummyv1alpha1.NamespacedThing{
183+
ObjectMeta: metav1.ObjectMeta{
184+
Name: "my-test-thing",
185+
Namespace: stateNamespace,
186+
},
187+
Spec: dummyv1alpha1.ThingSpec{
188+
Username: "Colonel Mustard",
189+
},
190+
}, withGroupKind("remote.example.corp", "RemoteThing")),
191+
localObject: newUnstructured(&dummyv1alpha1.NamespacedThing{
192+
ObjectMeta: metav1.ObjectMeta{
193+
Name: "testcluster-my-test-thing",
194+
Namespace: stateNamespace,
195+
Labels: map[string]string{
196+
agentNameLabel: "textor-the-doctor",
197+
remoteObjectClusterLabel: "testcluster",
198+
remoteObjectNameHashLabel: "c346c8ceb5d104cc783d09b95e8ea7032c190948",
199+
},
200+
Annotations: map[string]string{
201+
remoteObjectNameAnnotation: "my-test-thing",
202+
remoteObjectNamespaceAnnotation: stateNamespace,
203+
},
204+
},
205+
Spec: dummyv1alpha1.ThingSpec{
206+
Username: "Colonel Mustard",
207+
},
208+
}),
209+
existingState: "",
210+
211+
expectedRemoteObject: newUnstructured(&dummyv1alpha1.NamespacedThing{
212+
ObjectMeta: metav1.ObjectMeta{
213+
Name: "my-test-thing",
214+
Namespace: stateNamespace,
215+
Finalizers: []string{
216+
deletionFinalizer,
217+
},
218+
},
219+
Spec: dummyv1alpha1.ThingSpec{
220+
Username: "Colonel Mustard",
221+
},
222+
}, withGroupKind("remote.example.corp", "RemoteThing")),
223+
expectedLocalObject: newUnstructured(&dummyv1alpha1.NamespacedThing{
224+
ObjectMeta: metav1.ObjectMeta{
225+
Name: "testcluster-my-test-thing",
226+
Namespace: stateNamespace,
227+
Labels: map[string]string{
228+
agentNameLabel: "textor-the-doctor",
229+
remoteObjectClusterLabel: "testcluster",
230+
remoteObjectNameHashLabel: "c346c8ceb5d104cc783d09b95e8ea7032c190948",
231+
},
232+
Annotations: map[string]string{
233+
remoteObjectNameAnnotation: "my-test-thing",
234+
remoteObjectNamespaceAnnotation: stateNamespace,
235+
},
236+
},
237+
Spec: dummyv1alpha1.ThingSpec{
238+
Username: "Colonel Mustard",
239+
},
240+
}),
241+
expectedState: `{"apiVersion":"remote.example.corp/v1alpha1","kind":"RemoteThing","metadata":{"name":"my-test-thing","namespace":"kcp-system"},"spec":{"username":"Colonel Mustard"}}`,
242+
},
243+
}
244+
245+
credentials := newUnstructured(&corev1.Secret{
246+
ObjectMeta: metav1.ObjectMeta{
247+
Name: "mandatory-credentials",
248+
Namespace: stateNamespace,
249+
Labels: map[string]string{
250+
"hello": "world",
251+
},
252+
},
253+
Data: map[string][]byte{
254+
"password": []byte("hunter2"),
255+
},
256+
})
257+
for _, testcase := range testcases {
258+
t.Run(testcase.name, func(t *testing.T) {
259+
localClient := buildFakeClient(testcase.localObject, credentials)
260+
remoteClient := buildFakeClient(testcase.remoteObject, credentials)
261+
262+
syncer, err := NewResourceSyncer(
263+
// zap.Must(zap.NewDevelopment()).Sugar(),
264+
zap.NewNop().Sugar(),
265+
localClient,
266+
remoteClient,
267+
testcase.pubRes,
268+
testcase.localCRD,
269+
testcase.remoteAPIGroup,
270+
nil,
271+
stateNamespace,
272+
"textor-the-doctor",
273+
)
274+
if err != nil {
275+
t.Fatalf("Failed to create syncer: %v", err)
276+
}
277+
278+
localCtx := context.Background()
279+
remoteCtx := kontext.WithCluster(localCtx, clusterName)
280+
ctx := NewContext(localCtx, remoteCtx)
281+
282+
// setup a custom state backend that we can prime
283+
var backend *kubernetesBackend
284+
syncer.newObjectStateStore = func(primaryObject, stateCluster syncSide) ObjectStateStore {
285+
// .Process() is called multiple times, but we want the state to persist between reconciles.
286+
if backend == nil {
287+
backend = newKubernetesBackend(stateNamespace, primaryObject, stateCluster)
288+
if testcase.existingState != "" {
289+
if err := backend.Put(testcase.remoteObject, clusterName, []byte(testcase.existingState)); err != nil {
290+
t.Fatalf("Failed to prime state store: %v", err)
291+
}
292+
}
293+
}
294+
295+
return &objectStateStore{
296+
backend: backend,
297+
}
298+
}
299+
300+
var requeue bool
301+
302+
if testcase.performRequeues {
303+
target := testcase.remoteObject.DeepCopy()
304+
305+
for i := 0; true; i++ {
306+
if i > 20 {
307+
t.Fatalf("Detected potential infinite loop, stopping after %d requeues.", i)
308+
}
309+
310+
requeue, err = syncer.Process(ctx, target)
311+
if err != nil {
312+
break
313+
}
314+
315+
if !requeue {
316+
break
317+
}
318+
319+
if err = remoteClient.Get(remoteCtx, ctrlruntimeclient.ObjectKeyFromObject(target), target); err != nil {
320+
// it's possible for the processing to have deleted the remote object,
321+
// so a NotFound is valid here
322+
if apierrors.IsNotFound(err) {
323+
break
324+
}
325+
326+
t.Fatalf("Failed to get updated remote object: %v", err)
327+
}
328+
}
329+
} else {
330+
requeue, err = syncer.Process(ctx, testcase.remoteObject)
331+
}
332+
333+
finalRemoteObject, getErr := getFinalObjectVersion(remoteCtx, remoteClient, testcase.remoteObject, testcase.expectedRemoteObject)
334+
if getErr != nil {
335+
t.Fatalf("Failed to get final remote object: %v", getErr)
336+
}
337+
338+
finalLocalObject, getErr := getFinalObjectVersion(localCtx, localClient, testcase.localObject, testcase.expectedLocalObject)
339+
if getErr != nil {
340+
t.Fatalf("Failed to get final local object: %v", getErr)
341+
}
342+
343+
if testcase.customVerification != nil {
344+
testcase.customVerification(t, requeue, err, finalRemoteObject, finalLocalObject, testcase)
345+
} else {
346+
if err != nil {
347+
t.Fatalf("Processing failed: %v", err)
348+
}
349+
350+
assertObjectsEqual(t, "local", testcase.expectedLocalObject, finalLocalObject)
351+
assertObjectsEqual(t, "remote", testcase.expectedRemoteObject, finalRemoteObject)
352+
353+
if testcase.expectedState != "" {
354+
if backend == nil {
355+
t.Fatal("Cannot check object state, state store was never instantiated.")
356+
}
357+
358+
finalState, err := backend.Get(testcase.expectedRemoteObject, clusterName)
359+
if err != nil {
360+
t.Fatalf("Failed to get final state: %v", err)
361+
} else if !bytes.Equal(finalState, []byte(testcase.expectedState)) {
362+
t.Fatalf("States do not match:\n%s", diff.StringDiff(testcase.expectedState, string(finalState)))
363+
}
364+
}
365+
}
366+
})
367+
}
368+
}

0 commit comments

Comments
 (0)