Files
internet_use_control/internal/web/usage_test.go
Sam Rolfe b58993d994 Phase 7: telemetry and usage (daily rollup + week dashboard)
Build on Phases 1–6. Telemetry already arrived on
restrict/<device>/telemetry; this phase stores it in a daily rollup and
shows it.

- store: migration 003 adds telemetry_daily (device, day, app seconds,
  site seconds). InsertTelemetry maintains the affected days in the same
  transaction; RebuildRollups backfills and repairs it. UsageRange and
  TopUsage serve the week grid and top lists. CapAudit enforces the
  size-capped append-only audit log from AGENTS.md §7.
- web: GET /devices/{id}/usage — Monday-to-Sunday totals plus the top 10
  apps and top 10 sites, using CSP-safe native <progress> bars.
- theme.js: light/dark from the OS preference, with a nav toggle, loaded
  as an external script so the strict CSP still holds.
- cmd/control: `rollup` subcommand; rollup rebuild and audit cap at
  start-up and every 15 minutes.
- Version 0.5.0-phase7 (compose, Makefile, docs).

Tests: go test ./... and go vet ./... pass in golang:1.25-alpine.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-13 16:18:56 +11:00

95 lines
3.0 KiB
Go

package web
import (
"fmt"
"net/http"
"strings"
"testing"
"time"
"gitea.lab.audasmedia.com.au/sam/internet_use_control/internal/store"
)
func TestUsageScreenShowsAWeek(t *testing.T) {
app := newTestApp(t)
dev := createTestDevice(t, app)
ctx := t.Context()
// Seed the whole current week: apps and domains for every day.
start := weekStart(time.Now())
for i := 0; i < 7; i++ {
day := start.AddDate(0, 0, i).Format("2006-01-02")
if _, err := app.store.InsertTelemetry(ctx, dev.ID, day, []store.TelemetryEntry{
{App: "com.roblox.client", Seconds: (i + 1) * 600},
{App: "com.spotify.music", Seconds: 300},
{Domain: "roblox.com", Seconds: (i + 1) * 600},
}); err != nil {
t.Fatalf("InsertTelemetry %s: %v", day, err)
}
}
rec := app.do(t, http.MethodGet, fmt.Sprintf("/devices/%d/usage", dev.ID), nil)
if rec.Code != http.StatusOK {
t.Fatalf("GET usage = %d, want 200; body=%s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
for _, want := range []string{
"Usage", "Top apps", "Top sites", "App time", "Site time",
"com.roblox.client", "com.spotify.music", "roblox.com",
fmt.Sprintf("%d", dev.ID), "Harry&#39;s Pixel",
} {
if !strings.Contains(body, want) {
t.Errorf("usage page missing %q", want)
}
}
// Seven day rows: the weekday abbreviations must all be present.
for _, wd := range []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"} {
if !strings.Contains(body, ">"+wd+" <") {
t.Errorf("usage page missing the %s row", wd)
}
}
// A week with no data renders the empty state, never an error.
rec = app.do(t, http.MethodGet,
fmt.Sprintf("/devices/%d/usage?week=2020-01-01", dev.ID), nil)
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "No usage for this week") {
t.Errorf("empty week = %d, want the empty state", rec.Code)
}
}
func TestWeekHelpers(t *testing.T) {
loc := time.UTC
// A Sunday belongs to the week that started the previous Monday.
sunday := time.Date(2026, 10, 18, 12, 0, 0, 0, loc) // Sunday
got := weekStart(sunday)
want := time.Date(2026, 10, 12, 0, 0, 0, 0, loc)
if !got.Equal(want) {
t.Errorf("weekStart(Sun) = %s, want %s", got, want)
}
// A bad ?week= value falls back to now instead of failing.
now := time.Date(2026, 10, 15, 9, 0, 0, 0, loc)
if got := parseWeekAnchor("not-a-date", now); !got.Equal(now) {
t.Errorf("parseWeekAnchor(bad) = %s, want now", got)
}
if got := parseWeekAnchor("2026-10-12", now); got.Day() != 12 || got.Month() != time.October {
t.Errorf("parseWeekAnchor(valid) = %s", got)
}
if got := formatSeconds(0); got != "0m" {
t.Errorf("formatSeconds(0) = %q", got)
}
if got := formatSeconds(3600); got != "1h 00m" {
t.Errorf("formatSeconds(3600) = %q", got)
}
if got := formatSeconds(30); got != "under a minute" {
t.Errorf("formatSeconds(30) = %q", got)
}
if got := percent(50, 100); got != 50 {
t.Errorf("percent(50,100) = %d", got)
}
if got := percent(50, 0); got != 0 {
t.Errorf("percent with max 0 = %d, want 0", got)
}
}