Skip to content

Commit d76d663

Browse files
authored
test: add CRD roundtrip and webhook fuzz tests (#220)
Add Go native fuzz tests for Account and AccountInfo CRD types (JSON roundtrip using k8s equality.Semantic) and Account webhook validators (panic-free on arbitrary input). Seed corpus runs as part of regular `go test`. Add `task fuzz` for running mutation-based fuzzing with configurable duration. Closes #219 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 a095e04 commit d76d663

File tree

3 files changed

+140
-0
lines changed

3 files changed

+140
-0
lines changed

Taskfile.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ tasks:
7979
cmds:
8080
- "{{ .LOCAL_BIN}}/kcp start"
8181

82+
fuzz:
83+
desc: "Run fuzz tests with a configurable duration (default 30s per target)"
84+
vars:
85+
FUZZTIME: '{{.FUZZTIME | default "30s"}}'
86+
cmds:
87+
- go test ./api/v1alpha1/ -run=^$ -fuzz=FuzzAccountRoundTrip -fuzztime={{.FUZZTIME}} -count=1
88+
- go test ./api/v1alpha1/ -run=^$ -fuzz=FuzzAccountInfoRoundTrip -fuzztime={{.FUZZTIME}} -count=1
89+
- go test ./api/v1alpha1/ -run=^$ -fuzz=FuzzAccountValidateCreate -fuzztime={{.FUZZTIME}} -count=1
90+
- go test ./api/v1alpha1/ -run=^$ -fuzz=FuzzAccountValidateUpdate -fuzztime={{.FUZZTIME}} -count=1
91+
8292
docker:kind:
8393
desc: "Build container image with current tag from kind cluster and load it"
8494
vars:
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package v1alpha1
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"k8s.io/apimachinery/pkg/api/equality"
8+
)
9+
10+
func FuzzAccountRoundTrip(f *testing.F) {
11+
f.Add([]byte(`{"apiVersion":"core.platform-mesh.io/v1alpha1","kind":"Account","metadata":{"name":"test"},"spec":{"type":"org","displayName":"Test"}}`))
12+
f.Add([]byte(`{"spec":{"type":"account","displayName":"a","description":"desc","creator":"user","extensions":[{"apiVersion":"v1","kind":"Ext","specGoTemplate":{}}]}}`))
13+
f.Add([]byte(`{}`))
14+
15+
f.Fuzz(func(t *testing.T, data []byte) {
16+
fuzzRoundTrip(t, data, &Account{}, &Account{})
17+
})
18+
}
19+
20+
func FuzzAccountInfoRoundTrip(f *testing.F) {
21+
f.Add([]byte(`{"apiVersion":"core.platform-mesh.io/v1alpha1","kind":"AccountInfo","metadata":{"name":"test"},"spec":{"fga":{"store":{"id":"s1"}},"account":{"name":"a","generatedClusterId":"c1","originClusterId":"c2","path":"/p","url":"https://example.com","type":"org"},"organization":{"name":"o","generatedClusterId":"c3","originClusterId":"c4","path":"/o","url":"https://example.com","type":"org"},"clusterInfo":{"ca":"cert"}}}`))
22+
f.Add([]byte(`{"spec":{"fga":{"store":{"id":""}},"account":{"name":"","generatedClusterId":"","originClusterId":"","path":"","url":"","type":""},"organization":{"name":"","generatedClusterId":"","originClusterId":"","path":"","url":"","type":""},"clusterInfo":{"ca":""},"oidc":{"issuerUrl":"https://auth.example.com","clients":{"app1":{"clientId":"c1"}}}}}`))
23+
f.Add([]byte(`{}`))
24+
25+
f.Fuzz(func(t *testing.T, data []byte) {
26+
fuzzRoundTrip(t, data, &AccountInfo{}, &AccountInfo{})
27+
})
28+
}
29+
30+
// fuzzRoundTrip unmarshals arbitrary JSON into obj, marshals it back, unmarshals
31+
// into obj2, and checks semantic equality. We use equality.Semantic.DeepEqual from
32+
// k8s.io/apimachinery which treats nil and empty slices/maps as equivalent — the
33+
// standard Kubernetes comparison semantic for API objects.
34+
func fuzzRoundTrip[T any](t *testing.T, data []byte, obj *T, obj2 *T) {
35+
t.Helper()
36+
37+
if err := json.Unmarshal(data, obj); err != nil {
38+
return
39+
}
40+
41+
roundtripped, err := json.Marshal(obj)
42+
if err != nil {
43+
t.Fatalf("failed to marshal: %v", err)
44+
}
45+
46+
if err := json.Unmarshal(roundtripped, obj2); err != nil {
47+
t.Fatalf("failed to unmarshal roundtripped data: %v", err)
48+
}
49+
50+
if !equality.Semantic.DeepEqual(obj, obj2) {
51+
t.Errorf("roundtrip mismatch for %T", obj)
52+
}
53+
}

