feat(ai-image): AI生图开发
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
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));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user