Tau.Acuvim/portal/TESTING.md
Diseri Pearson e17921a122 Add portal: customer-facing white-labeled monitoring stack
New top-level portal/ project, peer to console/ and firmware/. Delivers a
.NET 10 + React 18 + TimescaleDB + Grafana stack, one container set per
customer behind Traefik. Built in 12 phases per FrontEndPrompt spec; no
changes to existing console or firmware.

Backend (src/Tau.Acuvim.Portal/):
- .NET 10 minimal API, Serilog, ASP.NET Identity (cookie auth, lockout).
- Single AppDbContext with identity / app / monitoring schemas.
- MigrateAsync + TimescaleBootstrapper (idempotent hypertable creation)
  + IdentityBootstrapper (seeded admin + branding) on startup.
- Pure CostCalculator + DB-backed RateService for tariffs (effective-dated,
  TOU periods, VAT, fixed charges, per-municipality timezone).
- BrandingService with logo upload to mounted volume.
- Time-series ingest + bucketed query services (time_bucket aggregates,
  ON CONFLICT for idempotent re-delivery).
- ConfigOverviewService with redaction-by-construction (passwords never in
  payload).
- DataProtection keys persisted to /data/keys volume for cookie survival
  across container restarts.

Frontend (frontend/):
- React 18 + TypeScript + Vite + Ant Design 5 + TanStack Query.
- BrandingProvider + ThemedRoot for live re-themed white-labelling.
- RequireAuth / RequireRole guards.
- Pages: Login, Dashboard, Dashboards (embedded Grafana), Sites (admin),
  Settings tabs (Branding / Rates / Users / Grafana / App config).

Infra:
- Dev (docker-compose.yml) and prod (docker-compose.prod.yml) compose
  files. Three services per customer; Traefik subdomain + same-origin
  /grafana path-prefix routing wired with labels.
- Grafana 11 with provisioned timescaledb datasource (uid pinned) and
  starter power-overview.json dashboard with device template variable.
- Compose project name documented as lowercase (Compose v2 requirement).

Tests (tests/Tau.Acuvim.Portal.Tests/):
- xUnit, 40 tests. Covers CostCalculator (period match, TZ, overlap,
  VAT, fixed), ConnectionStringResolver (all 4 precedence branches incl.
  Production refusal), TariffValidator, DayOfWeekFlag.
- All passing locally against .NET 10.

Docs:
- README.md (onboarding + 11 spec sections), OPERATIONS.md (per-customer
  provisioning, secret rotation, backup, troubleshooting), TESTING.md
  (manual integration scenarios, frontend test scaffolding recipe).

Production safety guards:
- Refuses to start if Authentication:DefaultAdminPassword is unchanged
  default in Production.
- Refuses to start if Database:AutoProvisionLocalTimescaleDb=true in
  Production.
- Prod Grafana ships with anonymous off and auth mode unset (three
  options documented in README Security) so iframe refuses to load
  until a deliberate prod auth choice is made.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:30:30 +02:00

6.6 KiB
Raw Blame History

Tau Acuvim Portal — Testing

This file covers what's checked-in (backend unit tests) and what to do manually or add later (integration, frontend).

Backend unit tests

xUnit project under portal/tests/Tau.Acuvim.Portal.Tests, matching the console's test stack (xUnit 2.9.3, Microsoft.NET.Test.Sdk 17.14.1, EF Core InMemory 10.0.8, coverlet 6.0.4).

cd C:\AcuvimDev\Tau.Acuvim\portal\tests\Tau.Acuvim.Portal.Tests
dotnet test

What's covered:

Suite What it locks down
CostCalculatorTests Period selection by day-of-week + time, overlap-first-wins, default-rate fallback, VAT, fixed charges, includeFixedMonthlyCharge=false, timezone conversion (sample inside vs outside the local-time window), empty-samples edge case, null-tariff guard.
ConnectionStringResolverTests All four precedence branches: explicit conn string wins; auto-provision in non-Production builds from TimescaleDb block; auto-provision in Production throws; no conn string + auto-provision off throws.
TariffValidatorTests Tariff-level rules (name, effective dates, non-negative rates, VAT 0100) and per-period rules (name, days, time order, midnight-wrap rejection, negative rate, bad time format).
DayOfWeekFlagTests Each weekday maps correctly; Weekdays/Weekends/All compose.

What's not covered by code (deliberate)

Authorization smoke tests

A WebApplicationFactory<Program> test that signs in a fake user and hits an admin endpoint would catch role-attribute regressions. Building one cleanly means swapping the EF provider, neutering MigrateAsync / TimescaleBootstrapper / the NpgSql health check, and stubbing cookie sign-in. That's a fair amount of plumbing for "the policy attribute is wired."

