| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953 |
- import { INestApplication } from '@nestjs/common';
- import { JwtService } from '@nestjs/jwt';
- import { Test } from '@nestjs/testing';
- import * as request from 'supertest';
- import { configureApp } from '../src/app.setup';
- import { TemplateAiClient } from '../src/modules/templates/template-ai.client';
- import { TemplateTaskFiles } from '../src/modules/templates/template-task-files';
- import { PrismaService } from '../src/prisma/prisma.service';
- process.env.GCS_BUCKET ??= 'test-bucket';
- process.env.GCS_PROJECT_ID ??= 'test-project';
- process.env.GCS_CLIENT_EMAIL ??= 'test@example.com';
- process.env.GCS_PRIVATE_KEY ??= 'test-key';
- describe('Templates (e2e)', () => {
- let app: INestApplication;
- let jwtService: JwtService;
- const executeTemplate = jest.fn();
- const getResult = jest.fn();
- const templateTaskFiles = {
- downloadToTempFile: jest.fn(),
- prepareStep2Bundle: jest.fn(),
- cleanup: jest.fn(),
- };
- const systemConfigs: Array<Record<string, unknown>> = [];
- const hairTemplates: Array<Record<string, unknown>> = [];
- const hairTemplateMakeTasks: Array<Record<string, unknown>> = [];
- const aiHairSimulationTasks: Array<Record<string, unknown>> = [];
- let nextSystemConfigId = 1n;
- let nextTemplateId = 101n;
- let nextTaskId = 88n;
- function filterHairTemplates(params: {
- where?: Record<string, unknown>;
- orderBy?: Record<string, unknown>;
- skip?: number;
- take?: number;
- }) {
- let items = [...hairTemplates];
- const typedWhere = (params.where ?? {}) as {
- hairstylistId?: bigint;
- isDeleted?: number;
- category?: string;
- status?: string;
- OR?: Array<{
- templateName?: { contains: string };
- tags?: { contains: string };
- description?: { contains: string };
- }>;
- };
- if (typeof typedWhere.isDeleted === 'number') {
- items = items.filter((item) => item.isDeleted === typedWhere.isDeleted);
- }
- if (typeof typedWhere.hairstylistId === 'bigint') {
- items = items.filter((item) => item.hairstylistId === typedWhere.hairstylistId);
- }
- if (typedWhere.category) {
- items = items.filter((item) => item.category === typedWhere.category);
- }
- if (typedWhere.status) {
- items = items.filter((item) => item.status === typedWhere.status);
- }
- if (Array.isArray(typedWhere.OR) && typedWhere.OR.length > 0) {
- const keyword =
- typedWhere.OR[0]?.templateName?.contains ??
- typedWhere.OR[1]?.tags?.contains ??
- typedWhere.OR[2]?.description?.contains ??
- '';
- items = items.filter((item) => {
- return (
- String(item.templateName ?? '').includes(keyword) ||
- String(item.tags ?? '').includes(keyword) ||
- String(item.description ?? '').includes(keyword)
- );
- });
- }
- if (
- Array.isArray(params.orderBy) &&
- params.orderBy[0] &&
- 'id' in params.orderBy[0] &&
- params.orderBy[0].id === 'desc'
- ) {
- items.sort((a, b) => Number(b.id) - Number(a.id));
- }
- return typeof params.take === 'number'
- ? items.slice(Number(params.skip ?? 0), Number(params.skip ?? 0) + params.take)
- : items;
- }
- const prisma = {
- systemConfig: {
- createMany: jest.fn(async ({ data }: { data: Array<Record<string, unknown>> }) => {
- for (const item of data) {
- systemConfigs.push({
- id: nextSystemConfigId,
- isDeleted: 0,
- createTime: new Date(),
- updateTime: new Date(),
- remark: '',
- ...item,
- });
- nextSystemConfigId += 1n;
- }
- return { count: data.length };
- }),
- create: jest.fn(async ({ data }: { data: Record<string, unknown> }) => {
- const created = {
- id: nextSystemConfigId,
- isDeleted: 0,
- createTime: new Date(),
- updateTime: new Date(),
- remark: '',
- ...data,
- };
- systemConfigs.push(created);
- nextSystemConfigId += 1n;
- return created;
- }),
- findMany: jest.fn(async ({ where, orderBy, take }: Record<string, unknown>) => {
- let items = [...systemConfigs];
- if (where && typeof where === 'object') {
- const typedWhere = where as {
- isDeleted?: number;
- configGroup?: string | { in: string[] };
- };
- if (typeof typedWhere.isDeleted === 'number') {
- items = items.filter((item) => item.isDeleted === typedWhere.isDeleted);
- }
- if (typeof typedWhere.configGroup === 'string') {
- items = items.filter((item) => item.configGroup === typedWhere.configGroup);
- }
- const configGroupFilter = typedWhere.configGroup;
- if (
- configGroupFilter &&
- typeof configGroupFilter === 'object' &&
- 'in' in configGroupFilter &&
- Array.isArray(configGroupFilter.in)
- ) {
- items = items.filter((item) =>
- configGroupFilter.in.includes(String(item.configGroup)),
- );
- }
- }
- if (Array.isArray(orderBy)) {
- items.sort((a, b) => {
- const sortA = Number(a.sort ?? 0);
- const sortB = Number(b.sort ?? 0);
- if (sortA !== sortB) {
- return sortA - sortB;
- }
- return Number(a.id) - Number(b.id);
- });
- }
- if (typeof take === 'number') {
- items = items.slice(0, take);
- }
- return items;
- }),
- deleteMany: jest.fn(async () => ({ count: 0 })),
- },
- hairTemplate: {
- findFirst: jest.fn(async ({ where }: { where: Record<string, unknown> }) => {
- return (
- hairTemplates.find((item) => {
- return (
- item.id === where.id &&
- item.isDeleted === where.isDeleted &&
- item.hairstylistId === where.hairstylistId
- );
- }) ?? null
- );
- }),
- findMany: jest.fn(async ({ where, orderBy, skip = 0, take }: Record<string, unknown>) => {
- return filterHairTemplates({
- where: where as Record<string, unknown> | undefined,
- orderBy: orderBy as Record<string, unknown> | undefined,
- skip: Number(skip),
- take: typeof take === 'number' ? Number(take) : undefined,
- });
- }),
- count: jest.fn(async ({ where }: { where: Record<string, unknown> }) => {
- return filterHairTemplates({ where }).length;
- }),
- update: jest.fn(async ({ where, data }: { where: { id: bigint }; data: Record<string, unknown> }) => {
- const target = hairTemplates.find((item) => item.id === where.id);
- if (!target) {
- throw new Error('template not found');
- }
- Object.assign(target, data);
- return target;
- }),
- updateMany: jest.fn(async ({
- where,
- data,
- }: {
- where: {
- id?: bigint | { in?: bigint[] };
- hairstylistId?: bigint;
- isDeleted?: number;
- };
- data: Record<string, unknown>;
- }) => {
- let items = [...hairTemplates];
- if (typeof where.isDeleted === 'number') {
- items = items.filter((item) => item.isDeleted === where.isDeleted);
- }
- if (typeof where.hairstylistId === 'bigint') {
- items = items.filter((item) => item.hairstylistId === where.hairstylistId);
- }
- if (typeof where.id === 'bigint') {
- items = items.filter((item) => item.id === where.id);
- } else {
- const idFilter = where.id;
- if (
- idFilter &&
- typeof idFilter === 'object' &&
- 'in' in idFilter &&
- Array.isArray(idFilter.in)
- ) {
- const ids = idFilter.in;
- items = items.filter((item) => ids.includes(item.id as bigint));
- }
- }
- items.forEach((item) => Object.assign(item, data));
- return { count: items.length };
- }),
- deleteMany: jest.fn(async () => ({ count: 0 })),
- },
- hairTemplateMakeTask: {
- findFirst: jest.fn(async ({ where }: { where: Record<string, unknown> }) => {
- return (
- hairTemplateMakeTasks.find((item) => {
- return item.id === where.id && item.isDeleted === where.isDeleted;
- }) ?? null
- );
- }),
- findMany: jest.fn(async ({ where, orderBy }: Record<string, unknown>) => {
- const typedWhere = (where ?? {}) as {
- isDeleted?: number;
- templateId?: { in?: bigint[] };
- };
- let items = [...hairTemplateMakeTasks];
- if (typeof typedWhere.isDeleted === 'number') {
- items = items.filter((item) => item.isDeleted === typedWhere.isDeleted);
- }
- if (typedWhere.templateId?.in) {
- items = items.filter((item) => typedWhere.templateId?.in?.includes(item.templateId as bigint));
- }
- if (Array.isArray(orderBy) && orderBy[0] && 'id' in orderBy[0] && orderBy[0].id === 'desc') {
- items.sort((a, b) => Number(b.id) - Number(a.id));
- }
- return items;
- }),
- update: jest.fn(async ({ where, data }: { where: { id: bigint }; data: Record<string, unknown> }) => {
- const target = hairTemplateMakeTasks.find((item) => item.id === where.id);
- if (!target) {
- throw new Error('task not found');
- }
- Object.assign(target, data);
- return target;
- }),
- updateMany: jest.fn(async ({
- where,
- data,
- }: {
- where: {
- templateId?: bigint | { in?: bigint[] };
- isDeleted?: number;
- };
- data: Record<string, unknown>;
- }) => {
- let items = [...hairTemplateMakeTasks];
- if (typeof where.isDeleted === 'number') {
- items = items.filter((item) => item.isDeleted === where.isDeleted);
- }
- if (typeof where.templateId === 'bigint') {
- items = items.filter((item) => item.templateId === where.templateId);
- } else {
- const templateIdFilter = where.templateId;
- if (
- templateIdFilter &&
- typeof templateIdFilter === 'object' &&
- 'in' in templateIdFilter &&
- Array.isArray(templateIdFilter.in)
- ) {
- const templateIds = templateIdFilter.in;
- items = items.filter((item) =>
- templateIds.includes(item.templateId as bigint),
- );
- }
- }
- items.forEach((item) => Object.assign(item, data));
- return { count: items.length };
- }),
- deleteMany: jest.fn(async () => ({ count: 0 })),
- },
- aiHairSimulationTask: {
- count: jest.fn(async ({ where }: { where: Record<string, unknown> }) => {
- const typedWhere = where as {
- taskStatus?: string;
- isDeleted?: number;
- templateId?: { in?: string[] };
- taskFinishTime?: { gte?: Date; lt?: Date };
- };
- let items = [...aiHairSimulationTasks];
- if (typeof typedWhere.taskStatus === 'string') {
- items = items.filter((item) => item.taskStatus === typedWhere.taskStatus);
- }
- if (typeof typedWhere.isDeleted === 'number') {
- items = items.filter((item) => item.isDeleted === typedWhere.isDeleted);
- }
- if (typedWhere.templateId?.in) {
- items = items.filter((item) =>
- typedWhere.templateId?.in?.includes(String(item.templateId)),
- );
- }
- if (typedWhere.taskFinishTime?.gte instanceof Date) {
- items = items.filter(
- (item) =>
- item.taskFinishTime instanceof Date &&
- item.taskFinishTime >= typedWhere.taskFinishTime!.gte!,
- );
- }
- if (typedWhere.taskFinishTime?.lt instanceof Date) {
- items = items.filter(
- (item) =>
- item.taskFinishTime instanceof Date &&
- item.taskFinishTime < typedWhere.taskFinishTime!.lt!,
- );
- }
- return items.length;
- }),
- },
- $transaction: jest.fn(async (callback: (tx: {
- hairTemplate: { create: (args: { data: Record<string, unknown> }) => Promise<Record<string, unknown>> };
- hairTemplateMakeTask: { create: (args: { data: Record<string, unknown> }) => Promise<Record<string, unknown>> };
- }) => Promise<unknown>) =>
- callback({
- hairTemplate: {
- create: async ({ data }) => {
- const created = {
- id: nextTemplateId,
- createTime: new Date(),
- updateTime: new Date(),
- isDeleted: 0,
- ...data,
- };
- hairTemplates.push(created);
- nextTemplateId += 1n;
- return created;
- },
- },
- hairTemplateMakeTask: {
- create: async ({ data }) => {
- const created = {
- id: nextTaskId,
- createTime: new Date(),
- updateTime: new Date(),
- isDeleted: 0,
- ...data,
- };
- hairTemplateMakeTasks.push(created);
- nextTaskId += 1n;
- return created;
- },
- },
- })),
- };
- async function signToken() {
- return jwtService.signAsync({
- sub: 1,
- stylistId: '1',
- email: 'stylist@example.com',
- username: 'Ogawa',
- });
- }
- beforeAll(async () => {
- const { AppModule } = await import('../src/app.module');
- const moduleRef = await Test.createTestingModule({
- imports: [AppModule],
- })
- .overrideProvider(TemplateAiClient)
- .useValue({ executeTemplate, getResult })
- .overrideProvider(TemplateTaskFiles)
- .useValue(templateTaskFiles)
- .overrideProvider(PrismaService)
- .useValue(prisma)
- .compile();
- app = moduleRef.createNestApplication();
- configureApp(app);
- await app.init();
- jwtService = app.get(JwtService);
- });
- beforeEach(() => {
- executeTemplate.mockReset();
- getResult.mockReset();
- templateTaskFiles.downloadToTempFile.mockReset();
- templateTaskFiles.prepareStep2Bundle.mockReset();
- templateTaskFiles.cleanup.mockReset();
- systemConfigs.length = 0;
- hairTemplates.length = 0;
- hairTemplateMakeTasks.length = 0;
- aiHairSimulationTasks.length = 0;
- nextSystemConfigId = 1n;
- nextTemplateId = 101n;
- nextTaskId = 88n;
- });
- afterAll(async () => {
- await app.close();
- });
- it('/api/v1/templates/assets (GET) returns grouped asset options', async () => {
- const token = await signToken();
- const uniqueSuffix = `${Date.now()}-assets`;
- await prisma.systemConfig.createMany({
- data: [
- {
- configKey: `e2e-template-model-${uniqueSuffix}`,
- configName: '模特',
- configGroup: 'ai-model',
- configValue: 'https://cdn.example.com/model.jpg',
- valueType: 'STRING',
- sort: 1,
- },
- {
- configKey: `e2e-template-clothes-${uniqueSuffix}`,
- configName: '服装',
- configGroup: 'clothing',
- configValue: 'https://cdn.example.com/clothes.jpg',
- valueType: 'STRING',
- sort: 2,
- },
- {
- configKey: `e2e-template-background-${uniqueSuffix}`,
- configName: '背景',
- configGroup: 'background',
- configValue: 'https://cdn.example.com/background.jpg',
- valueType: 'STRING',
- sort: 3,
- },
- ] as never,
- });
- const response = await request(app.getHttpServer())
- .get('/api/v1/templates/assets')
- .set('Authorization', `Bearer ${token}`)
- .expect(200);
- expect(response.body.data.models).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- name: '模特',
- url: 'https://cdn.example.com/model.jpg',
- }),
- ]),
- );
- expect(response.body.data.clothes).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- name: '服装',
- url: 'https://cdn.example.com/clothes.jpg',
- }),
- ]),
- );
- expect(response.body.data.backgrounds).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- name: '背景',
- url: 'https://cdn.example.com/background.jpg',
- }),
- ]),
- );
- });
- it('/api/v1/templates/make-tasks (POST) creates template and task rows', async () => {
- const token = await signToken();
- const uniqueSuffix = `${Date.now()}-create`;
- await prisma.systemConfig.create({
- data: {
- configKey: `e2e-template-step1-${uniqueSuffix}`,
- configName: 'step1',
- configGroup: 'template-step1',
- configValue: 'https://cdn.example.com/template-step1.json',
- valueType: 'STRING',
- sort: 1,
- } as never,
- });
- executeTemplate.mockResolvedValueOnce({
- taskId: `e2e-template-step1-task-${uniqueSuffix}`,
- });
- const response = await request(app.getHttpServer())
- .post('/api/v1/templates/make-tasks')
- .set('Authorization', `Bearer ${token}`)
- .send({
- templateName: `e2e-template-${uniqueSuffix}`,
- category: 'LONG',
- status: 'DRAFT',
- description: 'e2e description',
- tags: ['首次建议', '层次'],
- refHairUrl: 'https://cdn.example.com/ref.jpg',
- modelUrl: 'https://cdn.example.com/model.jpg',
- clothesUrl: 'https://cdn.example.com/clothes.jpg',
- backgroundUrl: 'https://cdn.example.com/background.jpg',
- })
- .expect(201);
- expect(response.body.data).toEqual({
- templateId: expect.any(String),
- taskId: expect.any(String),
- totalTaskId: expect.stringContaining('tmpl_'),
- taskStatus: 'PROCESSING',
- });
- expect(executeTemplate).toHaveBeenCalledWith(
- 'https://cdn.example.com/template-step1.json',
- {
- images: ['https://cdn.example.com/ref.jpg'],
- },
- );
- });
- it('/api/v1/templates/:templateId (GET) returns template basic info', async () => {
- const token = await signToken();
- hairTemplates.push({
- id: 43n,
- templateName: '柔和层次长发',
- coverUrl: 'https://cdn.example.com/cover.jpg',
- templateUrl: 'https://cdn.example.com/template.zip',
- status: 'DRAFT',
- category: 'LONG',
- tags: '层次,首次建议',
- description: '适合轻盈感修饰',
- hairstylistId: 1n,
- isDeleted: 0,
- });
- const response = await request(app.getHttpServer())
- .get('/api/v1/templates/43')
- .set('Authorization', `Bearer ${token}`)
- .expect(200);
- expect(response.body.data).toEqual({
- id: '43',
- templateName: '柔和层次长发',
- status: 'DRAFT',
- category: 'LONG',
- tags: ['层次', '首次建议'],
- description: '适合轻盈感修饰',
- coverUrl: 'https://cdn.example.com/cover.jpg',
- templateUrl: 'https://cdn.example.com/template.zip',
- });
- });
- it('/api/v1/templates/:templateId (PATCH) updates only template basic info', async () => {
- const token = await signToken();
- hairTemplates.push({
- id: 43n,
- templateName: '旧模板名',
- coverUrl: 'https://cdn.example.com/cover.jpg',
- templateUrl: 'https://cdn.example.com/template.zip',
- status: 'DRAFT',
- category: 'LONG',
- tags: '旧标签',
- description: '旧描述',
- modelUrl: 'https://cdn.example.com/model.jpg',
- clothesUrl: 'https://cdn.example.com/clothes.jpg',
- backgroundUrl: 'https://cdn.example.com/background.jpg',
- refHairUrl: 'https://cdn.example.com/ref.jpg',
- hairstylistId: 1n,
- isDeleted: 0,
- });
- const response = await request(app.getHttpServer())
- .patch('/api/v1/templates/43')
- .set('Authorization', `Bearer ${token}`)
- .send({
- templateName: '空气感中长发',
- status: 'PUBLIC',
- category: 'MEDIUM',
- tags: ['空气感', '层次'],
- description: '新描述',
- })
- .expect(200);
- expect(response.body.data).toEqual({
- id: '43',
- templateName: '空气感中长发',
- status: 'PUBLIC',
- category: 'MEDIUM',
- tags: ['空气感', '层次'],
- description: '新描述',
- coverUrl: 'https://cdn.example.com/cover.jpg',
- templateUrl: 'https://cdn.example.com/template.zip',
- });
- expect(hairTemplates[0]).toEqual(
- expect.objectContaining({
- templateName: '空气感中长发',
- status: 'PUBLIC',
- category: 'MEDIUM',
- tags: '空气感,层次',
- description: '新描述',
- modelUrl: 'https://cdn.example.com/model.jpg',
- clothesUrl: 'https://cdn.example.com/clothes.jpg',
- backgroundUrl: 'https://cdn.example.com/background.jpg',
- refHairUrl: 'https://cdn.example.com/ref.jpg',
- }),
- );
- });
- it('/api/v1/templates (GET) filters and paginates stylist templates', async () => {
- const token = await signToken();
- hairTemplates.push(
- {
- id: 101n,
- templateName: '空气感中长发',
- category: 'MEDIUM',
- status: 'PUBLIC',
- description: '空气感提案',
- tags: '空气感,脸周',
- coverUrl: 'https://cdn.example.com/101.jpg',
- templateUrl: 'https://cdn.example.com/101.zip',
- usedCount: 5,
- hairstylistId: 1n,
- isDeleted: 0,
- },
- {
- id: 102n,
- templateName: '厚重短发',
- category: 'SHORT',
- status: 'DRAFT',
- description: '短发提案',
- tags: '短发',
- coverUrl: 'https://cdn.example.com/102.jpg',
- templateUrl: 'https://cdn.example.com/102.zip',
- usedCount: 1,
- hairstylistId: 1n,
- isDeleted: 0,
- },
- {
- id: 103n,
- templateName: '其他美发师模板',
- category: 'MEDIUM',
- status: 'PUBLIC',
- description: '不应出现',
- tags: '空气感',
- coverUrl: 'https://cdn.example.com/103.jpg',
- templateUrl: 'https://cdn.example.com/103.zip',
- usedCount: 3,
- hairstylistId: 2n,
- isDeleted: 0,
- },
- );
- hairTemplateMakeTasks.push({
- id: 88n,
- templateId: 101n,
- taskStatus: 'STEP2_SUCCESS',
- isDeleted: 0,
- });
- const response = await request(app.getHttpServer())
- .get('/api/v1/templates?q=空气感&category=MEDIUM&status=PUBLIC&page=1&pageSize=1')
- .set('Authorization', `Bearer ${token}`)
- .expect(200);
- expect(response.body.data).toEqual({
- items: [
- {
- id: '101',
- name: '空气感中长发',
- category: 'MEDIUM',
- status: 'PUBLIC',
- description: '空气感提案',
- tags: ['空气感', '脸周'],
- image: 'https://cdn.example.com/101.jpg',
- makeTaskId: '88',
- makeTaskStatus: 'STEP2_SUCCESS',
- usedCount: 5,
- },
- ],
- total: 1,
- page: 1,
- pageSize: 1,
- });
- });
- it('/api/v1/templates/summary (GET) returns real template dashboard summary', async () => {
- const token = await signToken();
- hairTemplates.push(
- {
- id: 101n,
- templateName: '空气感中长发',
- category: 'MEDIUM',
- status: 'PUBLIC',
- description: '空气感提案',
- tags: '空气感,脸周',
- coverUrl: 'https://cdn.example.com/101.jpg',
- templateUrl: 'https://cdn.example.com/101.zip',
- usedCount: 5,
- hairstylistId: 1n,
- isDeleted: 0,
- },
- {
- id: 102n,
- templateName: '厚重短发',
- category: 'SHORT',
- status: 'DRAFT',
- description: '短发提案',
- tags: '短发',
- coverUrl: 'https://cdn.example.com/102.jpg',
- templateUrl: 'https://cdn.example.com/102.zip',
- usedCount: 1,
- hairstylistId: 1n,
- isDeleted: 0,
- },
- );
- aiHairSimulationTasks.push(
- {
- id: 501n,
- templateId: '101',
- taskStatus: 'SUCCESSES',
- taskFinishTime: new Date(),
- isDeleted: 0,
- },
- {
- id: 502n,
- templateId: '999',
- taskStatus: 'SUCCESSES',
- taskFinishTime: new Date(),
- isDeleted: 0,
- },
- );
- const response = await request(app.getHttpServer())
- .get('/api/v1/templates/summary')
- .set('Authorization', `Bearer ${token}`)
- .expect(200);
- expect(response.body.data).toEqual({
- total: 2,
- published: 1,
- weeklyUsed: 1,
- draft: 1,
- });
- });
- it('/api/v1/templates/:templateId (DELETE) soft deletes owned template', async () => {
- const token = await signToken();
- hairTemplates.push({
- id: 101n,
- templateName: '空气感中长发',
- category: 'MEDIUM',
- status: 'PUBLIC',
- description: '空气感提案',
- tags: '空气感,脸周',
- coverUrl: 'https://cdn.example.com/101.jpg',
- templateUrl: 'https://cdn.example.com/101.zip',
- hairstylistId: 1n,
- isDeleted: 0,
- });
- hairTemplateMakeTasks.push({
- id: 88n,
- templateId: 101n,
- taskStatus: 'STEP2_SUCCESS',
- isDeleted: 0,
- });
- const response = await request(app.getHttpServer())
- .delete('/api/v1/templates/101')
- .set('Authorization', `Bearer ${token}`)
- .expect(200);
- expect(response.body.data).toEqual({ success: true });
- expect(hairTemplates.find((item) => item.id === 101n)?.isDeleted).toBe(1);
- expect(hairTemplateMakeTasks.find((item) => item.id === 88n)?.isDeleted).toBe(1);
- });
- it('/api/v1/templates/bulk-delete (POST) soft deletes current stylist templates only', async () => {
- const token = await signToken();
- hairTemplates.push(
- {
- id: 101n,
- templateName: '空气感中长发',
- category: 'MEDIUM',
- status: 'PUBLIC',
- description: '空气感提案',
- tags: '空气感,脸周',
- coverUrl: 'https://cdn.example.com/101.jpg',
- templateUrl: 'https://cdn.example.com/101.zip',
- hairstylistId: 1n,
- isDeleted: 0,
- },
- {
- id: 102n,
- templateName: '厚重短发',
- category: 'SHORT',
- status: 'DRAFT',
- description: '短发提案',
- tags: '短发',
- coverUrl: 'https://cdn.example.com/102.jpg',
- templateUrl: 'https://cdn.example.com/102.zip',
- hairstylistId: 1n,
- isDeleted: 0,
- },
- {
- id: 103n,
- templateName: '其他美发师模板',
- category: 'MEDIUM',
- status: 'PUBLIC',
- description: '不应删除',
- tags: '空气感',
- coverUrl: 'https://cdn.example.com/103.jpg',
- templateUrl: 'https://cdn.example.com/103.zip',
- hairstylistId: 2n,
- isDeleted: 0,
- },
- );
- hairTemplateMakeTasks.push(
- {
- id: 88n,
- templateId: 101n,
- taskStatus: 'STEP2_SUCCESS',
- isDeleted: 0,
- },
- {
- id: 89n,
- templateId: 102n,
- taskStatus: 'PROCESSING',
- isDeleted: 0,
- },
- {
- id: 90n,
- templateId: 103n,
- taskStatus: 'STEP2_SUCCESS',
- isDeleted: 0,
- },
- );
- const response = await request(app.getHttpServer())
- .post('/api/v1/templates/bulk-delete')
- .set('Authorization', `Bearer ${token}`)
- .send({
- ids: ['101', '102', '103'],
- })
- .expect(201);
- expect(response.body.data).toEqual({ deletedCount: 2 });
- expect(hairTemplates.find((item) => item.id === 101n)?.isDeleted).toBe(1);
- expect(hairTemplates.find((item) => item.id === 102n)?.isDeleted).toBe(1);
- expect(hairTemplates.find((item) => item.id === 103n)?.isDeleted).toBe(0);
- expect(hairTemplateMakeTasks.find((item) => item.id === 88n)?.isDeleted).toBe(1);
- expect(hairTemplateMakeTasks.find((item) => item.id === 89n)?.isDeleted).toBe(1);
- expect(hairTemplateMakeTasks.find((item) => item.id === 90n)?.isDeleted).toBe(0);
- });
- it('/api/v1/templates/make-tasks/:taskId (GET) promotes step1 success and returns detail', async () => {
- const token = await signToken();
- hairTemplates.push({
- id: 101n,
- templateName: '柔和层次长发',
- coverUrl: '',
- templateUrl: '',
- status: 'DRAFT',
- hairstylistId: 1n,
- isDeleted: 0,
- });
- hairTemplateMakeTasks.push({
- id: 88n,
- totalTaskId: 'tmpl_123',
- taskStatus: 'PROCESSING',
- templateId: 101n,
- step1TaskId: 'step1-task-1',
- step2TaskId: '',
- refHairUrl: 'https://cdn.example.com/ref.jpg',
- modelUrl: 'https://cdn.example.com/model.jpg',
- clothesUrl: 'https://cdn.example.com/clothes.jpg',
- backgroundUrl: 'https://cdn.example.com/background.jpg',
- step1ResultImg: '',
- step2ResultImg: '',
- step1Resource: JSON.stringify({
- templateUrl: 'https://cdn.example.com/template-step1.json',
- }),
- step2Resource: JSON.stringify({}),
- createTime: new Date('2026-06-28T12:00:00.000Z'),
- updateTime: new Date(Date.now() - 20_000),
- isDeleted: 0,
- });
- getResult.mockResolvedValueOnce({
- status: 'successes',
- message: '',
- images: ['https://cdn.example.com/step1-result.jpg'],
- });
- templateTaskFiles.downloadToTempFile.mockResolvedValue(
- '/tmp/template-make/tmpl_123/hairstyle.jpg',
- );
- const response = await request(app.getHttpServer())
- .get('/api/v1/templates/make-tasks/88')
- .set('Authorization', `Bearer ${token}`)
- .expect(200);
- expect(getResult).toHaveBeenCalledWith('step1-task-1');
- expect(response.body.data).toEqual(
- expect.objectContaining({
- id: '88',
- templateId: '101',
- taskStatus: 'STEP1_SUCCESS',
- step1: expect.objectContaining({
- taskId: 'step1-task-1',
- status: 'STEP1_SUCCESS',
- resultImage: 'https://cdn.example.com/step1-result.jpg',
- }),
- }),
- );
- });
- });
|