diff --git a/package-lock.json b/package-lock.json index 38b8b76d..e9ba32a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1442,6 +1442,15 @@ "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", "dev": true }, + "@types/multer": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.5.tgz", + "integrity": "sha512-9b/0a8JyrR0r2nQhL73JR86obWL7cogfX12augvlrvcpciCo/hkvEsgu80Z4S2g2DHGVXHr8pUIi1VhqFJ8Ufw==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/node": { "version": "14.14.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.14.tgz", diff --git a/package.json b/package.json index 95855c23..2d62494f 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@types/express": "^4.17.7", "@types/jest": "26.0.14", "@types/lodash": "^4.14.168", + "@types/multer": "^1.4.5", "@types/node": "^14.0.27", "@types/passport-jwt": "^3.0.3", "@types/passport-local": "^1.0.33", diff --git a/src/shared/api-file.decorator.ts b/src/shared/api-file.decorator.ts new file mode 100644 index 00000000..4f0297c8 --- /dev/null +++ b/src/shared/api-file.decorator.ts @@ -0,0 +1,31 @@ +import { ApiPropertyOptions, ApiProperty } from "@nestjs/swagger"; + +export const ApiFile = (options?: ApiPropertyOptions): PropertyDecorator => ( + target: Object, + propertyKey: string | symbol, + ) => { + if (options?.isArray) { + ApiProperty({ + type: 'array', + items: { + type: 'file', + properties: { + [propertyKey]: { + type: 'string', + format: 'binary', + }, + }, + }, + })(target, propertyKey); + } else { + ApiProperty({ + type: 'file', + properties: { + [propertyKey]: { + type: 'string', + format: 'binary', + }, + }, + })(target, propertyKey); + } + }; \ No newline at end of file diff --git a/src/shared/fite-to-body.interceptor.ts b/src/shared/fite-to-body.interceptor.ts new file mode 100644 index 00000000..2e6f906b --- /dev/null +++ b/src/shared/fite-to-body.interceptor.ts @@ -0,0 +1,38 @@ +import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from "@nestjs/common"; +import { Observable } from "rxjs"; + +@Injectable() +export class FilesToBodyInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const ctx = context.switchToHttp(); + const req = ctx.getRequest(); + if (req.body && Array.isArray(req.files) && req.files.length) { + req.files.forEach((file: Express.Multer.File) => { + const { fieldname } = file; + if (!req.body[fieldname]) { + req.body[fieldname] = [file]; + } else { + req.body[fieldname].push(file); + } + }); + } + + return next.handle(); + } +} + +@Injectable() +export class FileToBodyInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const ctx = context.switchToHttp(); + const req = ctx.getRequest(); + if (req.body && req.file?.fieldname) { + const { fieldname } = req.file; + if (!req.body[fieldname]) { + req.body[fieldname] = req.file; + } + } + + return next.handle(); + } +} \ No newline at end of file diff --git a/src/test-runs/dto/create-test-request-base64.dto.ts b/src/test-runs/dto/create-test-request-base64.dto.ts new file mode 100644 index 00000000..5b578342 --- /dev/null +++ b/src/test-runs/dto/create-test-request-base64.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsBase64 } from 'class-validator'; +import { CreateTestRequestDto } from './create-test-request.dto'; + +export class CreateTestRequestBase64Dto extends CreateTestRequestDto { + @ApiProperty() + @Transform((value) => value.replace(/(\r\n|\n|\r)/gm, '')) + @IsBase64() + imageBase64: string; +} diff --git a/src/test-runs/dto/create-test-request-multipart.dto.ts b/src/test-runs/dto/create-test-request-multipart.dto.ts new file mode 100644 index 00000000..ebf0603a --- /dev/null +++ b/src/test-runs/dto/create-test-request-multipart.dto.ts @@ -0,0 +1,7 @@ +import { ApiFile } from '../../shared/api-file.decorator'; +import { CreateTestRequestDto } from './create-test-request.dto'; + +export class CreateTestRequestMultipartDto extends CreateTestRequestDto { + @ApiFile() + image: Express.Multer.File; +} diff --git a/src/test-runs/dto/create-test-request.dto.ts b/src/test-runs/dto/create-test-request.dto.ts index 187e1016..6b9cf6d8 100644 --- a/src/test-runs/dto/create-test-request.dto.ts +++ b/src/test-runs/dto/create-test-request.dto.ts @@ -1,15 +1,10 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsOptional, IsUUID, IsNumber, IsBoolean, IsBase64 } from 'class-validator'; +import { IsOptional, IsUUID, IsNumber, IsBoolean } from 'class-validator'; import { BaselineDataDto } from '../../shared/dto/baseline-data.dto'; import { IgnoreAreaDto } from './ignore-area.dto'; export class CreateTestRequestDto extends BaselineDataDto { - @ApiProperty() - @Transform((value) => value.replace(/(\r\n|\n|\r)/gm, '')) - @IsBase64() - imageBase64: string; - @ApiProperty() @IsUUID() buildId: string; @@ -21,11 +16,22 @@ export class CreateTestRequestDto extends BaselineDataDto { @ApiPropertyOptional() @IsOptional() @IsNumber() + @Transform((it) => parseFloat(it)) diffTollerancePercent?: number; @ApiPropertyOptional() @IsBoolean() @IsOptional() + @Transform((it) => { + switch (it) { + case 'true': + return true; + case 'false': + return false; + default: + return it; + } + }) merge?: boolean; @ApiPropertyOptional({ type: [IgnoreAreaDto] }) diff --git a/src/test-runs/test-runs.controller.ts b/src/test-runs/test-runs.controller.ts index 22faa601..37bfa940 100644 --- a/src/test-runs/test-runs.controller.ts +++ b/src/test-runs/test-runs.controller.ts @@ -10,9 +10,21 @@ import { Query, Post, ParseBoolPipe, - ParseIntPipe, + UseInterceptors, + UploadedFile, + UsePipes, + ValidationPipe, } from '@nestjs/common'; -import { ApiTags, ApiParam, ApiBearerAuth, ApiQuery, ApiSecurity, ApiOkResponse } from '@nestjs/swagger'; +import { + ApiTags, + ApiParam, + ApiBearerAuth, + ApiQuery, + ApiSecurity, + ApiOkResponse, + ApiConsumes, + ApiBody, +} from '@nestjs/swagger'; import { JwtAuthGuard } from '../auth/guards/auth.guard'; import { TestRun, TestStatus } from '@prisma/client'; import { TestRunsService } from './test-runs.service'; @@ -20,8 +32,11 @@ import { IgnoreAreaDto } from './dto/ignore-area.dto'; import { CommentDto } from '../shared/dto/comment.dto'; import { TestRunResultDto } from './dto/testRunResult.dto'; import { ApiGuard } from '../auth/guards/api.guard'; -import { CreateTestRequestDto } from './dto/create-test-request.dto'; import { TestRunDto } from './dto/testRun.dto'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { CreateTestRequestBase64Dto } from './dto/create-test-request-base64.dto'; +import { CreateTestRequestMultipartDto } from './dto/create-test-request-multipart.dto'; +import { FileToBodyInterceptor } from '../shared/fite-to-body.interceptor'; @ApiTags('test-runs') @Controller('test-runs') @@ -87,7 +102,24 @@ export class TestRunsController { @ApiSecurity('api_key') @ApiOkResponse({ type: TestRunResultDto }) @UseGuards(ApiGuard) - postTestRun(@Body() createTestRequestDto: CreateTestRequestDto): Promise { - return this.testRunsService.postTestRun(createTestRequestDto); + postTestRun(@Body() createTestRequestDto: CreateTestRequestBase64Dto): Promise { + const imageBuffer = Buffer.from(createTestRequestDto.imageBase64, 'base64'); + return this.testRunsService.postTestRun({ + createTestRequestDto, + imageBuffer, + }); + } + + @Post('/multipart') + @ApiSecurity('api_key') + @ApiBody({ type: CreateTestRequestMultipartDto }) + @ApiOkResponse({ type: TestRunResultDto }) + @ApiConsumes('multipart/form-data') + @UseGuards(ApiGuard) + @UseInterceptors(FileInterceptor('image'), FileToBodyInterceptor) + @UsePipes(new ValidationPipe({ transform: true })) + postTestRunMultipart(@Body() createTestRequestDto: CreateTestRequestMultipartDto): Promise { + const imageBuffer = createTestRequestDto.image.buffer; + return this.testRunsService.postTestRun({ createTestRequestDto, imageBuffer }); } } diff --git a/src/test-runs/test-runs.service.spec.ts b/src/test-runs/test-runs.service.spec.ts index 7c5ed654..c25c327a 100644 --- a/src/test-runs/test-runs.service.spec.ts +++ b/src/test-runs/test-runs.service.spec.ts @@ -17,6 +17,7 @@ import { BuildsService } from '../builds/builds.service'; import { TEST_PROJECT } from '../_data_'; import { getTestVariationUniqueData } from '../utils'; import { BaselineDataDto } from '../shared/dto/baseline-data.dto'; +import { CreateTestRequestBase64Dto } from './dto/create-test-request-base64.dto'; jest.mock('pixelmatch'); jest.mock('./dto/testRunResult.dto'); @@ -103,6 +104,7 @@ const initService = async ({ }; describe('TestRunsService', () => { let service: TestRunsService; + const imageBuffer = Buffer.from('Image'); const ignoreAreas = [{ x: 1, y: 2, width: 10, height: 20 }]; const tempIgnoreAreas = [{ x: 3, y: 4, width: 30, height: 40 }]; const baseTestRun: TestRun = { @@ -184,7 +186,6 @@ describe('TestRunsService', () => { buildId: 'buildId', projectId: 'projectId', name: 'Test name', - imageBase64: 'Image', os: 'OS', browser: 'browser', viewport: 'viewport', @@ -259,9 +260,9 @@ describe('TestRunsService', () => { const tryAutoApproveByNewBaselines = jest.fn(); service['tryAutoApproveByNewBaselines'] = tryAutoApproveByNewBaselines.mockResolvedValueOnce(testRunWithResult); - const result = await service.create(testVariation, createTestRequestDto); + const result = await service.create({ testVariation, createTestRequestDto, imageBuffer }); - expect(saveImageMock).toHaveBeenCalledWith('screenshot', Buffer.from(createTestRequestDto.imageBase64, 'base64')); + expect(saveImageMock).toHaveBeenCalledWith('screenshot', imageBuffer); expect(testRunCreateMock).toHaveBeenCalledWith({ data: { imageName, @@ -722,7 +723,6 @@ describe('TestRunsService', () => { buildId: 'buildId', projectId: 'projectId', name: 'Test name', - imageBase64: 'Image', os: 'OS', browser: 'browser', viewport: 'viewport', @@ -789,7 +789,7 @@ describe('TestRunsService', () => { branchName: createTestRequestDto.branchName, }; - await service.postTestRun(createTestRequestDto); + await service.postTestRun({ createTestRequestDto, imageBuffer }); expect(testVariationFindOrCreateMock).toHaveBeenCalledWith(createTestRequestDto.projectId, baselineData); expect(testRunFindManyMock).toHaveBeenCalledWith({ @@ -800,7 +800,7 @@ describe('TestRunsService', () => { }, }); expect(deleteMock).toHaveBeenCalledWith(testRun.id); - expect(createMock).toHaveBeenCalledWith(testVariation, createTestRequestDto); + expect(createMock).toHaveBeenCalledWith({ testVariation, createTestRequestDto, imageBuffer }); expect(service.calculateDiff).toHaveBeenCalledWith(testRun); expect(service['tryAutoApproveByPastBaselines']).toHaveBeenCalledWith(testVariation, testRun); expect(service['tryAutoApproveByNewBaselines']).toHaveBeenCalledWith(testVariation, testRun); diff --git a/src/test-runs/test-runs.service.ts b/src/test-runs/test-runs.service.ts index 3666aa27..350dc5b0 100644 --- a/src/test-runs/test-runs.service.ts +++ b/src/test-runs/test-runs.service.ts @@ -48,7 +48,13 @@ export class TestRunsService { }); } - async postTestRun(createTestRequestDto: CreateTestRequestDto): Promise { + async postTestRun({ + createTestRequestDto, + imageBuffer, + }: { + createTestRequestDto: CreateTestRequestDto; + imageBuffer: Buffer; + }): Promise { // creates variatioin if does not exist const testVariation = await this.testVariationService.findOrCreate(createTestRequestDto.projectId, { ...getTestVariationUniqueData(createTestRequestDto), @@ -69,7 +75,7 @@ export class TestRunsService { } // create test run result - const testRun = await this.create(testVariation, createTestRequestDto); + const testRun = await this.create({ testVariation, createTestRequestDto, imageBuffer }); // calculate diff let testRunWithResult = await this.calculateDiff(testRun); @@ -157,9 +163,16 @@ export class TestRunsService { return this.saveDiffResult(testRun.id, diffResult); } - async create(testVariation: TestVariation, createTestRequestDto: CreateTestRequestDto): Promise { + async create({ + testVariation, + createTestRequestDto, + imageBuffer, + }: { + testVariation: TestVariation; + createTestRequestDto: CreateTestRequestDto; + imageBuffer: Buffer; + }): Promise { // save image - const imageBuffer = Buffer.from(createTestRequestDto.imageBase64, 'base64'); const imageName = this.staticService.saveImage('screenshot', imageBuffer); const testRun = await this.prismaService.testRun.create({ diff --git a/src/test-variations/test-variations.service.spec.ts b/src/test-variations/test-variations.service.spec.ts index 81fb69f7..92611231 100644 --- a/src/test-variations/test-variations.service.spec.ts +++ b/src/test-variations/test-variations.service.spec.ts @@ -122,7 +122,6 @@ describe('TestVariationsService', () => { buildId: 'buildId', projectId: projectMock.id, name: 'Test name', - imageBase64: 'Image', os: 'OS', browser: 'browser', viewport: 'viewport', @@ -181,7 +180,6 @@ describe('TestVariationsService', () => { buildId: 'buildId', projectId: projectMock.id, name: 'Test name', - imageBase64: 'Image', os: 'OS', browser: 'browser', viewport: 'viewport', @@ -240,7 +238,6 @@ describe('TestVariationsService', () => { buildId: 'buildId', projectId: projectMock.id, name: 'Test name', - imageBase64: 'Image', os: 'OS', browser: 'browser', viewport: 'viewport', @@ -317,7 +314,6 @@ describe('TestVariationsService', () => { buildId: 'buildId', projectId: projectMock.id, name: 'Test name', - imageBase64: 'Image', os: 'OS', browser: 'browser', viewport: 'viewport', @@ -529,21 +525,27 @@ describe('TestVariationsService', () => { }); await new Promise((r) => setTimeout(r, 1)); - expect(testRunCreateMock).toHaveBeenNthCalledWith(1, testVariationMainBranch, { - ...testVariation, - buildId: build.id, - imageBase64: PNG.sync.write(image).toString('base64'), - diffTollerancePercent: 0, - merge: true, - ignoreAreas: JSON.parse(testVariation.ignoreAreas), + expect(testRunCreateMock).toHaveBeenNthCalledWith(1, { + testVariation: testVariationMainBranch, + createTestRequestDto: { + ...testVariation, + buildId: build.id, + diffTollerancePercent: 0, + merge: true, + ignoreAreas: JSON.parse(testVariation.ignoreAreas), + }, + imageBuffer: PNG.sync.write(image), }); - expect(testRunCreateMock).toHaveBeenNthCalledWith(2, testVariationMainBranch, { - ...testVariationSecond, - buildId: build.id, - imageBase64: PNG.sync.write(image).toString('base64'), - diffTollerancePercent: 0, - merge: true, - ignoreAreas: JSON.parse(testVariationSecond.ignoreAreas), + expect(testRunCreateMock).toHaveBeenNthCalledWith(2, { + testVariation: testVariationMainBranch, + createTestRequestDto: { + ...testVariationSecond, + buildId: build.id, + diffTollerancePercent: 0, + merge: true, + ignoreAreas: JSON.parse(testVariationSecond.ignoreAreas), + }, + imageBuffer: PNG.sync.write(image), }); expect(testRunCreateMock).toHaveBeenCalledTimes(2); expect(buildUpdateMock).toHaveBeenCalledWith(build.id, { isRunning: false }); diff --git a/src/test-variations/test-variations.service.ts b/src/test-variations/test-variations.service.ts index 72c9ddbe..d5d4473e 100644 --- a/src/test-variations/test-variations.service.ts +++ b/src/test-variations/test-variations.service.ts @@ -211,8 +211,6 @@ export class TestVariationsService { const baseline = this.staticService.getImage(sideBranchTestVariation.baselineName); if (baseline) { try { - const imageBase64 = PNG.sync.write(baseline).toString('base64'); - // get main branch variation const mainBranchTestVariation = await this.findOrCreate(projectId, { ...getTestVariationUniqueData(sideBranchTestVariation), @@ -223,13 +221,16 @@ export class TestVariationsService { const createTestRequestDto: CreateTestRequestDto = { ...sideBranchTestVariation, buildId: build.id, - imageBase64, diffTollerancePercent: 0, merge: true, ignoreAreas: JSON.parse(sideBranchTestVariation.ignoreAreas), }; - return this.testRunsService.create(mainBranchTestVariation, createTestRequestDto); + return this.testRunsService.create({ + testVariation: mainBranchTestVariation, + createTestRequestDto, + imageBuffer: PNG.sync.write(baseline), + }); } catch (err) { console.log(err); } diff --git a/test/builds.e2e-spec.ts b/test/builds.e2e-spec.ts index a6685883..82875ffc 100644 --- a/test/builds.e2e-spec.ts +++ b/test/builds.e2e-spec.ts @@ -56,7 +56,8 @@ describe('Builds (e2e)', () => { branchName: 'branchName', project: project.id, }; - return requestWithApiKey(app, 'post', '/builds', createBuildDto, user.apiKey) + return requestWithApiKey(app, 'post', '/builds', user.apiKey) + .send(createBuildDto) .expect(201) .expect((res) => { expect(res.body.projectId).toBe(project.id); @@ -73,7 +74,8 @@ describe('Builds (e2e)', () => { branchName: 'branchName', project: project.name, }; - return requestWithApiKey(app, 'post', '/builds', createBuildDto, user.apiKey) + return requestWithApiKey(app, 'post', '/builds', user.apiKey) + .send(createBuildDto) .expect(201) .expect((res) => { expect(res.body.projectId).toBe(project.id); @@ -93,7 +95,8 @@ describe('Builds (e2e)', () => { }; const build = await buildsService.create(createBuildDto); - return requestWithApiKey(app, 'post', '/builds', createBuildDto, user.apiKey) + return requestWithApiKey(app, 'post', '/builds', user.apiKey) + .send(createBuildDto) .expect(201) .expect((res) => { expect(res.body.id).toBe(build.id); @@ -112,7 +115,7 @@ describe('Builds (e2e)', () => { branchName: 'branchName', project: 'random', }; - return requestWithApiKey(app, 'post', '/builds', createBuildDto, user.apiKey).expect(404); + return requestWithApiKey(app, 'post', '/builds', user.apiKey).send(createBuildDto).expect(404); }); it('403', () => { @@ -120,7 +123,7 @@ describe('Builds (e2e)', () => { branchName: 'branchName', project: project.id, }; - return requestWithApiKey(app, 'post', '/builds', createBuildDto, '').expect(403); + return requestWithApiKey(app, 'post', '/builds', '').send(createBuildDto).expect(403); }); }); @@ -128,7 +131,8 @@ describe('Builds (e2e)', () => { it('200', async () => { const build = await buildsService.create({ project: project.id, branchName: 'develop' }); - return requestWithAuth(app, 'get', `/builds?projectId=${project.id}&take=${5}&skip=${0}`, {}, user.token) + return requestWithAuth(app, 'get', `/builds?projectId=${project.id}&take=${5}&skip=${0}`, user.token) + .send() .expect(200) .expect((res) => { expect(JSON.stringify(res.body)).toEqual( @@ -143,7 +147,7 @@ describe('Builds (e2e)', () => { }); it('401', async () => { - return requestWithAuth(app, 'get', `/builds?projectId=${project.id}&take=${5}&skip=${0}`, {}, '').expect(401); + return requestWithAuth(app, 'get', `/builds?projectId=${project.id}&take=${5}&skip=${0}`, '').send().expect(401); }); }); @@ -151,7 +155,8 @@ describe('Builds (e2e)', () => { it('200', async () => { const build = await buildsService.create({ project: project.id, branchName: 'develop' }); - return requestWithAuth(app, 'get', `/builds/${build.id}`, {}, user.token) + return requestWithAuth(app, 'get', `/builds/${build.id}`, user.token) + .send() .expect(200) .expect((res) => { expect(JSON.stringify(res.body)).toEqual(JSON.stringify(build)); @@ -163,13 +168,13 @@ describe('Builds (e2e)', () => { it('200', async () => { const build = await buildsService.create({ project: project.id, branchName: 'develop' }); - return requestWithAuth(app, 'delete', `/builds/${build.id}`, {}, user.token).expect(200); + return requestWithAuth(app, 'delete', `/builds/${build.id}`, user.token).send().expect(200); }); it('401', async () => { const build = await buildsService.create({ project: project.id, branchName: 'develop' }); - return requestWithAuth(app, 'delete', `/builds/${build.id}`, {}, '').expect(401); + return requestWithAuth(app, 'delete', `/builds/${build.id}`, '').send().expect(401); }); }); @@ -177,7 +182,8 @@ describe('Builds (e2e)', () => { it('200 jwt', async () => { const build = await buildsService.create({ project: project.id, branchName: 'develop' }); - return requestWithAuth(app, 'patch', `/builds/${build.id}`, {}, user.token) + return requestWithAuth(app, 'patch', `/builds/${build.id}`, user.token) + .send() .expect(200) .expect((res) => { expect(res.body.isRunning).toBe(false); @@ -187,7 +193,8 @@ describe('Builds (e2e)', () => { it('200 api', async () => { const build = await buildsService.create({ project: project.id, branchName: 'develop' }); - return requestWithApiKey(app, 'patch', `/builds/${build.id}`, {}, user.apiKey) + return requestWithApiKey(app, 'patch', `/builds/${build.id}`, user.apiKey) + .send() .expect(200) .expect((res) => { expect(res.body.isRunning).toBe(false); @@ -197,7 +204,7 @@ describe('Builds (e2e)', () => { it('403', async () => { const build = await buildsService.create({ project: project.id, branchName: 'develop' }); - return requestWithAuth(app, 'patch', `/builds/${build.id}`, {}, '').expect(403); + return requestWithAuth(app, 'patch', `/builds/${build.id}`, '').send().expect(403); }); }); diff --git a/test/preconditions.ts b/test/preconditions.ts index 53a3aed5..42c43a60 100644 --- a/test/preconditions.ts +++ b/test/preconditions.ts @@ -21,21 +21,18 @@ export const requestWithAuth = ( app: INestApplication, method: 'post' | 'get' | 'put' | 'delete' | 'patch', url: string, - body = {}, token: string ): Test => request(app.getHttpServer()) [method](url) - .set('Authorization', 'Bearer ' + token) - .send(body); + .set('Authorization', 'Bearer ' + token); export const requestWithApiKey = ( app: INestApplication, method: 'post' | 'get' | 'put' | 'delete' | 'patch', url: string, - body = {}, apiKey: string -): Test => request(app.getHttpServer())[method](url).set('apiKey', apiKey).send(body); +): Test => request(app.getHttpServer())[method](url).set('apiKey', apiKey); export const haveUserLogged = async (usersService: UsersService) => { const password = '123456'; @@ -57,12 +54,14 @@ export const haveTestRunCreated = async ( ): Promise<{ testRun: TestRunResultDto; build: BuildDto }> => { const build = await buildsService.create({ project: projectId, branchName }); const testRun = await testRunsService.postTestRun({ - projectId: build.projectId, - branchName: build.branchName, - imageBase64: readFileSync(imagePath).toString('base64'), - buildId: build.id, - name: 'Image name', - merge, + createTestRequestDto: { + projectId: build.projectId, + branchName: build.branchName, + buildId: build.id, + name: 'Image name', + merge, + }, + imageBuffer: readFileSync(imagePath), }); return { build, diff --git a/test/projects.e2e-spec.ts b/test/projects.e2e-spec.ts index db2fe474..63a713d5 100644 --- a/test/projects.e2e-spec.ts +++ b/test/projects.e2e-spec.ts @@ -52,55 +52,56 @@ describe('Projects (e2e)', () => { describe('POST /', () => { it('200', () => { - return requestWithAuth(app, 'post', '/projects', project, loggedUser.token) + return requestWithAuth(app, 'post', '/projects', loggedUser.token) + .send(project) .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.name).toBe(project.name); }); }); it('401', () => { - return requestWithAuth(app, 'post', '/projects', project, '').expect(401); + return requestWithAuth(app, 'post', '/projects', '').send(project).expect(401); }); }); describe('GET /', () => { it('200', async () => { - const res = await requestWithAuth(app, 'get', '/projects', {}, loggedUser.token).expect(200); + const res = await requestWithAuth(app, 'get', '/projects', loggedUser.token).send().expect(200); expect(res.body).toEqual(expect.arrayContaining(projectServiceMock.findAll())); }); it('401', async () => { - await requestWithAuth(app, 'get', '/projects', {}, '').expect(401); + await requestWithAuth(app, 'get', '/projects', '').send().expect(401); }); }); describe('DELETE /', () => { it('can delete', async () => { - const res = await requestWithAuth(app, 'delete', `/projects/${project.id}`, {}, loggedUser.token).expect(200); + const res = await requestWithAuth(app, 'delete', `/projects/${project.id}`, loggedUser.token).send().expect(200); expect(res.body).toStrictEqual(projectServiceMock.remove()); }); it('not valid UUID', async () => { - await requestWithAuth(app, 'delete', `/projects/123`, {}, loggedUser.token).expect(400); + await requestWithAuth(app, 'delete', `/projects/123`, loggedUser.token).send().expect(400); }); it('not valid token', async () => { - await requestWithAuth(app, 'delete', `/projects/${project.id}`, {}, 'asd').expect(401); + await requestWithAuth(app, 'delete', `/projects/${project.id}`, 'asd').send().expect(401); }); }); describe('PUT /', () => { it('can edit', async () => { - const res = await requestWithAuth(app, 'put', `/projects`, project, loggedUser.token).expect(200); + const res = await requestWithAuth(app, 'put', `/projects`, loggedUser.token).send(project).expect(200); expect(res.body).toStrictEqual(projectServiceMock.update()); }); it('not valid token', async () => { - await requestWithAuth(app, 'put', `/projects`, project, 'asd').expect(401); + await requestWithAuth(app, 'put', `/projects`, 'asd').send(project).expect(401); }); }); }); diff --git a/test/test-runs.e2e-spec.ts b/test/test-runs.e2e-spec.ts index 9257ad6c..cdb9162d 100644 --- a/test/test-runs.e2e-spec.ts +++ b/test/test-runs.e2e-spec.ts @@ -2,13 +2,17 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { AppModule } from '../src/app.module'; import { UsersService } from '../src/users/users.service'; -import { haveTestRunCreated, haveUserLogged } from './preconditions'; +import { haveTestRunCreated, haveUserLogged, requestWithApiKey } from './preconditions'; import { UserLoginResponseDto } from '../src/users/dto/user-login-response.dto'; import { TestRunsService } from '../src/test-runs/test-runs.service'; import { ProjectsService } from '../src/projects/projects.service'; -import { Project, TestStatus } from '@prisma/client'; +import { Build, Project, TestStatus } from '@prisma/client'; import { BuildsService } from '../src/builds/builds.service'; import { TestVariationsService } from '../src/test-variations/test-variations.service'; +import { readFileSync } from 'fs'; +import { CreateTestRequestMultipartDto } from 'src/test-runs/dto/create-test-request-multipart.dto'; +import { CreateTestRequestDto } from 'src/test-runs/dto/create-test-request.dto'; +import { BuildDto } from 'src/builds/dto/build.dto'; jest.useFakeTimers(); @@ -175,6 +179,30 @@ describe('TestRuns (e2e)', () => { }); }); + describe('POST /multipart', () => { + const url = '/test-runs/multipart'; + + it('should post multipart', async () => { + const build = await buildsService.create({ project: project.id, branchName: project.mainBranchName }); + + await requestWithApiKey(app, 'post', url, user.apiKey) + .set('Content-type', 'multipart/form-data') + .field('name', 'Multipart image') + .field('os', 'Windows') + .field('browser', 'Browser') + .field('viewport', '123x456') + .field('device', 'Desktop') + .field('branchName', project.mainBranchName) + .field('buildId', build.id) + .field('projectId', project.id) + .field('diffTollerancePercent', '0.12') + .field('merge', 'false') + .field('ignoreAreas', '[]') + .attach('image', image_v1) + .expect(201); + }); + }); + describe('POST /approve', () => { it('approve changes in new main branch', async () => { const { testRun: testRun1 } = await haveTestRunCreated( @@ -223,7 +251,7 @@ describe('TestRuns (e2e)', () => { testRunsService, project.id, 'develop', - image_v2, + image_v2 ); const featureBranchResult = await testRunsService.approve(testRun2.id); @@ -240,7 +268,7 @@ describe('TestRuns (e2e)', () => { testRunsService, project.id, 'develop', - image_v1, + image_v1 ); await testRunsService.approve(testRun1.id); const { testRun: testRun2 } = await haveTestRunCreated( @@ -256,7 +284,7 @@ describe('TestRuns (e2e)', () => { testRunsService, project.id, 'develop', - image_v1, + image_v1 ); const result = await testRunsService.approve(testRun3.id); diff --git a/test/users.e2e-spec.ts b/test/users.e2e-spec.ts index 9342323a..0136ab0b 100644 --- a/test/users.e2e-spec.ts +++ b/test/users.e2e-spec.ts @@ -19,7 +19,7 @@ describe('Users (e2e)', () => { }).compile(); app = moduleFixture.createNestApplication(); - usersService = moduleFixture.get(UsersService) + usersService = moduleFixture.get(UsersService); await app.init(); }); @@ -33,89 +33,86 @@ describe('Users (e2e)', () => { password: '123456', firstName: 'fName', lastName: 'lName', - } + }; return request(app.getHttpServer()) .post('/users/register') .send(user) .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.email).toBe(user.email); expect(res.body.firstName).toBe(user.firstName); expect(res.body.lastName).toBe(user.lastName); expect(res.body.apiKey).not.toBeNull(); - }) + }); }); it('POST /login', async () => { - const password = '123456' - const user = await usersService.create(generateUser(password)) + const password = '123456'; + const user = await usersService.create(generateUser(password)); const loginData: UserLoginRequestDto = { email: user.email, - password - } + password, + }; return request(app.getHttpServer()) .post('/users/login') .send(loginData) .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.id).toBe(user.id); expect(res.body.email).toBe(user.email); expect(res.body.firstName).toBe(user.firstName); expect(res.body.lastName).toBe(user.lastName); expect(res.body.apiKey).toBe(user.apiKey); expect(res.body.token).not.toBeNull(); - }) + }); }); it('GET /newApiKey', async () => { - const password = '123456' - const user = await usersService.create(generateUser(password)) + const password = '123456'; + const user = await usersService.create(generateUser(password)); const loggedUser = await usersService.login({ email: user.email, - password - }) + password, + }); - const res = await requestWithAuth(app, 'get', '/users/newApiKey', {}, loggedUser.token) - .expect(200) + const res = await requestWithAuth(app, 'get', '/users/newApiKey', loggedUser.token).send().expect(200); - const newUser = await usersService.findOne(user.id) + const newUser = await usersService.findOne(user.id); expect(res.text).not.toBe(user.apiKey); expect(res.text).toBe(newUser.apiKey); }); it('PUT /password', async () => { - const newPassword = 'newPassword' - const password = '123456' - const user = await usersService.create(generateUser(password)) + const newPassword = 'newPassword'; + const password = '123456'; + const user = await usersService.create(generateUser(password)); const loggedUser = await usersService.login({ email: user.email, - password - }) + password, + }); - await requestWithAuth(app, 'put', '/users/password', { password: newPassword }, loggedUser.token) - .expect(200) + await requestWithAuth(app, 'put', '/users/password', loggedUser.token).send({ password: newPassword }).expect(200); - const newUser = await usersService.findOne(user.id) + const newUser = await usersService.findOne(user.id); expect(compareSync(newPassword, newUser.password)).toBe(true); }); it('PUT /', async () => { - const password = '123456' - const user = await usersService.create(generateUser(password)) + const password = '123456'; + const user = await usersService.create(generateUser(password)); const editedUser = { email: `${uuidAPIKey.create().uuid}@example.com'`, firstName: 'EDITEDfName', lastName: 'EDITEDlName', - } + }; const loggedUser = await usersService.login({ email: user.email, - password - }) + password, + }); - const res = await requestWithAuth(app, 'put', '/users', editedUser, loggedUser.token) - .expect(200) + const res = await requestWithAuth(app, 'put', '/users', loggedUser.token).send(editedUser).expect(200); expect(res.body.id).toBe(user.id); expect(res.body.email).toBe(editedUser.email);