initial commit
This commit is contained in:
commit
885e7d6e02
22 changed files with 3001 additions and 0 deletions
520
internal/report/report_test.go
Normal file
520
internal/report/report_test.go
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue