package report import ( "strings" "testing" "time" "github.com/mr0xb/nxstats/internal/parser" ) func makeTestData() ReportData { t1 := time.Date(2024, 2, 20, 10, 0, 0, 0, time.UTC) t2 := time.Date(2024, 2, 20, 11, 0, 0, 0, time.UTC) t3 := time.Date(2024, 2, 20, 11, 30, 0, 0, time.UTC) return ReportData{ GeneratedAt: time.Now(), AccessEntries: []parser.AccessEntry{ {RemoteAddr: "1.1.1.1", Method: "GET", Path: "/", Status: 200, BytesSent: 1024, Time: t1, UserAgent: "TestAgent/1.0"}, {RemoteAddr: "2.2.2.2", Method: "POST", Path: "/api", Status: 201, BytesSent: 512, Time: t2, UserAgent: "curl/7.0"}, {RemoteAddr: "1.1.1.1", Method: "GET", Path: "/about", Status: 200, BytesSent: 2048, Time: t3, UserAgent: "TestAgent/1.0"}, {RemoteAddr: "3.3.3.3", Method: "GET", Path: "/", Status: 500, BytesSent: 128, Time: t3, UserAgent: "BadBot/1.0"}, }, ErrorEntries: []parser.ErrorEntry{ {Time: t1, Level: "error", PID: 100, TID: 200, Message: "connection refused"}, {Time: t2, Level: "warn", PID: 100, TID: 200, Message: "slow query"}, }, } } func TestGenerateHTML_ReturnsHTML(t *testing.T) { data := makeTestData() ComputeStats(&data) html, err := GenerateHTML(data) if err != nil { t.Fatalf("GenerateHTML error: %v", err) } if !strings.HasPrefix(strings.TrimSpace(html), "") { t.Error("expected output to start with ") } } func TestGenerateHTML_ContainsStatsCards(t *testing.T) { data := makeTestData() ComputeStats(&data) html, err := GenerateHTML(data) if err != nil { t.Fatalf("GenerateHTML error: %v", err) } checks := []string{ "Total Requests", "Unique IPs", "Total Bytes", "Error Rate", } for _, c := range checks { if !strings.Contains(html, c) { t.Errorf("expected %q in HTML output", c) } } } func TestGenerateHTML_ContainsSections(t *testing.T) { data := makeTestData() ComputeStats(&data) html, err := GenerateHTML(data) if err != nil { t.Fatalf("GenerateHTML error: %v", err) } sections := []string{ "Access Log", "Error Log", "Top IPs", "Top Paths", "chart", } for _, s := range sections { if !strings.Contains(html, s) { t.Errorf("expected section %q in HTML output", s) } } } func TestGenerateHTML_ContainsIPData(t *testing.T) { data := makeTestData() ComputeStats(&data) html, err := GenerateHTML(data) if err != nil { t.Fatalf("GenerateHTML error: %v", err) } if !strings.Contains(html, "1.1.1.1") { t.Error("expected top IP 1.1.1.1 in output") } } func TestComputeStats_Counts(t *testing.T) { data := makeTestData() ComputeStats(&data) if data.TotalRequests != 4 { t.Errorf("TotalRequests = %d, want 4", data.TotalRequests) } if data.UniqueIPs != 3 { t.Errorf("UniqueIPs = %d, want 3", data.UniqueIPs) } if data.TotalBytes != 3712 { t.Errorf("TotalBytes = %d, want 3712", data.TotalBytes) } } func TestComputeStats_ErrorRate(t *testing.T) { data := makeTestData() ComputeStats(&data) // 1 out of 4 requests is 5xx → 25% if data.ErrorRate < 24.9 || data.ErrorRate > 25.1 { t.Errorf("ErrorRate = %f, want ~25.0", data.ErrorRate) } } func TestComputeStats_TopIPs(t *testing.T) { data := makeTestData() ComputeStats(&data) if len(data.TopIPs) == 0 { t.Fatal("expected TopIPs to be non-empty") } if data.TopIPs[0].IP != "1.1.1.1" { t.Errorf("top IP = %q, want 1.1.1.1", data.TopIPs[0].IP) } if data.TopIPs[0].Count != 2 { t.Errorf("top IP count = %d, want 2", data.TopIPs[0].Count) } } func TestComputeStats_TopPaths(t *testing.T) { data := makeTestData() ComputeStats(&data) if len(data.TopPaths) == 0 { t.Fatal("expected TopPaths to be non-empty") } if data.TopPaths[0].Path != "/" { t.Errorf("top path = %q, want /", data.TopPaths[0].Path) } } func TestComputeStats_TopPathsTopStatus(t *testing.T) { // "/" gets two requests: one 200 and one 500. // TopStatus should be 200 (the most frequent), not an average (350). data := makeTestData() ComputeStats(&data) var slashEntry *PathCount for i := range data.TopPaths { if data.TopPaths[i].Path == "/" { slashEntry = &data.TopPaths[i] break } } if slashEntry == nil { t.Fatal("expected / in TopPaths") } if slashEntry.TopStatus != 200 { t.Errorf("TopStatus for / = %d, want 200 (most frequent, not an average)", slashEntry.TopStatus) } } func TestComputeStats_StatusCounts(t *testing.T) { data := makeTestData() ComputeStats(&data) if data.StatusCounts[200] != 2 { t.Errorf("StatusCounts[200] = %d, want 2", data.StatusCounts[200]) } if data.StatusCounts[500] != 1 { t.Errorf("StatusCounts[500] = %d, want 1", data.StatusCounts[500]) } } func TestGenerateHTML_SearchInputs(t *testing.T) { data := makeTestData() ComputeStats(&data) html, err := GenerateHTML(data) if err != nil { t.Fatalf("GenerateHTML error: %v", err) } // One search input per paginated table if strings.Count(html, `type="text"`) < 2 { t.Error("expected at least 2 search text inputs") } if !strings.Contains(html, "onkeyup") { t.Error("expected onkeyup handler on search inputs") } } func TestGenerateHTML_PaginationControls(t *testing.T) { data := makeTestData() ComputeStats(&data) html, err := GenerateHTML(data) if err != nil { t.Fatalf("GenerateHTML error: %v", err) } // Prev/Next buttons for both tables if strings.Count(html, "Prev") < 2 { t.Error("expected Prev button for each paginated table") } if strings.Count(html, "Next") < 2 { t.Error("expected Next button for each paginated table") } // Page-size selector if strings.Count(html, `