Webhook-to-SSE gateway with hierarchical topic routing and signature verification
at main 232 lines 6.4 kB view raw
1package main 2 3import ( 4 "net/url" 5 "testing" 6 "time" 7) 8 9func TestParseFilters_empty(t *testing.T) { 10 filters := ParseFilters(url.Values{}) 11 if len(filters) != 0 { 12 t.Fatalf("expected no filters, got %d", len(filters)) 13 } 14} 15 16func TestParseFilters_single(t *testing.T) { 17 v := url.Values{"filter": {"payload.ref:refs/heads/main"}} 18 filters := ParseFilters(v) 19 if len(filters) != 1 { 20 t.Fatalf("expected 1 filter, got %d", len(filters)) 21 } 22 if filters[0].Path != "payload.ref" { 23 t.Errorf("expected path payload.ref, got %s", filters[0].Path) 24 } 25 if filters[0].Value != "refs/heads/main" { 26 t.Errorf("expected value refs/heads/main, got %s", filters[0].Value) 27 } 28} 29 30func TestParseFilters_multiple(t *testing.T) { 31 v := url.Values{"filter": { 32 "payload.ref:refs/heads/main", 33 "headers.X-GitHub-Event:push", 34 }} 35 filters := ParseFilters(v) 36 if len(filters) != 2 { 37 t.Fatalf("expected 2 filters, got %d", len(filters)) 38 } 39} 40 41func TestParseFilters_colonInValue(t *testing.T) { 42 v := url.Values{"filter": {"payload.url:https://example.com"}} 43 filters := ParseFilters(v) 44 if len(filters) != 1 { 45 t.Fatalf("expected 1 filter, got %d", len(filters)) 46 } 47 if filters[0].Value != "https://example.com" { 48 t.Errorf("expected value with colon preserved, got %s", filters[0].Value) 49 } 50} 51 52func TestMatchAll_emptyFilters(t *testing.T) { 53 event := &Event{ 54 Payload: map[string]any{"ref": "refs/heads/main"}, 55 } 56 if !MatchAll(nil, event) { 57 t.Error("empty filters should match everything") 58 } 59} 60 61func TestMatchAll_singleMatch(t *testing.T) { 62 event := &Event{ 63 Payload: map[string]any{"ref": "refs/heads/main"}, 64 } 65 filters := []Filter{{Path: "payload.ref", Value: "refs/heads/main"}} 66 if !MatchAll(filters, event) { 67 t.Error("expected filter to match") 68 } 69} 70 71func TestMatchAll_singleNoMatch(t *testing.T) { 72 event := &Event{ 73 Payload: map[string]any{"ref": "refs/heads/develop"}, 74 } 75 filters := []Filter{{Path: "payload.ref", Value: "refs/heads/main"}} 76 if MatchAll(filters, event) { 77 t.Error("expected filter not to match") 78 } 79} 80 81func TestMatchAll_multipleAllMatch(t *testing.T) { 82 event := &Event{ 83 Headers: map[string]string{"X-GitHub-Event": "push"}, 84 Payload: map[string]any{"ref": "refs/heads/main"}, 85 } 86 filters := []Filter{ 87 {Path: "payload.ref", Value: "refs/heads/main"}, 88 {Path: "headers.X-GitHub-Event", Value: "push"}, 89 } 90 if !MatchAll(filters, event) { 91 t.Error("expected all filters to match") 92 } 93} 94 95func TestMatchAll_multipleOneFails(t *testing.T) { 96 event := &Event{ 97 Headers: map[string]string{"X-GitHub-Event": "push"}, 98 Payload: map[string]any{"ref": "refs/heads/develop"}, 99 } 100 filters := []Filter{ 101 {Path: "payload.ref", Value: "refs/heads/main"}, 102 {Path: "headers.X-GitHub-Event", Value: "push"}, 103 } 104 if MatchAll(filters, event) { 105 t.Error("expected AND filter to fail when one doesn't match") 106 } 107} 108 109func TestMatchAll_nestedDotPath(t *testing.T) { 110 event := &Event{ 111 Payload: map[string]any{ 112 "repository": map[string]any{ 113 "full_name": "chrisguidry/docketeer", 114 }, 115 }, 116 } 117 filters := []Filter{{Path: "payload.repository.full_name", Value: "chrisguidry/docketeer"}} 118 if !MatchAll(filters, event) { 119 t.Error("expected nested dot path to match") 120 } 121} 122 123func TestMatchAll_missingField(t *testing.T) { 124 event := &Event{ 125 Payload: map[string]any{"ref": "refs/heads/main"}, 126 } 127 filters := []Filter{{Path: "payload.nonexistent.field", Value: "anything"}} 128 if MatchAll(filters, event) { 129 t.Error("missing field should not match") 130 } 131} 132 133func TestMatchAll_topLevelFields(t *testing.T) { 134 event := &Event{ 135 ID: "abc-123", 136 Path: "github.com/chrisguidry/docketeer", 137 } 138 filters := []Filter{{Path: "path", Value: "github.com/chrisguidry/docketeer"}} 139 if !MatchAll(filters, event) { 140 t.Error("expected top-level path field to match") 141 } 142} 143 144func TestMatchAll_idField(t *testing.T) { 145 event := &Event{ID: "abc-123"} 146 filters := []Filter{{Path: "id", Value: "abc-123"}} 147 if !MatchAll(filters, event) { 148 t.Error("expected id field to match") 149 } 150} 151 152func TestMatchAll_timestampField(t *testing.T) { 153 event := &Event{Timestamp: time.Date(2026, 3, 4, 12, 0, 0, 0, time.UTC)} 154 filters := []Filter{{Path: "timestamp", Value: "2026-03-04T12:00:00Z"}} 155 if !MatchAll(filters, event) { 156 t.Error("expected timestamp field to match") 157 } 158} 159 160func TestMatchAll_headersNeedsTwoParts(t *testing.T) { 161 event := &Event{Headers: map[string]string{"X-Foo": "bar"}} 162 filters := []Filter{{Path: "headers", Value: "anything"}} 163 if MatchAll(filters, event) { 164 t.Error("headers without key should not match") 165 } 166} 167 168func TestMatchAll_unknownTopLevel(t *testing.T) { 169 event := &Event{} 170 filters := []Filter{{Path: "nonexistent", Value: "anything"}} 171 if MatchAll(filters, event) { 172 t.Error("unknown top-level field should not match") 173 } 174} 175 176func TestMatchAll_emptyPath(t *testing.T) { 177 event := &Event{} 178 filters := []Filter{{Path: "", Value: "anything"}} 179 if MatchAll(filters, event) { 180 t.Error("empty path should not match") 181 } 182} 183 184func TestMatchAll_payloadNonMapNavigation(t *testing.T) { 185 event := &Event{Payload: "just a string"} 186 filters := []Filter{{Path: "payload.deep.field", Value: "anything"}} 187 if MatchAll(filters, event) { 188 t.Error("navigating into non-map payload should not match") 189 } 190} 191 192func TestParseFilters_invalidFormat(t *testing.T) { 193 v := url.Values{"filter": {"no-colon-here"}} 194 filters := ParseFilters(v) 195 if len(filters) != 0 { 196 t.Errorf("expected 0 filters for invalid format, got %d", len(filters)) 197 } 198} 199 200func TestMatchAll_idWithSubpath(t *testing.T) { 201 event := &Event{ID: "abc-123"} 202 filters := []Filter{{Path: "id.sub", Value: "anything"}} 203 if MatchAll(filters, event) { 204 t.Error("id with subpath should not match") 205 } 206} 207 208func TestMatchAll_pathWithSubpath(t *testing.T) { 209 event := &Event{Path: "some/path"} 210 filters := []Filter{{Path: "path.sub", Value: "anything"}} 211 if MatchAll(filters, event) { 212 t.Error("path with subpath should not match") 213 } 214} 215 216func TestMatchAll_timestampWithSubpath(t *testing.T) { 217 event := &Event{} 218 filters := []Filter{{Path: "timestamp.sub", Value: "anything"}} 219 if MatchAll(filters, event) { 220 t.Error("timestamp with subpath should not match") 221 } 222} 223 224func TestMatchAll_payloadLeafValue(t *testing.T) { 225 event := &Event{ 226 Payload: map[string]any{"count": 42}, 227 } 228 filters := []Filter{{Path: "payload.count", Value: "42"}} 229 if !MatchAll(filters, event) { 230 t.Error("expected numeric leaf value to match via fmt.Sprintf") 231 } 232}