Skip to content

Commit ae7f42f

Browse files
committed
Added a unit test for DynamicRESTMapper
On-behalf-of: SAP [email protected] Signed-off-by: Robert Vasek <[email protected]>
1 parent e713aa5 commit ae7f42f

File tree

1 file changed

+255
-0
lines changed

1 file changed

+255
-0
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
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 dynamicrestmapper
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
24+
"k8s.io/apimachinery/pkg/api/meta"
25+
"k8s.io/apimachinery/pkg/runtime/schema"
26+
27+
"github.com/kcp-dev/logicalcluster/v3"
28+
)
29+
30+
func newDefaultRESTMapperWith(gvkrs []typeMeta) *DefaultRESTMapper {
31+
mapper := NewDefaultRESTMapper(nil)
32+
for _, typemeta := range gvkrs {
33+
mapper.AddSpecific(
34+
typemeta.groupVersionKind(),
35+
typemeta.groupVersionResourcePlural(),
36+
typemeta.groupVersionResourceSingular(),
37+
meta.RESTScopeRoot,
38+
)
39+
}
40+
return mapper
41+
}
42+
43+
func TestClusterRESTMapping(t *testing.T) {
44+
type applyPair struct {
45+
toRemove []typeMeta
46+
toAdd []typeMeta
47+
}
48+
49+
scenarios := []struct {
50+
dmapper *DynamicRESTMapper
51+
applyPairs map[logicalcluster.Name]applyPair
52+
expectedMappingsByCluster map[logicalcluster.Name]*DefaultRESTMapper
53+
}{
54+
// Empty dmapper should resolve to empty.
55+
{
56+
dmapper: NewDynamicRESTMapper(nil),
57+
expectedMappingsByCluster: map[logicalcluster.Name]*DefaultRESTMapper{},
58+
},
59+
// Single mapping should resolve to that mapping.
60+
{
61+
dmapper: NewDynamicRESTMapper(nil),
62+
applyPairs: map[logicalcluster.Name]applyPair{
63+
"one": {
64+
toAdd: []typeMeta{
65+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
66+
},
67+
},
68+
},
69+
expectedMappingsByCluster: map[logicalcluster.Name]*DefaultRESTMapper{
70+
"one": newDefaultRESTMapperWith([]typeMeta{
71+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
72+
}),
73+
},
74+
},
75+
// Removing from empty dmapper should resolve to empty.
76+
{
77+
dmapper: NewDynamicRESTMapper(nil),
78+
applyPairs: map[logicalcluster.Name]applyPair{
79+
"one": {
80+
toRemove: []typeMeta{
81+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
82+
},
83+
},
84+
},
85+
expectedMappingsByCluster: map[logicalcluster.Name]*DefaultRESTMapper{},
86+
},
87+
// Removing and adding the same entry should resolve to adding that entry.
88+
// This case can be triggered by an unrelated chage on the watched resource.
89+
{
90+
dmapper: NewDynamicRESTMapper(nil),
91+
applyPairs: map[logicalcluster.Name]applyPair{
92+
"one": {
93+
toRemove: []typeMeta{
94+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
95+
},
96+
toAdd: []typeMeta{
97+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
98+
},
99+
},
100+
},
101+
expectedMappingsByCluster: map[logicalcluster.Name]*DefaultRESTMapper{
102+
"one": newDefaultRESTMapperWith([]typeMeta{
103+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
104+
}),
105+
},
106+
},
107+
// Removing an entry and adding the same entry and an another one should resolve into having two entries.
108+
// This could be triggered by e.g. adding a new resource version to a CRD.
109+
{
110+
dmapper: NewDynamicRESTMapper(nil),
111+
applyPairs: map[logicalcluster.Name]applyPair{
112+
"one": {
113+
toRemove: []typeMeta{
114+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
115+
},
116+
toAdd: []typeMeta{
117+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
118+
newTypeMeta("api.example.com", "v2", "Object", "", "", meta.RESTScopeRoot),
119+
},
120+
},
121+
},
122+
expectedMappingsByCluster: map[logicalcluster.Name]*DefaultRESTMapper{
123+
"one": newDefaultRESTMapperWith([]typeMeta{
124+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
125+
newTypeMeta("api.example.com", "v2", "Object", "", "", meta.RESTScopeRoot),
126+
}),
127+
},
128+
},
129+
// Removing an existing entry and adding a new one should resolve into having only the new entry.
130+
// This could be triggered by e.g. deprecating an older version of a resource and adding a new one.
131+
{
132+
dmapper: &DynamicRESTMapper{
133+
byCluster: map[logicalcluster.Name]*DefaultRESTMapper{
134+
"one": newDefaultRESTMapperWith([]typeMeta{
135+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
136+
}),
137+
},
138+
},
139+
applyPairs: map[logicalcluster.Name]applyPair{
140+
"one": {
141+
toRemove: []typeMeta{
142+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
143+
},
144+
toAdd: []typeMeta{
145+
newTypeMeta("api.example.com", "v2", "Object", "", "", meta.RESTScopeRoot),
146+
},
147+
},
148+
},
149+
expectedMappingsByCluster: map[logicalcluster.Name]*DefaultRESTMapper{
150+
"one": newDefaultRESTMapperWith([]typeMeta{
151+
newTypeMeta("api.example.com", "v2", "Object", "", "", meta.RESTScopeRoot),
152+
}),
153+
},
154+
},
155+
// Removing all existing resources for a cluster should resolve to empty.
156+
{
157+
dmapper: &DynamicRESTMapper{
158+
byCluster: map[logicalcluster.Name]*DefaultRESTMapper{
159+
"one": newDefaultRESTMapperWith([]typeMeta{
160+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
161+
newTypeMeta("api.example.com", "v2", "Object", "", "", meta.RESTScopeRoot),
162+
}),
163+
},
164+
},
165+
applyPairs: map[logicalcluster.Name]applyPair{
166+
"one": {
167+
toRemove: []typeMeta{
168+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
169+
newTypeMeta("api.example.com", "v2", "Object", "", "", meta.RESTScopeRoot),
170+
},
171+
},
172+
},
173+
expectedMappingsByCluster: map[logicalcluster.Name]*DefaultRESTMapper{},
174+
},
175+
// Check that changes with more clusters are mapped correctly.
176+
{
177+
dmapper: &DynamicRESTMapper{
178+
byCluster: map[logicalcluster.Name]*DefaultRESTMapper{
179+
"one": newDefaultRESTMapperWith([]typeMeta{
180+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
181+
}),
182+
"two": newDefaultRESTMapperWith([]typeMeta{
183+
newTypeMeta("api.example.com", "v2", "Object", "", "", meta.RESTScopeRoot),
184+
}),
185+
},
186+
},
187+
applyPairs: map[logicalcluster.Name]applyPair{
188+
"one": {
189+
toRemove: []typeMeta{
190+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
191+
},
192+
toAdd: []typeMeta{
193+
newTypeMeta("api.example.com", "v2", "Object", "", "", meta.RESTScopeRoot),
194+
},
195+
},
196+
"two": {
197+
toRemove: []typeMeta{
198+
newTypeMeta("api.example.com", "v2", "Object", "", "", meta.RESTScopeRoot),
199+
},
200+
toAdd: []typeMeta{
201+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
202+
},
203+
},
204+
},
205+
expectedMappingsByCluster: map[logicalcluster.Name]*DefaultRESTMapper{
206+
"one": newDefaultRESTMapperWith([]typeMeta{
207+
newTypeMeta("api.example.com", "v2", "Object", "", "", meta.RESTScopeRoot),
208+
}),
209+
"two": newDefaultRESTMapperWith([]typeMeta{
210+
newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot),
211+
}),
212+
},
213+
},
214+
}
215+
216+
for i, s := range scenarios {
217+
for clusterName, apply := range s.applyPairs {
218+
s.dmapper.applyForCluster(clusterName, apply.toRemove, apply.toAdd)
219+
}
220+
221+
assert.Equal(t, s.expectedMappingsByCluster, s.dmapper.byCluster,
222+
"DynamicRESTMapper contains unexpected mapping", "case", i)
223+
}
224+
225+
// Test use-before-create.
226+
227+
objTypeMeta := newTypeMeta("api.example.com", "v1", "Object", "", "", meta.RESTScopeRoot)
228+
dmapper := NewDynamicRESTMapper(nil)
229+
oneMapper := dmapper.ForCluster("one")
230+
assert.NotNil(t, oneMapper, "DynamicRESTMapper.ForCluster() should never return nil")
231+
232+
res, err := oneMapper.ResourceFor(objTypeMeta.groupVersionResourcePlural())
233+
assert.Equal(t, schema.GroupVersionResource{}, res,
234+
"ResourceFor() on an empty mapper should return empty result")
235+
assert.ErrorIs(t, err, &meta.NoResourceMatchError{},
236+
"ResourceFor() on an empty mapper should return an error of type NoResourceMatchError")
237+
238+
// Test use-after-create.
239+
240+
dmapper.applyForCluster("one", nil, []typeMeta{objTypeMeta})
241+
res, err = oneMapper.ResourceFor(objTypeMeta.groupVersionResourceSingular())
242+
assert.Nil(t, err,
243+
"ResourceFor() on match should not return an error")
244+
assert.Equal(t, objTypeMeta.groupVersionResourcePlural(), res,
245+
"ResourceFor() on match should return non-empty result")
246+
247+
// Test use-after-delete.
248+
249+
dmapper.applyForCluster("one", []typeMeta{objTypeMeta}, nil)
250+
res, err = oneMapper.ResourceFor(objTypeMeta.groupVersionResourceSingular())
251+
assert.Equal(t, schema.GroupVersionResource{}, res,
252+
"ResourceFor() on an empty mapper should return empty result")
253+
assert.ErrorIs(t, err, &meta.NoResourceMatchError{},
254+
"ResourceFor() on an empty mapper should return an error of type NoResourceMatchError")
255+
}

0 commit comments

Comments
 (0)