171 lines
9.2 KiB
TypeScript
171 lines
9.2 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { randomUUID } from 'node:crypto';
|
|
import type { AddressInfo } from 'node:net';
|
|
import type { Database } from '@atlas/database';
|
|
import type { NestExpressApplication } from '@nestjs/platform-express';
|
|
import { afterAll, beforeAll, describe, it } from 'vitest';
|
|
import { DATABASE } from '../src/database/database.module.js';
|
|
import { createApplication } from '../src/main.js';
|
|
|
|
let app: NestExpressApplication;
|
|
let database: Database;
|
|
let baseUrl = '';
|
|
let workspaceId = '';
|
|
let userId = '';
|
|
let token = '';
|
|
|
|
describe('水印派生 API 集成测试', () => {
|
|
beforeAll(async () => {
|
|
Object.assign(process.env, {
|
|
NODE_ENV: 'test', SWAGGER_ENABLED: 'false',
|
|
AUTH_REGISTER_IP_LIMIT: '100', AUTH_REGISTER_EMAIL_LIMIT: '100',
|
|
});
|
|
const created = await createApplication();
|
|
app = created.app;
|
|
database = app.get<Database>(DATABASE);
|
|
await app.listen(0, '127.0.0.1');
|
|
baseUrl = `http://127.0.0.1:${(app.getHttpServer().address() as AddressInfo).port}`;
|
|
const response = await fetch(`${baseUrl}/api/auth/register`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
email: `watermark-derivative-${randomUUID()}@example.test`,
|
|
password: 'correct-horse-battery-staple-123',
|
|
password_confirmation: 'correct-horse-battery-staple-123',
|
|
}),
|
|
});
|
|
assert.equal(response.status, 201);
|
|
const body = await response.json() as { data: {
|
|
user: { id: string }; workspace: { id: string }; access_token: string;
|
|
} };
|
|
userId = body.data.user.id;
|
|
workspaceId = body.data.workspace.id;
|
|
token = body.data.access_token;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (workspaceId) {
|
|
await database.workspaceTransaction(workspaceId, async (tx) => {
|
|
await tx.query('DELETE FROM atlas.idempotency_requests WHERE workspace_id = $1', [workspaceId]);
|
|
await tx.query('DELETE FROM atlas.outbox_events WHERE workspace_id = $1', [workspaceId]);
|
|
await tx.query('DELETE FROM atlas.operations WHERE workspace_id = $1', [workspaceId]);
|
|
await tx.query('DELETE FROM atlas.media_aspect_ratio_derivatives WHERE workspace_id = $1', [workspaceId]);
|
|
await tx.query('DELETE FROM atlas.media_watermark_derivatives WHERE workspace_id = $1', [workspaceId]);
|
|
await tx.query('DELETE FROM atlas.watermark_template_versions WHERE workspace_id = $1', [workspaceId]);
|
|
await tx.query('DELETE FROM atlas.watermark_templates WHERE workspace_id = $1', [workspaceId]);
|
|
await tx.query('DELETE FROM atlas.media_assets WHERE workspace_id = $1', [workspaceId]);
|
|
await tx.query('DELETE FROM atlas.products WHERE workspace_id = $1', [workspaceId]);
|
|
await tx.query('DELETE FROM atlas.collection_items WHERE workspace_id = $1', [workspaceId]);
|
|
});
|
|
await database.query('DELETE FROM atlas.workspace_memberships WHERE user_id = $1', [userId]);
|
|
await database.query('DELETE FROM atlas.workspaces WHERE id = $1', [workspaceId]);
|
|
await database.query('DELETE FROM atlas.users WHERE id = $1', [userId]);
|
|
}
|
|
await app?.close();
|
|
});
|
|
|
|
it('HTTP 创建任务锁定模板版本,结果查询保持工作空间边界', async () => {
|
|
const { assetId, templateId, productId } = await database.workspaceTransaction(workspaceId, async (tx) => {
|
|
const item = await tx.query<{ id: string }>(
|
|
`INSERT INTO atlas.collection_items (workspace_id, source_kind, source_item_key)
|
|
VALUES ($1, 'manual', $2) RETURNING id`,
|
|
[workspaceId, `watermark-history-${randomUUID()}`],
|
|
);
|
|
const product = await tx.query<{ id: string }>(
|
|
`INSERT INTO atlas.products (workspace_id, collection_item_id)
|
|
VALUES ($1, $2) RETURNING id`,
|
|
[workspaceId, item.rows[0].id],
|
|
);
|
|
const asset = await tx.query<{ id: string }>(
|
|
`INSERT INTO atlas.media_assets
|
|
(workspace_id, storage_provider, bucket, object_key, content_type,
|
|
byte_size, checksum_sha256, width_px, height_px)
|
|
VALUES ($1, 'local_minio', 'atlas-media', $2, 'image/jpeg', 100,
|
|
$3, 120, 100) RETURNING id`,
|
|
[workspaceId, `test/watermark-${randomUUID()}.jpg`, randomUUID().replaceAll('-', '').padEnd(64, '0')],
|
|
);
|
|
const template = await tx.query<{ id: string }>(
|
|
`INSERT INTO atlas.watermark_templates (workspace_id, name, kind)
|
|
VALUES ($1, $2, 'text') RETURNING id`,
|
|
[workspaceId, `集成测试水印 ${randomUUID()}`],
|
|
);
|
|
await tx.query(
|
|
`INSERT INTO atlas.watermark_template_versions
|
|
(workspace_id, template_id, version, kind, text_content, font_code,
|
|
font_size_px, opacity, position, position_x, position_y, created_by_user_id)
|
|
VALUES ($1, $2, 1, 'text', 'Atlas', 'NotoSans', 20, 0.5,
|
|
'bottom_right', 1, 1, $3)`,
|
|
[workspaceId, template.rows[0].id, userId],
|
|
);
|
|
return { assetId: asset.rows[0].id, templateId: template.rows[0].id, productId: product.rows[0].id };
|
|
});
|
|
const key = randomUUID();
|
|
const create = () => fetch(`${baseUrl}/api/media-assets/${assetId}/watermark-derivatives`, {
|
|
method: 'POST', headers: {
|
|
Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', 'Idempotency-Key': key,
|
|
},
|
|
body: JSON.stringify({ watermark_template_id: templateId, product_id: productId }),
|
|
});
|
|
const response = await create();
|
|
assert.equal(response.status, 202);
|
|
const body = await response.json() as { data: {
|
|
id: string; watermark_template_id: string; watermark_template_version: number;
|
|
} };
|
|
assert.equal(body.data.watermark_template_id, templateId);
|
|
assert.equal(body.data.watermark_template_version, 1);
|
|
const replay = await create();
|
|
assert.equal(replay.status, 202);
|
|
assert.equal((await replay.json() as { data: { id: string } }).data.id, body.data.id);
|
|
const read = await fetch(`${baseUrl}/api/media-watermark-derivatives/${body.data.id}`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
assert.equal(read.status, 200);
|
|
assert.equal((await read.json() as { data: { id: string } }).data.id, body.data.id);
|
|
const stored = await database.workspaceTransaction(workspaceId, async (tx) =>
|
|
tx.query<{ product_id: string }>(
|
|
'SELECT product_id FROM atlas.media_watermark_derivatives WHERE id = $1',
|
|
[body.data.id],
|
|
),
|
|
);
|
|
assert.equal(stored.rows[0]?.product_id, productId);
|
|
|
|
const outputs = await database.workspaceTransaction(workspaceId, async (tx) => {
|
|
const ids: string[] = [];
|
|
for (const kind of ['watermark', 'ratio']) {
|
|
const asset = await tx.query<{ id: string }>(
|
|
`INSERT INTO atlas.media_assets
|
|
(workspace_id, storage_provider, bucket, object_key, content_type,
|
|
byte_size, checksum_sha256, width_px, height_px)
|
|
VALUES ($1, 'local_minio', 'atlas-media', $2, 'image/jpeg', 100,
|
|
$3, 1200, 1600) RETURNING id`,
|
|
[workspaceId, `test/${kind}-${randomUUID()}.jpg`, randomUUID().replaceAll('-', '').padEnd(64, '0')],
|
|
);
|
|
ids.push(asset.rows[0].id);
|
|
}
|
|
await tx.query(
|
|
`INSERT INTO atlas.media_watermark_derivatives
|
|
(workspace_id, product_id, source_asset_id, watermark_template_id,
|
|
watermark_template_version, output_asset_id, status, completed_at)
|
|
VALUES ($1, $2, $3, $4, 1, $5, 'succeeded', now())`,
|
|
[workspaceId, productId, assetId, templateId, ids[0]],
|
|
);
|
|
await tx.query(
|
|
`INSERT INTO atlas.media_aspect_ratio_derivatives
|
|
(workspace_id, product_id, source_asset_id, output_asset_id, status, completed_at)
|
|
VALUES ($1, $2, $3, $4, 'succeeded', now())`,
|
|
[workspaceId, productId, assetId, ids[1]],
|
|
);
|
|
return ids;
|
|
});
|
|
const history = await fetch(`${baseUrl}/api/products/${productId}/image-derivatives`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
assert.equal(history.status, 200);
|
|
const records = (await history.json() as { data: Array<{
|
|
kind: string; output_asset: { id: string; preview_url: string };
|
|
}> }).data;
|
|
assert.deepEqual(new Set(records.map((record) => record.kind)), new Set(['watermark', 'aspect_ratio_3_4']));
|
|
assert.deepEqual(new Set(records.map((record) => record.output_asset.id)), new Set(outputs));
|
|
assert.ok(records.every((record) => record.output_asset.preview_url));
|
|
});
|
|
});
|