Skip to content

Commit c86c8ea

Browse files
authored
postTestRunMultipart added (#124)
* postTestRunMultipart added
1 parent ca2df01 commit c86c8ea

17 files changed

+297
-114
lines changed

package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"@types/express": "^4.17.7",
6060
"@types/jest": "26.0.14",
6161
"@types/lodash": "^4.14.168",
62+
"@types/multer": "^1.4.5",
6263
"@types/node": "^14.0.27",
6364
"@types/passport-jwt": "^3.0.3",
6465
"@types/passport-local": "^1.0.33",

src/shared/api-file.decorator.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ApiPropertyOptions, ApiProperty } from "@nestjs/swagger";
2+
3+
export const ApiFile = (options?: ApiPropertyOptions): PropertyDecorator => (
4+
target: Object,
5+
propertyKey: string | symbol,
6+
) => {
7+
if (options?.isArray) {
8+
ApiProperty({
9+
type: 'array',
10+
items: {
11+
type: 'file',
12+
properties: {
13+
[propertyKey]: {
14+
type: 'string',
15+
format: 'binary',
16+
},
17+
},
18+
},
19+
})(target, propertyKey);
20+
} else {
21+
ApiProperty({
22+
type: 'file',
23+
properties: {
24+
[propertyKey]: {
25+
type: 'string',
26+
format: 'binary',
27+
},
28+
},
29+
})(target, propertyKey);
30+
}
31+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from "@nestjs/common";
2+
import { Observable } from "rxjs";
3+
4+
@Injectable()
5+
export class FilesToBodyInterceptor implements NestInterceptor {
6+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
7+
const ctx = context.switchToHttp();
8+
const req = ctx.getRequest();
9+
if (req.body && Array.isArray(req.files) && req.files.length) {
10+
req.files.forEach((file: Express.Multer.File) => {
11+
const { fieldname } = file;
12+
if (!req.body[fieldname]) {
13+
req.body[fieldname] = [file];
14+
} else {
15+
req.body[fieldname].push(file);
16+
}
17+
});
18+
}
19+
20+
return next.handle();
21+
}
22+
}
23+
24+
@Injectable()
25+
export class FileToBodyInterceptor implements NestInterceptor {
26+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
27+
const ctx = context.switchToHttp();
28+
const req = ctx.getRequest();
29+
if (req.body && req.file?.fieldname) {
30+
const { fieldname } = req.file;
31+
if (!req.body[fieldname]) {
32+
req.body[fieldname] = req.file;
33+
}
34+
}
35+
36+
return next.handle();
37+
}
38+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { Transform } from 'class-transformer';
3+
import { IsBase64 } from 'class-validator';
4+
import { CreateTestRequestDto } from './create-test-request.dto';
5+
6+
export class CreateTestRequestBase64Dto extends CreateTestRequestDto {
7+
@ApiProperty()
8+
@Transform((value) => value.replace(/(\r\n|\n|\r)/gm, ''))
9+
@IsBase64()
10+
imageBase64: string;
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ApiFile } from '../../shared/api-file.decorator';
2+
import { CreateTestRequestDto } from './create-test-request.dto';
3+
4+
export class CreateTestRequestMultipartDto extends CreateTestRequestDto {
5+
@ApiFile()
6+
image: Express.Multer.File;
7+
}

src/test-runs/dto/create-test-request.dto.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
22
import { Transform } from 'class-transformer';
3-
import { IsOptional, IsUUID, IsNumber, IsBoolean, IsBase64 } from 'class-validator';
3+
import { IsOptional, IsUUID, IsNumber, IsBoolean } from 'class-validator';
44
import { BaselineDataDto } from '../../shared/dto/baseline-data.dto';
55
import { IgnoreAreaDto } from './ignore-area.dto';
66

