|
|
@@ -0,0 +1,264 @@
|
|
|
+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 { AppModule } from '../src/app.module';
|
|
|
+import { AiSimulationClient } from '../src/modules/simulations/ai-simulation.client';
|
|
|
+import { PrismaService } from '../src/prisma/prisma.service';
|
|
|
+
|
|
|
+describe('Simulations (e2e)', () => {
|
|
|
+ let app: INestApplication;
|
|
|
+ let jwtService: JwtService;
|
|
|
+ let prisma: PrismaService;
|
|
|
+ const execute = jest.fn();
|
|
|
+ const getResult = jest.fn();
|
|
|
+
|
|
|
+ async function signToken() {
|
|
|
+ return jwtService.signAsync({
|
|
|
+ sub: 1,
|
|
|
+ stylistId: '1',
|
|
|
+ email: 'stylist@example.com',
|
|
|
+ username: 'Ogawa',
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ beforeAll(async () => {
|
|
|
+ execute.mockReset();
|
|
|
+ getResult.mockReset();
|
|
|
+
|
|
|
+ const moduleRef = await Test.createTestingModule({
|
|
|
+ imports: [AppModule],
|
|
|
+ })
|
|
|
+ .overrideProvider(AiSimulationClient)
|
|
|
+ .useValue({ execute, getResult })
|
|
|
+ .compile();
|
|
|
+
|
|
|
+ app = moduleRef.createNestApplication();
|
|
|
+ configureApp(app);
|
|
|
+ await app.init();
|
|
|
+ jwtService = app.get(JwtService);
|
|
|
+ prisma = app.get(PrismaService);
|
|
|
+ });
|
|
|
+
|
|
|
+ afterAll(async () => {
|
|
|
+ await prisma.aiHairSimulationTask.deleteMany({
|
|
|
+ where: {
|
|
|
+ taskId: {
|
|
|
+ startsWith: 'sim-e2e-task-',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ await prisma.aiHairSimulation.deleteMany({
|
|
|
+ where: {
|
|
|
+ simulateRemark: {
|
|
|
+ startsWith: 'sim-e2e-',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ await prisma.hairTemplate.deleteMany({
|
|
|
+ where: {
|
|
|
+ templateName: {
|
|
|
+ startsWith: 'sim-e2e-',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ await prisma.customer.deleteMany({
|
|
|
+ where: {
|
|
|
+ name: {
|
|
|
+ startsWith: 'sg-',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ await app.close();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('/api/v1/simulations (POST) creates an anonymous simulation', async () => {
|
|
|
+ const token = await signToken();
|
|
|
+ const uniqueSuffix = Date.now();
|
|
|
+ const template = await prisma.hairTemplate.create({
|
|
|
+ data: {
|
|
|
+ templateName: `sim-e2e-template-${uniqueSuffix}`,
|
|
|
+ coverUrl: '',
|
|
|
+ status: 'PUBLIC',
|
|
|
+ category: 'SHORT',
|
|
|
+ tags: '',
|
|
|
+ description: null,
|
|
|
+ modelUrl: '',
|
|
|
+ clothesUrl: '',
|
|
|
+ backgroundUrl: '',
|
|
|
+ refHairUrl: '',
|
|
|
+ hairstylistId: BigInt(1),
|
|
|
+ templateUrl: `https://cdn.example.com/${uniqueSuffix}.zip`,
|
|
|
+ isSystemTemplate: 'NO',
|
|
|
+ } as never,
|
|
|
+ });
|
|
|
+ execute.mockResolvedValueOnce({
|
|
|
+ taskId: `sim-e2e-task-${uniqueSuffix}`,
|
|
|
+ });
|
|
|
+
|
|
|
+ const response = await request(app.getHttpServer())
|
|
|
+ .post('/api/v1/simulations')
|
|
|
+ .set('Authorization', `Bearer ${token}`)
|
|
|
+ .send({
|
|
|
+ originImage: 'https://cdn.example.com/origin.jpg',
|
|
|
+ templateIds: [template.id.toString()],
|
|
|
+ simulateRemark: `sim-e2e-${uniqueSuffix}`,
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(201);
|
|
|
+
|
|
|
+ expect(execute).toHaveBeenCalledWith(
|
|
|
+ template.templateUrl,
|
|
|
+ 'https://cdn.example.com/origin.jpg',
|
|
|
+ );
|
|
|
+ expect(response.body.data).toEqual({
|
|
|
+ simulationId: expect.any(String),
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it('/api/v1/simulations/:id (GET) returns task detail for the owner', async () => {
|
|
|
+ const token = await signToken();
|
|
|
+ const uniqueSuffix = `${Date.now()}-detail`;
|
|
|
+ const template = await prisma.hairTemplate.create({
|
|
|
+ data: {
|
|
|
+ templateName: `sim-e2e-template-${uniqueSuffix}`,
|
|
|
+ coverUrl: 'https://cdn.example.com/cover.png',
|
|
|
+ status: 'PUBLIC',
|
|
|
+ category: 'MEDIUM',
|
|
|
+ tags: 'airy',
|
|
|
+ description: 'detail template',
|
|
|
+ modelUrl: '',
|
|
|
+ clothesUrl: '',
|
|
|
+ backgroundUrl: '',
|
|
|
+ refHairUrl: '',
|
|
|
+ hairstylistId: BigInt(1),
|
|
|
+ templateUrl: `https://cdn.example.com/${uniqueSuffix}.zip`,
|
|
|
+ isSystemTemplate: 'NO',
|
|
|
+ } as never,
|
|
|
+ });
|
|
|
+ execute.mockResolvedValueOnce({
|
|
|
+ taskId: `sim-e2e-task-${uniqueSuffix}`,
|
|
|
+ });
|
|
|
+
|
|
|
+ const created = await request(app.getHttpServer())
|
|
|
+ .post('/api/v1/simulations')
|
|
|
+ .set('Authorization', `Bearer ${token}`)
|
|
|
+ .send({
|
|
|
+ originImage: 'https://cdn.example.com/origin-detail.jpg',
|
|
|
+ templateIds: [template.id.toString()],
|
|
|
+ simulateRemark: `sim-e2e-${uniqueSuffix}`,
|
|
|
+ })
|
|
|
+ .expect(201);
|
|
|
+
|
|
|
+ const response = await request(app.getHttpServer())
|
|
|
+ .get(`/api/v1/simulations/${created.body.data.simulationId}`)
|
|
|
+ .set('Authorization', `Bearer ${token}`)
|
|
|
+ .expect(200);
|
|
|
+
|
|
|
+ expect(response.body.data).toMatchObject({
|
|
|
+ id: created.body.data.simulationId,
|
|
|
+ createdAt: expect.any(String),
|
|
|
+ originImage: 'https://cdn.example.com/origin-detail.jpg',
|
|
|
+ totalTaskCount: 1,
|
|
|
+ processingTaskCount: 1,
|
|
|
+ successTaskCount: 0,
|
|
|
+ failedTaskCount: 0,
|
|
|
+ overallStatus: 'PROCESSING',
|
|
|
+ tasks: [
|
|
|
+ expect.objectContaining({
|
|
|
+ templateId: template.id.toString(),
|
|
|
+ templateName: template.templateName,
|
|
|
+ templateCategory: template.category,
|
|
|
+ templateSource: 'STYLIST',
|
|
|
+ taskStatus: 'PROCESSING',
|
|
|
+ resultImages: [],
|
|
|
+ }),
|
|
|
+ ],
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it('/api/v1/simulations/customer/:customerId/gallery (GET) returns flattened result cards', async () => {
|
|
|
+ const token = await signToken();
|
|
|
+ const uniqueSuffix = `${Date.now()}-gallery`;
|
|
|
+ const customer = await prisma.customer.create({
|
|
|
+ data: {
|
|
|
+ hairstylistId: BigInt(1),
|
|
|
+ name: `sg-${Date.now()}`,
|
|
|
+ namePronunciation: '',
|
|
|
+ phone: `090-${String(Date.now()).slice(-4)}-3333`,
|
|
|
+ email: '',
|
|
|
+ avatar: 'https://cdn.example.com/customer.png',
|
|
|
+ expectContact: 'LINE',
|
|
|
+ planDate: new Date(),
|
|
|
+ nextFollowDate: new Date(),
|
|
|
+ } as never,
|
|
|
+ });
|
|
|
+ const template = await prisma.hairTemplate.create({
|
|
|
+ data: {
|
|
|
+ templateName: `sim-e2e-template-${uniqueSuffix}`,
|
|
|
+ coverUrl: 'https://cdn.example.com/cover.png',
|
|
|
+ status: 'PUBLIC',
|
|
|
+ category: 'SHORT',
|
|
|
+ tags: 'airy',
|
|
|
+ description: 'gallery template',
|
|
|
+ modelUrl: '',
|
|
|
+ clothesUrl: '',
|
|
|
+ backgroundUrl: '',
|
|
|
+ refHairUrl: '',
|
|
|
+ hairstylistId: BigInt(1),
|
|
|
+ templateUrl: `https://cdn.example.com/${uniqueSuffix}.zip`,
|
|
|
+ isSystemTemplate: 'NO',
|
|
|
+ } as never,
|
|
|
+ });
|
|
|
+ execute.mockResolvedValueOnce({
|
|
|
+ taskId: `sim-e2e-task-${uniqueSuffix}`,
|
|
|
+ });
|
|
|
+
|
|
|
+ const created = await request(app.getHttpServer())
|
|
|
+ .post('/api/v1/simulations')
|
|
|
+ .set('Authorization', `Bearer ${token}`)
|
|
|
+ .send({
|
|
|
+ customerId: customer.id.toString(),
|
|
|
+ originImage: 'https://cdn.example.com/origin-gallery.jpg',
|
|
|
+ templateIds: [template.id.toString()],
|
|
|
+ simulateRemark: `sim-e2e-${uniqueSuffix}`,
|
|
|
+ })
|
|
|
+ .expect(201);
|
|
|
+
|
|
|
+ await prisma.aiHairSimulationTask.updateMany({
|
|
|
+ where: {
|
|
|
+ simulationId: BigInt(created.body.data.simulationId),
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ taskStatus: 'SUCCESSES',
|
|
|
+ resultImages: JSON.stringify([
|
|
|
+ 'https://cdn.example.com/gallery-2.png',
|
|
|
+ 'https://cdn.example.com/gallery-1.png',
|
|
|
+ ]),
|
|
|
+ } as never,
|
|
|
+ });
|
|
|
+
|
|
|
+ const response = await request(app.getHttpServer())
|
|
|
+ .get(`/api/v1/simulations/customer/${customer.id}/gallery`)
|
|
|
+ .set('Authorization', `Bearer ${token}`);
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ expect(response.body.data.items).toEqual(
|
|
|
+ expect.arrayContaining([
|
|
|
+ expect.objectContaining({
|
|
|
+ simulationId: created.body.data.simulationId,
|
|
|
+ imageUrl: 'https://cdn.example.com/gallery-2.png',
|
|
|
+ templateName: template.templateName,
|
|
|
+ taskStatus: 'SUCCESSES',
|
|
|
+ }),
|
|
|
+ expect.objectContaining({
|
|
|
+ simulationId: created.body.data.simulationId,
|
|
|
+ imageUrl: 'https://cdn.example.com/gallery-1.png',
|
|
|
+ templateName: template.templateName,
|
|
|
+ taskStatus: 'SUCCESSES',
|
|
|
+ }),
|
|
|
+ ]),
|
|
|
+ );
|
|
|
+ });
|
|
|
+});
|