test: run launch persona smoke with app session

This commit is contained in:
Codex
2026-07-01 08:28:17 +08:00
parent 9d1bb47041
commit cf1ae6026b
6 changed files with 101 additions and 11 deletions

View File

@@ -1,4 +1,5 @@
import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import net from 'node:net';
@@ -10,6 +11,8 @@ const databaseUrl = process.env.DATABASE_URL || DEFAULT_DATABASE_URL;
const outputDir = process.env.LAUNCH_SMOKE_OUTPUT_DIR || 'docs/refactor/launch-artifacts';
const startServer = boolEnv('LAUNCH_SMOKE_START_SERVER', !process.env.LAUNCH_SMOKE_API_BASE);
const fixedPort = Number(process.env.LAUNCH_SMOKE_API_PORT || 0) || 0;
const authMode = normalizeAuthMode(process.env.LAUNCH_SMOKE_AUTH_MODE || 'app_session');
const authSessionSecret = process.env.AUTH_SESSION_SECRET || 'development-session-secret-change-me';
const ids = {
tenant: '00000000-0000-0000-0000-000000000001',
@@ -24,6 +27,7 @@ const ids = {
let apiBase = process.env.LAUNCH_SMOKE_API_BASE || process.env.API_BASE || '';
let serverProcess = null;
let serverLogs = '';
let authTokens = {};
function boolEnv(key, fallback) {
const value = process.env[key];
@@ -31,6 +35,13 @@ function boolEnv(key, fallback) {
return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
}
function normalizeAuthMode(value) {
const normalized = String(value || '').toLowerCase().replace(/[-_]/g, '');
if (normalized === 'legacy' || normalized === 'headers') return 'legacy';
if (normalized === 'appsession' || normalized === 'session' || normalized === 'tk') return 'app_session';
return 'app_session';
}
function nowIso() {
return new Date().toISOString();
}
@@ -92,6 +103,9 @@ async function startServerIfNeeded() {
...process.env,
PORT: String(port),
DATABASE_URL: databaseUrl,
AUTH_SESSION_SECRET: authSessionSecret,
ALLOW_LEGACY_AUTH_HEADERS: authMode === 'legacy' ? 'true' : 'false',
ALLOW_PLATFORM_ADMIN_KEY: authMode === 'legacy' ? 'true' : 'false',
MAX_JSON_BODY_BYTES: process.env.MAX_JSON_BODY_BYTES || '1048576',
MAX_IMPORT_JSON_BODY_BYTES: process.env.MAX_IMPORT_JSON_BODY_BYTES || '10485760',
},
@@ -120,12 +134,15 @@ function buildUrl(pathname, query = {}) {
}
async function request(pathname, options = {}) {
const token = authTokenFor(options.persona || 'student');
const useSession = authMode === 'app_session' && token;
const response = await fetch(buildUrl(pathname, options.query), {
method: options.method || 'GET',
headers: {
'content-type': 'application/json',
...(options.tenantId === false ? {} : { 'x-tenant-id': options.tenantId || ids.tenant }),
...(options.userId === false ? {} : { 'x-user-id': options.userId || ids.student }),
...(useSession ? { authorization: `Bearer ${token}` } : {}),
...(authMode === 'legacy' && options.userId !== false ? { 'x-user-id': options.userId || ids.student } : {}),
...(options.headers || {}),
},
body: options.body ? JSON.stringify(options.body) : undefined,
@@ -136,6 +153,55 @@ async function request(pathname, options = {}) {
return payload;
}
function createSessionToken() {
return `tk_${crypto.randomBytes(32).toString('base64url')}`;
}
function hashSessionToken(token) {
return crypto.createHmac('sha256', authSessionSecret).update(token).digest('hex');
}
function authTokenFor(persona) {
if (authMode !== 'app_session') return '';
return authTokens[persona] || authTokens.student || '';
}
async function createSmokeAuthSession(pool, persona, userId, tenantId = ids.tenant) {
if (authMode !== 'app_session') return '';
const token = createSessionToken();
await pool.query(
`
insert into app_private.auth_sessions (
tenant_id, user_id, token_hash, provider, expires_at, metadata
)
values (
$1, $2, $3, 'launch_persona_smoke',
now() + interval '2 hours',
$4::jsonb
)
`,
[
tenantId,
userId,
hashSessionToken(token),
JSON.stringify({ createdBy: 'launch-persona-smoke', persona, authMode }),
],
);
return token;
}
async function createSmokeAuthSessions(pool) {
if (authMode !== 'app_session') {
authTokens = {};
return;
}
authTokens = {
student: await createSmokeAuthSession(pool, 'student', ids.student, ids.tenant),
tenantAdmin: await createSmokeAuthSession(pool, 'tenantAdmin', ids.tenantAdmin, ids.tenant),
platformAdmin: await createSmokeAuthSession(pool, 'platformAdmin', ids.platformAdmin, ids.tenant),
};
}
async function one(pool, sql, params = []) {
const result = await pool.query(sql, params);
return result.rows[0] || null;
@@ -316,22 +382,26 @@ async function studentJourney(pool) {
}
async function tenantAdminJourney() {
const overview = await request('/api/tenant-admin/overview', { userId: ids.tenantAdmin });
const overview = await request('/api/tenant-admin/overview', { persona: 'tenantAdmin', userId: ids.tenantAdmin });
const dashboard = await request('/api/tenant-admin/dashboard', {
persona: 'tenantAdmin',
userId: ids.tenantAdmin,
query: { timeRange: '7d', regionId: ids.region },
});
const theme = await request('/api/tenant-admin/theme', { userId: ids.tenantAdmin });
const theme = await request('/api/tenant-admin/theme', { persona: 'tenantAdmin', userId: ids.tenantAdmin });
const students = await request('/api/tenant-admin/students', {
persona: 'tenantAdmin',
userId: ids.tenantAdmin,
query: { limit: 10 },
});
const conversion = await request('/api/referral/conversion-report', {
persona: 'tenantAdmin',
userId: ids.tenantAdmin,
query: { limit: 10 },
});
const studentDenied = await request('/api/tenant-admin/dashboard', { expectStatus: 403 });
const crossTenantDenied = await request('/api/tenant-admin/dashboard', {
persona: 'tenantAdmin',
tenantId: ids.partnerTenant,
userId: ids.tenantAdmin,
expectStatus: 403,
@@ -360,24 +430,28 @@ async function tenantAdminJourney() {
}
async function platformAdminJourney() {
const headers = { 'x-platform-admin-key': process.env.PLATFORM_ADMIN_API_KEY || 'local-platform-admin-key' };
const headers = authMode === 'legacy' ? { 'x-platform-admin-key': process.env.PLATFORM_ADMIN_API_KEY || 'local-platform-admin-key' } : {};
const overview = await request('/api/platform-admin/overview', {
persona: 'platformAdmin',
tenantId: false,
userId: false,
headers,
});
const tenants = await request('/api/platform-admin/tenants', {
persona: 'platformAdmin',
tenantId: false,
userId: false,
headers,
query: { limit: 10 },
});
const plans = await request('/api/platform-admin/plans', {
persona: 'platformAdmin',
tenantId: false,
userId: false,
headers,
});
const auditAlerts = await request('/api/platform-admin/audit-alerts', {
persona: 'platformAdmin',
tenantId: false,
userId: false,
headers,
@@ -450,12 +524,14 @@ async function runStep(name, fn) {
async function main() {
const pool = new pg.Pool({ connectionString: databaseUrl, max: 4 });
try {
await createSmokeAuthSessions(pool);
await startServerIfNeeded();
if (!apiBase) throw new Error('LAUNCH_SMOKE_API_BASE is required when LAUNCH_SMOKE_START_SERVER=false.');
const report = {
startedAt: nowIso(),
finishedAt: '',
apiBase,
authMode,
databaseUrl: databaseUrl.replace(/:\/\/([^:]+):([^@]+)@/, '://$1:[redacted]@'),
student: await runStep('student', () => studentJourney(pool)),
tenantAdmin: await runStep('tenant-admin', tenantAdminJourney),