77
export class CreateTestRequestDto extends BaselineDataDto {
8-
@ApiProperty()
9-
@Transform((value) => value.replace(/(\r\n|\n|\r)/gm, ''))
10-
@IsBase64()
11-
imageBase64: string;
12-
138
@ApiProperty()
149
@IsUUID()
1510
buildId: string;
@@ -21,11 +16,22 @@ export class CreateTestRequestDto extends BaselineDataDto {
2116
@ApiPropertyOptional()
2217
@IsOptional()
2318
@IsNumber()
19+
@Transform((it) => parseFloat(it))
2420
diffTollerancePercent?: number;
2521

2622
@ApiPropertyOptional()
2723
@IsBoolean()
2824
@IsOptional()
25+
@Transform((it) => {
26+
switch (it) {
27+
case 'true':
28+
return true;
29+
case 'false':
30+
return false;
31+
default:
32+
return it;
33+
}
34+
})
2935
merge?: boolean;
3036

3137
@ApiPropertyOptional({ type: [IgnoreAreaDto] })

src/test-runs/test-runs.controller.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,33 @@ import {
1010
Query,
1111
Post,
1212
ParseBoolPipe,
13-
ParseIntPipe,
13+
UseInterceptors,
14+
UploadedFile,
15+
UsePipes,
16+
ValidationPipe,
1417
} from '@nestjs/common';
15-
import { ApiTags, ApiParam, ApiBearerAuth, ApiQuery, ApiSecurity, ApiOkResponse } from '@nestjs/swagger';
18+
import {
19+
ApiTags,
20+
ApiParam,
21+
ApiBearerAuth,
22+
ApiQuery,
23+
ApiSecurity,
24+
ApiOkResponse,
25+
ApiConsumes,
26+
ApiBody,
27+
} from '@nestjs/swagger';
1628
import { JwtAuthGuard } from '../auth/guards/auth.guard';
1729
import { TestRun, TestStatus } from '@prisma/client';
1830
import { TestRunsService } from './test-runs.service';
1931
import { IgnoreAreaDto } from './dto/ignore-area.dto';
2032
import { CommentDto } from '../shared/dto/comment.dto';
2133
import { TestRunResultDto } from './dto/testRunResult.dto';
2234
import { ApiGuard } from '../auth/guards/api.guard';
23-
import { CreateTestRequestDto } from './dto/create-test-request.dto';
2435
import { TestRunDto } from './dto/testRun.dto';
36+
import { FileInterceptor } from '@nestjs/platform-express';
37+
import { CreateTestRequestBase64Dto } from './dto/create-test-request-base64.dto';
38+
import { CreateTestRequestMultipartDto } from './dto/create-test-request-multipart.dto';
39+
import { FileToBodyInterceptor } from '../shared/fite-to-body.interceptor';
2540

2641
@ApiTags('test-runs')
2742
@Controller('test-runs')
@@ -87,7 +102,24 @@ export class TestRunsController {
87102
@ApiSecurity('api_key')
88103
@ApiOkResponse({ type: TestRunResultDto })
89104
@UseGuards(ApiGuard)
90-
postTestRun(@Body() createTestRequestDto: CreateTestRequestDto): Promise<TestRunResultDto> {
91-
return this.testRunsService.postTestRun(createTestRequestDto);
105+
postTestRun(@Body() createTestRequestDto: CreateTestRequestBase64Dto): Promise<TestRunResultDto> {
106+
const imageBuffer = Buffer.from(createTestRequestDto.imageBase64, 'base64');
107+
return this.testRunsService.postTestRun({
108+
createTestRequestDto,
109+
imageBuffer,
110+
});
111+
}
112+
113+
@Post('/multipart')
114+
@ApiSecurity('api_key')
115+
@ApiBody({ type: CreateTestRequestMultipartDto })
116+
@ApiOkResponse({ type: TestRunResultDto })
117+
@ApiConsumes('multipart/form-data')
118+
@UseGuards(ApiGuard)
119+
@UseInterceptors(FileInterceptor('image'), FileToBodyInterceptor)
120+
@UsePipes(new ValidationPipe({ transform: true }))
121+
postTestRunMultipart(@Body() createTestRequestDto: CreateTestRequestMultipartDto): Promise<TestRunResultDto> {
122+
const imageBuffer = createTestRequestDto.image.buffer;
123+
return this.testRunsService.postTestRun({ createTestRequestDto, imageBuffer });
92124
}
93125
}

src/test-runs/test-runs.service.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { BuildsService } from '../builds/builds.service';
1717
import { TEST_PROJECT } from '../_data_';
1818
import { getTestVariationUniqueData } from '../utils';
1919
import { BaselineDataDto } from '../shared/dto/baseline-data.dto';
20+
import { CreateTestRequestBase64Dto } from './dto/create-test-request-base64.dto';
2021

2122
jest.mock('pixelmatch');
2223
jest.mock('./dto/testRunResult.dto');
@@ -103,6 +104,7 @@ const initService = async ({
103104
};
104105
describe('TestRunsService', () => {
105106
let service: TestRunsService;
107+
const imageBuffer = Buffer.from('Image');
106108
const ignoreAreas = [{ x: 1, y: 2, width: 10, height: 20 }];
107109
const tempIgnoreAreas = [{ x: 3, y: 4, width: 30, height: 40 }];
108110
const baseTestRun: TestRun = {
@@ -184,7 +186,6 @@ describe('TestRunsService', () => {
184186
buildId: 'buildId',
185187
projectId: 'projectId',
186188
name: 'Test name',
187-
imageBase64: 'Image',
188189
os: 'OS',
189190
browser: 'browser',
190191
viewport: 'viewport',
@@ -259,9 +260,9 @@ describe('TestRunsService', () => {
259260
const tryAutoApproveByNewBaselines = jest.fn();
260261
service['tryAutoApproveByNewBaselines'] = tryAutoApproveByNewBaselines.mockResolvedValueOnce(testRunWithResult);
261262

262-
const result = await service.create(testVariation, createTestRequestDto);
263+
const result = await service.create({ testVariation, createTestRequestDto, imageBuffer });
263264

264-
expect(saveImageMock).toHaveBeenCalledWith('screenshot', Buffer.from(createTestRequestDto.imageBase64, 'base64'));
265+
expect(saveImageMock).toHaveBeenCalledWith('screenshot', imageBuffer);
265266
expect(testRunCreateMock).toHaveBeenCalledWith({
266267
data: {
267268
imageName,
@@ -722,7 +723,6 @@ describe('TestRunsService', () => {
722723
buildId: 'buildId',
723724
projectId: 'projectId',
724725
name: 'Test name',
725-
imageBase64: 'Image',
726726
os: 'OS',
727727
browser: 'browser',
728728
viewport: 'viewport',
@@ -789,7 +789,7 @@ describe('TestRunsService', () => {
789789
branchName: createTestRequestDto.branchName,
790790
};
791791

792-
await service.postTestRun(createTestRequestDto);
792+
await service.postTestRun({ createTestRequestDto, imageBuffer });
793793

794794
expect(testVariationFindOrCreateMock).toHaveBeenCalledWith(createTestRequestDto.projectId, baselineData);
795795
expect(testRunFindManyMock).toHaveBeenCalledWith({
@@ -800,7 +800,7 @@ describe('TestRunsService', () => {
800800
},
801801
});
802802
expect(deleteMock).toHaveBeenCalledWith(testRun.id);
803-
expect(createMock).toHaveBeenCalledWith(testVariation, createTestRequestDto);
803+
expect(createMock).toHaveBeenCalledWith({ testVariation, createTestRequestDto, imageBuffer });
804804
expect(service.calculateDiff).toHaveBeenCalledWith(testRun);
805805
expect(service['tryAutoApproveByPastBaselines']).toHaveBeenCalledWith(testVariation, testRun);
806806
expect(service['tryAutoApproveByNewBaselines']).toHaveBeenCalledWith(testVariation, testRun);

src/test-runs/test-runs.service.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,13 @@ export class TestRunsService {
4848
});
4949
}
5050

51-
async postTestRun(createTestRequestDto: CreateTestRequestDto): Promise<TestRunResultDto> {
51+
async postTestRun({
52+
createTestRequestDto,
53+
imageBuffer,
54+
}: {
55+
createTestRequestDto: CreateTestRequestDto;
56+
imageBuffer: Buffer;
57+
}): Promise<TestRunResultDto> {
5258
// creates variatioin if does not exist
5359
const testVariation = await this.testVariationService.findOrCreate(createTestRequestDto.projectId, {
5460
...getTestVariationUniqueData(createTestRequestDto),
@@ -69,7 +75,7 @@ export class TestRunsService {
6975
}
7076

7177
// create test run result
72-
const testRun = await this.create(testVariation, createTestRequestDto);
78+
const testRun = await this.create({ testVariation, createTestRequestDto, imageBuffer });
7379

7480
// calculate diff
7581
let testRunWithResult = await this.calculateDiff(testRun);
@@ -157,9 +163,16 @@ export class TestRunsService {
157163
return this.saveDiffResult(testRun.id, diffResult);
158164
}
159165

160-
async create(testVariation: TestVariation, createTestRequestDto: CreateTestRequestDto): Promise<TestRun> {
166+
async create({
167+
testVariation,
168+
createTestRequestDto,
169+
imageBuffer,
170+
}: {
171+
testVariation: TestVariation;
172+
createTestRequestDto: CreateTestRequestDto;
173+
imageBuffer: Buffer;
174+
}): Promise<TestRun> {
161175
// save image
162-
const imageBuffer = Buffer.from(createTestRequestDto.imageBase64, 'base64');
163176
const imageName = this.staticService.saveImage('screenshot', imageBuffer);
164177

165178
const testRun = await this.prismaService.testRun.create({

0 commit comments

Comments
 (0)