test(b3-6): fuzz seeds — epoch-1 corpora for every target, protocol/auth/predicate-parity fuzz targets, make fuzz green (#1457)

* test(b3-6): fuzz seeds — epoch-1 corpora for every target, protocol + predicate-parity fuzz targets

Workstream 3. Every Fuzz* target `make fuzz` loops over now has a committed
corpus, so a plain `go test ./...` replays the real wire and not only the
hand-written f.Add shapes. 17 -> 20 targets, 3 -> 20 with a corpus, 98 corpus
files added.

Two new targets:

- ws/protocol_fuzz_test.go — FuzzHandleMessageDecode drives the inbound
  envelope decoder (handlers.go) through a headless NewHubForTest +
  NewTestClient; FuzzCommandPayloads drives all 24 payload decoders in
  commandConstructors, which are pure funcs of (userID, reqID, raw) and so
  need no hub at all. Between them they pin: a rejected frame yields no log
  fields and one invalid-count tick, an accepted frame yields the 64-byte
  capped fields and re-encodes to an equal envelope, a rejected payload never
  returns a command alongside its error, and a decoded command always carries
  the authenticated sender rather than a user id lifted from the payload.
- permissions/predicates_fuzz_test.go — FuzzPredicateParity continues the
  B2-5 parity tables by machine: each predicate against the two-layer
  override formula written out longhand, sentinel included (so "an
  unauthorized caller never learns a channel is archived" is pinned), plus
  CanAdmitSession == CanViewChannel and CanType == CanSendMessage.

Corpus entries are generated from protocol/fixtures/epoch-1 — every distinct
c2s frame of the 11 journeys for the two ws targets, and the role permission
values, channel types, message bodies, usernames, avatar URL and channel ids
those journeys carry for the rest. Replay costs <= 0.02s per target.

Test-only: no production file changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo

* docs(b3-6): evidence block for item 5 (fuzz seeds)

Seed counts per target, the two RED negative controls with their failing
excerpts, the replay wall clock, and — as the shared rules require — what was
found stale at HEAD for each of the item's four pointers and what was done
instead: the inbound decoders live in handlers.go/command.go not messages.go,
permissions.Subject has no wire form so parity replaces "round-trips", there
is no pure upload-admission function to fuzz, and there is no recovery-token
parser at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo

* test(b3-6): FuzzParseMentionTokens compares with db.LowerASCII, the OC-0131 rule — make fuzz green again

The target still asserted the Unicode fold (strings.ToLower) that OC-0131
removed from parseMentionTokens: usernames.username is COLLATE NOCASE, which
folds ASCII A-Z only, so the parser folds with db.LowerASCII to stay in step
with GetUserIDsByUsernames' equally ASCII-folded map key. Any mention of a
name starting with an uppercase non-ASCII letter (@Ǥ0, @Ł) therefore failed
the assertion, and `make fuzz` found one within four seconds.

The assertion now uses the same fold the code under test does. Nothing else
in the file changes, and no production behaviour is involved — the fold was
already correct; only the check disagreed with it.

30s of fuzzing on a cleared cache: PASS at 1,159,227 execs (it failed at
66,255 before).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo

* test(b3-6): fuzz seeds — every command constructor seeded, the auth payload decoder gets its own target, evidence corrected

commandConstructors registers 26 decoders, not the 24 the evidence block
claimed (the count missed the two E2EE keys), and only 16 had any input: ten
commands appear in no epoch-1 journey, so presence_update, call_ring,
call_decline, voice_token_refresh, voice_mute, voice_deafen, voice_camera,
voice_screenshare, voice_mod_deafen and voice_mod_kick were reachable only if
the fuzzer guessed the type string. Each now has a corpus entry carrying a
minimal valid payload taken from its own decoder struct, with the fixture
channel and user ids where they apply.

TestCommandPayloadSeedsCoverEveryConstructor is the guardrail that keeps that
true: it unions the hand-written seed list with the committed corpus and fails
when a registered command has neither, or when a seed names a command nothing
registers. Removing one corpus entry fails it by name.

auth was decoded by neither target. It is not in the constructor table —
authenticateConn reads it before the hub knows the client — so its two corpus
entries were inert under FuzzCommandPayloads. They move to FuzzAuthPayload,
which pins the property that matters in a handshake a stranger controls: no
numeric field takes a value its Go type cannot hold, and the token that will
be hashed is the string the JSON carried. The production decode is inline
behind a live socket read and a session lookup, so the target mirrors the
struct and the comment says why rather than reshaping production to expose it.

Corpus entries now credit the journey that owns the frame: the ping frame to
ping.json, the auth frame to fresh-connect.json.

Test-only: no production file changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo

* test(b3-6): fuzz seeds — auth target gates on the real epoch constants; corpus reader fails on a malformed entry

FuzzAuthPayload was proving encoding/json behaviour against a copy of the
handshake struct and nothing more. It now mirrors the two rejections
authenticateConn actually makes — the decode error and the empty token as one
(serve_auth.go:58), then the epoch window (:62) — using minClientEpoch and
ProtocolEpoch themselves, so moving either constant or that gate turns the
target red instead of leaving it quietly stale. The load-bearing case is the
absent epoch: every client up to v1.2.0-alpha.4 predates the field and relies
on the zero value being inside the window, so raising minClientEpoch above 0
now fails here rather than in the field. Setting it to 1 locally fails both
fixture-derived corpus entries and two seeds.

Deciding "absent" needed care, and fuzzing found that out in three seconds:
encoding/json falls back to a case-INSENSITIVE tag match, so "epoCh" populates
Epoch while an exact key lookup calls the field missing. The probe now decodes
into a *int, which is the same matching the server does, and three seeds pin
the rule.

corpusFirstString skipped a corpus file with no string(...) argument, which
would have let a malformed entry masquerade as a seeded command while the
coverage test still passed. It is now a failure naming the file.

The struct comment cited serve_auth.go:44; the struct starts at :45.

Test-only: no production file changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo

* test(b3-6): fuzz seeds — token expectation uses struct decoding semantics; parity oracle states the zero-permission ordering (Codex P2s on #1457)

FuzzAuthPayload derived the expected token from an exact key lookup, so
{"token":"a","TOKEN":"b"} failed the target: encoding/json resolves both keys
to the tagged field and the last one wins, leaving the handshake holding "b"
while the lookup expected "a". The expectation now comes from a probe struct
carrying the same json:"token" tag, so it follows the decoder's field
resolution rather than the raw key set — the same correction the epoch probe
already needed. A corpus entry pins it; reverting the probe fails on that
entry by name.

rawHas mirrors Subject.Has, which applies the Administrator bypass before the
zero-permission refusal, so an administrator holds an empty mask where
HasPerm(_, 0) is false. Parity with production is this target's purpose, so
the ordering stays; what changes is that the oracle's contract comment now
states it instead of claiming the tidier rule, and
TestSubjectHasZeroPermIsAdminBypassed records the divergence as observed
behaviour with a message that says to move both together if it is ever
changed deliberately.

The evidence block gains the call-site survey behind that: every leaf caller
of Subject.Has names a permissions.* constant, the variable-forwarding
wrappers are all reached with named constants, and the one table-driven site
has two rows.

No production code changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-08-30 14:30:53 +00:00
committed by GitHub
co-authored by Claude Fable 5
parent 8cb0ec9e35
commit 1e9ac9a842
113 changed files with 1150 additions and 2 deletions
@@ -0,0 +1,2 @@
go test fuzz v1
string("報告書.pdf")
@@ -0,0 +1,2 @@
go test fuzz v1
string("emoji.bin")
@@ -0,0 +1,2 @@
go test fuzz v1
string("alice-avatar.png")
@@ -0,0 +1,2 @@
go test fuzz v1
string("Ünïcode Ärchive.tar.gz")
@@ -0,0 +1,2 @@
go test fuzz v1
string("https://cdn.example.com/avatars/1/abc.webp?v=2")
@@ -0,0 +1,2 @@
go test fuzz v1
string("https://example.com:8443/a.png#f")
@@ -0,0 +1,2 @@
go test fuzz v1
string("https://fixtures.invalid/alice-avatar.png")
@@ -0,0 +1,2 @@
go test fuzz v1
string("fixture profile text")
@@ -0,0 +1,2 @@
go test fuzz v1
string("Alice Fixture")
@@ -0,0 +1,2 @@
go test fuzz v1
string("fixture custom status")
@@ -0,0 +1,2 @@
go test fuzz v1
string("Tr0ub4dor&3-owncord")
@@ -0,0 +1,2 @@
go test fuzz v1
string("correct horse battery staple")
@@ -0,0 +1,2 @@
go test fuzz v1
string("alice")
@@ -0,0 +1,2 @@
go test fuzz v1
string("bob")
@@ -0,0 +1,2 @@
go test fuzz v1
string("Alice Fixture")
@@ -0,0 +1,2 @@
go test fuzz v1
string("edited text")
@@ -0,0 +1,2 @@
go test fuzz v1
string("hello epoch one")
@@ -0,0 +1,2 @@
go test fuzz v1
string("hello over dm")
@@ -0,0 +1,2 @@
go test fuzz v1
string("sent while away")
+215
View File
@@ -0,0 +1,215 @@
package permissions
import (
"errors"
"testing"
)
// Subject has no wire form — it is an in-memory value a caller resolves, so
// there is nothing to round-trip. What B2-5 established instead is PARITY:
// every call site that used to hand-roll a security rule now delegates to one
// predicate, and the parity tables in permissions/, service/ and ws/ pin each
// predicate's verdict case by case. This target is those tables continued by
// machine: it recomputes each verdict from the raw permission bits — the
// two-layer override formula written out longhand, not EffectiveChannelPerms
// — and demands the predicate agree, for arbitrary role bits, arbitrary
// override masks, arbitrary channel types and every combination of the
// archive and DM flags.
//
// The committed corpus is seeded from protocol/fixtures/epoch-1: the four
// role permission values the ready payload carries (Owner 2147483647,
// Admin 1073741823, Moderator 3145727, Member 7779) against the channel
// types those journeys use.
// rawEffective is EffectivePerms/EffectiveChannelPerms written out as the
// formula their doc comments state, so the parity check does not lean on the
// helper it is meant to be independent of.
func rawEffective(rolePerms int64, o ChannelOverride) int64 {
roleLayer := (rolePerms &^ o.Deny) | o.Allow
return (roleLayer &^ o.UserDeny) | o.UserAllow
}
// rawHas is Subject.Has written out IN PRODUCTION'S ORDER, which is not the
// tidy one: the Administrator bypass is applied first and returns true
// unconditionally, so an administrator "holds" a zero permission even though
// HasPerm's own all-of rule says an empty mask is never held
// (HasPerm(Administrator, 0) == false). Only a non-administrator reaches the
// zero-perm refusal, and only then does the two-layer mask get consulted.
//
// Parity is this target's whole purpose, so the oracle keeps that ordering
// rather than the contract the doc comments imply.
// TestSubjectHasZeroPermIsAdminBypassed records the divergence as observed
// behaviour, and the item's evidence block carries the call-site survey
// showing no production caller passes 0 today.
func rawHas(rolePerms int64, o ChannelOverride, perm int64) bool {
if rolePerms&Administrator != 0 {
return true
}
if perm == 0 {
return false
}
return rawEffective(rolePerms, o)&perm == perm
}
// TestSubjectHasZeroPermIsAdminBypassed records, as OBSERVED behaviour and not
// as a property anything should rely on, the one place Subject.Has and HasPerm
// disagree: Subject.Has applies the Administrator bypass before the zero-perm
// refusal, so an administrator holds the empty mask while HasPerm never does.
// No production call site passes 0 (every one names a permissions.* constant),
// so nothing turns on it today — but rawHas mirrors the ordering and this test
// is what says so out loud. If the ordering is ever deliberately changed,
// rawHas and this test move together.
func TestSubjectHasZeroPermIsAdminBypassed(t *testing.T) {
if HasPerm(Administrator, 0) {
t.Fatal("HasPerm(_, 0) = true; the all-of rule on an empty mask must stay false")
}
if !(Subject{RolePerms: Administrator}).Has(0) {
t.Fatal("observed behaviour changed: Subject.Has(0) no longer returns true for an administrator — update rawHas and this test together")
}
if (Subject{RolePerms: ReadMessages}).Has(0) {
t.Fatal("Subject.Has(0) = true for a non-administrator; only the Administrator bypass may reach that")
}
}
// wantErr asserts a predicate's verdict against the recomputed one: nil means
// allowed, anything else must be that sentinel (missing() wraps
// ErrPermissionDenied, so errors.Is is the right comparison).
func wantErr(t *testing.T, name string, s Subject, got, want error) {
t.Helper()
switch {
case want == nil && got != nil:
t.Fatalf("%s(%+v) = %v, want allowed", name, s, got)
case want != nil && !errors.Is(got, want):
t.Fatalf("%s(%+v) = %v, want %v", name, s, got, want)
}
}
// sameVerdict reports whether two predicates answered identically. missing()
// builds a fresh error each call, so identity comparison would report a
// difference between two denials that are in fact the same one.
func sameVerdict(a, b error) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
return a.Error() == b.Error()
}
// FuzzPredicateParity checks every B2-5 predicate against the raw-bit
// computation it replaced, plus the two definitional identities the package
// documents: CanAdmitSession is CanViewChannel, and CanType is
// CanSendMessage.
func FuzzPredicateParity(f *testing.F) {
// The four role permission values epoch-1's ready payload carries.
const (
owner = int64(2147483647)
admin = int64(1073741823)
mod = int64(3145727)
member = int64(7779)
)
types := []string{"text", "voice", "dm", "announcement", "", "TEXT"}
roles := []int64{0, member, mod, admin, owner, -1, Administrator}
for _, role := range roles {
for _, typ := range types {
f.Add(role, int64(0), int64(0), int64(0), int64(0), typ, false, false, false)
f.Add(role, int64(0), ReadMessages, int64(0), int64(0), typ, false, true, false)
f.Add(role, int64(0), int64(0), ManageMessages, SendMessages, typ, true, true, true)
f.Add(role, ConnectVoice, MuteMembers, int64(0), ReadMessages, typ, true, false, true)
}
}
f.Fuzz(func(t *testing.T,
rolePerms, allow, deny, userAllow, userDeny int64,
chanType string,
archived, dmParticipant, dmBlocked bool,
) {
o := ChannelOverride{Allow: allow, Deny: deny, UserAllow: userAllow, UserDeny: userDeny}
s := Subject{
RolePerms: rolePerms,
Override: o,
Channel: ChannelRef{Type: chanType, Archived: archived},
DMParticipant: dmParticipant,
DMBlocked: dmBlocked,
}
has := func(perm int64) bool { return rawHas(rolePerms, o, perm) }
isDM := chanType == "dm"
// Subject.Has is the one bit predicate every rule is built on.
for _, perm := range []int64{0, ReadMessages, SendMessages, ManageMessages, ConnectVoice, MuteMembers, ReadMessages | SendMessages, ReadMessages | MuteMembers} {
if s.Has(perm) != has(perm) {
t.Fatalf("Subject.Has(%#x) on %+v = %v, raw-bit computation says %v", perm, s, s.Has(perm), has(perm))
}
}
// Visibility: permission before archive, so an unauthorized caller
// learns nothing about the channel; a DM answers to membership alone.
var wantView error
switch {
case isDM && !dmParticipant:
wantView = ErrNotDMParticipant
case isDM:
wantView = nil
case !has(ReadMessages):
wantView = ErrPermissionDenied
case archived:
wantView = ErrArchived
}
wantErr(t, "CanViewChannel", s, CanViewChannel(s), wantView)
// Posting: READ+SEND, MANAGE on top for an announcement, never into
// an archive; a DM needs membership and no block.
var wantSend error
switch {
case isDM && !dmParticipant:
wantSend = ErrNotDMParticipant
case isDM && dmBlocked:
wantSend = ErrBlocked
case isDM:
wantSend = nil
case !has(ReadMessages | SendMessages):
wantSend = ErrPermissionDenied
case chanType == "announcement" && !has(ManageMessages):
wantSend = ErrPermissionDenied
case archived:
wantSend = ErrArchived
}
wantErr(t, "CanSendMessage", s, CanSendMessage(s), wantSend)
// Voice: the CONNECT bit first (so a channel type is never disclosed
// to someone without it), then the room, then the archive.
var wantVoice error
switch {
case !has(ConnectVoice):
wantVoice = ErrPermissionDenied
case isDM && !dmParticipant:
wantVoice = ErrNotDMParticipant
case isDM && dmBlocked:
wantVoice = ErrBlocked
case !isDM && chanType != "voice":
wantVoice = ErrNotVoiceChannel
case archived:
wantVoice = ErrArchived
}
wantErr(t, "CanJoinVoice", s, CanJoinVoice(s), wantVoice)
// Moderation authority in the TARGET's channel: DM membership first,
// then effective READ+MUTE there. Archive does not gate it.
var wantMod error
switch {
case isDM && !dmParticipant:
wantMod = ErrNotDMParticipant
case !has(ReadMessages | MuteMembers):
wantMod = ErrPermissionDenied
}
wantErr(t, "CanModerateVoice", s, CanModerateVoice(s), wantMod)
// The two definitional identities. A future edit that gives session
// admission its own rule, or lets someone announce a post they cannot
// make, breaks here rather than silently.
if !sameVerdict(CanAdmitSession(s), CanViewChannel(s)) {
t.Fatalf("CanAdmitSession(%+v) = %v but CanViewChannel = %v — session admission is visibility by definition", s, CanAdmitSession(s), CanViewChannel(s))
}
if !sameVerdict(CanType(s), CanSendMessage(s)) {
t.Fatalf("CanType(%+v) = %v but CanSendMessage = %v — a typing indicator announces a post (S-01)", s, CanType(s), CanSendMessage(s))
}
})
}
@@ -0,0 +1,6 @@
go test fuzz v1
int64(7779)
int64(0)
int64(1)
int64(1)
int64(0)
@@ -0,0 +1,6 @@
go test fuzz v1
int64(7779)
int64(0)
int64(0)
int64(0)
int64(2)
@@ -0,0 +1,6 @@
go test fuzz v1
int64(3145727)
int64(1048576)
int64(0)
int64(0)
int64(1048576)
@@ -0,0 +1,6 @@
go test fuzz v1
int64(2147483647)
int64(0)
int64(0)
int64(0)
int64(2147483647)
@@ -0,0 +1,4 @@
go test fuzz v1
int64(1073741823)
int64(0)
int64(1048576)
@@ -0,0 +1,4 @@
go test fuzz v1
int64(7779)
int64(0)
int64(2)
@@ -0,0 +1,4 @@
go test fuzz v1
int64(3145727)
int64(1073741824)
int64(0)
@@ -0,0 +1,4 @@
go test fuzz v1
int64(2147483647)
int64(0)
int64(2147483647)
@@ -0,0 +1,10 @@
go test fuzz v1
int64(1073741823)
int64(0)
int64(2)
int64(0)
int64(0)
string("text")
bool(false)
bool(false)
bool(false)
@@ -0,0 +1,10 @@
go test fuzz v1
int64(7779)
int64(0)
int64(0)
int64(0)
int64(0)
string("dm")
bool(false)
bool(true)
bool(false)
@@ -0,0 +1,10 @@
go test fuzz v1
int64(7779)
int64(0)
int64(0)
int64(0)
int64(0)
string("dm")
bool(true)
bool(true)
bool(false)
@@ -0,0 +1,10 @@
go test fuzz v1
int64(7779)
int64(0)
int64(0)
int64(0)
int64(0)
string("dm")
bool(false)
bool(true)
bool(true)
@@ -0,0 +1,10 @@
go test fuzz v1
int64(7779)
int64(0)
int64(0)
int64(0)
int64(0)
string("text")
bool(false)
bool(false)
bool(false)
@@ -0,0 +1,10 @@
go test fuzz v1
int64(7779)
int64(0)
int64(0)
int64(0)
int64(0)
string("text")
bool(true)
bool(false)
bool(false)
@@ -0,0 +1,10 @@
go test fuzz v1
int64(7779)
int64(0)
int64(2)
int64(2)
int64(0)
string("text")
bool(false)
bool(false)
bool(false)
@@ -0,0 +1,10 @@
go test fuzz v1
int64(7779)
int64(0)
int64(0)
int64(0)
int64(0)
string("voice")
bool(false)
bool(false)
bool(false)
@@ -0,0 +1,10 @@
go test fuzz v1
int64(3145727)
int64(0)
int64(0)
int64(0)
int64(0)
string("announcement")
bool(false)
bool(false)
bool(false)
@@ -0,0 +1,10 @@
go test fuzz v1
int64(3145727)
int64(0)
int64(0)
int64(0)
int64(1048576)
string("voice")
bool(false)
bool(false)
bool(false)
@@ -0,0 +1,10 @@
go test fuzz v1
int64(2147483647)
int64(0)
int64(0)
int64(0)
int64(0)
string("dm")
bool(false)
bool(false)
bool(true)
@@ -0,0 +1,10 @@
go test fuzz v1
int64(2147483647)
int64(0)
int64(0)
int64(0)
int64(0)
string("voice")
bool(true)
bool(true)
bool(false)
@@ -0,0 +1,2 @@
go test fuzz v1
string("plugin.v2.wasm")
@@ -0,0 +1,2 @@
go test fuzz v1
string("main.wasm")
@@ -0,0 +1,2 @@
go test fuzz v1
string("assets/icons/icon.png")
+8 -2
View File
@@ -4,6 +4,8 @@ import (
"regexp"
"strings"
"testing"
"github.com/J3vb/OwnCord/Server/db"
)
// fuzzSpellingRe mirrors the token charset mentionTokenRe captures: letters,
@@ -71,8 +73,12 @@ func FuzzParseMentionTokens(f *testing.F) {
if strings.Contains(sp, "@") {
t.Fatalf("spelling %q retained an '@' -- address-shaped text leaked a mention (content %q)", sp, content)
}
if sp != strings.ToLower(sp) {
t.Fatalf("spelling %q is not lowercased", sp)
// db.LowerASCII, not strings.ToLower: parseMentionTokens folds
// ASCII only, because usernames.username is COLLATE NOCASE and a
// Unicode fold would desync the token from
// GetUserIDsByUsernames' equally ASCII-folded map key (OC-0131).
if sp != db.LowerASCII(sp) {
t.Fatalf("spelling %q is not ASCII-lowercased", sp)
}
if !fuzzSpellingRe.MatchString(sp) {
t.Fatalf("spelling %q outside the token charset (content %q)", sp, content)
@@ -0,0 +1,2 @@
go test fuzz v1
string("hello epoch one")
@@ -0,0 +1,2 @@
go test fuzz v1
string("@Alice Fixture edited text")
@@ -0,0 +1,2 @@
go test fuzz v1
string("@alice @bob hello over dm")
@@ -0,0 +1,2 @@
go test fuzz v1
string("@everyone @here @alice")
@@ -0,0 +1,2 @@
go test fuzz v1
string("party_parrot")
@@ -0,0 +1,2 @@
go test fuzz v1
string(":thumbsup:")
@@ -0,0 +1,2 @@
go test fuzz v1
string("100")
@@ -0,0 +1,2 @@
go test fuzz v1
string("emoji.bin")
@@ -0,0 +1,2 @@
go test fuzz v1
string("550e8400-e29b-41d4-a716-446655440000")
@@ -0,0 +1,2 @@
go test fuzz v1
string(".550e8400-e29b-41d4-a716-446655440000")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\x00\x00\x00 ftypisom\x00\x00\x02\x00")
+2
View File
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("OggS\x00\x02\x00\x00")
+2
View File
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("%PDF-1.7\n%\xe2\xe3\xcf\xd3")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("RIFF$\x00\x00\x00WEBPVP8 ")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("PK\x03\x04\x14\x00\x00\x00\b\x00")
+463
View File
@@ -0,0 +1,463 @@
package ws
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
"testing"
)
// Inbound protocol parsing, fuzzed at the three seams a hostile client can
// reach: the envelope every frame arrives in, the per-command payload decoders
// the envelope's type selects, and the auth frame — which is decoded before
// any of that, by authenticateConn.
//
// messages.go is almost entirely OUTBOUND builders — parseChannelID is its
// only inbound decoder — so "protocol parsing" is handleMessageDecode
// (handlers.go) plus commandConstructors (command.go). Every constructor in
// that map is a pure function of (userID, reqID, raw), so both targets run
// with no DB, no socket and (for the payloads) no hub at all.
//
// The committed corpus under testdata/fuzz/ is generated from
// protocol/fixtures/epoch-1: every distinct c2s frame of the recorded
// journeys, with the transcript's placeholders (<id:string>, <seq:number>,
// <token:string>, <id:number>) replaced by concrete values, named for the
// journey that owns the frame. A plain `go test ./ws` replays it, so CI's fuzz
// replay covers the real wire and not only the hand-written shapes seeded
// below. Ten command types appear in no journey; their entries carry a minimal
// valid payload instead, and TestCommandPayloadSeedsCoverEveryConstructor
// fails if a newly registered command arrives without one.
// envelopeLogFieldCap mirrors the unnamed 64 at handlers.go:190 and :194 — the
// cap handleMessageDecode applies to the client-controlled type and id before
// they reach a log line. Changing it there must change it here.
const envelopeLogFieldCap = 64
func capLogField(s string) string {
if len(s) > envelopeLogFieldCap {
return s[:envelopeLogFieldCap]
}
return s
}
// FuzzHandleMessageDecode drives the envelope decoder with arbitrary frames.
// Contract: it accepts exactly the frames that unmarshal into an envelope;
// a rejected frame yields no log fields at all (never a partial success) and
// counts once against the client's consecutive-invalid budget; an accepted
// frame yields the capped envelope fields and re-encodes to something that
// decodes back to an equal envelope.
func FuzzHandleMessageDecode(f *testing.F) {
seeds := [][]byte{
nil,
[]byte(""),
[]byte(" "),
[]byte("{}"),
[]byte("null"),
[]byte("[]"),
[]byte("0"),
[]byte(`"ping"`),
[]byte(`{"type":"ping"}`),
[]byte(`{"type":"ping","id":"r1","payload":{}}`),
[]byte(`{"type":123}`),
[]byte(`{"id":42}`),
[]byte(`{"payload":{"channel_id":1}}`),
[]byte(`{"type":"ping"}{"type":"ping"}`),
[]byte(`{"type":"ping","payload":null}`),
[]byte(`{"type":"ping","payload":{"a":1,"a":2}}`),
[]byte(`{"TYPE":"ping"}`),
[]byte(`{"type":"\ud800"}`),
[]byte(`{"type":"a<b&c>d","payload":{"s":"<script>"}}`),
[]byte(`{"type":"` + strings.Repeat("t", 200) + `"}`),
[]byte(`{"id":"` + strings.Repeat("i", 200) + `"}`),
[]byte(`{"type":"` + strings.Repeat("é", 40) + `"}`),
}
for _, s := range seeds {
f.Add(s)
}
f.Fuzz(func(t *testing.T, raw []byte) {
// A fresh hub and client per input: the decoder mutates the client's
// consecutive-invalid counter, so sharing one would make each input's
// verdict depend on the ones before it.
hub := NewHubForTest()
c := NewTestClient(hub, 1, make(chan []byte, 4))
env, msgType, reqID, ok := hub.handleMessageDecode(c, raw)
// The probe restates the implementation, so it only pins that the
// accept/reject split stays a plain envelope unmarshal; the cap, the
// invalid counter and the round-trip below are the load-bearing checks.
var probe envelope
wantOK := json.Unmarshal(raw, &probe) == nil
if ok != wantOK {
t.Fatalf("handleMessageDecode(%q) ok = %v, but json.Unmarshal into an envelope succeeds = %v", raw, ok, wantOK)
}
c.mu.Lock()
invalid := c.invalidCount
c.mu.Unlock()
if !ok {
// Rejected: the caller stops, so nothing client-controlled may
// have been handed back for it to log or dispatch on.
if msgType != "" || reqID != "" {
t.Fatalf("handleMessageDecode(%q) rejected the frame but returned msgType %q / reqID %q", raw, msgType, reqID)
}
if invalid != 1 {
t.Fatalf("handleMessageDecode(%q) rejected the frame but left invalidCount = %d, want 1", raw, invalid)
}
return
}
if invalid != 0 {
t.Fatalf("handleMessageDecode(%q) accepted the frame but left invalidCount = %d, want 0", raw, invalid)
}
if want := capLogField(env.Type); msgType != want {
t.Fatalf("handleMessageDecode(%q) msgType = %q, want %q (envelope type capped at %d bytes)", raw, msgType, want, envelopeLogFieldCap)
}
if want := capLogField(env.ID); reqID != want {
t.Fatalf("handleMessageDecode(%q) reqID = %q, want %q (envelope id capped at %d bytes)", raw, reqID, want, envelopeLogFieldCap)
}
if len(msgType) > envelopeLogFieldCap || len(reqID) > envelopeLogFieldCap {
t.Fatalf("handleMessageDecode(%q) returned uncapped log fields: msgType %d bytes, reqID %d bytes", raw, len(msgType), len(reqID))
}
// Round-trip: a frame that decodes must re-encode to something that
// decodes back to an equal envelope.
reencoded, err := json.Marshal(env)
if err != nil {
t.Fatalf("handleMessageDecode(%q) accepted the frame but its envelope will not marshal: %v", raw, err)
}
var env2 envelope
if err := json.Unmarshal(reencoded, &env2); err != nil {
t.Fatalf("handleMessageDecode(%q) envelope re-encoded to %q, which no longer decodes: %v", raw, reencoded, err)
}
if env2.Type != env.Type || env2.ID != env.ID {
t.Fatalf("handleMessageDecode(%q) envelope round-trip changed the header: (%q,%q) -> (%q,%q)", raw, env.Type, env.ID, env2.Type, env2.ID)
}
if !equalJSON(t, env.Payload, env2.Payload) {
t.Fatalf("handleMessageDecode(%q) envelope round-trip changed the payload: %q -> %q", raw, env.Payload, env2.Payload)
}
})
}
// equalJSON compares two raw payloads by value rather than by bytes:
// json.Marshal compacts a json.RawMessage and HTML-escapes <, > and & inside
// it, so a byte comparison would report a difference where the decoded value
// is identical.
func equalJSON(t *testing.T, a, b json.RawMessage) bool {
t.Helper()
if len(a) == 0 || len(b) == 0 {
return len(a) == 0 && len(b) == 0
}
var av, bv any
if err := json.Unmarshal(a, &av); err != nil {
t.Fatalf("payload %q came out of a successful envelope decode but does not itself decode: %v", a, err)
}
if err := json.Unmarshal(b, &bv); err != nil {
t.Fatalf("re-encoded payload %q does not decode: %v", b, err)
}
return reflect.DeepEqual(av, bv)
}
// FuzzCommandPayloads drives every registered command's payload decoder.
// Contract: no panic; an error never comes back alongside a command (no
// partial success); a decoded command reports the type it was registered
// under and the AUTHENTICATED sender, never a user id lifted out of the
// payload; and decoding is deterministic.
func FuzzCommandPayloads(f *testing.F) {
const (
fuzzUserID = int64(7)
fuzzReqID = "req-fuzz"
)
for _, s := range commandPayloadSeeds {
f.Add(s.msgType, []byte(s.payload))
}
f.Fuzz(func(t *testing.T, msgType string, payload []byte) {
ctor, ok := getCommandConstructor(msgType)
if !ok {
return
}
cmd, err := ctor(fuzzUserID, fuzzReqID, json.RawMessage(payload))
if err != nil {
if cmd != nil {
t.Fatalf("%s(%q) returned command %#v alongside error %v — a rejected payload must yield no command", msgType, payload, cmd, err)
}
return
}
if cmd == nil {
t.Fatalf("%s(%q) returned (nil, nil)", msgType, payload)
}
if cmd.Type() != msgType {
t.Fatalf("%s(%q) decoded to a command of type %q", msgType, payload, cmd.Type())
}
if cmd.UserID() != fuzzUserID {
t.Fatalf("%s(%q) decoded to UserID %d, want the authenticated %d — the payload must never set the sender", msgType, payload, cmd.UserID(), fuzzUserID)
}
again, err2 := ctor(fuzzUserID, fuzzReqID, json.RawMessage(payload))
if err2 != nil {
t.Fatalf("%s(%q) decoded once but failed on the identical second call: %v", msgType, payload, err2)
}
if !reflect.DeepEqual(cmd, again) {
t.Fatalf("%s(%q) is not deterministic: %#v then %#v", msgType, payload, cmd, again)
}
})
}
// commandPayloadSeeds is FuzzCommandPayloads' hand-written seed list: the
// adversarial shapes. The valid per-command payloads live in the committed
// corpus, so both halves are checked by
// TestCommandPayloadSeedsCoverEveryConstructor.
var commandPayloadSeeds = []struct {
msgType string
payload string
}{
{MsgTypePing, `{}`},
{MsgTypeChatSend, `{"channel_id":1,"content":"hello"}`},
{MsgTypeChatSend, `{"channel_id":"1","content":"","attachments":[],"reply_to":null}`},
{MsgTypeChatSend, `{"channel_id":1,"user_id":999,"content":"spoof"}`},
{MsgTypeChatEdit, `{"message_id":1,"content":"edited"}`},
{MsgTypeChatDelete, `{"message_id":1}`},
{MsgTypeTypingStart, `{"channel_id":0}`},
{MsgTypeMarkRead, `{"channel_id":-1}`},
{MsgTypeChannelFocus, `{"channel_id":1.5}`},
{MsgTypeReactionAdd, `{"message_id":1,"emoji":"x"}`},
{MsgTypeVoiceJoin, `{"channel_id":9223372036854775808}`},
{MsgTypeVoiceLeave, `garbage-not-json`},
{MsgTypeVoiceModMute, `{"channel_id":1,"user_id":2,"muted":true}`},
{MsgTypeVoiceModMove, `{"user_id":2,"to_channel_id":0}`},
{MsgTypeChatCommand, `{"channel_id":1,"command":" ","args":[]}`},
{MsgTypeVoiceE2EEOffer, `{"target_user_id":2,"encrypted_key":"AA==","iv":"AA=="}`},
{"not_a_command", `{}`},
{"", ``},
}
// corpusFirstString returns the first string(...) argument of every committed
// corpus entry for target — for FuzzCommandPayloads that is the message type.
func corpusFirstString(t *testing.T, target string) []string {
t.Helper()
dir := filepath.Join("testdata", "fuzz", target)
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("reading corpus dir %s: %v", dir, err)
}
var out []string
for _, e := range entries {
if e.IsDir() {
continue
}
body, err := os.ReadFile(filepath.Join(dir, e.Name()))
if err != nil {
t.Fatalf("reading corpus entry %s: %v", e.Name(), err)
}
found := false
for line := range strings.SplitSeq(string(body), "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "string(") || !strings.HasSuffix(line, ")") {
continue
}
v, uerr := strconv.Unquote(line[len("string(") : len(line)-1])
if uerr != nil {
t.Fatalf("corpus entry %s: cannot unquote %s: %v", e.Name(), line, uerr)
}
out = append(out, v)
found = true
break
}
if !found {
// Skipping it silently would let a malformed entry masquerade as a
// seeded command: the coverage check would pass while the corpus
// file contributed nothing.
t.Fatalf("corpus entry %s has no string(...) argument, so it seeds no message type", filepath.Join(dir, e.Name()))
}
}
return out
}
// TestCommandPayloadSeedsCoverEveryConstructor is the guardrail that keeps the
// corpus honest: registering a command in commandConstructors without a seed
// leaves its decoder reachable only if the fuzzer guesses the type string,
// which is not coverage. Ten of the 26 constructors appear in no epoch-1
// journey, so their corpus entries carry a minimal valid payload instead.
func TestCommandPayloadSeedsCoverEveryConstructor(t *testing.T) {
seeded := make(map[string]bool, len(commandConstructors))
for _, s := range commandPayloadSeeds {
seeded[s.msgType] = true
}
for _, typ := range corpusFirstString(t, "FuzzCommandPayloads") {
seeded[typ] = true
}
var missing []string
for typ := range commandConstructors {
if !seeded[typ] {
missing = append(missing, typ)
}
}
sort.Strings(missing)
if len(missing) > 0 {
t.Fatalf("%d of %d command constructors have no seed or corpus entry: %v\n"+
"add one to commandPayloadSeeds or to testdata/fuzz/FuzzCommandPayloads/",
len(missing), len(commandConstructors), missing)
}
// The reverse direction: a seed for a type nothing registers is dead
// weight, and usually a rename that left the corpus behind. The two
// deliberate negatives are the unregistered-type path itself.
for typ := range seeded {
if typ == "" || typ == "not_a_command" {
continue
}
if _, ok := commandConstructors[typ]; !ok {
t.Errorf("seed/corpus entry for %q, which is not a registered command", typ)
}
}
}
// authPayloadMirror mirrors the anonymous struct authenticateConn decodes the
// auth payload into (serve_auth.go:45). auth is deliberately NOT in
// commandConstructors — the handshake runs before the hub knows the client —
// and that decode is inline behind a live websocket read and a session lookup,
// so it cannot be called headlessly. Factoring it out would be a production
// change, which this item may not make; mirroring it is the smallest reachable
// parse, and this comment is the drift warning that buys.
type authPayloadMirror struct {
Token string `json:"token"`
LastSeq uint64 `json:"last_seq"`
ActiveChannelID int64 `json:"active_channel_id"`
Epoch int `json:"epoch"`
}
// FuzzAuthPayload drives the handshake frame: envelope, the auth type gate,
// and the payload decode. The property worth fuzzing is that none of the three
// numeric fields a client controls can take a value its Go type cannot hold —
// a last_seq of -1 silently becoming 2^64-1 would let a reconnecting client
// skip its entire replay — and that the token the handshake goes on to hash is
// the string the JSON actually carried.
func FuzzAuthPayload(f *testing.F) {
seeds := []string{
`{"type":"auth","payload":{"token":"t","last_seq":0}}`,
`{"type":"auth","payload":{"token":"t","last_seq":-1}}`,
`{"type":"auth","payload":{"token":"t","last_seq":18446744073709551616}}`,
`{"type":"auth","payload":{"token":"t","last_seq":1.5}}`,
`{"type":"auth","payload":{"token":"t","active_channel_id":9223372036854775808}}`,
`{"type":"auth","payload":{"token":"t","epoch":-1}}`,
`{"type":"auth","payload":{"token":"t","epoch":2}}`,
`{"type":"auth","payload":{"token":"t","epoch":99999999999999999999}}`,
// encoding/json matches a tag case-insensitively when no exact key is
// present, so these three DO set Epoch and are not "absent".
`{"type":"auth","payload":{"token":"t","epoCh":10}}`,
`{"type":"auth","payload":{"token":"t","EPOCH":1}}`,
`{"type":"auth","payload":{"token":"t","epoch":1,"EPOCH":9}}`,
`{"type":"auth","payload":{"token":""}}`,
`{"type":"auth","payload":{"token":null}}`,
`{"type":"auth","payload":{"token":"a","token":"b"}}`,
`{"type":"auth","payload":[]}`,
`{"type":"auth"}`,
`{"type":"AUTH","payload":{"token":"t"}}`,
`{"type":"ping","payload":{"token":"t"}}`,
}
for _, s := range seeds {
f.Add([]byte(s))
}
numeric := []struct {
key string
fits func(string) error
}{
{"last_seq", func(s string) error { _, err := strconv.ParseUint(s, 10, 64); return err }},
{"active_channel_id", func(s string) error { _, err := strconv.ParseInt(s, 10, 64); return err }},
{"epoch", func(s string) error { _, err := strconv.Atoi(s); return err }},
}
f.Fuzz(func(t *testing.T, raw []byte) {
var env envelope
if json.Unmarshal(raw, &env) != nil {
return
}
if env.Type != MsgTypeAuth {
return // serve_auth.go:40 closes the connection before this parse
}
var p authPayloadMirror
structErr := json.Unmarshal(env.Payload, &p)
var fields map[string]json.RawMessage
if json.Unmarshal(env.Payload, &fields) != nil {
if structErr == nil {
t.Fatalf("auth payload %q is not a JSON object, yet decoding it into the handshake struct succeeded", env.Payload)
}
return
}
for _, n := range numeric {
rawField, ok := fields[n.key]
if !ok {
continue
}
var num json.Number
if json.Unmarshal(rawField, &num) != nil {
continue // not a JSON number; the struct decode rejects it too
}
if n.fits(string(num)) != nil && structErr == nil {
t.Fatalf("auth payload %q: %s = %s does not fit its field type, yet the handshake decode succeeded as %+v", env.Payload, n.key, num, p)
}
}
// The handshake gate, mirrored from serve_auth.go:58 (the decode error
// and the empty token are one rejection) and :62 (the epoch window),
// using the real constants so that moving either one turns this target
// red instead of leaving it quietly stale.
decoded := structErr == nil && p.Token != ""
admitted := decoded && p.Epoch >= minClientEpoch && p.Epoch <= ProtocolEpoch
if minClientEpoch > ProtocolEpoch {
t.Fatalf("epoch window [%d, %d] is empty — no client can complete the handshake", minClientEpoch, ProtocolEpoch)
}
// Absent epoch means 0 (serve_auth.go:53-55): every client up to
// v1.2.0-alpha.4 predates the field, so a payload that omits it and
// carries a token must still be admitted. Raising minClientEpoch above
// 0 locks those clients out, and fails here rather than in the field.
//
// "Omits" is decided by decoding into a *int rather than by looking
// the key up in fields: encoding/json falls back to a
// case-INSENSITIVE tag match, so "epoCh" populates Epoch while an
// exact-key probe calls it absent. Fuzzing caught that in three
// seconds; the pointer probe is the same matching the server does.
var epochProbe struct {
Epoch *int `json:"epoch"`
}
hasEpoch := json.Unmarshal(env.Payload, &epochProbe) == nil && epochProbe.Epoch != nil
if !hasEpoch && decoded && !admitted {
t.Fatalf("auth payload %q omits epoch, as every client up to v1.2.0-alpha.4 does, but the epoch window [%d, %d] refuses the default 0", env.Payload, minClientEpoch, ProtocolEpoch)
}
if !admitted {
return
}
// Admitted: the token authenticateConn goes on to hash must be the
// string the JSON carried, not something a struct tag reshaped.
//
// The expectation is decoded through a probe carrying the SAME
// `json:"token"` tag, not looked up in fields: with
// {"token":"a","TOKEN":"b"} both keys resolve to the tagged field and
// the last one wins, so the exact-key lookup would expect "a" while
// the handshake legitimately holds "b". Same lesson as the epoch
// probe above — the oracle has to use the decoder's field resolution,
// not the raw key set.
var tokenProbe struct {
Token *string `json:"token"`
}
if json.Unmarshal(env.Payload, &tokenProbe) == nil && tokenProbe.Token != nil && *tokenProbe.Token != p.Token {
t.Fatalf("auth payload %q: token is %q through the handshake struct but %q through a probe with the same tag", env.Payload, p.Token, *tokenProbe.Token)
}
})
}
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"id\":\"req-1\",\"payload\":{\"token\":\"a\",\"TOKEN\":\"b\"},\"type\":\"auth\"}")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"id\":\"req-1\",\"payload\":{\"last_seq\":1,\"token\":\"0123456789abcdef0123456789abcdef\"},\"type\":\"auth\"}")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"id\":\"req-1\",\"payload\":{\"active_channel_id\":1,\"last_seq\":1,\"token\":\"0123456789abcdef0123456789abcdef\"},\"type\":\"auth\"}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("call_decline")
[]byte("{\"channel_id\":3}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("call_ring")
[]byte("{\"channel_id\":3}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("chat_delete")
[]byte("{\"message_id\":1}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("chat_edit")
[]byte("{\"content\":\"edited text\",\"message_id\":1}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("chat_send")
[]byte("{\"channel_id\":1,\"content\":\"hello epoch one\"}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("chat_send")
[]byte("{\"channel_id\":3,\"content\":\"hello over dm\"}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("mark_read")
[]byte("{\"channel_id\":1}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("ping")
[]byte("{}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("presence_update")
[]byte("{\"status\":\"online\",\"custom_status\":\"fixture custom status\"}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("reaction_add")
[]byte("{\"emoji\":\"👍\",\"message_id\":1}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("reaction_remove")
[]byte("{\"emoji\":\"👍\",\"message_id\":1}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("chat_send")
[]byte("{\"channel_id\":1,\"content\":\"sent while away\"}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("typing_start")
[]byte("{\"channel_id\":1}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("voice_camera")
[]byte("{\"enabled\":true}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("voice_deafen")
[]byte("{\"deafened\":true}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("voice_e2ee_announce")
[]byte("{\"public_key\":\"YWxpY2UtZWNkaC1wdWJsaWMta2V5LWZpeHR1cmU=\",\"signature\":\"YWxpY2Utc2lnbmF0dXJlLW92ZXItaGVyLWVjZGgta2V5\"}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("voice_e2ee_offer")
[]byte("{\"encrypted_key\":\"ZW5jcnlwdGVkLXJvb20ta2V5LWZpeHR1cmU=\",\"iv\":\"aXYtZml4dHVyZS0xMg==\",\"target_user_id\":1}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("voice_join")
[]byte("{\"channel_id\":2}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("voice_leave")
[]byte("{}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("voice_e2ee_announce")
[]byte("{\"public_key\":\"Ym9iLWVjZGgtcHVibGljLWtleS1maXh0dXJl\"}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("voice_mod_deafen")
[]byte("{\"channel_id\":2,\"user_id\":1,\"deafened\":true}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("voice_mod_kick")
[]byte("{\"user_id\":1}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("voice_mute")
[]byte("{\"muted\":true}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("voice_screenshare")
[]byte("{\"enabled\":true}")
@@ -0,0 +1,3 @@
go test fuzz v1
string("voice_token_refresh")
[]byte("{}")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"id\":\"req-1\",\"payload\":{\"message_id\":1},\"type\":\"chat_delete\"}")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"id\":\"req-1\",\"payload\":{\"content\":\"edited text\",\"message_id\":1},\"type\":\"chat_edit\"}")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"id\":\"req-1\",\"payload\":{\"channel_id\":1,\"content\":\"hello epoch one\"},\"type\":\"chat_send\"}")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"id\":\"req-1\",\"payload\":{\"channel_id\":3,\"content\":\"hello over dm\"},\"type\":\"chat_send\"}")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"id\":\"req-1\",\"payload\":{\"last_seq\":1,\"token\":\"0123456789abcdef0123456789abcdef\"},\"type\":\"auth\"}")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"payload\":{\"channel_id\":1},\"type\":\"mark_read\"}")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"payload\":{},\"type\":\"ping\"}")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"id\":\"req-1\",\"payload\":{\"emoji\":\"👍\",\"message_id\":1},\"type\":\"reaction_add\"}")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"id\":\"req-1\",\"payload\":{\"emoji\":\"👍\",\"message_id\":1},\"type\":\"reaction_remove\"}")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"id\":\"req-1\",\"payload\":{\"active_channel_id\":1,\"last_seq\":1,\"token\":\"0123456789abcdef0123456789abcdef\"},\"type\":\"auth\"}")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"id\":\"req-1\",\"payload\":{\"channel_id\":1,\"content\":\"sent while away\"},\"type\":\"chat_send\"}")

Some files were not shown because too many files have changed in this diff Show More