Webhook-to-SSE gateway with hierarchical topic routing and signature verification
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}