package collector import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "git.tornberg.me/go-otel/internal/storage" "git.tornberg.me/go-otel/internal/websocket" ) type mockStore struct { entries []websocket.HistoryEntry } func (m *mockStore) Append(ctx context.Context, entry websocket.HistoryEntry) error { m.entries = append(m.entries, entry) return nil } func (m *mockStore) GetHistory(ctx context.Context, limit int) ([]websocket.HistoryEntry, error) { return m.entries, nil } func (m *mockStore) Search(ctx context.Context, opts storage.SearchOptions, limit int) ([]websocket.HistoryEntry, error) { var filtered []websocket.HistoryEntry for _, e := range m.entries { if opts.Type != "" && e.Type != opts.Type { continue } if opts.Query != "" && !strings.Contains(string(e.Data), opts.Query) { continue } filtered = append(filtered, e) } return filtered, nil } func TestCollectorWithStore(t *testing.T) { store := &mockStore{} coll := New(nil, 10, store) coll.addToHistory(websocket.DataTypeLogs, json.RawMessage(`{"msg": "test"}`)) // Give it a moment since it's asynchronous timeout := time.After(100 * time.Millisecond) for { select { case <-timeout: t.Fatal("timed out waiting for entry to be appended to store") default: if len(store.entries) > 0 { goto OK } time.Sleep(10 * time.Millisecond) } } OK: if store.entries[0].Type != "logs" { t.Errorf("expected type 'logs', got %s", store.entries[0].Type) } } func TestCollectorHistoryLimit(t *testing.T) { coll := New(nil, 10, nil) for i := 0; i < 10; i++ { coll.addToHistory(websocket.DataTypeLogs, json.RawMessage(`{"msg": "test"}`)) } history := coll.GetHistory(10) if len(history) != 10 { t.Errorf("expected 10 entries, got %d", len(history)) } history = coll.GetHistory(5) if len(history) != 5 { t.Errorf("expected 5 entries, got %d", len(history)) } // Should default to historySize (10) history = coll.GetHistory(0) if len(history) != 10 { t.Errorf("expected default limit to be historySize (10), got %d", len(history)) } // Should not crash on large limit, just return what it has history = coll.GetHistory(100) if len(history) != 10 { t.Errorf("expected 10 entries even with limit 100, got %d", len(history)) } } func TestHandleHistoryByTypeUsesStore(t *testing.T) { store := &mockStore{ entries: []websocket.HistoryEntry{ {Type: "traces", Timestamp: 1, Data: json.RawMessage(`{"id":1}`)}, {Type: "traces", Timestamp: 2, Data: json.RawMessage(`{"id":2}`)}, {Type: "logs", Timestamp: 3, Data: json.RawMessage(`{"id":3}`)}, }, } // Ring buffer is much smaller than the store's content, so a correct // implementation must route through the store rather than the ring buffer. coll := New(nil, 1, store) req := httptest.NewRequest(http.MethodGet, "/api/history/traces", nil) rec := httptest.NewRecorder() coll.HandleHistoryByType("traces")(rec, req) var results []websocket.HistoryEntry if err := json.Unmarshal(rec.Body.Bytes(), &results); err != nil { t.Fatalf("failed to decode response: %v", err) } if len(results) != 2 { t.Fatalf("expected 2 trace entries from the store, got %d", len(results)) } } func TestHandleTraceByID(t *testing.T) { store := &mockStore{ entries: []websocket.HistoryEntry{ {Type: "traces", Timestamp: 1, Data: json.RawMessage(`{"traceId":"abc123"}`)}, {Type: "logs", Timestamp: 2, Data: json.RawMessage(`{"traceId":"abc123","msg":"related"}`)}, {Type: "traces", Timestamp: 3, Data: json.RawMessage(`{"traceId":"other"}`)}, }, } coll := New(nil, 10, store) req := httptest.NewRequest(http.MethodGet, "/api/traces/abc123", nil) req.SetPathValue("traceId", "abc123") rec := httptest.NewRecorder() coll.HandleTraceByID(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } var results []websocket.HistoryEntry if err := json.Unmarshal(rec.Body.Bytes(), &results); err != nil { t.Fatalf("failed to decode response: %v", err) } if len(results) != 2 { t.Fatalf("expected 2 entries referencing trace abc123, got %d", len(results)) } } func TestHandleWaitRateLimited(t *testing.T) { coll := New(nil, 10, nil) var lastCode int for i := 0; i < 10; i++ { req := httptest.NewRequest(http.MethodGet, "/api/history/wait?q=nope&timeout=0", nil) req.RemoteAddr = "203.0.113.1:12345" rec := httptest.NewRecorder() coll.HandleWait(rec, req) lastCode = rec.Code if lastCode == http.StatusTooManyRequests { break } } if lastCode != http.StatusTooManyRequests { t.Fatalf("expected rate limiting to eventually return 429, last code was %d", lastCode) } }