import { createHash, randomUUID } from 'node:crypto'; import { createApplicationDatabase, type Database, type DatabaseTransaction } from '@atlas/database'; import { describe, expect, it, vi } from 'vitest'; import { IdempotencyService } from '../src/common/idempotency.service.js'; import { MediaService } from '../src/media/media.service.js'; const bytes = Buffer.from('validated-product-image'); vi.mock('../src/media/product-image-download.js', () => ({ downloadProductImage: vi.fn(async () => ({ bytes, contentType: 'image/png', width: 800, height: 800 })), })); /** 归属验证只在回滚事务中创建数据,存储和公网下载由测试替身承担。 */ describe.skipIf(!process.env.DATABASE_URL)('商品上传与网络图片数据库隔离', () => { it('候选只属于当前商品,重复素材可独立绑定,FK 与 RLS 拒绝越权', async () => { const db = createApplicationDatabase(process.env.DATABASE_URL!); const rollback = Symbol('rollback'); try { await db.transaction(async (tx) => { const workspaceId = randomUUID(), otherWorkspace = randomUUID(), userId = randomUUID(); await tx.query("INSERT INTO atlas.users (id, email, password_hash) VALUES ($1, $2, 'test-only')", [userId, `${userId}@example.test`]); for (const id of [workspaceId, otherWorkspace]) await tx.query("INSERT INTO atlas.workspaces (id, name, slug) VALUES ($1::uuid, '图片归属测试', $1::text)", [id]); const setWorkspace = (id: string) => tx.query("SELECT set_config('app.workspace_id', $1, true)", [id]); const product = async (ws: string) => { await setWorkspace(ws); const id = randomUUID(), itemId = randomUUID(); await tx.query("INSERT INTO atlas.collection_items (id, workspace_id, source_kind, source_item_key) VALUES ($1::uuid, $2, 'manual', $1::text)", [itemId, ws]); await tx.query('INSERT INTO atlas.products (id, workspace_id, collection_item_id) VALUES ($1, $2, $3)', [id, ws, itemId]); return id; }; const productA = await product(workspaceId), productB = await product(workspaceId), foreignProduct = await product(otherWorkspace); await setWorkspace(workspaceId); const assetA = randomUUID(), assetB = randomUUID(), legacyAsset = randomUUID(); const checksum = createHash('sha256').update(bytes).digest('hex'); for (const id of [assetA, assetB, legacyAsset]) await tx.query(`INSERT INTO atlas.media_assets (id, workspace_id, storage_provider, bucket, object_key, content_type, byte_size, checksum_sha256, width_px, height_px) VALUES ($1::uuid, $2, 'local_minio', 'test', $1::text, 'image/png', $3, $4, 800, 800)`, [id, workspaceId, bytes.length, id === assetA ? checksum : id]); const upload = async (productId: string | null, assetId: string) => tx.query(`INSERT INTO atlas.media_upload_intents (workspace_id, purpose, storage_provider, bucket, object_key, expected_content_type, expected_byte_size, expected_checksum_sha256, status, asset_id, expires_at, completed_at, product_id) VALUES ($1, 'product_image', 'local_minio', 'test', $2, 'image/png', $3, $4, 'ready', $5, now() + interval '1 hour', now(), $6)`, [workspaceId, randomUUID(), bytes.length, checksum, assetId, productId]); const imported = async (productId: string | null, assetId: string) => tx.query(`INSERT INTO atlas.product_image_imports (workspace_id, product_id, source_url, asset_id) VALUES ($1, $2, 'https://example.test/shared.png', $3)`, [workspaceId, productId, assetId]); await upload(productA, assetA); await upload(productB, assetB); await upload(null, legacyAsset); await imported(productA, assetA); await imported(productB, assetB); await imported(null, legacyAsset); const scoped = { workspaceTransaction: async (ws: string, work: (tx: DatabaseTransaction) => Promise) => { await setWorkspace(ws); return work(tx); } } as Database; const media = new MediaService(new IdempotencyService(scoped, { IDEMPOTENCY_KEY_SECRET: 'image-test-secret' } as never), scoped, { OBJECT_STORAGE_ENDPOINT: 'http://127.0.0.1:9000', OBJECT_STORAGE_REGION: 'us-east-1', OBJECT_STORAGE_BUCKET: 'test', OBJECT_STORAGE_PROVIDER: 'local_minio', objectStorageForcePathStyle: true, objectStorageAccessKey: 'test', objectStorageSecretKey: 'test', } as never); Object.assign(media, { ensureBucket: async () => undefined, inspectObject: async () => ({ byteSize: bytes.length, checksum, contentType: 'image/png', width: 800, height: 800, }) }); const ids = (rows: Array<{ id: string }>) => rows.map((row) => row.id).sort(); expect(ids(await media.productUploads(workspaceId, productA))).toEqual([assetA]); expect(ids(await media.productUploads(workspaceId, productB))).toEqual([assetB]); expect(ids(await media.productImports(workspaceId, productA))).toEqual([assetA]); expect(ids(await media.productImports(workspaceId, productB))).toEqual([assetB]); const input = { purpose: 'product_image' as const, product_id: productB, filename: 'shared.png', content_type: 'image/png' as const, byte_size: bytes.length, checksum_sha256: checksum }; const key = randomUUID(); const intent = await media.createIntent(workspaceId, userId, key, input); const uploadId = (intent.body as { data: { upload_id: string } }).data.upload_id; const repeated = await media.createIntent(workspaceId, userId, key, input); expect((repeated.body as { data: { upload_id: string } }).data.upload_id).toBe(uploadId); expect((await tx.query('SELECT product_id FROM atlas.media_upload_intents WHERE id = $1', [uploadId])).rows[0].product_id).toBe(productB); await media.completeIntent(workspaceId, userId, uploadId, randomUUID(), { checksum_sha256: checksum }); expect(ids(await media.productUploads(workspaceId, productB))).toEqual([assetA, assetB].sort()); expect(ids(await media.productUploads(workspaceId, productA))).toEqual([assetA]); await media.importProductImage(workspaceId, userId, randomUUID(), { product_id: productB, url: 'https://example.test/shared.png' }); expect(ids(await media.productImports(workspaceId, productB))).toEqual([assetA]); expect(ids(await media.productImports(workspaceId, productA))).toEqual([assetA]); const forbidden = async (work: () => Promise) => { await tx.query('SAVEPOINT forbidden_image'); try { await expect(work()).rejects.toMatchObject({ code: '23503' }); } finally { await tx.query('ROLLBACK TO SAVEPOINT forbidden_image'); } }; await forbidden(() => upload(foreignProduct, assetA)); await forbidden(() => imported(foreignProduct, assetA)); await tx.query('SAVEPOINT unknown_product'); await expect(media.createIntent(workspaceId, userId, randomUUID(), { ...input, product_id: foreignProduct })).rejects.toMatchObject({ status: 404 }); await tx.query('ROLLBACK TO SAVEPOINT unknown_product'); expect(await media.productUploads(otherWorkspace, productA)).toEqual([]); expect(await media.productImports(otherWorkspace, productA)).toEqual([]); throw rollback; }); } catch (error) { if (error !== rollback) throw error; } finally { await db.close(); } }); });