diff --git a/Backend/docs/release-checklist.md b/Backend/docs/release-checklist.md new file mode 100644 index 0000000..e53f070 --- /dev/null +++ b/Backend/docs/release-checklist.md @@ -0,0 +1,30 @@ +# Release Checklist (Phase 10) + +## Functional verification +- [ ] Device registration and heartbeat flow works for both camera and client roles. +- [ ] Camera-client linking and command dispatch/ack flows work end-to-end. +- [ ] Motion start/end events are persisted and notify linked clients. +- [ ] On-demand stream request/accept/end flow works with media credentials. +- [ ] Recording placeholder creation, finalize, and download URL generation work. +- [ ] Push fallback queue delivers notifications when realtime channel is offline. + +## Security and reliability +- [ ] Rate limit thresholds validated in staging. +- [ ] Audit logs capture stream/event/recording sensitive operations. +- [ ] Env vars present: auth base URL, media provider, database, MinIO, secrets. +- [ ] Worker loops running: recordings worker and push worker. + +## Observability +- [ ] `/ops/live` and `/ops/ready` healthy. +- [ ] `/ops/metrics` reports request counters and status counters. +- [ ] Structured request logs are visible in deployment log sink. + +## Pre-release load and tests +- [ ] Unit tests pass (`bun test`). +- [ ] Smoke load script passes (`bun run load:smoke`). +- [ ] Manual run with simulator in two tabs confirms camera+client happy path. + +## Rollout +- [ ] Deploy to staging and run checklist above. +- [ ] Rollout production gradually and watch error/status metrics. +- [ ] Keep rollback procedure ready (previous image/version + migration compatibility). diff --git a/Backend/package.json b/Backend/package.json index 541ec08..1928bb5 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -39,6 +39,8 @@ "scripts": { "start": "bun run index.ts", "dev": "bun --watch index.ts", + "test": "bun test", + "load:smoke": "bun run scripts/load-smoke.ts", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:push": "drizzle-kit push", diff --git a/Backend/public/mobile-sim.html b/Backend/public/mobile-sim.html index ee43891..7f47e86 100644 --- a/Backend/public/mobile-sim.html +++ b/Backend/public/mobile-sim.html @@ -118,9 +118,19 @@

Mobile Client Simulator

- Use this page like a phone app: register device role, connect socket with bearer device token, and run camera/client - actions. You must already be signed in via Better Auth in this browser session. + Use this page like a phone app: authenticate, register device role, connect socket with bearer device token, and run + camera/client actions.

+
+

App-Like Onboarding Flow

+
    +
  1. Sign up/sign in in the Auth panel.
  2. +
  3. Pick role (camera or client) and register device.
  4. +
  5. Connect socket to become online.
  6. +
  7. Client links to camera, requests stream; camera accepts automatically via command handling.
  8. +
  9. Camera finalizes recording; client fetches download URL and polls push inbox when offline.
  10. +
+
diff --git a/Backend/scripts/load-smoke.ts b/Backend/scripts/load-smoke.ts new file mode 100644 index 0000000..14eb0e8 --- /dev/null +++ b/Backend/scripts/load-smoke.ts @@ -0,0 +1,26 @@ +const baseUrl = process.env.LOAD_BASE_URL ?? 'http://localhost:3000'; +const rounds = Number(process.env.LOAD_ROUNDS ?? 25); + +const run = async () => { + const start = Date.now(); + + for (let i = 0; i < rounds; i += 1) { + const [live, ready, metrics] = await Promise.all([ + fetch(`${baseUrl}/ops/live`), + fetch(`${baseUrl}/ops/ready`), + fetch(`${baseUrl}/ops/metrics`), + ]); + + if (!live.ok || !ready.ok || !metrics.ok) { + throw new Error(`Smoke load failed at round ${i + 1}`); + } + } + + const totalMs = Date.now() - start; + console.log(`Completed ${rounds} rounds in ${totalMs}ms`); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/Backend/tests/device-token.test.ts b/Backend/tests/device-token.test.ts new file mode 100644 index 0000000..141aa69 --- /dev/null +++ b/Backend/tests/device-token.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from 'bun:test'; + +import { createDeviceToken, verifyDeviceToken } from '../utils/device-token'; + +describe('device token', () => { + test('roundtrips valid payload', () => { + const token = createDeviceToken({ + userId: 'user-1', + deviceId: 'device-1', + role: 'client', + }, 60); + + const payload = verifyDeviceToken(token); + + expect(payload).not.toBeNull(); + expect(payload?.userId).toBe('user-1'); + expect(payload?.deviceId).toBe('device-1'); + expect(payload?.role).toBe('client'); + }); + + test('rejects malformed tokens', () => { + expect(verifyDeviceToken('bad-token')).toBeNull(); + }); +}); diff --git a/Backend/tests/env.test.ts b/Backend/tests/env.test.ts new file mode 100644 index 0000000..a9f6fd7 --- /dev/null +++ b/Backend/tests/env.test.ts @@ -0,0 +1,25 @@ +import { afterEach, describe, expect, test } from 'bun:test'; + +import { getBetterAuthBaseUrl, getFirstDefinedEnv } from '../utils/env'; + +const ORIGINAL_ENV = { ...process.env }; + +afterEach(() => { + process.env = { ...ORIGINAL_ENV }; +}); + +describe('env helpers', () => { + test('getFirstDefinedEnv returns first set value', () => { + process.env.TEST_A = ''; + process.env.TEST_B = ' value-b '; + + expect(getFirstDefinedEnv('TEST_A', 'TEST_B')).toBe('value-b'); + }); + + test('getBetterAuthBaseUrl prefers BETTER_AUTH_BASE_URL over legacy var', () => { + process.env.BETTER_AUTH_BASE_URL = 'http://base-url:4000'; + process.env.BETTER_AUTH_URL = 'http://legacy:3000'; + + expect(getBetterAuthBaseUrl()).toBe('http://base-url:4000'); + }); +});