# 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). ```powershell 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` 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: ```csharp public class AuthorizationSmokeTests : IClassFixture> { … } ``` — 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: ```powershell cd portal/frontend npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vitest/coverage-v8 ``` Add to `vite.config.ts`: ```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 ```powershell cd C:\AcuvimDev\Tau.Acuvim\portal docker compose down -v # wipe DB + Grafana volumes docker compose up -d ``` ### 1. Migrate + seed ```powershell cd src/Tau.Acuvim.Portal dotnet run ``` Expect in the log: - `Database connection resolved via …` - `Applied migration ''` (one per pending) - `TimescaleDB hypertable for monitoring.PowerMeasurements is ready (chunk interval: 7 days)` - `Seeded default admin admin@example.com` ### 2. Frontend up ```powershell 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=&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 ` 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.