Skip to content

Commit b49cb7b

Browse files
authored
test: add CRD roundtrip and webhook fuzz tests (#485)
Add Go native fuzz tests for all five CRD types (Store, AuthorizationModel, APIExportPolicy, IdentityProviderConfiguration, Invite) verifying JSON roundtrip fidelity, plus a fuzz test for the IDP webhook validator ensuring it never panics on arbitrary input. Add `task fuzz` for running mutation-based fuzzing with configurable duration. Closes #484 Ref platform-mesh/backlog#231 On-behalf-of: @SAP <bastian.echterhoelter@sap.com> Signed-off-by: Bastian Echterhölter <bastian.echterhoelter@sap.com>
1 parent d0ce93a commit b49cb7b

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

Taskfile.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,18 @@ tasks:
118118
cmds:
119119
- go run ./cmd/main.go operator
120120

121+
fuzz:
122+
desc: "Run fuzz tests with a configurable duration (default 30s per target)"
123+
vars:
124+
FUZZTIME: '{{.FUZZTIME | default "30s"}}'
125+
cmds:
126+
- go test ./api/v1alpha1/ -run=^$ -fuzz=FuzzStoreRoundTrip -fuzztime={{.FUZZTIME}} -count=1
127+
- go test ./api/v1alpha1/ -run=^$ -fuzz=FuzzAuthorizationModelRoundTrip -fuzztime={{.FUZZTIME}} -count=1
128+
- go test ./api/v1alpha1/ -run=^$ -fuzz=FuzzAPIExportPolicyRoundTrip -fuzztime={{.FUZZTIME}} -count=1
129+
- go test ./api/v1alpha1/ -run=^$ -fuzz=FuzzIdentityProviderConfigurationRoundTrip -fuzztime={{.FUZZTIME}} -count=1
130+
- go test ./api/v1alpha1/ -run=^$ -fuzz=FuzzInviteRoundTrip -fuzztime={{.FUZZTIME}} -count=1
131+
- go test ./internal/webhook/ -run=^$ -fuzz=FuzzIdentityProviderConfigurationValidateCreate -fuzztime={{.FUZZTIME}} -count=1
132+
121133
docker:kind:load:
122134
desc: "Build container image with current tag from kind cluster and load it into kind"
123135
vars:
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package v1alpha1
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"k8s.io/apimachinery/pkg/api/equality"
8+
)
9+
10+
func FuzzStoreRoundTrip(f *testing.F) {
11+
f.Add([]byte(`{"spec":{"coreModule":"module","tuples":[{"object":"doc:1","relation":"viewer","user":"user:anne"}]}}`))
12+
f.Add([]byte(`{"status":{"storeID":"s1","authorizationModelID":"am1","managedTuples":[{"object":"o","relation":"r","user":"u"}]}}`))
13+
f.Add([]byte(`{}`))
14+
15+
f.Fuzz(func(t *testing.T, data []byte) {
16+
fuzzRoundTrip(t, data, &Store{}, &Store{})
17+
})
18+
}
19+
20+
func FuzzAuthorizationModelRoundTrip(f *testing.F) {
21+
f.Add([]byte(`{"spec":{"storeRef":{"name":"store","cluster":"cl1"},"model":"model openfga/v1","tuples":[{"object":"doc:1","relation":"viewer","user":"user:anne"}]}}`))
22+
f.Add([]byte(`{"status":{"managedTuples":[{"object":"o","relation":"r","user":"u"}]}}`))
23+
f.Add([]byte(`{}`))
24+
25+
f.Fuzz(func(t *testing.T, data []byte) {
26+
fuzzRoundTrip(t, data, &AuthorizationModel{}, &AuthorizationModel{})
27+
})
28+
}
29+
30+
func FuzzAPIExportPolicyRoundTrip(f *testing.F) {
31+
f.Add([]byte(`{"spec":{"apiExportRef":{"name":"export","clusterPath":"root:org"},"allowPathExpressions":["root:org:*"]}}`))
32+
f.Add([]byte(`{"status":{"managedAllowExpressions":["root:org:ws1"]}}`))
33+
f.Add([]byte(`{}`))
34+
35+
f.Fuzz(func(t *testing.T, data []byte) {
36+
fuzzRoundTrip(t, data, &APIExportPolicy{}, &APIExportPolicy{})
37+
})
38+
}
39+
40+
func FuzzIdentityProviderConfigurationRoundTrip(f *testing.F) {
41+
f.Add([]byte(`{"spec":{"registrationAllowed":true,"clients":[{"clientType":"confidential","clientName":"app","redirectURIs":["https://app/callback"]}]}}`))
42+
f.Add([]byte(`{"status":{"managedClients":{"app":{"clientID":"c1","registrationClientURI":"https://kc/clients/c1"}}}}`))
43+
f.Add([]byte(`{}`))
44+
45+
f.Fuzz(func(t *testing.T, data []byte) {
46+
fuzzRoundTrip(t, data, &IdentityProviderConfiguration{}, &IdentityProviderConfiguration{})
47+
})
48+
}
49+
50+
func FuzzInviteRoundTrip(f *testing.F) {
51+
f.Add([]byte(`{"spec":{"email":"user@example.com"}}`))
52+
f.Add([]byte(`{"spec":{"email":""}}`))
53+
f.Add([]byte(`{}`))
54+
55+
f.Fuzz(func(t *testing.T, data []byte) {
56+
fuzzRoundTrip(t, data, &Invite{}, &Invite{})
57+
})
58+
}
59+
60+
// fuzzRoundTrip unmarshals arbitrary JSON into obj, marshals it back, unmarshals
61+
// into obj2, and checks semantic equality. We use equality.Semantic.DeepEqual from
62+
// k8s.io/apimachinery which treats nil and empty slices/maps as equivalent — the
63+
// standard Kubernetes comparison semantic for API objects.
64+
func fuzzRoundTrip[T any](t *testing.T, data []byte, obj *T, obj2 *T) {
65+
t.Helper()
66+
67+
if err := json.Unmarshal(data, obj); err != nil {
68+
return
69+
}
70+
71+
roundtripped, err := json.Marshal(obj)
72+
if err != nil {
73+
t.Fatalf("failed to marshal: %v", err)
74+
}
75+
76+
if err := json.Unmarshal(roundtripped, obj2); err != nil {
77+
t.Fatalf("failed to unmarshal roundtripped data: %v", err)
78+
}
79+
80+
if !equality.Semantic.DeepEqual(obj, obj2) {
81+
t.Errorf("roundtrip mismatch for %T", obj)
82+
}
83+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package webhook
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/platform-mesh/security-operator/api/v1alpha1"
8+
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
)
11+
12+
func FuzzIdentityProviderConfigurationValidateCreate(f *testing.F) {
13+
f.Add("my-realm", "admin,master,system")
14+
f.Add("master", "")
15+
f.Add("", "")
16+
f.Add(" ", "blocked")
17+
f.Add("valid-realm", "org1,org2")
18+
19+
f.Fuzz(func(t *testing.T, name, denyListCSV string) {
20+
var denyList []string
21+
if denyListCSV != "" {
22+
denyList = splitCSV(denyListCSV)
23+
}
24+
25+
idp := &v1alpha1.IdentityProviderConfiguration{
26+
ObjectMeta: metav1.ObjectMeta{Name: name},
27+
}
28+
v := &identityProviderConfigurationValidator{
29+
keycloakClient: fakeRealmChecker{exists: false},
30+
realmDenyList: denyList,
31+
}
32+
33+
// Must not panic — validation errors are expected
34+
_, _ = v.ValidateCreate(context.Background(), idp)
35+
})
36+
}
37+
38+
func splitCSV(s string) []string {
39+
var result []string
40+
start := 0
41+
for i := range len(s) {
42+
if s[i] == ',' {
43+
result = append(result, s[start:i])
44+
start = i + 1
45+
}
46+
}
47+
result = append(result, s[start:])
48+
return result
49+
}

0 commit comments

Comments
 (0)