| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- 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 SESSION_TOKEN = process.env.SMOKE_SESSION_TOKEN || '';
- 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 = `parse-storage-${Date.now()}`;
- const errors = [];
- let probeAssetId = '';
- if (!SESSION_TOKEN) {
- fail('Missing SMOKE_SESSION_TOKEN. Provide one test user Parse session token.');
- }
- for (const key of ['systemStorage', 'upload', 'fileAsset', 'authCredit']) {
- if (!fn[key]) fail(`CLOUD_FN.${key} is empty. Deploy and fill src/app/services/cloud-functions.ts before running validation.`);
- }
- if (errors.length) exitWithErrors();
- try {
- await run('authCredit.balance keeps APIG reachable', async () => {
- const result = await call(fn.authCredit, { action: 'balance' });
- assertOk(result, 'authCredit.balance should succeed');
- assert(result?.data?.apigId === '6pFf6EAdKT', `APIG id must stay 6pFf6EAdKT: ${summarize(result)}`);
- assert(Number.isFinite(Number(result?.data?.balance)), 'authCredit.balance must return numeric balance');
- });
- await run('systemStorage writes VideoWorkflowEntity', async () => {
- const upsert = await call(fn.systemStorage, {
- action: 'upsert',
- entityType: probeType,
- entityId: probeId,
- status: 'active',
- data: {
- marker: probeId,
- source: 'parse-storage-postdeploy',
- },
- });
- assertOk(upsert, 'systemStorage.upsert should succeed');
- assert(upsert?.data?.data?.marker === probeId, `systemStorage.upsert should return probe payload: ${summarize(upsert)}`);
- });
- await run('systemStorage reads VideoWorkflowEntity', async () => {
- const get = await call(fn.systemStorage, { action: 'get', entityType: probeType, entityId: probeId });
- assertOk(get, 'systemStorage.get should succeed');
- assert(get?.data?.data?.marker === probeId, `systemStorage.get should return the probe payload: ${summarize(get)}`);
- });
- await run('systemStorage writes VideoWorkflowAudit detail string', async () => {
- const audit = await call(fn.systemStorage, {
- action: 'audit',
- auditAction: 'parse-storage-postdeploy',
- entityType: probeType,
- entityId: probeId,
- summary: 'Parse storage postdeploy probe',
- detail: { marker: probeId },
- });
- assertOk(audit, 'systemStorage.audit should succeed');
- assert(audit?.data?.detail?.marker === probeId, `systemStorage.audit should return parsed detail: ${summarize(audit)}`);
- });
- await run('upload issues user-partition qiniu key', async () => {
- const upload = await call(fn.upload, {
- action: 'createUploadToken',
- filename: 'parse-storage-postdeploy-probe.txt',
- mimeType: 'text/plain',
- kind: 'probe',
- bizId: probeId,
- });
- assertOk(upload, 'upload.createUploadToken should succeed');
- const qiniuKey = String(upload?.data?.key || '');
- assert(qiniuKey.startsWith('users/'), `upload key must start with users/: ${summarize(upload)}`);
- const registered = await call(fn.fileAsset, {
- action: 'register',
- qiniuKey,
- url: upload?.data?.url || '',
- bucket: upload?.data?.bucket || 'nova-repos',
- mimeType: 'text/plain',
- kind: 'export',
- sizeBytes: 0,
- sourceModule: 'parse-storage-postdeploy',
- bizType: probeType,
- bizId: probeId,
- metadata: { marker: probeId },
- });
- assertOk(registered, 'fileAsset.register should succeed');
- probeAssetId = String(registered?.data?.assetId || registered?.data?.id || '');
- assert(probeAssetId, `fileAsset.register must return assetId: ${summarize(registered)}`);
- assert(registered?.data?.qiniuKey === qiniuKey, `fileAsset.register must preserve qiniuKey: ${summarize(registered)}`);
- });
- await run('fileAsset reads VideoWorkflowFileAsset', async () => {
- const get = await call(fn.fileAsset, { action: 'get', assetId: probeAssetId });
- assertOk(get, 'fileAsset.get should succeed');
- assert(get?.data?.metadata?.marker === probeId, `fileAsset.get should return probe metadata: ${summarize(get)}`);
- });
- } finally {
- if (probeAssetId) {
- await run('cleanup VideoWorkflowFileAsset', async () => {
- const deleted = await call(fn.fileAsset, { action: 'delete', assetId: probeAssetId });
- assertOk(deleted, 'fileAsset.delete cleanup should succeed');
- });
- }
- await run('cleanup VideoWorkflowEntity', async () => {
- const deleted = await call(fn.systemStorage, { action: 'delete', entityType: probeType, entityId: probeId });
- assertOk(deleted, 'systemStorage.delete cleanup should succeed');
- });
- }
- if (errors.length) exitWithErrors();
- console.log('Parse storage 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}`);
- }
- }
- async function call(id, body) {
- 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: SESSION_TOKEN, ...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}: ${summarize(result)}`);
- }
- 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 summarize(result) {
- return JSON.stringify(result, (key, value) => key === 'token' ? '<redacted>' : value).slice(0, 700);
- }
- 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));
- }
- 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;
- }
|