| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- 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 STRICT_PROTECTED = process.env.SMOKE_STRICT_PROTECTED === '1';
- 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 checks = [
- protectedCheck('manifest.list', 'manifest', { action: 'list' }, validateArrayData),
- protectedCheck('task.list', 'task', { action: 'list' }, validateArrayData),
- protectedCheck('history.list', 'history', { action: 'list' }, validateArrayData),
- protectedCheck('result.list', 'result', { action: 'list' }, validateArrayData),
- protectedCheck('remix.listAll', 'remix', { action: 'listAll' }, validateRemixListAllData),
- protectedCheck('voice.listProfiles', 'voice', { action: 'listProfiles' }, validateArrayData),
- check('jimeng.unknownAction', 'jimeng', { action: 'unknown' }, 'failure'),
- check('douyin.unknownRoute', 'douyin', { action: 'call', route: 'unknown' }, 'failure'),
- check('douyinInsight.authGuard', 'douyinInsight', { action: 'listTopics' }, 'failure'),
- check('proxy.unknownAction', 'proxy', { action: 'foo' }, 'failure'),
- check('quickly.configured', 'quickly', { action: 'query', taskId: 'probe' }, 'any'),
- protectedCheck(
- 'upload.token',
- 'upload',
- { action: 'createUploadToken', filename: 'probe.txt', mimeType: 'text/plain', kind: 'probe', bizId: 'smoke' },
- validateUploadToken
- ),
- protectedCheck('authCredit.balance', 'authCredit', { action: 'balance' }, validateAuthCreditBalance),
- protectedCheck('systemStorage.stats', 'systemStorage', { action: 'stats' }, validateSystemStorageStats),
- protectedCheck('fileAsset.stats', 'fileAsset', { action: 'stats' }, validateFileAssetStats),
- ];
- function check(name, key, body, expect) {
- return { name, key, body, expect, validate: null };
- }
- function protectedCheck(name, key, body, validate) {
- const expect = SESSION_TOKEN ? 'success' : STRICT_PROTECTED ? 'failure' : 'any';
- return { name, key, body, expect, validate };
- }
- async function call(id, body) {
- const payload = { id, _ApplicationId: APP_ID, ...body };
- if (SESSION_TOKEN) payload.sessionToken = SESSION_TOKEN;
- let lastError = null;
- for (let attempt = 1; attempt <= 3; 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(payload),
- });
- const text = await response.text();
- try {
- const data = JSON.parse(text);
- if (isRetryableGatewayResult(data) && attempt < 3) {
- await sleep(500 * attempt);
- continue;
- }
- return data;
- } catch {
- return { code: response.status, success: false, error: text };
- }
- } catch (error) {
- lastError = error;
- await sleep(500 * attempt);
- }
- }
- throw lastError || new Error('fetch failed');
- }
- let failed = 0;
- for (const item of checks) {
- const id = fn[item.key] || '';
- if (!id) {
- console.log(`SKIP ${item.name}: ${item.key} function id is empty`);
- continue;
- }
- try {
- const result = await call(id, item.body);
- const ok = matchesExpectation(result, item.expect) && runValidator(item, result);
- if (!ok) failed += 1;
- console.log(`${ok ? 'OK' : 'FAIL'} ${item.name}: ${summarize(result)}`);
- } catch (error) {
- failed += 1;
- console.log(`FAIL ${item.name}: ${error?.message || error}`);
- }
- }
- if (failed > 0) {
- console.error(`Cloud function smoke failed: ${failed}`);
- process.exit(1);
- }
- console.log('Cloud function smoke passed');
- function matchesExpectation(result, expect) {
- if (expect === 'any') return true;
- const code = Number(result?.code || 0);
- const success = result?.success !== false && code === 200;
- if (expect === 'success') return success;
- return code >= 400 || result?.success === false;
- }
- function runValidator(item, result) {
- if (item.expect !== 'success' || !item.validate) return true;
- try {
- return item.validate(result);
- } catch {
- return false;
- }
- }
- function validateUploadToken(result) {
- const key = String(result?.data?.key || '');
- return !!result?.data?.token && key.startsWith('users/');
- }
- function validateArrayData(result) {
- return Array.isArray(result?.data);
- }
- function validateRemixListAllData(result) {
- return Array.isArray(result?.data) || (result?.data && typeof result.data === 'object' && !Array.isArray(result.data));
- }
- function validateAuthCreditBalance(result) {
- return Number.isFinite(Number(result?.data?.balance));
- }
- function validateSystemStorageStats(result) {
- return Number.isFinite(Number(result?.data?.entities?.total ?? result?.data?.totalEntities ?? 0));
- }
- function validateFileAssetStats(result) {
- return Number.isFinite(Number(result?.data?.total ?? 0));
- }
- function summarize(result) {
- return JSON.stringify(result).slice(0, 700);
- }
- 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 isRetryableGatewayResult(result) {
- const message = `${result?.error || ''} ${result?.message || ''}`;
- return /必须提供\s*id|fetch failed|Failed to fetch|NetworkError|Load failed/i.test(message);
- }
- function sleep(ms) {
- return new Promise((resolve) => setTimeout(resolve, ms));
- }
|