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>
6.6 KiB
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 0–100) 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/loginwhen 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
- Browse
http://localhost:5174/login→ branded card from template defaults. - Sign in as
admin@example.com/ChangeMe123!. - Settings → Branding → change primary colour + footer + upload a small PNG. Save → re-themes everywhere without reload.
- Settings → Rates → create municipality "Cape Town" TZ
Africa/Johannesburg→ add tariff with three periods (peak/standard/off-peak per the README example). - Settings → Users → create
user@example.com/Password1, not admin. Sign out, log in as user → only Dashboard and Dashboards in nav;/admin/sitesshows 403. Sign back in as admin. - Sites → create "Warehouse A" linked to Cape Town → add device with external ID
ABC0001-MAIN-01. - 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). GET /api/measurements?deviceId=<guid>&from=2026-05-17T00:00:00Z&to=2026-05-17T23:59:59Z&bucket=hour→ one bucket withsampleCount: 2.- Dashboards → "Power Overview" iframe loads; device dropdown lists the device; chart renders the two samples.
- Settings → App config → confirm DB host =
timescaledb, port5432, dbpower_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. TimescaleBootstrappercomplains 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/measurementsreturns{accepted: 0, rejected: N}— the device'sExternalIddoesn't match anything in themonitoring.Devicestable. Create the device first under Sites.- Dashboards page shows iframe but no chart — datasource UID mismatch. Confirm
grafana/provisioning/datasources/timescaledb.ymlsetsuid: timescaledband the dashboard JSON's panels reference the same.