> ## Documentation Index
> Fetch the complete documentation index at: https://engineering.unkey.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Fuzz tests

> Finding edge cases with randomized inputs

## What fuzzing does

Fuzz testing feeds random inputs to your code and watches for crashes, panics, or assertion failures. It finds bugs that humans do not think to test for, such as malformed UTF-8, integer overflows, and nil pointer dereferences.

Go has built-in fuzzing since Go 1.18. Unkey fuzz tests must use `pkg/fuzz` on top of Go's native fuzzing API. `pkg/fuzz` provides deterministic seeds and converts fuzzer-controlled byte slices into typed values without hiding inputs from the coverage-guided fuzzer.

Every fuzz test must call `fuzz.Seed(f)`, accept `data []byte`, create a `fuzz.Consumer` with `fuzz.New(t, data)`, and derive generated values from that consumer. Do not fuzz typed parameters directly, and do not use `math/rand` or ad hoc byte slicing inside fuzz tests.

When the fuzzer finds a failure, Go saves that input so the bug becomes a regression test.

## When to write fuzz tests

Fuzz tests are best for code that processes untrusted input: parsing, encoding, decoding, validation, and cryptographic operations.

They are less useful for business logic with complex preconditions.

## Writing your first fuzz test

Start a fuzz test by naming the property it protects. Seed inputs are examples of the property, not the property itself. Add a docstring when the property is a security boundary, parser invariant, or regression from a production bug.

```go theme={"theme":"kanagawa-wave"}
import "github.com/unkeyed/unkey/pkg/fuzz"

// FuzzParseConfigPreservesTimeoutBounds guarantees that every accepted config
// produces a non-negative timeout. Invalid config may be rejected, but accepted
// config must be safe to execute.
func FuzzParseConfig(f *testing.F) {
    fuzz.Seed(f)

    f.Fuzz(func(t *testing.T, data []byte) {
        c := fuzz.New(t, data)
        input := c.String()

        cfg, err := ParseConfig(input)
        if err != nil {
            return
        }

        require.NotNil(t, cfg)
        require.GreaterOrEqual(t, cfg.Timeout, 0)
    })
}
```

## Skipping invalid inputs

Use `t.Skip()` for inputs that do not meet required preconditions. `pkg/fuzz` also calls `t.Skip()` when the consumer runs out of bytes, which keeps every generated value tied to fuzzer-controlled input.

```go theme={"theme":"kanagawa-wave"}
if len(key) != 16 && len(key) != 24 && len(key) != 32 {
    t.Skip("invalid key size")
}
```

## Testing security properties

Use fuzzing to validate tamper detection and authentication guarantees.

Document security properties in the fuzz test docstring. A future reader must know whether the fuzzer protects parser safety, signature verification, canonical encoding, authorization boundaries, or another guarantee.

## Running fuzz tests

During normal test runs, fuzz tests execute only with their seed corpus:

```bash theme={"theme":"kanagawa-wave"}
mise exec -- rask ./pkg/encryption
```

To fuzz locally:

```bash theme={"theme":"kanagawa-wave"}
mise exec -- go test -fuzz=FuzzParseConfig -fuzztime=30s ./pkg/config/
```

When fuzzing finds a failure, Go saves the input to `testdata/fuzz/<TestName>/` so it becomes part of the seed corpus.

## What to do when fuzzing finds a bug

Write a deterministic unit test for the failing input, then fix the bug. Keep the fuzz corpus in `testdata` to prevent regressions.

## Corpus files

Fuzz tests live in regular Go test files. Commit saved failure inputs under
`testdata/fuzz/<TestName>/` so normal test runs replay them.
