forked from wangziqi/gongxue-base
256 lines
9.9 KiB
JavaScript
256 lines
9.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import pg from 'pg';
|
|
import { spawn } from 'node:child_process';
|
|
|
|
const databaseUrl = process.env.DATABASE_URL || 'postgresql://postgres:postgres@127.0.0.1:54322/postgres';
|
|
const tenantId = '00000000-0000-0000-0000-000000000001';
|
|
|
|
const ids = {
|
|
okAsset: '20000000-0000-0000-0000-000000000801',
|
|
badAsset: '20000000-0000-0000-0000-000000000802',
|
|
scanBadAsset: '20000000-0000-0000-0000-000000000803',
|
|
};
|
|
|
|
const checksumA = 'a'.repeat(64);
|
|
const checksumB = 'b'.repeat(64);
|
|
|
|
async function runWorkerOnce() {
|
|
const child = spawn(process.execPath, ['apps/worker/dist/apps/worker/src/index.js', '--once', '--job', 'assets'], {
|
|
cwd: process.cwd(),
|
|
env: {
|
|
...process.env,
|
|
DATABASE_URL: databaseUrl,
|
|
WORKER_ASSET_BATCH_SIZE: '20',
|
|
WORKER_ASSET_MIN_AGE_SECONDS: '0',
|
|
WORKER_ASSET_RECHECK_INTERVAL_SECONDS: '0',
|
|
WORKER_ASSET_REQUEST_TIMEOUT_MS: '5000',
|
|
STORAGE_REQUIRE_TENANT_PREFIX: 'true',
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
windowsHide: true,
|
|
});
|
|
let output = '';
|
|
child.stdout.on('data', chunk => {
|
|
output += chunk.toString();
|
|
});
|
|
child.stderr.on('data', chunk => {
|
|
output += chunk.toString();
|
|
});
|
|
const code = await new Promise(resolve => child.on('exit', resolve));
|
|
assert.equal(code, 0, `worker should exit 0\n${output}`);
|
|
assert.match(output, /assets batch processed=\d+/, 'worker output should include assets summary');
|
|
return output;
|
|
}
|
|
|
|
function countFromWorkerOutput(output, name) {
|
|
const match = output.match(new RegExp(`${name}=(\\d+)`));
|
|
return match ? Number(match[1]) : 0;
|
|
}
|
|
|
|
async function cleanup(pool) {
|
|
await pool.query(
|
|
`
|
|
delete from public.audit_logs
|
|
where tenant_id = $1
|
|
and target_type = 'content_asset'
|
|
and target_id in ($2, $3, $4)
|
|
`,
|
|
[tenantId, ids.okAsset, ids.badAsset, ids.scanBadAsset],
|
|
);
|
|
await pool.query(
|
|
`
|
|
delete from public.content_asset_security_scan_events
|
|
where tenant_id = $1 and asset_id in ($2::uuid, $3::uuid, $4::uuid)
|
|
`,
|
|
[tenantId, ids.okAsset, ids.badAsset, ids.scanBadAsset],
|
|
);
|
|
await pool.query(
|
|
`
|
|
delete from public.content_assets
|
|
where tenant_id = $1 and id in ($2::uuid, $3::uuid, $4::uuid)
|
|
`,
|
|
[tenantId, ids.okAsset, ids.badAsset, ids.scanBadAsset],
|
|
);
|
|
}
|
|
|
|
async function seed(pool) {
|
|
await pool.query(
|
|
`
|
|
insert into public.content_assets (
|
|
id, tenant_id, asset_key, title, asset_type, storage_provider,
|
|
bucket, object_key, file_name, mime_type, file_size_bytes, checksum_sha256,
|
|
visibility, status, upload_status, verified_at, verified_size_bytes,
|
|
verified_checksum_sha256, verification_details,
|
|
security_scan_status, security_scan_provider, security_scan_summary,
|
|
security_flags, source,
|
|
created_at, updated_at
|
|
)
|
|
values
|
|
(
|
|
$1, $2, 'asset-worker-ok', '资源复检正常 PDF', 'pdf', 'local_dev',
|
|
'tenant-assets', $3, 'ok.pdf', 'application/pdf', 4096, $4,
|
|
'tenant', 'active', 'verified', now() - interval '2 days', 4096,
|
|
$4, '{"source":"asset-worker-test"}'::jsonb,
|
|
'pending', 'metadata_rules', '{}'::jsonb,
|
|
'{}'::jsonb, 'integration-test',
|
|
now() - interval '2 days', now() - interval '2 days'
|
|
),
|
|
(
|
|
$5, $2, 'asset-worker-bad', '资源复检异常 PDF', 'pdf', 'local_dev',
|
|
'tenant-assets', $6, 'bad.pdf', 'application/pdf', 1024, $7,
|
|
'tenant', 'active', 'verified', now() - interval '2 days', 2048,
|
|
$7, '{"source":"asset-worker-test"}'::jsonb,
|
|
'pending', 'metadata_rules', '{}'::jsonb,
|
|
'{}'::jsonb, 'integration-test',
|
|
now() - interval '2 days', now() - interval '2 days'
|
|
),
|
|
(
|
|
$8, $2, 'asset-worker-scan-bad', '资源安全扫描异常 PDF', 'pdf', 'local_dev',
|
|
'tenant-assets', $9, 'scan-bad.pdf', 'application/pdf', 4096, $4,
|
|
'tenant', 'active', 'verified', now() - interval '2 days', 4096,
|
|
$4, '{"source":"asset-worker-test"}'::jsonb,
|
|
'pending', 'metadata_rules', '{"securityScanForceFail":true}'::jsonb,
|
|
'{}'::jsonb, 'integration-test',
|
|
now() - interval '2 days', now() - interval '2 days'
|
|
)
|
|
`,
|
|
[
|
|
ids.okAsset,
|
|
tenantId,
|
|
`${tenantId}/assets/worker-ok.pdf`,
|
|
checksumA,
|
|
ids.badAsset,
|
|
`${tenantId}/assets/worker-bad.pdf`,
|
|
checksumB,
|
|
ids.scanBadAsset,
|
|
`${tenantId}/assets/worker-scan-bad.pdf`,
|
|
],
|
|
);
|
|
}
|
|
|
|
async function main() {
|
|
const pool = new pg.Pool({ connectionString: databaseUrl });
|
|
let seeded = false;
|
|
try {
|
|
await pool.query('begin');
|
|
await cleanup(pool);
|
|
await seed(pool);
|
|
await pool.query('commit');
|
|
seeded = true;
|
|
|
|
const output = await runWorkerOnce();
|
|
assert.ok(countFromWorkerOutput(output, 'verified') >= 1, 'worker should verify at least one asset');
|
|
assert.ok(countFromWorkerOutput(output, 'failed') >= 2, 'worker should fail metadata and security scan assets');
|
|
|
|
const assets = await pool.query(
|
|
`
|
|
select id, status, upload_status, verification_details,
|
|
security_scan_status, security_scan_provider, security_scan_summary,
|
|
security_flags
|
|
from public.content_assets
|
|
where tenant_id = $1 and id in ($2::uuid, $3::uuid, $4::uuid)
|
|
order by id
|
|
`,
|
|
[tenantId, ids.okAsset, ids.badAsset, ids.scanBadAsset],
|
|
);
|
|
const okAsset = assets.rows.find(row => row.id === ids.okAsset);
|
|
const badAsset = assets.rows.find(row => row.id === ids.badAsset);
|
|
const scanBadAsset = assets.rows.find(row => row.id === ids.scanBadAsset);
|
|
assert.equal(okAsset?.status, 'active', 'verified asset should remain active');
|
|
assert.equal(okAsset?.upload_status, 'verified', 'verified asset should remain verified');
|
|
assert.equal(okAsset?.security_scan_status, 'passed', 'verified asset should pass security scan');
|
|
assert.equal(okAsset?.security_scan_provider, 'metadata_rules', 'verified asset should record scan provider');
|
|
assert.equal(okAsset?.verification_details?.assetWorker?.lastResult, 'verified', 'verified asset should record worker result');
|
|
assert.equal(okAsset?.security_flags?.assetRecheckFailed, undefined, 'verified asset should not keep recheck failure flag');
|
|
|
|
assert.equal(badAsset?.status, 'draft', 'mismatched asset should be unpublished');
|
|
assert.equal(badAsset?.upload_status, 'failed', 'mismatched asset should be marked failed');
|
|
assert.equal(badAsset?.security_scan_status, 'skipped', 'mismatched asset should skip security scan');
|
|
assert.equal(badAsset?.security_flags?.assetRecheckFailed, true, 'mismatched asset should record security flag');
|
|
assert.deepEqual(
|
|
badAsset?.verification_details?.assetWorker?.issues,
|
|
['file_size_mismatch'],
|
|
'mismatched asset should record exact issue',
|
|
);
|
|
assert.equal(scanBadAsset?.status, 'draft', 'security failed asset should be unpublished');
|
|
assert.equal(scanBadAsset?.upload_status, 'verified', 'security failed asset should keep upload verification evidence');
|
|
assert.equal(scanBadAsset?.security_scan_status, 'failed', 'security failed asset should record failed scan status');
|
|
assert.equal(scanBadAsset?.security_flags?.assetSecurityScanFailed, true, 'security failed asset should record security flag');
|
|
assert.ok(
|
|
scanBadAsset?.security_scan_summary?.issueCodes?.includes('security_scan_forced_failure'),
|
|
'security failed asset should record issue code',
|
|
);
|
|
|
|
const scanEvents = await pool.query(
|
|
`
|
|
select asset_id, scan_status, risk_level, issue_codes, provider, details
|
|
from public.content_asset_security_scan_events
|
|
where tenant_id = $1 and asset_id in ($2::uuid, $3::uuid)
|
|
order by created_at asc
|
|
`,
|
|
[tenantId, ids.okAsset, ids.scanBadAsset],
|
|
);
|
|
assert.ok(
|
|
scanEvents.rows.some(row => row.asset_id === ids.okAsset && row.scan_status === 'passed' && row.provider === 'metadata_rules'),
|
|
'worker should write passed security scan event',
|
|
);
|
|
assert.ok(
|
|
scanEvents.rows.some(row => row.asset_id === ids.scanBadAsset && row.scan_status === 'failed' && row.issue_codes.includes('security_scan_forced_failure')),
|
|
'worker should write failed security scan event',
|
|
);
|
|
|
|
const skippedScanEvents = await pool.query(
|
|
`
|
|
select asset_id, scan_status, risk_level, issue_codes, provider, details
|
|
from public.content_asset_security_scan_events
|
|
where tenant_id = $1 and asset_id = $2::uuid
|
|
order by created_at asc
|
|
`,
|
|
[tenantId, ids.badAsset],
|
|
);
|
|
assert.ok(
|
|
skippedScanEvents.rows.some(row => row.scan_status === 'skipped' && row.issue_codes.includes('file_size_mismatch')),
|
|
'worker should write skipped scan event when metadata verification fails',
|
|
);
|
|
|
|
const audits = await pool.query(
|
|
`
|
|
select action, details
|
|
from public.audit_logs
|
|
where tenant_id = $1
|
|
and target_type = 'content_asset'
|
|
and target_id in ($2, $3, $4)
|
|
order by created_at asc
|
|
`,
|
|
[tenantId, ids.okAsset, ids.badAsset, ids.scanBadAsset],
|
|
);
|
|
assert.ok(
|
|
audits.rows.some(row => row.action === 'content.asset.rechecked' && row.details?.result === 'verified'),
|
|
'worker should write verified audit log',
|
|
);
|
|
assert.ok(
|
|
audits.rows.some(row => row.action === 'content.asset.recheck_failed' && row.details?.issues?.includes('file_size_mismatch')),
|
|
'worker should write failed audit log',
|
|
);
|
|
assert.ok(
|
|
audits.rows.some(row => row.action === 'content.asset.security_scan_failed' && row.details?.issueCodes?.includes('security_scan_forced_failure')),
|
|
'worker should write failed security scan audit log',
|
|
);
|
|
|
|
console.log('Asset worker integration test complete.');
|
|
} catch (error) {
|
|
await pool.query('rollback').catch(() => {});
|
|
throw error;
|
|
} finally {
|
|
if (seeded) {
|
|
await cleanup(pool).catch(() => {});
|
|
}
|
|
await pool.end();
|
|
}
|
|
}
|
|
|
|
main().catch(error => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|