|
| 1 | +// Copyright 2026 The go-github AUTHORS. All rights reserved. |
| 2 | +// |
| 3 | +// Use of this source code is governed by a BSD-style |
| 4 | +// license that can be found in the LICENSE file. |
| 5 | + |
| 6 | +package github |
| 7 | + |
| 8 | +import ( |
| 9 | + "encoding/json" |
| 10 | + "fmt" |
| 11 | + "testing" |
| 12 | +) |
| 13 | + |
| 14 | +// FuzzParseWebHook tests ParseWebHook against arbitrary event types and payloads. |
| 15 | +// It verifies that no input triggers a panic or nil pointer dereference. |
| 16 | +// |
| 17 | +// This fuzz test is intended for integration with OSS-Fuzz (https://google.github.io/oss-fuzz/) |
| 18 | +// for continuous fuzzing in the cloud. |
| 19 | +// |
| 20 | +// To run: |
| 21 | +// |
| 22 | +// go test -fuzz=^FuzzParseWebHook$ -fuzztime=30s . |
| 23 | +func FuzzParseWebHook(f *testing.F) { |
| 24 | + seeds := []struct { |
| 25 | + eventType string |
| 26 | + payload string |
| 27 | + }{ |
| 28 | + {"push", `{"ref": "refs/heads/main", "before": "000000", "after": "123456", "commits": [{"id": "abc", "message": "msg", "added": [], "removed": [], "modified": []}]}`}, |
| 29 | + {"pull_request", `{"action": "opened", "number": 1, "pull_request": {"title": "test", "state": "open", "user": {"login": "u"}}}`}, |
| 30 | + {"issues", `{"action": "opened", "issue": {"number": 42, "title": "bug", "state": "open"}}`}, |
| 31 | + {"release", `{"action": "published", "release": {"tag_name": "v1.0.0", "draft": false}}`}, |
| 32 | + {"check_run", `{"action": "created", "check_run": {"status": "in_progress", "id": 1}}`}, |
| 33 | + {"check_suite", `{"action": "completed", "check_suite": {"id": 1, "status": "completed"}}`}, |
| 34 | + {"workflow_run", `{"action": "requested", "workflow_run": {"id": 123, "status": "queued"}}`}, |
| 35 | + {"workflow_job", `{"action": "queued", "workflow_job": {"id": 1, "status": "queued"}}`}, |
| 36 | + {"discussion", `{"action": "created", "discussion": {"title": "hello", "number": 1}}`}, |
| 37 | + {"ping", `{"zen": "Keep it logically awesome.", "hook_id": 1}`}, |
| 38 | + {"repository", `{"action": "created", "repository": {"name": "test-repo", "private": false}}`}, |
| 39 | + {"star", `{"action": "created", "starred_at": "2026-03-11T00:00:00Z"}`}, |
| 40 | + {"create", `{"ref": "main", "ref_type": "branch"}`}, |
| 41 | + {"delete", `{"ref": "old-branch", "ref_type": "branch"}`}, |
| 42 | + {"fork", `{"forkee": {"name": "forked-repo"}}`}, |
| 43 | + {"deployment", `{"action": "created", "deployment": {"id": 1, "ref": "main"}}`}, |
| 44 | + {"deployment_status", `{"action": "created", "deployment_status": {"id": 1, "state": "pending"}}`}, |
| 45 | + {"member", `{"action": "added", "member": {"login": "user"}}`}, |
| 46 | + {"public", `{"repository": {"name": "now-public"}}`}, |
| 47 | + {"commit_comment", `{"action": "created", "comment": {"id": 1, "body": "comment"}}`}, |
| 48 | + } |
| 49 | + for _, s := range seeds { |
| 50 | + f.Add(s.eventType, []byte(s.payload)) |
| 51 | + } |
| 52 | + |
| 53 | + for _, messageType := range MessageTypes() { |
| 54 | + proto := EventForType(messageType) |
| 55 | + if proto == nil { |
| 56 | + f.Add(messageType, []byte(`{}`)) |
| 57 | + continue |
| 58 | + } |
| 59 | + // Generate a seed by marshaling the zero-value struct so the fuzzer |
| 60 | + // starts from a structurally valid JSON skeleton for each event type. |
| 61 | + b, err := json.Marshal(proto) |
| 62 | + if err != nil { |
| 63 | + f.Add(messageType, []byte(`{}`)) |
| 64 | + continue |
| 65 | + } |
| 66 | + f.Add(messageType, b) |
| 67 | + } |
| 68 | + |
| 69 | + f.Fuzz(func(_ *testing.T, eventType string, payload []byte) { |
| 70 | + if len(payload) > 1<<20 { |
| 71 | + return |
| 72 | + } |
| 73 | + event, err := ParseWebHook(eventType, payload) |
| 74 | + if err != nil { |
| 75 | + return |
| 76 | + } |
| 77 | + if event != nil { |
| 78 | + // Traverse all fields recursively to catch nil pointer dereferences |
| 79 | + _ = fmt.Sprintf("%+v", event) |
| 80 | + } |
| 81 | + }) |
| 82 | +} |
0 commit comments