| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- import fs from 'node:fs';
- import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- const FN_URL = process.env.SMOKE_FN_URL || 'https://server.fmode.cn/api/functions';
- const APP_ID = process.env.SMOKE_PARSE_APP_ID || 'ncloudmaster';
- const TOKEN_A = process.env.STORAGE_GOVERNANCE_SESSION_A || process.env.SMOKE_SESSION_TOKEN_A || '';
- const TOKEN_B = process.env.STORAGE_GOVERNANCE_SESSION_B || process.env.SMOKE_SESSION_TOKEN_B || '';
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
- const rootDir = path.resolve(__dirname, '..', '..');
- const cloudFunctionsPath = path.join(rootDir, 'src', 'app', 'services', 'cloud-functions.ts');
- const fn = readCloudFunctionIds(cloudFunctionsPath);
- const probeType = 'governanceProbe';
- const probeId = `postdeploy-${Date.now()}`;
- const errors = [];
- let probeQiniuKey = '';
- let probeAssetId = '';
- if (!TOKEN_A || !TOKEN_B) {
- fail('Missing STORAGE_GOVERNANCE_SESSION_A and STORAGE_GOVERNANCE_SESSION_B. Provide two different test user Parse session tokens.');
- }
- for (const key of ['systemStorage', 'upload', 'authCredit', 'fileAsset']) {
- if (!fn[key]) fail(`CLOUD_FN.${key} is empty. Deploy and fill src/app/services/cloud-functions.ts before running postdeploy validation.`);
- }
- if (errors.length) exitWithErrors();
- await run('authCredit.balance user A', async () => {
- const result = await call(fn.authCredit, { action: 'balance' }, TOKEN_A);
- assertOk(result, 'authCredit.balance should succeed for user A');
- assert(Number.isFinite(Number(result?.data?.balance)), 'authCredit.balance must return numeric balance');
- });
- await run('systemStorage user A write/read/delete', async () => {
- const upsert = await call(fn.systemStorage, {
- action: 'upsert',
- entityType: probeType,
- entityId: probeId,
- data: { marker: probeId, owner: 'A', createdBy: 'storage-governance-postdeploy' },
- status: 'active',
- }, TOKEN_A);
- assertOk(upsert, 'systemStorage.upsert should succeed for user A');
- const get = await call(fn.systemStorage, { action: 'get', entityType: probeType, entityId: probeId }, TOKEN_A);
- assertOk(get, 'systemStorage.get should return user A probe');
- assert(get?.data?.data?.marker === probeId || get?.data?.marker === probeId, 'systemStorage.get should return the probe payload');
- });
- await run('systemStorage cross-account isolation', async () => {
- const result = await call(fn.systemStorage, { action: 'get', entityType: probeType, entityId: probeId }, TOKEN_B);
- const data = result?.data;
- const hasAProbe = data?.data?.marker === probeId || data?.marker === probeId;
- assert(!hasAProbe, 'user B must not read user A governance probe');
- });
- await run('upload token user partition', async () => {
- const result = await call(fn.upload, {
- action: 'createUploadToken',
- filename: 'postdeploy-probe.txt',
- mimeType: 'text/plain',
- kind: 'probe',
- bizId: probeId,
- }, TOKEN_A);
- assertOk(result, 'upload token should succeed for user A');
- const key = String(result?.data?.key || '');
- assert(key.startsWith('users/'), `upload key must start with users/, got: ${key || '(empty)'}`);
- probeQiniuKey = key;
- });
- await run('fileAsset register/read/delete user A', async () => {
- assert(probeQiniuKey, 'upload token key is required before registering file asset metadata');
- const register = await call(fn.fileAsset, {
- action: 'register',
- qiniuKey: probeQiniuKey,
- url: '',
- bucket: 'postdeploy-probe',
- mimeType: 'text/plain',
- kind: 'export',
- sizeBytes: 0,
- sourceModule: 'storage-governance-postdeploy',
- bizType: probeType,
- bizId: probeId,
- metadata: { marker: probeId },
- }, TOKEN_A);
- assertOk(register, 'fileAsset.register should create VideoWorkflowFileAsset metadata');
- probeAssetId = String(register?.data?.assetId || register?.data?.id || '');
- assert(probeAssetId, `fileAsset.register must return assetId: ${JSON.stringify(register).slice(0, 500)}`);
- assert(register?.data?.qiniuKey === probeQiniuKey, 'fileAsset.register must preserve qiniuKey');
- const get = await call(fn.fileAsset, { action: 'get', assetId: probeAssetId }, TOKEN_A);
- assertOk(get, 'fileAsset.get should return registered metadata');
- assert(get?.data?.qiniuKey === probeQiniuKey, 'fileAsset.get must return the registered qiniuKey');
- const deleted = await call(fn.fileAsset, { action: 'delete', assetId: probeAssetId }, TOKEN_A);
- assertOk(deleted, 'fileAsset.delete should soft-delete temporary metadata');
- });
- await run('fileAsset stats user A', async () => {
- const result = await call(fn.fileAsset, { action: 'stats' }, TOKEN_A);
- assertOk(result, 'fileAsset.stats should succeed for user A');
- assert(Number.isFinite(Number(result?.data?.total ?? 0)), 'fileAsset.stats must return numeric total');
- });
- await run('cleanup user A probe', async () => {
- const result = await call(fn.systemStorage, { action: 'delete', entityType: probeType, entityId: probeId }, TOKEN_A);
- assertOk(result, 'systemStorage.delete should clean up user A probe');
- });
- if (errors.length) exitWithErrors();
- console.log('Storage governance postdeploy validation passed');
- async function run(name, task) {
- try {
- await task();
- console.log(`OK ${name}`);
- } catch (error) {
- errors.push(`${name}: ${error?.message || error}`);
- console.error(`FAIL ${name}: ${error?.message || error}`);
- if (name !== 'cleanup user A probe') {
- if (probeAssetId) {
- await call(fn.fileAsset, { action: 'delete', assetId: probeAssetId }, TOKEN_A).catch(() => null);
- }
- await call(fn.systemStorage, { action: 'delete', entityType: probeType, entityId: probeId }, TOKEN_A).catch(() => null);
- }
- }
- }
- async function call(id, body, sessionToken) {
- let lastResult = null;
- let lastError = null;
- for (let attempt = 1; attempt <= 4; attempt += 1) {
- try {
- const response = await fetch(FN_URL, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Parse-Application-Id': APP_ID,
- },
- body: JSON.stringify({ id, _ApplicationId: APP_ID, sessionToken, ...body }),
- });
- const text = await response.text();
- let result;
- try {
- result = JSON.parse(text);
- } catch {
- result = { code: response.status, success: false, error: text };
- }
- lastResult = result;
- if (attempt < 4 && isRetryableResult(result)) {
- await sleep(600 * attempt);
- continue;
- }
- return result;
- } catch (error) {
- lastError = error;
- if (attempt < 4 && isRetryableMessage(error?.message || '')) {
- await sleep(600 * attempt);
- continue;
- }
- throw error;
- }
- }
- if (lastResult) return lastResult;
- throw lastError || new Error('fetch failed');
- }
- function assertOk(result, message) {
- assert(Number(result?.code || 0) === 200 && result?.success !== false, `${message}: ${JSON.stringify(result).slice(0, 500)}`);
- }
- function assert(condition, message) {
- if (!condition) throw new Error(message);
- }
- function fail(message) {
- errors.push(message);
- }
- function exitWithErrors() {
- for (const error of errors) console.error(`FAIL ${error}`);
- process.exit(1);
- }
- function readCloudFunctionIds(filePath) {
- const text = fs.readFileSync(filePath, 'utf8');
- const ids = {};
- const pattern = /^\s*([a-zA-Z][a-zA-Z0-9_]*)\s*:\s*'([^']*)'/gm;
- let match;
- while ((match = pattern.exec(text))) {
- ids[match[1]] = match[2];
- }
- return ids;
- }
- function isRetryableResult(result) {
- const message = `${result?.error || ''} ${result?.message || ''}`;
- return Number(result?.code || 0) >= 500 || isRetryableMessage(message);
- }
- function isRetryableMessage(message) {
- return /fetch failed|Failed to fetch|NetworkError|Load failed|ECONNRESET|ETIMEDOUT|EAI_AGAIN/i.test(message || '');
- }
- function sleep(ms) {
- return new Promise((resolve) => setTimeout(resolve, ms));
- }
|