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.json
  • backend/src/common/interceptors/organization-filter.interceptor.ts
  • backend/src/rbac/authorization-policy.service.ts

Task 1: Build shared E2E foundation (Day 1)

Files:

  • Create: backend/test/helpers/auth.helper.ts

  • Create: backend/test/helpers/tenant.helper.ts

  • Create: backend/test/helpers/request.helper.ts

  • Create: backend/test/helpers/factory.helper.ts

  • Create: backend/test/helpers/cleanup.helper.ts

  • Test: backend/test/auth/auth.controller.e2e-spec.ts

  • Step 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.ts

  • Modify: backend/test/rbac/role.controller.e2e-spec.ts

  • Create: backend/test/tenant/multi-tenant-isolation.e2e-spec.ts

  • Test: 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

  • Step 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.ts

  • Create: backend/test/devices/device-lifecycle.e2e-spec.ts

  • Modify: backend/test/helpers/factory.helper.ts

  • Test: backend/test/farm/farm-management.e2e-spec.ts, backend/test/devices/device-lifecycle.e2e-spec.ts

  • Step 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.ts

  • Modify: backend/test/helpers/factory.helper.ts

  • Test: backend/test/telemetry/telemetry.controller.e2e-spec.ts

  • Step 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.ts

  • Modify: backend/test/automation/automation-flow.e2e-spec.ts

  • Create: backend/test/smoke/smoke.e2e-spec.ts

  • Test: backend/test/dashboard/dashboard-aggregation.e2e-spec.ts, backend/test/automation/automation-flow.e2e-spec.ts, backend/test/smoke/smoke.e2e-spec.ts

  • Step 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.json

  • Modify: .github/workflows/<backend-ci-workflow>.yml

  • Create/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:smoke
  • cd backend && bun run test:e2e:smoke
  • cd backend && bun run test:e2e:smoke
  • cd 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.