api/v1alpha1/webhook_fuzz_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package v1alpha1
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
)
9+
10+
func FuzzAccountValidateCreate(f *testing.F) {
11+
f.Add("my-org", "org", "admin,root,system")
12+
f.Add("ab", "org", "")
13+
f.Add("", "", "blocked")
14+
f.Add("valid-name", "account", "")
15+
f.Add("a", "unknown-type", "a")
16+
17+
f.Fuzz(func(t *testing.T, name, accountType, denyListCSV string) {
18+
var denyList []string
19+
if denyListCSV != "" {
20+
denyList = splitCSV(denyListCSV)
21+
}
22+
23+
account := &Account{
24+
ObjectMeta: metav1.ObjectMeta{Name: name},
25+
Spec: AccountSpec{Type: AccountType(accountType)},
26+
}
27+
validator := &AccountValidator{
28+
OrganizationNameDenyList: denyList,
29+
AccountTypeAllowList: []AccountType{AccountTypeAccount, AccountTypeOrg},
30+
}
31+
32+
// Must not panic — validation errors are expected
33+
_, _ = validator.ValidateCreate(context.Background(), account)
34+
})
35+
}
36+
37+
func FuzzAccountValidateUpdate(f *testing.F) {
38+
f.Add("old-org", "org", "new-org", "org", "admin,root")
39+
f.Add("admin", "account", "admin", "org", "admin")
40+
f.Add("", "", "", "", "")
41+
42+
f.Fuzz(func(t *testing.T, oldName, oldType, newName, newType, denyListCSV string) {
43+
var denyList []string
44+
if denyListCSV != "" {
45+
denyList = splitCSV(denyListCSV)
46+
}
47+
48+
oldAccount := &Account{
49+
ObjectMeta: metav1.ObjectMeta{Name: oldName},
50+
Spec: AccountSpec{Type: AccountType(oldType)},
51+
}
52+
newAccount := &Account{
53+
ObjectMeta: metav1.ObjectMeta{Name: newName},
54+
Spec: AccountSpec{Type: AccountType(newType)},
55+
}
56+
validator := &AccountValidator{
57+
OrganizationNameDenyList: denyList,
58+
AccountTypeAllowList: []AccountType{AccountTypeAccount, AccountTypeOrg},
59+
}
60+
61+
// Must not panic — validation errors are expected
62+
_, _ = validator.ValidateUpdate(context.Background(), oldAccount, newAccount)
63+
})
64+
}
65+
66+
func splitCSV(s string) []string {
67+
var result []string
68+
start := 0
69+
for i := range len(s) {
70+
if s[i] == ',' {
71+
result = append(result, s[start:i])
72+
start = i + 1
73+
}
74+
}
75+
result = append(result, s[start:])
76+
return result
77+
}

0 commit comments

Comments
 (0)