E2E Critical User Journeys Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a deterministic backend E2E suite that covers all 12 critical user journeys with smoke/full split and CI-ready reporting.
Architecture: Extend the existing backend/test/* E2E style (Nest app boot + Supertest + DB cleanup) with shared helpers for auth, tenant seed, and request wrappers. Implement journey coverage in three risk-first phases (security/isolation → core flows → aggregation/automation) while tagging a fast smoke subset that must stay under 7 minutes. Keep tests organization-scoped and assert status + payload + persistence side effects.
Tech Stack: NestJS 11, Jest (backend/test/jest-e2e.json), Supertest, TypeORM/DataSource SQL cleanup, Bun workspace scripts, Biome.
File Structure (planned changes)
Create
backend/test/helpers/auth.helper.ts— register/login/refresh/logout helpers returning tokens/cookies.backend/test/helpers/tenant.helper.ts— seed org A/B users + role assignment utilities.backend/test/helpers/request.helper.ts— authenticated request builders with org-aware headers/cookies.backend/test/helpers/factory.helper.ts— area/pond/device/telemetry reusable fixture builders.backend/test/helpers/cleanup.helper.ts— deterministic cleanup (table truncation/delete order).backend/test/smoke/smoke.e2e-spec.ts— minimal happy-path smoke suite (journeys 1/2/3/8/12).backend/test/tenant/multi-tenant-isolation.e2e-spec.ts— explicit org A/B isolation tests.backend/test/farm/farm-management.e2e-spec.ts— journey 6.backend/test/devices/device-lifecycle.e2e-spec.ts— journey 7.backend/test/dashboard/dashboard-aggregation.e2e-spec.ts— journey 10.
Modify
backend/test/auth/auth.controller.e2e-spec.ts— refactor to helper usage + strengthen negative/error contract assertions.backend/test/rbac/role.controller.e2e-spec.ts— enforce role allow/deny matrix for journey 2.backend/test/telemetry/telemetry.controller.e2e-spec.ts— split valid vs invalid ingestion assertions for journeys 8/9.backend/test/automation/automation-flow.e2e-spec.ts— align with trigger assertions for journey 11.backend/package.json— add smoke/full scripts..github/workflows/*(existing backend CI workflow file) — wire smoke-on-PR and full-nightly/critical-label.
Validate / reference while implementing
backend/test/jest-e2e.jsonbackend/src/common/interceptors/organization-filter.interceptor.tsbackend/src/rbac/authorization-policy.service.ts
Task 1: Build shared E2E foundation (Day 1)
Files:
Create:
backend/test/helpers/auth.helper.tsCreate:
backend/test/helpers/tenant.helper.tsCreate:
backend/test/helpers/request.helper.tsCreate:
backend/test/helpers/factory.helper.tsCreate:
backend/test/helpers/cleanup.helper.tsTest:
backend/test/auth/auth.controller.e2e-spec.tsStep 1: Write failing helper-adoption test in auth E2E
// backend/test/auth/auth.controller.e2e-spec.ts
it('uses shared auth helper for login flow', async () => {
const session = await loginUser(app, {
email: 'test@example.com',
password: 'Password123',
});
expect(session.accessToken).toBeDefined();
expect(session.refreshCookie).toContain('refreshToken=');
});
- Step 2: Run target test to confirm failure
Run: cd backend && bun test -- test/auth/auth.controller.e2e-spec.ts --testNamePattern="uses shared auth helper for login flow"
Expected: FAIL with loginUser is not defined.
- Step 3: Implement minimal shared helpers
// backend/test/helpers/auth.helper.ts
export async function loginUser(
app: INestApplication,
input: { email: string; password: string },
): Promise<{ accessToken: string; refreshCookie: string }> {
const res = await request(app.getHttpServer())
.post('/api/v1/auth/login')
.send(input)
.expect(200);
const setCookies = (res.headers['set-cookie'] as string[] | undefined) ?? [];
const refreshCookie =
setCookies.find((cookie) => cookie.startsWith('refreshToken=')) ?? '';
return { accessToken: res.body.accessToken, refreshCookie };
}
// backend/test/helpers/tenant.helper.ts
export interface TenantSeedResult {
orgA: { accessToken: string; userId: string };
orgB: { accessToken: string; userId: string };
}
// backend/test/helpers/cleanup.helper.ts
export async function cleanupCoreTables(dataSource: DataSource): Promise<void> {
await dataSource.query('DELETE FROM telemetry');
await dataSource.query('DELETE FROM device_ponds');
await dataSource.query('DELETE FROM devices');
await dataSource.query('DELETE FROM ponds');
await dataSource.query('DELETE FROM areas');
await dataSource.query('DELETE FROM organizations');
await dataSource.query('DELETE FROM users');
}
- Step 4: Refactor one existing spec to use helpers
// backend/test/auth/auth.controller.e2e-spec.ts
import { loginUser } from '@/../test/helpers/auth.helper';
- Step 5: Run focused regression tests
Run: cd backend && bun test -- test/auth/auth.controller.e2e-spec.ts test/telemetry/telemetry.controller.e2e-spec.ts
Expected: PASS.
- Step 6: Commit
git add backend/test/helpers/auth.helper.ts backend/test/helpers/tenant.helper.ts backend/test/helpers/request.helper.ts backend/test/helpers/factory.helper.ts backend/test/helpers/cleanup.helper.ts backend/test/auth/auth.controller.e2e-spec.ts
git commit -m "test(e2e): add shared helpers for auth tenant and cleanup"
Task 2: Phase 1 — Security & Isolation journeys (1-4)
Files:
Modify:
backend/test/auth/auth.controller.e2e-spec.tsModify:
backend/test/rbac/role.controller.e2e-spec.tsCreate:
backend/test/tenant/multi-tenant-isolation.e2e-spec.tsTest:
backend/test/auth/auth.controller.e2e-spec.ts,backend/test/rbac/role.controller.e2e-spec.ts,backend/test/tenant/multi-tenant-isolation.e2e-spec.tsStep 1: Write failing isolation and RBAC deny tests
// backend/test/tenant/multi-tenant-isolation.e2e-spec.ts
it('rejects org A reading org B pond', async () => {
await request(app.getHttpServer())
.get(`/api/v1/farm/ponds/${orgB.pondId}`)
.set('Authorization', `Bearer ${orgA.accessToken}`)
.expect(404);
});
// backend/test/rbac/role.controller.e2e-spec.ts
it('returns 403 when permission is missing', async () => {
await request(app.getHttpServer())
.post('/api/v1/roles')
.set('Authorization', `Bearer ${viewerToken}`)
.send({ name: 'blocked-role' })
.expect(403);
});
- Step 2: Run only new phase-1 tests and confirm failure
Run: cd backend && bun test -- test/tenant/multi-tenant-isolation.e2e-spec.ts test/rbac/role.controller.e2e-spec.ts --testNamePattern="rejects|returns 403"
Expected: FAIL before setup updates.
- Step 3: Implement tenant seed + role matrix fixtures
// backend/test/helpers/tenant.helper.ts
export async function seedTwoOrganizations(app: INestApplication): Promise<TenantSeedResult> {
// register/login org A and org B users, return tokens and ids
}
- Step 4: Add explicit error-contract assertions (401/403/404/422)
expect(response.body).toHaveProperty('statusCode', 403);
expect(response.body).toHaveProperty('message');
- Step 5: Run full phase-1 suite
Run: cd backend && bun test -- test/auth/auth.controller.e2e-spec.ts test/rbac/role.controller.e2e-spec.ts test/tenant/multi-tenant-isolation.e2e-spec.ts
Expected: PASS.
- Step 6: Commit
git add backend/test/auth/auth.controller.e2e-spec.ts backend/test/rbac/role.controller.e2e-spec.ts backend/test/tenant/multi-tenant-isolation.e2e-spec.ts backend/test/helpers/tenant.helper.ts
git commit -m "test(e2e): cover auth rbac isolation and error contracts"
Task 3: Phase 2A — Onboarding + Farm + Device lifecycle (journeys 5-7)
Files:
Create:
backend/test/farm/farm-management.e2e-spec.tsCreate:
backend/test/devices/device-lifecycle.e2e-spec.tsModify:
backend/test/helpers/factory.helper.tsTest:
backend/test/farm/farm-management.e2e-spec.ts,backend/test/devices/device-lifecycle.e2e-spec.tsStep 1: Write failing farm pagination/filter and device lifecycle tests
it('lists ponds scoped to organization with pagination', async () => {
const res = await request(app.getHttpServer())
.get('/api/v1/farm/ponds?page=1&limit=10&search=pond-a')
.set('Authorization', `Bearer ${orgA.accessToken}`)
.expect(200);
expect(res.body.data.every((p: { organizationId: string }) => p.organizationId === orgA.organizationId)).toBe(true);
});
it('deactivates device and blocks telemetry ingestion', async () => {
await request(app.getHttpServer())
.patch(`/api/v1/devices/${deviceId}/deactivate`)
.set('Authorization', `Bearer ${ownerToken}`)
.expect(200);
await request(app.getHttpServer())
.post('/api/v1/telemetry')
.set('Authorization', `Bearer ${ownerToken}`)
.send({ deviceId, data: { temperature: 28.1 } })
.expect(422);
});
- Step 2: Run new files to confirm red state
Run: cd backend && bun test -- test/farm/farm-management.e2e-spec.ts test/devices/device-lifecycle.e2e-spec.ts
Expected: FAIL.
- Step 3: Implement data factories for org/farm/device setup
export async function createAreaPondDeviceFixture(app: INestApplication, token: string): Promise<{ areaId: string; pondId: string; deviceId: string }> {
// create area -> pond -> device and return ids
}
- Step 4: Complete happy + negative path assertions
expect(res.body.total).toBeGreaterThan(0);
expect(forbidden.status).toBe(403);
- Step 5: Run phase-2A regression
Run: cd backend && bun test -- test/farm/farm-management.e2e-spec.ts test/devices/device-lifecycle.e2e-spec.ts
Expected: PASS.
- Step 6: Commit
git add backend/test/farm/farm-management.e2e-spec.ts backend/test/devices/device-lifecycle.e2e-spec.ts backend/test/helpers/factory.helper.ts
git commit -m "test(e2e): add onboarding farm and device lifecycle journeys"
Task 4: Phase 2B — Telemetry valid/invalid ingestion (journeys 8-9)
Files:
Modify:
backend/test/telemetry/telemetry.controller.e2e-spec.tsModify:
backend/test/helpers/factory.helper.tsTest:
backend/test/telemetry/telemetry.controller.e2e-spec.tsStep 1: Add failing invalid payload rejection tests
it('rejects telemetry payload with unknown parameter schema', async () => {
await request(app.getHttpServer())
.post('/api/v1/telemetry')
.set('Authorization', `Bearer ${accessToken}`)
.send({ deviceId, data: { temperature: 'bad' } })
.expect(400);
});
it('does not persist invalid payload', async () => {
const before = await dataSource.query('SELECT COUNT(*)::int AS count FROM telemetry');
await request(app.getHttpServer())
.post('/api/v1/telemetry')
.set('Authorization', `Bearer ${accessToken}`)
.send({ deviceId, data: {} })
.expect(400);
const after = await dataSource.query('SELECT COUNT(*)::int AS count FROM telemetry');
expect(after[0].count).toBe(before[0].count);
});
- Step 2: Run telemetry-only tests and verify failures
Run: cd backend && bun test -- test/telemetry/telemetry.controller.e2e-spec.ts --testNamePattern="rejects telemetry payload|does not persist invalid payload"
Expected: FAIL.
- Step 3: Tighten valid ingestion assertions
expect(response.body).toHaveProperty('message', 'Telemetry saved successfully');
const rows = await dataSource.query('SELECT device_id FROM telemetry WHERE device_id = $1', [deviceId]);
expect(rows.length).toBeGreaterThan(0);
- Step 4: Run telemetry suite
Run: cd backend && bun test -- test/telemetry/telemetry.controller.e2e-spec.ts
Expected: PASS.
- Step 5: Commit
git add backend/test/telemetry/telemetry.controller.e2e-spec.ts backend/test/helpers/factory.helper.ts
git commit -m "test(e2e): separate telemetry valid and invalid ingestion coverage"
Task 5: Phase 3 — Dashboard aggregation + automation trigger + smoke journey (10-12)
Files:
Create:
backend/test/dashboard/dashboard-aggregation.e2e-spec.tsModify:
backend/test/automation/automation-flow.e2e-spec.tsCreate:
backend/test/smoke/smoke.e2e-spec.tsTest:
backend/test/dashboard/dashboard-aggregation.e2e-spec.ts,backend/test/automation/automation-flow.e2e-spec.ts,backend/test/smoke/smoke.e2e-spec.tsStep 1: Add failing dashboard aggregation and automation trigger tests
it('returns aggregated dashboard metrics after telemetry ingestion', async () => {
await seedTelemetrySeries(app, token, deviceId);
const res = await request(app.getHttpServer())
.get('/api/v1/dashboard')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body).toHaveProperty('waterQuality');
});
it('triggers automation action when threshold exceeded', async () => {
await postTelemetry(app, token, deviceId, { temperature: 35.2 });
const execution = await request(app.getHttpServer())
.get(`/api/v1/automation/rules/${ruleId}/executions`)
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(execution.body.data.length).toBeGreaterThan(0);
});
- Step 2: Run new dashboard/automation tests for red state
Run: cd backend && bun test -- test/dashboard/dashboard-aggregation.e2e-spec.ts test/automation/automation-flow.e2e-spec.ts
Expected: FAIL.
- Step 3: Build smoke suite with fastest happy paths
// backend/test/smoke/smoke.e2e-spec.ts
describe('Smoke critical journeys', () => {
it('auth register/login/me', async () => {/* ... */});
it('rbac deny path', async () => {/* ... */});
it('tenant isolation read deny', async () => {/* ... */});
it('telemetry ingest latest', async () => {/* ... */});
});
- Step 4: Run smoke suite and capture runtime
Run: cd backend && time bun test -- test/smoke/smoke.e2e-spec.ts
Expected: PASS and runtime <= 7 minutes.
- Step 5: Run full E2E regression
Run: cd backend && bun run test:e2e
Expected: PASS.
- Step 6: Commit
git add backend/test/dashboard/dashboard-aggregation.e2e-spec.ts backend/test/automation/automation-flow.e2e-spec.ts backend/test/smoke/smoke.e2e-spec.ts
git commit -m "test(e2e): add dashboard automation and smoke critical journeys"
Task 6: CI wiring + reports + runbook completion criteria
Files:
Modify:
backend/package.jsonModify:
.github/workflows/<backend-ci-workflow>.ymlCreate/Modify:
backend/test/README.md(only if requested by reviewer policy)Step 1: Add smoke/full scripts in backend package
{
"scripts": {
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:e2e:smoke": "jest --config ./test/jest-e2e.json --runTestsByPath test/smoke/smoke.e2e-spec.ts",
"test:e2e:full": "jest --config ./test/jest-e2e.json"
}
}
- Step 2: Add failing CI check expectation first (if pipeline test harness exists)
Run: gh workflow run <workflow-name> --ref <branch> (or local workflow simulation command used in repo)
Expected: smoke job missing / failing before workflow update.
- Step 3: Wire CI jobs and junit artifact
- name: Run smoke e2e
run: cd backend && bun run test:e2e:smoke -- --reporters=default --reporters=jest-junit
- name: Upload junit
uses: actions/upload-artifact@v4
with:
name: backend-e2e-junit
path: backend/junit.xml
- Step 4: Run lint + format after edits
Run: cd backend && bun run lint:fix && bun run format
Expected: PASS.
- Step 5: Verify DoD execution stability
Run:
cd backend && bun run test:e2e:smokecd backend && bun run test:e2e:smokecd backend && bun run test:e2e:smokecd backend && bun run test:e2e:full
Expected: smoke passes 3 consecutive runs; full passes once within CI budget target.
- Step 6: Commit
git add backend/package.json .github/workflows/<backend-ci-workflow>.yml
git commit -m "ci(test): split e2e smoke and full with junit artifact"
Spec Coverage Checklist
- Journey 1 (auth): Task 2
- Journey 2 (RBAC): Task 2
- Journey 3 (multi-tenant isolation): Task 2
- Journey 4 (401/403/404/422 contract): Task 2
- Journey 5 (org onboarding): Task 3
- Journey 6 (farm management): Task 3
- Journey 7 (device lifecycle): Task 3
- Journey 8 (telemetry valid ingestion): Task 4
- Journey 9 (telemetry invalid rejection/no persistence): Task 4
- Journey 10 (dashboard aggregation): Task 5
- Journey 11 (automation trigger flow): Task 5
- Journey 12 (release smoke journey): Task 5
- CI split + artifacts + stability evidence: Task 6
Self-review (placeholder/type consistency)
- No TBD/TODO placeholders.
- Every task includes concrete files, tests, commands, and expected outcomes.
- Command set aligned with existing backend scripts (
test:e2e) and added smoke/full scripts in Task 6.