679 lines
20 KiB
Go
679 lines
20 KiB
Go
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), "<!DOCTYPE html>") {
|
|
t.Error("expected output to start with <!DOCTYPE html>")
|
|
}
|
|
}
|
|
|
|
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, `<select `) < 2 {
|
|
t.Error("expected page-size <select> for each paginated table")
|
|
}
|
|
// makePaginator JS factory must be present
|
|
if !strings.Contains(html, "makePaginator") {
|
|
t.Error("expected makePaginator JS function")
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTML_AccessDataEmbeddedAsJSON(t *testing.T) {
|
|
data := makeTestData()
|
|
ComputeStats(&data)
|
|
|
|
html, err := GenerateHTML(data)
|
|
if err != nil {
|
|
t.Fatalf("GenerateHTML error: %v", err)
|
|
}
|
|
// The access tbody must be empty in the static HTML — rows are JS-rendered
|
|
if !strings.Contains(html, `<tbody id="accessTbody"></tbody>`) {
|
|
t.Error("accessTbody should be empty in static HTML (rows rendered by JS)")
|
|
}
|
|
// But the IP must still appear inside the embedded JSON data
|
|
if !strings.Contains(html, "1.1.1.1") {
|
|
t.Error("access entry IP must be present in embedded JSON")
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTML_WithGeoLocations(t *testing.T) {
|
|
data := makeTestData()
|
|
ComputeStats(&data)
|
|
data.GeoLocations = []GeoPoint{
|
|
{IP: "1.1.1.1", Lat: 37.751, Lon: -97.822, Country: "United States", CountryCode: "US", Count: 2},
|
|
}
|
|
|
|
html, err := GenerateHTML(data)
|
|
if err != nil {
|
|
t.Fatalf("GenerateHTML error: %v", err)
|
|
}
|
|
if !strings.Contains(html, "leaflet") {
|
|
t.Error("expected Leaflet.js reference when GeoLocations present")
|
|
}
|
|
// Flag helper must be present
|
|
if !strings.Contains(html, "countryFlag") {
|
|
t.Error("expected countryFlag JS function")
|
|
}
|
|
// Country code must be in the embedded geo JSON
|
|
if !strings.Contains(html, `"cc":"US"`) {
|
|
t.Error("expected CC field in geoJSON output")
|
|
}
|
|
// Map must appear before Top IPs (i.e. after Charts, not at the end)
|
|
mapIdx := strings.Index(html, `id="map"`)
|
|
topIPsIdx := strings.Index(html, "Top IPs")
|
|
if mapIdx == -1 || topIPsIdx == -1 {
|
|
t.Fatal("expected both map and Top IPs sections")
|
|
}
|
|
if mapIdx > topIPsIdx {
|
|
t.Error("map section should appear before Top IPs (i.e. after Charts)")
|
|
}
|
|
}
|
|
|
|
// ── multi-day fixture ──────────────────────────────────────────────────────
|
|
|
|
func makeMultiDayEntries() ([]parser.AccessEntry, []parser.ErrorEntry) {
|
|
day1 := time.Date(2024, 2, 20, 10, 0, 0, 0, time.UTC)
|
|
day2a := time.Date(2024, 2, 21, 14, 0, 0, 0, time.UTC)
|
|
day2b := time.Date(2024, 2, 21, 15, 0, 0, 0, time.UTC)
|
|
access := []parser.AccessEntry{
|
|
{RemoteAddr: "1.1.1.1", Method: "GET", Path: "/", Status: 200, BytesSent: 1024, Time: day1},
|
|
{RemoteAddr: "2.2.2.2", Method: "POST", Path: "/api", Status: 500, BytesSent: 256, Time: day2a},
|
|
{RemoteAddr: "1.1.1.1", Method: "GET", Path: "/", Status: 200, BytesSent: 512, Time: day2b},
|
|
}
|
|
errors := []parser.ErrorEntry{
|
|
{Time: day1, Level: "error", PID: 1, TID: 2, Message: "day1 error"},
|
|
{Time: day2a, Level: "warn", PID: 3, TID: 4, Message: "day2 warn"},
|
|
}
|
|
return access, errors
|
|
}
|
|
|
|
// ── DeriveDailyFilename ────────────────────────────────────────────────────
|
|
|
|
func TestDeriveDailyFilename(t *testing.T) {
|
|
cases := []struct{ output, date, want string }{
|
|
{"report.html", "2024-02-20", "report-2024-02-20.html"},
|
|
{"stats.html", "2024-03-01", "stats-2024-03-01.html"},
|
|
{"report", "2024-02-20", "report-2024-02-20"},
|
|
}
|
|
for _, c := range cases {
|
|
got := DeriveDailyFilename(c.output, c.date)
|
|
if got != c.want {
|
|
t.Errorf("DeriveDailyFilename(%q, %q) = %q, want %q", c.output, c.date, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── GroupByDay ─────────────────────────────────────────────────────────────
|
|
|
|
func TestGroupByDay_Partitioning(t *testing.T) {
|
|
access, errors := makeMultiDayEntries()
|
|
days := GroupByDay("report.html", access, errors, nil, "")
|
|
if len(days) != 2 {
|
|
t.Fatalf("expected 2 days, got %d", len(days))
|
|
}
|
|
if days[0].Date.Format("2006-01-02") != "2024-02-20" {
|
|
t.Errorf("expected oldest day first, got %s", days[0].Date.Format("2006-01-02"))
|
|
}
|
|
if days[1].Date.Format("2006-01-02") != "2024-02-21" {
|
|
t.Errorf("expected newest day second, got %s", days[1].Date.Format("2006-01-02"))
|
|
}
|
|
}
|
|
|
|
func TestGroupByDay_RequestCounts(t *testing.T) {
|
|
access, errors := makeMultiDayEntries()
|
|
days := GroupByDay("report.html", access, errors, nil, "")
|
|
if days[0].Data.TotalRequests != 1 {
|
|
t.Errorf("day1 requests = %d, want 1", days[0].Data.TotalRequests)
|
|
}
|
|
if days[1].Data.TotalRequests != 2 {
|
|
t.Errorf("day2 requests = %d, want 2", days[1].Data.TotalRequests)
|
|
}
|
|
}
|
|
|
|
func TestGroupByDay_ErrorPartitioning(t *testing.T) {
|
|
access, errors := makeMultiDayEntries()
|
|
days := GroupByDay("report.html", access, errors, nil, "")
|
|
if len(days[0].Data.ErrorEntries) != 1 {
|
|
t.Errorf("day1 error entries = %d, want 1", len(days[0].Data.ErrorEntries))
|
|
}
|
|
if len(days[1].Data.ErrorEntries) != 1 {
|
|
t.Errorf("day2 error entries = %d, want 1", len(days[1].Data.ErrorEntries))
|
|
}
|
|
}
|
|
|
|
func TestGroupByDay_Filenames(t *testing.T) {
|
|
access, errors := makeMultiDayEntries()
|
|
days := GroupByDay("report.html", access, errors, nil, "")
|
|
if days[0].Filename != "report-2024-02-20.html" {
|
|
t.Errorf("day1 filename = %q, want report-2024-02-20.html", days[0].Filename)
|
|
}
|
|
if days[1].Filename != "report-2024-02-21.html" {
|
|
t.Errorf("day2 filename = %q, want report-2024-02-21.html", days[1].Filename)
|
|
}
|
|
}
|
|
|
|
func TestGroupByDay_GeoFiltering(t *testing.T) {
|
|
access, errors := makeMultiDayEntries()
|
|
geoPoints := map[string]GeoPoint{
|
|
"1.1.1.1": {IP: "1.1.1.1", Lat: 37.7, Lon: -97.8, Country: "United States", CountryCode: "US"},
|
|
"2.2.2.2": {IP: "2.2.2.2", Lat: 51.5, Lon: -0.1, Country: "United Kingdom", CountryCode: "GB"},
|
|
}
|
|
days := GroupByDay("report.html", access, errors, geoPoints, "")
|
|
// day1 only has 1.1.1.1
|
|
if len(days[0].Data.GeoLocations) != 1 {
|
|
t.Errorf("day1 geo = %d, want 1", len(days[0].Data.GeoLocations))
|
|
}
|
|
if days[0].Data.GeoLocations[0].IP != "1.1.1.1" {
|
|
t.Errorf("day1 geo IP = %q, want 1.1.1.1", days[0].Data.GeoLocations[0].IP)
|
|
}
|
|
// day2 has both IPs
|
|
if len(days[1].Data.GeoLocations) != 2 {
|
|
t.Errorf("day2 geo = %d, want 2", len(days[1].Data.GeoLocations))
|
|
}
|
|
}
|
|
|
|
func TestGroupByDay_EmptyInput(t *testing.T) {
|
|
days := GroupByDay("report.html", nil, nil, nil, "")
|
|
if len(days) != 0 {
|
|
t.Errorf("expected 0 days, got %d", len(days))
|
|
}
|
|
}
|
|
|
|
func TestGroupByDay_StatsComputed(t *testing.T) {
|
|
access, errors := makeMultiDayEntries()
|
|
days := GroupByDay("report.html", access, errors, nil, "")
|
|
// day2 has one 500 → error rate 50%
|
|
if days[1].Data.ErrorRate < 49.9 || days[1].Data.ErrorRate > 50.1 {
|
|
t.Errorf("day2 error rate = %.2f, want ~50.0", days[1].Data.ErrorRate)
|
|
}
|
|
}
|
|
|
|
// ── GenerateIndexHTML ──────────────────────────────────────────────────────
|
|
|
|
func makeTestIndexData() IndexData {
|
|
return IndexData{
|
|
GeneratedAt: time.Date(2024, 2, 22, 12, 0, 0, 0, time.UTC),
|
|
TotalRequests: 300,
|
|
TotalUniqueIPs: 42,
|
|
TotalBytes: 1 << 20,
|
|
OverallErrorRate: 3.5,
|
|
Days: []DaySummary{
|
|
{
|
|
Date: time.Date(2024, 2, 21, 0, 0, 0, 0, time.UTC),
|
|
Filename: "report-2024-02-21.html",
|
|
Requests: 200, UniqueIPs: 30, TotalBytes: 700000, ErrorRate: 5.0,
|
|
},
|
|
{
|
|
Date: time.Date(2024, 2, 20, 0, 0, 0, 0, time.UTC),
|
|
Filename: "report-2024-02-20.html",
|
|
Requests: 100, UniqueIPs: 12, TotalBytes: 348000, ErrorRate: 1.0,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestGenerateIndexHTML_ReturnsHTML(t *testing.T) {
|
|
idx := makeTestIndexData()
|
|
html, err := GenerateIndexHTML(idx)
|
|
if err != nil {
|
|
t.Fatalf("GenerateIndexHTML error: %v", err)
|
|
}
|
|
if !strings.HasPrefix(strings.TrimSpace(html), "<!DOCTYPE html>") {
|
|
t.Error("expected output to start with <!DOCTYPE html>")
|
|
}
|
|
}
|
|
|
|
func TestGenerateIndexHTML_ContainsAggregateCards(t *testing.T) {
|
|
idx := makeTestIndexData()
|
|
html, _ := GenerateIndexHTML(idx)
|
|
for _, want := range []string{"Total Requests", "Unique IPs", "Total Bytes", "Overall Error Rate", "Days Covered"} {
|
|
if !strings.Contains(html, want) {
|
|
t.Errorf("missing card label %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenerateIndexHTML_ContainsDayLinks(t *testing.T) {
|
|
idx := makeTestIndexData()
|
|
html, _ := GenerateIndexHTML(idx)
|
|
if !strings.Contains(html, `href="report-2024-02-21.html"`) {
|
|
t.Error("expected link to report-2024-02-21.html")
|
|
}
|
|
if !strings.Contains(html, "2024-02-20") {
|
|
t.Error("expected date 2024-02-20 in table")
|
|
}
|
|
}
|
|
|
|
func TestGenerateIndexHTML_ErrorRateColouring(t *testing.T) {
|
|
idx := makeTestIndexData()
|
|
html, _ := GenerateIndexHTML(idx)
|
|
if !strings.Contains(html, "err-med") {
|
|
t.Error("expected err-med class for 5% error rate")
|
|
}
|
|
if !strings.Contains(html, "err-low") {
|
|
t.Error("expected err-low class for 1% error rate")
|
|
}
|
|
}
|
|
|
|
func TestGenerateIndexHTML_DaysNewestFirst(t *testing.T) {
|
|
idx := makeTestIndexData()
|
|
html, _ := GenerateIndexHTML(idx)
|
|
i21 := strings.Index(html, "2024-02-21")
|
|
i20 := strings.Index(html, "2024-02-20")
|
|
if i21 == -1 || i20 == -1 {
|
|
t.Fatal("expected both dates in index HTML")
|
|
}
|
|
if i21 > i20 {
|
|
t.Error("newest day (2024-02-21) should appear before older day in table")
|
|
}
|
|
}
|
|
|
|
// ── Back link in daily reports ─────────────────────────────────────────────
|
|
|
|
func TestGenerateHTML_BackLink(t *testing.T) {
|
|
data := makeTestData()
|
|
ComputeStats(&data)
|
|
data.IndexFile = "report.html"
|
|
|
|
html, err := GenerateHTML(data)
|
|
if err != nil {
|
|
t.Fatalf("GenerateHTML error: %v", err)
|
|
}
|
|
if !strings.Contains(html, `href="report.html"`) {
|
|
t.Error("expected back link href when IndexFile is set")
|
|
}
|
|
if !strings.Contains(html, "Back to Index") {
|
|
t.Error("expected 'Back to Index' text in nav")
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTML_NoBackLinkByDefault(t *testing.T) {
|
|
data := makeTestData()
|
|
ComputeStats(&data)
|
|
// IndexFile is zero value ""
|
|
html, err := GenerateHTML(data)
|
|
if err != nil {
|
|
t.Fatalf("GenerateHTML error: %v", err)
|
|
}
|
|
if strings.Contains(html, "Back to Index") {
|
|
t.Error("expected no back link when IndexFile is empty")
|
|
}
|
|
}
|
|
|
|
// ── Theme selection ────────────────────────────────────────────────────────
|
|
|
|
func TestThemeNames_ContainsAllThemes(t *testing.T) {
|
|
names := ThemeNames()
|
|
for _, want := range []string{"cyberpunk", "purplerain", "cactus"} {
|
|
found := false
|
|
for _, n := range names {
|
|
if n == want {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("ThemeNames() = %v, missing %q", names, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTML_DefaultThemeWhenEmpty(t *testing.T) {
|
|
data := makeTestData()
|
|
ComputeStats(&data)
|
|
// Theme left as zero value "".
|
|
|
|
html, err := GenerateHTML(data)
|
|
if err != nil {
|
|
t.Fatalf("GenerateHTML error: %v", err)
|
|
}
|
|
if !strings.Contains(html, cyberpunkTheme.Chart1) {
|
|
t.Error("expected cyberpunk theme colors when Theme is empty")
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTML_UnknownThemeFallsBackToDefault(t *testing.T) {
|
|
data := makeTestData()
|
|
ComputeStats(&data)
|
|
data.Theme = "does-not-exist"
|
|
|
|
html, err := GenerateHTML(data)
|
|
if err != nil {
|
|
t.Fatalf("GenerateHTML error: %v", err)
|
|
}
|
|
if !strings.Contains(html, cyberpunkTheme.Chart1) {
|
|
t.Error("expected fallback to cyberpunk theme colors for unknown theme name")
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTML_PurplerainTheme(t *testing.T) {
|
|
data := makeTestData()
|
|
ComputeStats(&data)
|
|
data.Theme = "purplerain"
|
|
|
|
html, err := GenerateHTML(data)
|
|
if err != nil {
|
|
t.Fatalf("GenerateHTML error: %v", err)
|
|
}
|
|
if !strings.Contains(html, purplerainTheme.Chart1) {
|
|
t.Error("expected purplerain theme colors in output")
|
|
}
|
|
if strings.Contains(html, cyberpunkTheme.Chart1) {
|
|
t.Error("did not expect cyberpunk theme colors when purplerain is selected")
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTML_CactusTheme(t *testing.T) {
|
|
data := makeTestData()
|
|
ComputeStats(&data)
|
|
data.Theme = "cactus"
|
|
data.GeoLocations = []GeoPoint{
|
|
{IP: "1.1.1.1", Lat: 37.751, Lon: -97.822, Country: "United States", CountryCode: "US", Count: 2},
|
|
}
|
|
|
|
html, err := GenerateHTML(data)
|
|
if err != nil {
|
|
t.Fatalf("GenerateHTML error: %v", err)
|
|
}
|
|
if !strings.Contains(html, cactusTheme.Chart1) {
|
|
t.Error("expected cactus theme colors in output")
|
|
}
|
|
if !strings.Contains(html, "light_all") {
|
|
t.Error("expected cactus theme to use a light map basemap")
|
|
}
|
|
}
|
|
|
|
func TestGenerateIndexHTML_ThemeApplied(t *testing.T) {
|
|
idx := makeTestIndexData()
|
|
idx.Theme = "purplerain"
|
|
|
|
html, err := GenerateIndexHTML(idx)
|
|
if err != nil {
|
|
t.Fatalf("GenerateIndexHTML error: %v", err)
|
|
}
|
|
if !strings.Contains(html, purplerainTheme.Chart1) {
|
|
t.Error("expected purplerain theme colors in index output")
|
|
}
|
|
}
|
|
|
|
func TestGroupByDay_PropagatesTheme(t *testing.T) {
|
|
access, errors := makeMultiDayEntries()
|
|
days := GroupByDay("report.html", access, errors, nil, "cactus")
|
|
for _, d := range days {
|
|
if d.Data.Theme != "cactus" {
|
|
t.Errorf("day %s Theme = %q, want cactus", d.Filename, d.Data.Theme)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTML_DayTitleInHeader(t *testing.T) {
|
|
data := makeTestData()
|
|
ComputeStats(&data)
|
|
data.DayTitle = "2024-02-20"
|
|
html, err := GenerateHTML(data)
|
|
if err != nil {
|
|
t.Fatalf("GenerateHTML error: %v", err)
|
|
}
|
|
if !strings.Contains(html, "2024-02-20") {
|
|
t.Error("expected DayTitle in report HTML")
|
|
}
|
|
}
|
|
|
|
// ── map API key ────────────────────────────────────────────────────────────
|
|
|
|
func TestTileURLWithKey(t *testing.T) {
|
|
cases := []struct{ url, key, want string }{
|
|
{"https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png", "XYZ",
|
|
"https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png?key=XYZ"},
|
|
{"https://tiles.example/{z}/{x}/{y}.png?style=dark", "XYZ",
|
|
"https://tiles.example/{z}/{x}/{y}.png?style=dark&key=XYZ"},
|
|
{"https://tiles.example/{z}/{x}/{y}.png", "a b&c",
|
|
"https://tiles.example/{z}/{x}/{y}.png?key=a+b%26c"},
|
|
{"https://tiles.example/{z}/{x}/{y}.png", "",
|
|
"https://tiles.example/{z}/{x}/{y}.png"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := tileURLWithKey(c.url, c.key); got != c.want {
|
|
t.Errorf("tileURLWithKey(%q, %q) = %q, want %q", c.url, c.key, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTML_MapAPIKeyInTileURL(t *testing.T) {
|
|
data := makeTestData()
|
|
ComputeStats(&data)
|
|
data.GeoLocations = []GeoPoint{
|
|
{IP: "1.1.1.1", Lat: 37.751, Lon: -97.822, Country: "United States", CountryCode: "US", Count: 2},
|
|
}
|
|
data.MapAPIKey = "secret-key"
|
|
|
|
html, err := GenerateHTML(data)
|
|
if err != nil {
|
|
t.Fatalf("GenerateHTML error: %v", err)
|
|
}
|
|
if !strings.Contains(html, "?key=secret-key") {
|
|
t.Error("expected map API key appended to the tile URL")
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTML_NoMapAPIKeyLeavesTileURL(t *testing.T) {
|
|
data := makeTestData()
|
|
ComputeStats(&data)
|
|
data.GeoLocations = []GeoPoint{
|
|
{IP: "1.1.1.1", Lat: 37.751, Lon: -97.822, Country: "United States", CountryCode: "US", Count: 2},
|
|
}
|
|
|
|
html, err := GenerateHTML(data)
|
|
if err != nil {
|
|
t.Fatalf("GenerateHTML error: %v", err)
|
|
}
|
|
if strings.Contains(html, "key=") {
|
|
t.Error("did not expect a key query parameter when MapAPIKey is empty")
|
|
}
|
|
}
|