-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathzz_logic_test.go
More file actions
275 lines (238 loc) · 7.17 KB
/
Copy pathzz_logic_test.go
File metadata and controls
275 lines (238 loc) · 7.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
// SPDX-License-Identifier: AGPL-3.0-or-later
package webhook
import (
"net/http"
"net/http/httptest"
"sync"
"sync/atomic"
"testing"
"time"
)
// --- Options ---
func TestWithHTTPTimeoutSetsClientTimeout(t *testing.T) {
t.Parallel()
wc := NewClient("http://example.invalid", func() uint32 { return 1 },
WithHTTPTimeout(42*time.Millisecond))
if wc == nil {
t.Fatal("NewClient returned nil")
}
defer wc.Close()
if wc.client.Timeout != 42*time.Millisecond {
t.Fatalf("client.Timeout = %v, want 42ms", wc.client.Timeout)
}
}
func TestWithRetryBackoffSetsInitialBackoff(t *testing.T) {
t.Parallel()
wc := NewClient("http://example.invalid", func() uint32 { return 1 },
WithRetryBackoff(250*time.Microsecond))
if wc == nil {
t.Fatal("NewClient returned nil")
}
defer wc.Close()
if wc.initialBackoff != 250*time.Microsecond {
t.Fatalf("initialBackoff = %v, want 250us", wc.initialBackoff)
}
}
func TestNewClientEmptyURLReturnsNil(t *testing.T) {
t.Parallel()
if NewClient("", func() uint32 { return 1 }) != nil {
t.Fatal("empty URL should return nil")
}
}
// --- Emit ---
func TestEmitOnNilReceiverIsNoOp(t *testing.T) {
t.Parallel()
var wc *Client
wc.Emit("boom", nil) // should not panic
}
func TestEmitAfterCloseIsNoOp(t *testing.T) {
t.Parallel()
var calls atomic.Uint32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
w.WriteHeader(200)
}))
defer srv.Close()
wc := NewClient(srv.URL, func() uint32 { return 42 })
wc.Close()
wc.Emit("after_close", nil)
// Give any pending delivery a chance
time.Sleep(20 * time.Millisecond)
if n := calls.Load(); n != 0 {
t.Fatalf("calls = %d, want 0 (emit after close should be no-op)", n)
}
}
func TestEmitDeliversToServerAndIncrementsEventID(t *testing.T) {
t.Parallel()
var recvd atomic.Uint32
var lastBody []byte
var mu sync.Mutex
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
lastBody = make([]byte, r.ContentLength)
_, _ = r.Body.Read(lastBody)
mu.Unlock()
recvd.Add(1)
w.WriteHeader(200)
}))
defer srv.Close()
wc := NewClient(srv.URL, func() uint32 { return 0xABCD },
WithRetryBackoff(1*time.Millisecond))
wc.Emit("test-event", map[string]int{"k": 1})
wc.Emit("second", nil)
wc.Close() // drain
if got := recvd.Load(); got < 2 {
t.Fatalf("recvd = %d, want at least 2", got)
}
mu.Lock()
body := string(lastBody)
mu.Unlock()
if body == "" {
t.Fatal("expected non-empty body")
}
}
func TestEmitDropsWhenChannelFull(t *testing.T) {
t.Parallel()
// Use a handler that blocks so the dispatcher goroutine is stuck on post.
// The channel capacity is 1024; we fill it up and one more.
block := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
<-block
w.WriteHeader(200)
}))
defer func() { close(block); srv.Close() }()
wc := NewClient(srv.URL, func() uint32 { return 1 },
WithRetryBackoff(1*time.Millisecond))
defer wc.Close()
// The run goroutine pulls one message immediately and blocks on post.
// Now emit 1024 more to fill the channel, then one extra to trigger drop.
for i := 0; i < 1024; i++ {
wc.Emit("fill", i)
}
// Small settle period in case the first one hasn't been pulled yet.
time.Sleep(10 * time.Millisecond)
// At this point the channel should be full (one in-flight post + 1024 buffered).
// The next Emit should either go through (if the in-flight was dequeued already)
// or increment Dropped. Do several to guarantee a drop.
for i := 0; i < 100; i++ {
wc.Emit("overflow", i)
}
if d := wc.Dropped(); d == 0 {
t.Fatalf("Dropped = 0, want > 0 after overflowing 1024-slot buffer")
}
}
// --- Dropped ---
func TestDroppedOnNilReturnsZero(t *testing.T) {
t.Parallel()
var wc *Client
if got := wc.Dropped(); got != 0 {
t.Fatalf("nil.Dropped() = %d, want 0", got)
}
}
func TestDroppedReturnsCounterValue(t *testing.T) {
t.Parallel()
wc := &Client{}
wc.dropped.Store(17)
if got := wc.Dropped(); got != 17 {
t.Fatalf("Dropped = %d, want 17", got)
}
}
// --- post ---
func TestPost2xxReturnsWithoutRetry(t *testing.T) {
t.Parallel()
var calls atomic.Uint32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
w.WriteHeader(200)
}))
defer srv.Close()
wc := NewClient(srv.URL, func() uint32 { return 1 },
WithRetryBackoff(1*time.Millisecond))
wc.Emit("ok", nil)
wc.Close()
if got := calls.Load(); got != 1 {
t.Fatalf("calls = %d, want 1 (2xx should not retry)", got)
}
}
func TestPost4xxDoesNotRetry(t *testing.T) {
t.Parallel()
var calls atomic.Uint32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
w.WriteHeader(404)
}))
defer srv.Close()
wc := NewClient(srv.URL, func() uint32 { return 1 },
WithRetryBackoff(1*time.Millisecond))
wc.Emit("bad", nil)
wc.Close()
if got := calls.Load(); got != 1 {
t.Fatalf("calls = %d, want 1 (4xx should not retry)", got)
}
}
func TestPost5xxRetriesUpToMaxRetries(t *testing.T) {
t.Parallel()
var calls atomic.Uint32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
w.WriteHeader(503)
}))
defer srv.Close()
wc := NewClient(srv.URL, func() uint32 { return 1 },
WithRetryBackoff(1*time.Millisecond))
wc.Emit("retry", nil)
wc.Close()
if got := calls.Load(); got != MaxRetries {
t.Fatalf("calls = %d, want %d (5xx should retry to max)", got, MaxRetries)
}
}
func TestPost5xxThenSuccessStopsRetrying(t *testing.T) {
t.Parallel()
var calls atomic.Uint32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if calls.Add(1) == 1 {
w.WriteHeader(500)
return
}
w.WriteHeader(200)
}))
defer srv.Close()
wc := NewClient(srv.URL, func() uint32 { return 1 },
WithRetryBackoff(1*time.Millisecond))
wc.Emit("recover", nil)
wc.Close()
if got := calls.Load(); got != 2 {
t.Fatalf("calls = %d, want 2 (success after one 5xx)", got)
}
}
func TestPostNetworkErrorRetries(t *testing.T) {
t.Parallel()
// Close an unused server to get a dead port, then retry.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
url := srv.URL
srv.Close() // port becomes dead
wc := NewClient(url, func() uint32 { return 1 },
WithRetryBackoff(1*time.Millisecond),
WithHTTPTimeout(50*time.Millisecond))
wc.Emit("deadport", nil)
// Network errors retry until MaxRetries, backoff is tiny (1ms → 2ms → 4ms).
// Close waits for done; that's enough to be sure all retries exhausted.
wc.Close()
// No assertion beyond non-panic & clean shutdown (post path covered).
}
func TestPostMarshalErrorPath(t *testing.T) {
t.Parallel()
// A channel value cannot be JSON-marshaled; triggers the marshal error branch.
var calls atomic.Uint32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
}))
defer srv.Close()
wc := NewClient(srv.URL, func() uint32 { return 1 },
WithRetryBackoff(1*time.Millisecond))
wc.Emit("bad-data", make(chan int))
wc.Close()
if got := calls.Load(); got != 0 {
t.Fatalf("calls = %d, want 0 (unmarshalable payload should never hit wire)", got)
}
}