Add it if you start seeing role bypass regressions. Skeleton:

public class AuthorizationSmokeTests : IClassFixture<WebApplicationFactory<Program>> {  }

— with WebApplicationFactoryConfigureWebHost swapping AppDbContext for InMemory, removing the hosted services that need Timescale, and configuring a test authentication scheme.

Frontend component tests

Recommend Vitest + React Testing Library + jsdom. Not scaffolded in this repo on purpose — a half-built test runner is worse than none.

When you want them:

cd portal/frontend
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vitest/coverage-v8

Add to vite.config.ts:

test: { environment: 'jsdom', setupFiles: ['./src/test-setup.ts'], globals: true }

High-value first tests:

  • RequireAuth — renders children when authed, redirects to /login when not.
  • RequireRole — renders 403 panel when role missing, children when present.
  • BrandingProvider — applies CSS variables and document.title after fetch resolves.
  • PeriodEditor — toggling day-of-week buttons updates the bitmask correctly (this is the trickiest pure-logic spot in the UI).

Manual integration scenario

End-to-end smoke test. Run after any phase change.

0. Reset

cd C:\AcuvimDev\Tau.Acuvim\portal
docker compose down -v          # wipe DB + Grafana volumes
docker compose up -d

1. Migrate + seed

cd src/Tau.Acuvim.Portal
dotnet run

Expect in the log:

  • Database connection resolved via …
  • Applied migration '<name>' (one per pending)
  • TimescaleDB hypertable for monitoring.PowerMeasurements is ready (chunk interval: 7 days)
  • Seeded default admin admin@example.com

2. Frontend up

cd portal/frontend
npm install
npm run dev

3. Walk the surface

  1. Browse http://localhost:5174/login → branded card from template defaults.
  2. Sign in as admin@example.com / ChangeMe123!.
  3. Settings → Branding → change primary colour + footer + upload a small PNG. Save → re-themes everywhere without reload.
  4. Settings → Rates → create municipality "Cape Town" TZ Africa/Johannesburg → add tariff with three periods (peak/standard/off-peak per the README example).
  5. Settings → Users → create user@example.com / Password1, not admin. Sign out, log in as user → only Dashboard and Dashboards in nav; /admin/sites shows 403. Sign back in as admin.
  6. Sites → create "Warehouse A" linked to Cape Town → add device with external ID ABC0001-MAIN-01.
  7. Ingest via Swagger (/swagger) or:
    POST /api/ingest/measurements
    [
      {"time":"2026-05-17T10:00:00Z","deviceExternalId":"ABC0001-MAIN-01","activePowerKw":12.5,"energyImportedKwh":1000},
      {"time":"2026-05-17T10:15:00Z","deviceExternalId":"ABC0001-MAIN-01","activePowerKw":13.1,"energyImportedKwh":1003.3}
    ]
    
    {accepted:2, rejected:0}. Re-post → same result, no duplicates (ON CONFLICT).
  8. GET /api/measurements?deviceId=<guid>&from=2026-05-17T00:00:00Z&to=2026-05-17T23:59:59Z&bucket=hour → one bucket with sampleCount: 2.
  9. Dashboards → "Power Overview" iframe loads; device dropdown lists the device; chart renders the two samples.
  10. Settings → App config → confirm DB host = timescaledb, port 5432, db power_monitoring, no password anywhere.

4. Edges to poke

Check Expected
Wrong password 6 times 6th try locks the account for 15 min (Identity lockout).
dotnet run with ASPNETCORE_ENVIRONMENT=Production + default admin password App refuses to start with explicit error.
Restart dotnet run against existing DB "Seeded default admin" line absent (idempotent). Bootstrapper re-confirms Timescale hypertable without error.
Delete a site with devices+measurements Cascade removes everything; UI updates.
Create a period with EndTime ≤ StartTime in Settings → Rates Backend 400 with "no midnight wrap" message.
GET /api/admin/config-overview as non-admin 403.
Upload a 3 MB JPG to branding logo 400 "Logo must be <= 2 MB".

Local dev troubleshooting

  • EF migration error on startup — usually a schema drift since the last migration. Run dotnet ef migrations add <Name> to capture the diff, commit, restart.
  • TimescaleBootstrapper complains about hypertable — the table was already created by a previous run with different settings. Drop the table or recreate the DB volume.
  • POST /api/ingest/measurements returns {accepted: 0, rejected: N} — the device's ExternalId doesn't match anything in the monitoring.Devices table. Create the device first under Sites.
  • Dashboards page shows iframe but no chart — datasource UID mismatch. Confirm grafana/provisioning/datasources/timescaledb.yml sets uid: timescaledb and the dashboard JSON's panels reference the same.