feat(release): add phase10 tests, smoke load script, release checklist, and onboarding simulator flow
This commit is contained in:
30
Backend/docs/release-checklist.md
Normal file
30
Backend/docs/release-checklist.md
Normal file
@@ -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).
|
||||||
@@ -39,6 +39,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run index.ts",
|
"start": "bun run index.ts",
|
||||||
"dev": "bun --watch index.ts",
|
"dev": "bun --watch index.ts",
|
||||||
|
"test": "bun test",
|
||||||
|
"load:smoke": "bun run scripts/load-smoke.ts",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
|
|||||||
@@ -118,9 +118,19 @@
|
|||||||
<div class="page">
|
<div class="page">
|
||||||
<h1>Mobile Client Simulator</h1>
|
<h1>Mobile Client Simulator</h1>
|
||||||
<p>
|
<p>
|
||||||
Use this page like a phone app: register device role, connect socket with bearer device token, and run camera/client
|
Use this page like a phone app: authenticate, register device role, connect socket with bearer device token, and run
|
||||||
actions. You must already be signed in via Better Auth in this browser session.
|
camera/client actions.
|
||||||
</p>
|
</p>
|
||||||
|
<section class="panel" style="margin-bottom:16px">
|
||||||
|
<h2>App-Like Onboarding Flow</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Sign up/sign in in the <strong>Auth</strong> panel.</li>
|
||||||
|
<li>Pick role (<code>camera</code> or <code>client</code>) and register device.</li>
|
||||||
|
<li>Connect socket to become online.</li>
|
||||||
|
<li>Client links to camera, requests stream; camera accepts automatically via command handling.</li>
|
||||||
|
<li>Camera finalizes recording; client fetches download URL and polls push inbox when offline.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
|
|||||||
26
Backend/scripts/load-smoke.ts
Normal file
26
Backend/scripts/load-smoke.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
24
Backend/tests/device-token.test.ts
Normal file
24
Backend/tests/device-token.test.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
25
Backend/tests/env.test.ts
Normal file
25
Backend/tests/env.test.ts
Normal file
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user