-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfanout_test.go
482 lines (418 loc) · 12.5 KB
/
fanout_test.go
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"testing"
"time"
)
// TestCloneHeaders verifies that headers are properly cloned and sensitive headers are detected
func TestCloneHeaders(t *testing.T) {
// Setup test cases
tests := []struct {
name string
headers http.Header
sensitive bool
}{
{
name: "Regular Headers",
headers: http.Header{
"Content-Type": []string{"application/json"},
"User-Agent": []string{"test-agent"},
},
sensitive: false,
},
{
name: "Sensitive Headers",
headers: http.Header{
"Content-Type": []string{"application/json"},
"Authorization": []string{"Bearer abc123"},
},
sensitive: true,
},
}
// Run test cases
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := cloneHeaders(tc.headers)
// Verify all headers were cloned correctly
if !reflect.DeepEqual(result, tc.headers) {
t.Errorf("Headers not cloned correctly\nExpected: %v\nGot: %v", tc.headers, result)
}
})
}
}
// TestIsRetryableError verifies that certain errors are correctly identified as retryable
func TestIsRetryableError(t *testing.T) {
tests := []struct {
name string
err error
retryable bool
}{
{
name: "nil error",
err: nil,
retryable: false,
},
{
name: "connection refused",
err: errors.New("dial tcp: connection refused"),
retryable: true,
},
{
name: "timeout error",
err: errors.New("context deadline exceeded (Client.Timeout exceeded)"),
retryable: true,
},
{
name: "connection reset",
err: errors.New("connection reset by peer"),
retryable: true,
},
{
name: "no host error",
err: errors.New("no such host"),
retryable: true,
},
{
name: "non-retryable error",
err: errors.New("some other error"),
retryable: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := isRetryableError(tc.err)
if result != tc.retryable {
t.Errorf("isRetryableError(%v) = %v, want %v", tc.err, result, tc.retryable)
}
})
}
}
// TestAddJitter checks that jitter is within expected range
func TestAddJitter(t *testing.T) {
baseDuration := 100 * time.Millisecond
iterations := 100
min := float64(baseDuration) * 0.8
max := float64(baseDuration) * 1.2
for i := 0; i < iterations; i++ {
result := addJitter(baseDuration)
if float64(result) < min || float64(result) > max {
t.Errorf("Jitter out of expected range: got %v, want between %v and %v",
result, time.Duration(min), time.Duration(max))
}
}
}
// TestMinDuration verifies min function returns the smaller of two durations
func TestMinDuration(t *testing.T) {
tests := []struct {
a, b, expected time.Duration
}{
{100 * time.Millisecond, 200 * time.Millisecond, 100 * time.Millisecond},
{500 * time.Millisecond, 100 * time.Millisecond, 100 * time.Millisecond},
{1 * time.Second, 1 * time.Second, 1 * time.Second},
}
for i, tc := range tests {
t.Run(fmt.Sprintf("Case %d", i), func(t *testing.T) {
result := min(tc.a, tc.b)
if result != tc.expected {
t.Errorf("min(%v, %v) = %v, want %v", tc.a, tc.b, result, tc.expected)
}
})
}
}
// TestWriteJSON checks that JSON responses are written correctly
func TestWriteJSON(t *testing.T) {
tests := []struct {
name string
data interface{}
expected string
}{
{
name: "Simple map",
data: map[string]string{"key": "value"},
expected: `{"key":"value"}`,
},
{
name: "Response object",
data: Response{
Target: "http://example.com",
Status: 200,
Body: "OK",
Attempts: 0, // Due to omitempty tag, this won't appear in JSON when 0
},
// Updated expected value - removed attempts since it has omitempty tag
expected: `{"target":"http://example.com","status":200,"body":"OK","latency":0}`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create a response recorder
w := httptest.NewRecorder()
// Call writeJSON
err := writeJSON(w, tc.data)
if err != nil {
t.Fatalf("writeJSON returned error: %v", err)
}
// Check response
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
// Trim newlines because the encoder adds them
got := strings.TrimSpace(string(body))
if got != tc.expected {
t.Errorf("writeJSON wrote %q, want %q", got, tc.expected)
}
})
}
}
// TestWriteJSONError checks error responses
func TestWriteJSONError(t *testing.T) {
tests := []struct {
name string
message string
status int
expected string
}{
{
name: "Not found error",
message: "Resource not found",
status: http.StatusNotFound,
expected: `{"error":"Resource not found"}`,
},
{
name: "Bad request error",
message: "Invalid input",
status: http.StatusBadRequest,
expected: `{"error":"Invalid input"}`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create a response recorder
w := httptest.NewRecorder()
// Call writeJSONError
writeJSONError(w, tc.message, tc.status)
// Check response
resp := w.Result()
if resp.StatusCode != tc.status {
t.Errorf("writeJSONError status = %d, want %d", resp.StatusCode, tc.status)
}
body, _ := io.ReadAll(resp.Body)
// Trim newlines because the encoder adds them
got := strings.TrimSpace(string(body))
if got != tc.expected {
t.Errorf("writeJSONError wrote %q, want %q", got, tc.expected)
}
})
}
}
// TestHealthCheck verifies the health endpoint returns correct status
func TestHealthCheck(t *testing.T) {
// Create a request to pass to the handler
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
// Call the handler
healthCheck(w, req)
// Check the status code
resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Errorf("healthCheck returned wrong status code: got %v want %v",
resp.StatusCode, http.StatusOK)
}
// Check the response body
var result map[string]string
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("Could not decode response: %v", err)
}
if result["status"] != "healthy" {
t.Errorf("healthCheck returned wrong status: got %v want %v",
result["status"], "healthy")
}
}
// TestEchoHandlerSimpleMode tests the echo handler in simple mode
func TestEchoHandlerSimpleMode(t *testing.T) {
// Save and restore original env vars
originalHeader := os.Getenv("ECHO_MODE_HEADER")
originalResponse := os.Getenv("ECHO_MODE_RESPONSE")
defer func() {
os.Setenv("ECHO_MODE_HEADER", originalHeader)
os.Setenv("ECHO_MODE_RESPONSE", originalResponse)
}()
// Set environment for this test
os.Setenv("ECHO_MODE_HEADER", "false")
os.Setenv("ECHO_MODE_RESPONSE", "simple")
// Create a request with a test body
body := []byte(`{"test":"data"}`)
req := httptest.NewRequest("POST", "/fanout", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
// Call the handler
echoHandler(w, req)
// Check response
resp := w.Result()
if resp.StatusCode != http.StatusAccepted {
t.Errorf("echoHandler returned wrong status code: got %v want %v",
resp.StatusCode, http.StatusAccepted)
}
// Check response body
var result map[string]string
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("Could not decode response: %v", err)
}
if result["status"] != "echoed" {
t.Errorf("echoHandler returned wrong status: got %v want %v",
result["status"], "echoed")
}
}
// TestEchoHandlerFullMode tests the echo handler in full mode
func TestEchoHandlerFullMode(t *testing.T) {
// Save and restore original env vars
originalHeader := os.Getenv("ECHO_MODE_HEADER")
originalResponse := os.Getenv("ECHO_MODE_RESPONSE")
defer func() {
os.Setenv("ECHO_MODE_HEADER", originalHeader)
os.Setenv("ECHO_MODE_RESPONSE", originalResponse)
}()
// Set environment for this test
os.Setenv("ECHO_MODE_HEADER", "true")
os.Setenv("ECHO_MODE_RESPONSE", "full")
// Create a request with a test body
body := []byte(`{"test":"data"}`)
req := httptest.NewRequest("POST", "/fanout", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
// Call the handler
echoHandler(w, req)
// Check response
resp := w.Result()
// Verify header
if resp.Header.Get("X-Echo-Mode") != "active" {
t.Errorf("echoHandler should set X-Echo-Mode header")
}
// Check response body
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("Could not decode response: %v", err)
}
// Check body contains correct fields
if _, ok := result["headers"]; !ok {
t.Errorf("echoHandler response missing headers")
}
if bodyStr, ok := result["body"]; !ok || bodyStr != string(body) {
t.Errorf("echoHandler response missing or incorrect body: %v", result["body"])
}
}
// TestSendRequest tests the request sending functionality with mocks
func TestSendRequest(t *testing.T) {
// Create a mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check for retry header
retryCount := r.Header.Get("X-Retry-Count")
if retryCount == "1" {
// Simulate success after retry
w.WriteHeader(http.StatusOK)
w.Write([]byte("Success after retry"))
return
}
// On first attempt, return server error to trigger retry
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Server error"))
}))
defer server.Close()
// Configure client
client := &http.Client{
Timeout: 1 * time.Second,
}
// Create an http request
req, err := http.NewRequest("GET", "http://example.com", nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
// Set app-level retry count for test
maxRetries = 2
// Call sendRequest with mock server URL
resp := sendRequest(context.Background(), client, server.URL, req, nil)
// Verify response
if resp.Status != http.StatusOK {
t.Errorf("Expected status 200, got %d: %s", resp.Status, resp.Error)
}
if resp.Attempts != 2 {
t.Errorf("Expected 2 attempts, got %d", resp.Attempts)
}
if !strings.Contains(resp.Body, "Success after retry") {
t.Errorf("Body doesn't contain expected response: %s", resp.Body)
}
}
// TestSendRequestNetworkError tests retry on network errors
func TestSendRequestNetworkError(t *testing.T) {
// Configure client
client := &http.Client{
Timeout: 100 * time.Millisecond,
}
// Create an http request
req, err := http.NewRequest("GET", "http://example.com", nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
// Set app-level retry count for test
maxRetries = 1
// Call sendRequest with a non-existent endpoint (will cause error)
resp := sendRequest(context.Background(), client, "http://nonexistent.example", req, nil)
// Verify response reports an error
if resp.Status != http.StatusServiceUnavailable {
t.Errorf("Expected status 503, got %d", resp.Status)
}
if resp.Error == "" {
t.Error("Expected error message but got none")
}
}
// TestLogLevels verifies that log levels are respected
func TestLogLevels(t *testing.T) {
// Set a known log level
origLogLevel := logLevel
defer func() { logLevel = origLogLevel }()
tests := []struct {
setLevel int
messageLevel int
shouldBeSent bool
}{
{LogLevelInfo, LogLevelDebug, false}, // Debug not logged when level is Info
{LogLevelInfo, LogLevelInfo, true}, // Info logged when level is Info
{LogLevelInfo, LogLevelWarn, true}, // Warn logged when level is Info
{LogLevelInfo, LogLevelError, true}, // Error logged when level is Info
{LogLevelError, LogLevelWarn, false}, // Warn not logged when level is Error
{LogLevelDebug, LogLevelInfo, true}, // Info logged when level is Debug
}
for i, tc := range tests {
t.Run(fmt.Sprintf("Case %d", i), func(t *testing.T) {
logLevel = tc.setLevel
// Create a temporary log queue to check if messages get queued
origLogQueue := logQueue
testQueue := make(chan LogEntry, 1)
logQueue = testQueue
defer func() { logQueue = origLogQueue }()
// Send log with the given level
logWithLevel(tc.messageLevel, nil, "Test message")
// Check if message was enqueued
var received bool
select {
case <-testQueue:
received = true
default:
received = false
}
if received != tc.shouldBeSent {
t.Errorf("Log with level %d when configured level is %d: got queued=%v, want queued=%v",
tc.messageLevel, tc.setLevel, received, tc.shouldBeSent)
}
})
}
}