Files
Maxence MaireauxandGitHub f2abd7cf53 feat: add fuzz tests and fix bugs found by fuzzing (#583)
* test(currency): add fuzz tests for amount parsing and serialization

Add three fuzz targets: GetAmountWithPrecisionFromString, AmountRoundTrip,
and GetStringAmountFromBigIntWithPrecision. Includes regression corpus
for a negative number serialization bug found during fuzzing.

* fix(currency): handle negative numbers in GetStringAmountFromBigIntWithPrecision

The sign was placed after the decimal point for negative amounts
(e.g. -1 with precision 26 produced "0.-00...001"). Extract the sign
before formatting and prepend it to the result.

* test(query): add fuzz test for ParseJSON with round-trip verification

Fuzzes the query DSL parser with random JSON inputs. Verifies no panics,
successful Build calls, and marshal/re-parse round-trip consistency.
Includes regression corpus for empty $and set serialization.

* fix(query): marshal empty $and/$or sets as [] instead of null

set.MarshalJSON serialized a nil items slice as null, which could not
be re-parsed by ParseJSON. Initialize to an empty slice when nil so
the round-trip produces valid JSON.

* test(oidc): add fuzz tests for Audience, Time, SpaceDelimitedArray, and Locales

Fuzz JSON unmarshaling for security-critical OIDC types. Includes
round-trip verification for Audience and SpaceDelimitedArray, and
panic-safety checks for Time and Locales.

* test: add fuzz tests for JWT scopes, licence validation, and pagination

- jwt: fuzz checkScopes and ClaimsFromRequest header parsing
- licence: fuzz JWT token validation against embedded RSA key
- paginate: fuzz UnmarshalCursor (base64->JSON) and BigInt.FromString
  in a separate subpackage to avoid Docker-dependent TestMain

* test(time): add fuzz tests for ParseTime, UnmarshalJSON, and Scan

Three fuzz targets covering time parsing, JSON unmarshaling, and
database scanning with round-trip verification. Includes regression
corpus for year 0000 with timezone offset producing negative years
that cannot be re-parsed (Go stdlib limitation).

* test(metadata): add fuzz tests for Scan, Merge, and UnmarshalValue

Fuzz JSON scanning from database, metadata merging invariants,
and the generic UnmarshalValue function with panic recovery.

* test(currency): add fuzz tests for GetCurrencyAndPrecisionFromAsset and FormatAsset

Fuzz asset string parsing (EUR/2 format) and currency formatting
against the ISO4217 currency map.

* test(oidc): add fuzz tests for JWTTokenRequest and mergeAndMarshalClaims

Fuzz JWT token request round-trip and claims merging. Includes
regression corpus for null Audience round-trip failure.

* fix(oidc): handle null JSON value in Audience.UnmarshalJSON

Audience marshals as null when empty, but UnmarshalJSON did not
accept null back, breaking round-trip consistency. Add nil case
to the type switch.

* fix(time): skip round-trip check for negative years in FuzzParseTime

Negative years produced by UTC conversion are not representable in
RFC3339, causing the round-trip reparse to fail.

* fix: address CodeRabbit review comments on fuzz tests

- query/fuzz_test.go: fail on marshal error instead of silently returning
- paginate/fuzz_test.go: add semantic validation with base64+JSON oracle
- currency/amount.go: guard against nil *big.Int to prevent panic
- time/fuzz_test.go: also skip year 0 and year > 9999 in round-trip check

* fix(metadata): avoid masking unexpected panics in FuzzUnmarshalValue

Skip invalid JSON inputs instead of catching all panics with
defer/recover, so genuine unexpected panics surface properly.
2026-03-27 11:11:25 +00:00

132 lines
2.8 KiB
Go

package oidc
import (
"encoding/json"
"testing"
)
func FuzzAudienceUnmarshalJSON(f *testing.F) {
// Valid inputs
f.Add([]byte(`"single-audience"`))
f.Add([]byte(`["aud1", "aud2"]`))
f.Add([]byte(`["aud1"]`))
f.Add([]byte(`[]`))
f.Add([]byte(`""`))
// Edge cases
f.Add([]byte(`null`))
f.Add([]byte(`42`))
f.Add([]byte(`[42]`))
f.Add([]byte(`[null]`))
f.Add([]byte(`{}`))
f.Add([]byte(`[["nested"]]`))
f.Add([]byte(`true`))
f.Fuzz(func(t *testing.T, data []byte) {
var a Audience
err := a.UnmarshalJSON(data)
if err != nil {
return
}
// Round-trip: marshal and re-unmarshal
marshaled, err := json.Marshal(a)
if err != nil {
t.Fatalf("marshal failed after successful unmarshal: %v", err)
}
var reparsed Audience
if err := reparsed.UnmarshalJSON(marshaled); err != nil {
t.Fatalf("round-trip unmarshal failed: %q -> %v", string(marshaled), err)
}
if len(a) != len(reparsed) {
t.Errorf("round-trip length mismatch: %d vs %d", len(a), len(reparsed))
}
})
}
func FuzzTimeUnmarshalJSON(f *testing.F) {
// Float (unix timestamp)
f.Add([]byte(`1609459200`))
f.Add([]byte(`1609459200.5`))
f.Add([]byte(`0`))
f.Add([]byte(`-1`))
// String (ISO8601 / RFC3339)
f.Add([]byte(`"2021-01-01T00:00:00Z"`))
f.Add([]byte(`"2021-01-01T00:00:00.000000000Z"`))
// Edge cases
f.Add([]byte(`null`))
f.Add([]byte(`""`))
f.Add([]byte(`"not-a-date"`))
f.Add([]byte(`true`))
f.Add([]byte(`{}`))
f.Add([]byte(`[]`))
f.Add([]byte(`99999999999999`))
f.Fuzz(func(t *testing.T, data []byte) {
var ts Time
// Must not panic
_ = ts.UnmarshalJSON(data)
})
}
func FuzzSpaceDelimitedArrayUnmarshalJSON(f *testing.F) {
f.Add([]byte(`"openid profile email"`))
f.Add([]byte(`"openid"`))
f.Add([]byte(`""`))
f.Add([]byte(`"a b c d e"`))
// Edge cases
f.Add([]byte(`null`))
f.Add([]byte(`42`))
f.Add([]byte(`" "`))
f.Add([]byte(`" leading"`))
f.Add([]byte(`"trailing "`))
f.Fuzz(func(t *testing.T, data []byte) {
var s SpaceDelimitedArray
err := s.UnmarshalJSON(data)
if err != nil {
return
}
// Round-trip
marshaled, err := json.Marshal(s)
if err != nil {
t.Fatalf("marshal failed: %v", err)
}
var reparsed SpaceDelimitedArray
if err := reparsed.UnmarshalJSON(marshaled); err != nil {
t.Fatalf("round-trip failed: %q -> %v", string(marshaled), err)
}
if len(s) != len(reparsed) {
t.Errorf("round-trip length mismatch: %d vs %d", len(s), len(reparsed))
}
})
}
func FuzzLocalesUnmarshalJSON(f *testing.F) {
f.Add([]byte(`["en", "fr", "de"]`))
f.Add([]byte(`"en fr de"`))
f.Add([]byte(`[]`))
f.Add([]byte(`""`))
f.Add([]byte(`null`))
// Edge cases
f.Add([]byte(`"invalid-locale-tag-xxxxx"`))
f.Add([]byte(`[42]`))
f.Add([]byte(`"en"`))
f.Add([]byte(`"und"`))
f.Fuzz(func(t *testing.T, data []byte) {
var l Locales
// Must not panic
_ = l.UnmarshalJSON(data)
})
}