package aggregate import ( "context" "os" "strings" "testing" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" "notashelf.dev/watchdog/internal/config" ) func TestMetricsAggregator_RecordPageview(t *testing.T) { registry := NewPathRegistry(100) cfg := config.Config{ Site: config.SiteConfig{ Collect: config.CollectConfig{ Pageviews: true, Country: true, Device: true, Referrer: "domain", }, }, } agg := NewMetricsAggregator(registry, NewCustomEventRegistry(100), &cfg) // Record pageview with all dimensions agg.RecordPageview("/home", "US", "desktop", "google.com", "") // Verify metric was recorded expected := ` # HELP web_pageviews_total Total number of pageviews # TYPE web_pageviews_total counter web_pageviews_total{country="US",device="desktop",path="/home",referrer="google.com"} 1 ` if err := testutil.CollectAndCompare(agg.pageviews, strings.NewReader(expected)); err != nil { t.Errorf("unexpected metric value: %v", err) } } func TestMetricsAggregator_RecordPageview_MinimalDimensions(t *testing.T) { registry := NewPathRegistry(100) cfg := config.Config{ Site: config.SiteConfig{ Collect: config.CollectConfig{ Pageviews: true, Country: false, Device: false, Referrer: "off", }, }, } agg := NewMetricsAggregator(registry, NewCustomEventRegistry(100), &cfg) // Record pageview with only path agg.RecordPageview("/home", "", "", "", "") // Verify metric was recorded expected := ` # HELP web_pageviews_total Total number of pageviews # TYPE web_pageviews_total counter web_pageviews_total{path="/home"} 1 ` if err := testutil.CollectAndCompare(agg.pageviews, strings.NewReader(expected)); err != nil { t.Errorf("unexpected metric value: %v", err) } } func TestMetricsAggregator_PathOverflow(t *testing.T) { // Create registry with limit of 2 registry := NewPathRegistry(2) cfg := config.Config{ Site: config.SiteConfig{ Collect: config.CollectConfig{ Pageviews: true, }, }, } agg := NewMetricsAggregator(registry, NewCustomEventRegistry(100), &cfg) // Add two paths successfully registry.Add("/path1") registry.Add("/path2") // Try to add third path - should be rejected if registry.Add("/path3") { t.Error("expected path to be rejected") } // Record overflow agg.RecordPathOverflow() // Verify overflow metric expected := ` # HELP web_path_overflow_total Paths rejected due to cardinality limit # TYPE web_path_overflow_total counter web_path_overflow_total 1 ` if err := testutil.CollectAndCompare(agg.pathOverflow, strings.NewReader(expected)); err != nil { t.Errorf("unexpected metric value: %v", err) } } func TestMetricsAggregator_RecordCustomEvent(t *testing.T) { registry := NewPathRegistry(100) cfg := config.Config{ Site: config.SiteConfig{ Collect: config.CollectConfig{ Pageviews: true, }, CustomEvents: []string{"signup", "purchase"}, }, } agg := NewMetricsAggregator(registry, NewCustomEventRegistry(100), &cfg) // Record custom event agg.RecordCustomEvent("signup") // Verify metric was recorded expected := ` # HELP web_custom_events_total Total number of custom events # TYPE web_custom_events_total counter web_custom_events_total{event="signup"} 1 ` if err := testutil.CollectAndCompare(agg.customEvents, strings.NewReader(expected)); err != nil { t.Errorf("unexpected metric value: %v", err) } } func TestMetricsAggregator_RecordCustomEvent_MultipleEvents(t *testing.T) { registry := NewPathRegistry(100) cfg := config.Config{ Site: config.SiteConfig{ Collect: config.CollectConfig{ Pageviews: true, }, CustomEvents: []string{"signup", "purchase"}, }, } agg := NewMetricsAggregator(registry, NewCustomEventRegistry(100), &cfg) // Record multiple events agg.RecordCustomEvent("signup") agg.RecordCustomEvent("signup") agg.RecordCustomEvent("purchase") // Verify metrics if err := testutil.CollectAndCompare(agg.customEvents, strings.NewReader(` # HELP web_custom_events_total Total number of custom events # TYPE web_custom_events_total counter web_custom_events_total{event="purchase"} 1 web_custom_events_total{event="signup"} 2 `)); err != nil { t.Errorf("unexpected metric value: %v", err) } } func TestMetricsAggregator_MustRegister(t *testing.T) { registry := NewPathRegistry(100) cfg := config.Config{ Site: config.SiteConfig{ Collect: config.CollectConfig{ Pageviews: true, }, }, } promRegistry := prometheus.NewRegistry() agg := NewMetricsAggregator(registry, NewCustomEventRegistry(100), &cfg) // Register metrics agg.MustRegister(promRegistry) // Record some metrics to ensure they show up agg.RecordPageview("/test", "", "", "", "") agg.RecordPathOverflow() // Verify metrics can be gathered metrics, err := promRegistry.Gather() if err != nil { t.Fatalf("failed to gather metrics: %v", err) } // Should have at least pageviews and path overflow metrics if len(metrics) < 2 { t.Errorf("expected at least 2 metric families, got %d", len(metrics)) } } func TestMetricsAggregator_Shutdown_ConfigurableStatePath(t *testing.T) { registry := NewPathRegistry(100) // Create temp directory for test tmpDir := t.TempDir() statePath := tmpDir + "/custom-hll.state" cfg := config.Config{ Site: config.SiteConfig{ SaltRotation: "daily", Collect: config.CollectConfig{ Pageviews: true, }, }, Server: config.ServerConfig{ StatePath: statePath, }, } agg := NewMetricsAggregator(registry, NewCustomEventRegistry(100), &cfg) // Add some unique visitors so there's state to save agg.AddUnique("192.168.1.1", "Mozilla/5.0") // Shutdown should save to configured path ctx := context.Background() if err := agg.Shutdown(ctx); err != nil { t.Fatalf("Shutdown failed: %v", err) } // Verify file was created at configured path if _, err := os.Stat(statePath); os.IsNotExist(err) { t.Errorf("state file was not created at configured path: %s", statePath) } } func TestMetricsAggregator_Shutdown_DefaultStatePath(t *testing.T) { registry := NewPathRegistry(100) cfg := config.Config{ Site: config.SiteConfig{ Domains: []string{"example.com"}, // Required for validation SaltRotation: "daily", Collect: config.CollectConfig{ Pageviews: true, }, }, Limits: config.LimitsConfig{ MaxPaths: 1000, MaxSources: 500, }, Server: config.ServerConfig{ // StatePath not set - validation should set default StatePath: "", }, } // Validate to apply defaults if err := cfg.Validate(); err != nil { t.Fatalf("config validation failed: %v", err) } // Verify default was applied expectedDefault := "/var/lib/watchdog/hll.state" if cfg.Server.StatePath != expectedDefault { t.Errorf( "expected default StatePath %q, got %q", expectedDefault, cfg.Server.StatePath, ) } agg := NewMetricsAggregator(registry, NewCustomEventRegistry(100), &cfg) // Add some unique visitors agg.AddUnique("192.168.1.1", "Mozilla/5.0") // Shutdown should save to default path ctx := context.Background() if err := agg.Shutdown(ctx); err != nil { // Might fail due to permissions on /var/lib, which is OK for this test // We're just verifying the code path works t.Logf("Shutdown returned error (might be expected): %v", err) } }