Skip to content

Commit ae5f555

Browse files
committed
add tests
1 parent a937681 commit ae5f555

3 files changed

Lines changed: 197 additions & 1 deletion

File tree

flowtracker_test.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package flowtracker
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"net/http/httptest"
11+
"os"
12+
"strings"
13+
"testing"
14+
"time"
15+
)
16+
17+
func NewServer() http.Handler {
18+
mux := http.NewServeMux()
19+
mux.HandleFunc("/", Handler)
20+
mw := NewMiddleware()
21+
return mw(mux)
22+
}
23+
24+
func TestDefaultExporter_OK(t *testing.T) {
25+
// capture stdout
26+
old := os.Stdout
27+
r, w, _ := os.Pipe()
28+
os.Stdout = w
29+
30+
server := NewServer()
31+
32+
req := httptest.NewRequest(http.MethodGet, "/", nil)
33+
rr := httptest.NewRecorder()
34+
35+
server.ServeHTTP(rr, req)
36+
37+
time.Sleep(1 * time.Second)
38+
39+
// restore stdout
40+
w.Close()
41+
os.Stdout = old
42+
43+
// read logs
44+
var buf bytes.Buffer
45+
written, err := io.Copy(&buf, r)
46+
if err != nil {
47+
t.Error("Error should be nil")
48+
}
49+
fmt.Println(written)
50+
51+
logs := buf.String()
52+
53+
if !strings.Contains(logs, "FLOW_LOG: {\"trace_id\":\"trace-") {
54+
t.Fatalf("expected log not found: %s", logs)
55+
}
56+
57+
}
58+
59+
func Handler(w http.ResponseWriter, r *http.Request) {
60+
ctx := r.Context()
61+
62+
// 1. Simulate some logic
63+
processPayment(ctx)
64+
65+
// 2. Simulate a Database call
66+
fetchUserProfile(ctx, "user_123")
67+
68+
// 3. Simulate external Microservice call
69+
callShippingService(ctx)
70+
71+
w.Write([]byte("Order Processed"))
72+
}
73+
74+
func processPayment(ctx context.Context) {
75+
// Start a sub-span
76+
ctx, finish := StartSpan(ctx, "Process Payment")
77+
defer finish()
78+
79+
time.Sleep(50 * time.Millisecond) // Simulate work
80+
AddTag(ctx, "currency", "USD")
81+
}
82+
83+
func fetchUserProfile(ctx context.Context, userID string) {
84+
ctx, finish := StartSpan(ctx, "DB: Select User")
85+
defer finish()
86+
87+
AddTag(ctx, "db.query", "SELECT * FROM users...")
88+
time.Sleep(120 * time.Millisecond) // Simulate DB latency
89+
}
90+
91+
func callShippingService(ctx context.Context) {
92+
ctx, finish := StartSpan(ctx, "HTTP: Shipping Service")
93+
defer finish()
94+
95+
// Nested span example: Logic inside the external call preparation
96+
func(innerCtx context.Context) {
97+
_, end := StartSpan(innerCtx, "Calculate Weight")
98+
defer end()
99+
time.Sleep(10 * time.Millisecond)
100+
}(ctx)
101+
102+
time.Sleep(200 * time.Millisecond) // Simulate network call
103+
}
104+
105+
func TestEndToEndFlow_ConsoleExporter(t *testing.T) {
106+
// 1. Capture Standard Output
107+
// We replace os.Stdout with a pipe so we can read what the library prints.
108+
oldStdout := os.Stdout
109+
r, w, _ := os.Pipe()
110+
os.Stdout = w
111+
112+
// Ensure we restore stdout even if the test crashes
113+
defer func() {
114+
os.Stdout = oldStdout
115+
}()
116+
117+
// 2. Define a simple handler that mimics real work
118+
mockHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
119+
ctx := r.Context()
120+
121+
// Simulate some logic with a span
122+
ctx, finish := StartSpan(ctx, "BusinessLogic")
123+
AddTag(ctx, "test.tag", "integration-check")
124+
time.Sleep(10 * time.Millisecond) // Simulate work
125+
finish()
126+
127+
w.WriteHeader(http.StatusOK)
128+
w.Write([]byte("OK"))
129+
})
130+
131+
// 3. Setup the Server with the Middleware
132+
// We use the default NewMiddleware which uses the ConsoleExporter
133+
mw := NewMiddleware()
134+
server := httptest.NewServer(mw(mockHandler))
135+
defer server.Close()
136+
137+
// 4. Make the HTTP Request
138+
resp, err := http.Get(server.URL + "/test-endpoint")
139+
if err != nil {
140+
t.Fatalf("Failed to make request: %v", err)
141+
}
142+
defer resp.Body.Close()
143+
144+
if resp.StatusCode != http.StatusOK {
145+
t.Errorf("Expected 200 OK, got %d", resp.StatusCode)
146+
}
147+
148+
// 5. WAIT for Async Export
149+
// The middleware runs the export in a 'go func()'.
150+
// We must sleep briefly to let that goroutine finish printing to stdout.
151+
time.Sleep(50 * time.Millisecond)
152+
153+
// 6. Close the Write end of the pipe and read the output
154+
w.Close()
155+
var buf bytes.Buffer
156+
io.Copy(&buf, r)
157+
output := buf.String()
158+
159+
// 7. Verify the Output
160+
t.Logf("Captured Output: %s", output)
161+
162+
if !strings.Contains(output, "FLOW_LOG:") {
163+
t.Fatalf("Expected output to contain 'FLOW_LOG:', but got empty or wrong data.")
164+
}
165+
166+
// 8. Validate the JSON structure
167+
// Extract the JSON part after the prefix
168+
jsonPart := strings.TrimSpace(strings.TrimPrefix(output, "FLOW_LOG:"))
169+
170+
var trace Trace
171+
if err := json.Unmarshal([]byte(jsonPart), &trace); err != nil {
172+
t.Fatalf("Failed to unmarshal JSON from logs: %v", err)
173+
}
174+
175+
// 9. Assert Data Integrity
176+
if trace.TraceID == "" {
177+
t.Error("TraceID should not be empty")
178+
}
179+
180+
// Expect 2 spans: The Root Span (GET /test-endpoint) + "BusinessLogic"
181+
if len(trace.Spans) != 2 {
182+
t.Errorf("Expected 2 spans, got %d", len(trace.Spans))
183+
}
184+
185+
// Check if our specific tag exists
186+
foundTag := false
187+
for _, s := range trace.Spans {
188+
if s.Tags != nil && s.Tags["test.tag"] == "integration-check" {
189+
foundTag = true
190+
break
191+
}
192+
}
193+
if !foundTag {
194+
t.Error("Did not find the expected 'test.tag' in the trace output")
195+
}
196+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/spdeepak/flowtracker
22

3-
go 1.24
3+
go 1.24

go.sum

Whitespace-only changes.

0 commit comments

Comments
 (0)