Files
gongxue-base/scripts/remote-auth-jwt-smoke.js
2026-06-30 02:10:34 +08:00

287 lines
10 KiB
JavaScript

import { fileURLToPath, pathToFileURL } from 'node:url';
const DEFAULT_TIMEOUT_MS = 10_000;
function envString(env, key, fallback = '') {
return typeof env[key] === 'string' && env[key].trim() ? env[key].trim() : fallback;
}
function envBoolean(env, key, fallback = false) {
const value = envString(env, key).toLowerCase();
if (!value) return fallback;
return ['1', 'true', 'yes', 'on'].includes(value);
}
function envNumber(env, key, fallback) {
const value = Number(envString(env, key));
return Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback;
}
function normalizeBaseUrl(value) {
return value.replace(/\/+$/, '');
}
function redactToken(value) {
if (!value) return '<empty>';
if (value.length <= 16) return '<redacted>';
return `${value.slice(0, 6)}...${value.slice(-6)}`;
}
function buildConfig(env = process.env) {
const apiBaseUrl = envString(env, 'AUTH_SMOKE_API_BASE_URL', envString(env, 'API_BASE', ''));
const tenantId = envString(env, 'AUTH_SMOKE_TENANT_ID', envString(env, 'TENANT_ID', ''));
const studentToken = envString(env, 'AUTH_SMOKE_STUDENT_ACCESS_TOKEN', envString(env, 'SUPABASE_ACCESS_TOKEN', ''));
const tenantAdminToken = envString(env, 'AUTH_SMOKE_TENANT_ADMIN_ACCESS_TOKEN');
const platformAdminToken = envString(env, 'AUTH_SMOKE_PLATFORM_ADMIN_ACCESS_TOKEN');
const requireAdminTokens = envBoolean(env, 'AUTH_SMOKE_REQUIRE_ADMIN_TOKENS', false);
const missing = [];
if (!apiBaseUrl) missing.push('AUTH_SMOKE_API_BASE_URL');
if (!tenantId) missing.push('AUTH_SMOKE_TENANT_ID');
if (!studentToken) missing.push('AUTH_SMOKE_STUDENT_ACCESS_TOKEN');
if (requireAdminTokens && !tenantAdminToken) missing.push('AUTH_SMOKE_TENANT_ADMIN_ACCESS_TOKEN');
if (requireAdminTokens && !platformAdminToken) missing.push('AUTH_SMOKE_PLATFORM_ADMIN_ACCESS_TOKEN');
if (missing.length > 0) {
throw new Error(`Missing required remote auth smoke env: ${missing.join(', ')}`);
}
return {
apiBaseUrl: normalizeBaseUrl(apiBaseUrl),
tenantId,
wrongTenantId: envString(env, 'AUTH_SMOKE_WRONG_TENANT_ID'),
timeoutMs: envNumber(env, 'AUTH_SMOKE_TIMEOUT_MS', DEFAULT_TIMEOUT_MS),
requireAdminTokens,
student: {
token: studentToken,
expectedUserId: envString(env, 'AUTH_SMOKE_EXPECTED_STUDENT_USER_ID'),
},
tenantAdmin: tenantAdminToken
? {
token: tenantAdminToken,
expectedUserId: envString(env, 'AUTH_SMOKE_EXPECTED_TENANT_ADMIN_USER_ID'),
}
: null,
platformAdmin: platformAdminToken
? {
token: platformAdminToken,
expectedUserId: envString(env, 'AUTH_SMOKE_EXPECTED_PLATFORM_ADMIN_USER_ID'),
tenantId: envString(env, 'AUTH_SMOKE_PLATFORM_TENANT_ID', tenantId),
}
: null,
};
}
function authHeaders(token, tenantId) {
return {
authorization: `Bearer ${token}`,
...(tenantId ? { 'x-tenant-id': tenantId } : {}),
};
}
async function requestJson(config, path, { token, tenantId = config.tenantId, method = 'GET' } = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
const url = new URL(path, config.apiBaseUrl);
try {
const response = await fetch(url, {
method,
headers: {
accept: 'application/json',
...(token ? authHeaders(token, tenantId) : {}),
},
signal: controller.signal,
});
const text = await response.text();
let payload = {};
if (text.trim()) {
try {
payload = JSON.parse(text);
} catch {
payload = { raw: text.slice(0, 500) };
}
}
return { status: response.status, ok: response.ok, payload };
} finally {
clearTimeout(timeout);
}
}
function resultCollector({ quiet = false } = {}) {
const results = [];
function pass(name, detail = {}) {
results.push({ status: 'pass', name, detail });
if (!quiet) console.log(`PASS ${name}`);
}
function fail(name, message, detail = {}) {
results.push({ status: 'fail', name, message, detail });
if (!quiet) {
console.error(`FAIL ${name}: ${message}`);
if (Object.keys(detail).length > 0) console.error(JSON.stringify(detail, null, 2));
}
}
return { results, pass, fail };
}
function expectStatus(collector, name, response, expectedStatuses) {
const expected = Array.isArray(expectedStatuses) ? expectedStatuses : [expectedStatuses];
if (expected.includes(response.status)) {
collector.pass(name, { status: response.status });
return true;
}
collector.fail(name, `Expected HTTP ${expected.join('/')} but got ${response.status}`, { payload: response.payload });
return false;
}
function expectSupabaseJwtSession(collector, name, response, expectedUserId = '') {
if (!expectStatus(collector, `${name}.status`, response, 200)) return;
const source = response.payload?.session?.source;
if (source === 'supabase_jwt') collector.pass(`${name}.session_source`);
else collector.fail(`${name}.session_source`, 'Expected session.source=supabase_jwt', { source });
if (expectedUserId) {
const actualUserId = response.payload?.user?.id || response.payload?.item?.userId;
if (actualUserId === expectedUserId) collector.pass(`${name}.expected_user`);
else collector.fail(`${name}.expected_user`, 'Authenticated user does not match expected id', { actualUserId, expectedUserId });
}
}
async function runRemoteAuthSmoke(inputConfig, options = {}) {
const config = inputConfig?.apiBaseUrl ? inputConfig : buildConfig(options.env || process.env);
const collector = resultCollector({ quiet: options.quiet });
if (!options.quiet) {
console.log(`Remote auth smoke target: ${config.apiBaseUrl}`);
console.log(`Student token: ${redactToken(config.student.token)}`);
if (config.tenantAdmin) console.log(`Tenant admin token: ${redactToken(config.tenantAdmin.token)}`);
if (config.platformAdmin) console.log(`Platform admin token: ${redactToken(config.platformAdmin.token)}`);
}
const studentMe = await requestJson(config, '/api/auth/me', {
token: config.student.token,
tenantId: config.tenantId,
});
expectSupabaseJwtSession(collector, 'student.auth_me', studentMe, config.student.expectedUserId);
const studentProfile = await requestJson(config, '/api/profile/me', {
token: config.student.token,
tenantId: config.tenantId,
});
expectStatus(collector, 'student.profile_me', studentProfile, 200);
if (config.student.expectedUserId && studentProfile.payload?.item?.userId !== config.student.expectedUserId) {
collector.fail('student.profile_expected_user', 'Profile user does not match expected student id', {
actualUserId: studentProfile.payload?.item?.userId,
expectedUserId: config.student.expectedUserId,
});
} else {
collector.pass('student.profile_expected_user');
}
const studentTenantAdmin = await requestJson(config, '/api/tenant-admin/overview', {
token: config.student.token,
tenantId: config.tenantId,
});
expectStatus(collector, 'student.tenant_admin_denied', studentTenantAdmin, 403);
const studentPlatformAdmin = await requestJson(config, '/api/platform-admin/overview', {
token: config.student.token,
tenantId: '',
});
expectStatus(collector, 'student.platform_admin_denied', studentPlatformAdmin, 403);
const badTokenProfile = await requestJson(config, '/api/profile/me', {
token: 'invalid.jwt.token',
tenantId: config.tenantId,
});
expectStatus(collector, 'invalid_token.profile_denied', badTokenProfile, 401);
if (config.wrongTenantId) {
const wrongTenantProfile = await requestJson(config, '/api/profile/me', {
token: config.student.token,
tenantId: config.wrongTenantId,
});
expectStatus(collector, 'student.wrong_tenant_denied', wrongTenantProfile, [401, 403, 404]);
}
if (config.tenantAdmin) {
const tenantAdminMe = await requestJson(config, '/api/auth/me', {
token: config.tenantAdmin.token,
tenantId: config.tenantId,
});
expectSupabaseJwtSession(collector, 'tenant_admin.auth_me', tenantAdminMe, config.tenantAdmin.expectedUserId);
const tenantOverview = await requestJson(config, '/api/tenant-admin/overview', {
token: config.tenantAdmin.token,
tenantId: config.tenantId,
});
expectStatus(collector, 'tenant_admin.overview', tenantOverview, 200);
const tenantPlatformAdmin = await requestJson(config, '/api/platform-admin/overview', {
token: config.tenantAdmin.token,
tenantId: '',
});
expectStatus(collector, 'tenant_admin.platform_admin_denied', tenantPlatformAdmin, 403);
} else {
collector.pass('tenant_admin.optional_skipped');
}
if (config.platformAdmin) {
const platformOverview = await requestJson(config, '/api/platform-admin/overview', {
token: config.platformAdmin.token,
tenantId: '',
});
expectStatus(collector, 'platform_admin.overview', platformOverview, 200);
if (config.platformAdmin.tenantId) {
const platformMe = await requestJson(config, '/api/auth/me', {
token: config.platformAdmin.token,
tenantId: config.platformAdmin.tenantId,
});
expectSupabaseJwtSession(collector, 'platform_admin.auth_me', platformMe, config.platformAdmin.expectedUserId);
}
} else {
collector.pass('platform_admin.optional_skipped');
}
const failed = collector.results.filter(item => item.status === 'fail');
if (!options.quiet) {
console.log(`\nRemote Auth/JWKS smoke checks: ${collector.results.length - failed.length} passed, ${failed.length} failed.`);
}
if (failed.length > 0) {
const error = new Error(`Remote Auth/JWKS smoke failed with ${failed.length} failing checks`);
error.results = collector.results;
throw error;
}
return collector.results;
}
async function main() {
try {
await runRemoteAuthSmoke();
} catch (error) {
console.error(error.message);
if (/Missing required remote auth smoke env/.test(error.message)) {
console.error(`
Required example:
AUTH_SMOKE_API_BASE_URL=https://api.example.com
AUTH_SMOKE_TENANT_ID=<tenant-uuid>
AUTH_SMOKE_STUDENT_ACCESS_TOKEN=<real-supabase-access-token>
Recommended full pre-production example:
AUTH_SMOKE_TENANT_ADMIN_ACCESS_TOKEN=<tenant-admin-supabase-access-token>
AUTH_SMOKE_PLATFORM_ADMIN_ACCESS_TOKEN=<platform-admin-supabase-access-token>
AUTH_SMOKE_WRONG_TENANT_ID=<another-tenant-uuid>
AUTH_SMOKE_REQUIRE_ADMIN_TOKENS=true
`);
}
process.exitCode = 1;
}
}
const currentFile = fileURLToPath(import.meta.url);
if (process.argv[1] && fileURLToPath(pathToFileURL(process.argv[1])) === currentFile) {
await main();
}
export { buildConfig, runRemoteAuthSmoke };