aboutsummaryrefslogtreecommitdiff
path: root/hotline/field_fuzz_test.go
diff options
context:
space:
mode:
authorJeff Halter <868228+jhalter@users.noreply.github.com>2026-07-10 09:48:18 -0700
committerJeff Halter <868228+jhalter@users.noreply.github.com>2026-07-10 09:48:18 -0700
commit21f24d24fd6f501b32f15a2bef41c89cc461f623 (patch)
treeba4952fb82f3ef9c265228633f27536866e23931 /hotline/field_fuzz_test.go
parentae44fb222ec73cae8441f5a5d9a21f8585fe587f (diff)
Overhaul regression testing: e2e suite, fuzzing, CI, and bug fixes
Add a protocol-level end-to-end suite (in-process fully wired server on an ephemeral port pair, driven by hotline.Client over TCP) covering handshake, login, public and private chat, message board, threaded news, file list/download/upload, account admin, disconnect notification, and shutdown broadcast. The harness retries on a fresh port pair when another process steals a probed port before ListenAndServe binds it, and Server gains WithConnectionRateLimit so tests can disable the per-IP connection throttle. Add native fuzz tests for Transaction, Field, and flattened file object decoding, and fix the bugs the new tests surfaced: - Transaction.Write panicked on out-of-range attacker-controlled size fields, and transactionScanner's uint32 length addition could wrap and yield a truncated token. The information fork size declared in an untrusted fork header is now bounded too. - Client keepalive read c.done unsynchronized while Disconnect replaces it under the mutex. - The shared Agreement's Seek+ReadAll login path raced concurrent logins; the server now prefers an AgreementBytes() snapshot. Fill unit-test gaps (main's config-copy helpers, file resume data, ReloaderFunc, R2 error injection and env validation) and add a CI test workflow (build/vet + race-enabled shuffled suite), fixed lint workflow triggers with golangci-lint v2.6, and Makefile test/cover/ lint/fuzz targets.
Diffstat (limited to 'hotline/field_fuzz_test.go')
-rw-r--r--hotline/field_fuzz_test.go76
1 files changed, 76 insertions, 0 deletions
diff --git a/hotline/field_fuzz_test.go b/hotline/field_fuzz_test.go
new file mode 100644
index 0000000..1af4f5e
--- /dev/null
+++ b/hotline/field_fuzz_test.go
@@ -0,0 +1,76 @@
+package hotline
+
+import (
+ "io"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// FuzzFieldScanner verifies the field-framing split func never panics or advances past its input.
+func FuzzFieldScanner(f *testing.F) {
+ f.Add([]byte{0x00, 0x65, 0x00, 0x03, 0x68, 0x61, 0x69}) // FieldData "hai"
+ f.Add([]byte{0x00, 0x65, 0xff, 0xff}) // declared size larger than input
+ f.Add([]byte{0x00}) // shorter than the size field
+
+ f.Fuzz(func(t *testing.T, data []byte) {
+ advance, token, err := FieldScanner(data, false)
+ if err != nil {
+ return
+ }
+ if advance == 0 {
+ assert.Nil(t, token, "no advance must produce no token")
+ return
+ }
+ require.LessOrEqual(t, advance, len(data), "scanner advanced past its input")
+ require.Len(t, token, advance, "token length must match advance")
+ })
+}
+
+// FuzzFieldWrite feeds raw untrusted bytes to the Field decoder. If decoding succeeds,
+// re-encoding must reproduce exactly the bytes that were consumed.
+func FuzzFieldWrite(f *testing.F) {
+ f.Add([]byte{0x00, 0x65, 0x00, 0x03, 0x68, 0x61, 0x69}) // FieldData "hai"
+ f.Add([]byte{0x00, 0x65, 0x00, 0x00}) // empty data
+ f.Add([]byte{0x00, 0x65, 0xff, 0xff, 0x00}) // declared size overruns buffer
+
+ f.Fuzz(func(t *testing.T, data []byte) {
+ var field Field
+ n, err := field.Write(data)
+ if err != nil {
+ return
+ }
+
+ encoded, err := io.ReadAll(&field)
+ require.NoError(t, err)
+ assert.Equal(t, data[:n], encoded, "re-encoding a decoded field must reproduce the consumed bytes")
+ })
+}
+
+// TestField_RoundTrip pins encode→decode symmetry for fields built with NewField.
+func TestField_RoundTrip(t *testing.T) {
+ tests := []struct {
+ name string
+ field Field
+ }{
+ {name: "with data", field: NewField(FieldData, []byte("hello"))},
+ {name: "empty data", field: NewField(FieldUserPassword, []byte{})},
+ {name: "binary data", field: NewField(FieldUserIconID, []byte{0x07, 0xd1})},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ encoded, err := io.ReadAll(&tt.field)
+ require.NoError(t, err)
+
+ var decoded Field
+ n, err := decoded.Write(encoded)
+ require.NoError(t, err)
+ assert.Equal(t, len(encoded), n)
+ assert.Equal(t, tt.field.Type, decoded.Type)
+ assert.Equal(t, tt.field.FieldSize, decoded.FieldSize)
+ assert.Equal(t, tt.field.Data, decoded.Data)
+ })
+ }
+}