Skip to content

Commit aec6614

Browse files
author
Artem
committed
#RI-3817 improve port validation + add UTests
1 parent 5eae362 commit aec6614

File tree

6 files changed

+310
-2
lines changed

6 files changed

+310
-2
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { DatabaseImportResponse } from 'src/modules/database-import/dto/database-import.response';
2+
import { BadRequestException, ForbiddenException } from '@nestjs/common';
3+
import { mockDatabase } from 'src/__mocks__/databases';
4+
import { ValidationError } from 'class-validator';
5+
6+
export const mockDatabasesToImportArray = new Array(10).fill(mockDatabase);
7+
8+
export const mockDatabaseImportFile = {
9+
originalname: 'filename.json',
10+
mimetype: 'application/json',
11+
size: 1,
12+
buffer: Buffer.from(JSON.stringify(mockDatabasesToImportArray)),
13+
};
14+
15+
export const mockDatabaseImportResponse = Object.assign(new DatabaseImportResponse(), {
16+
total: 10,
17+
success: 7,
18+
errors: [new ValidationError(), new BadRequestException(), new ForbiddenException()],
19+
});
20+
21+
export const mockDatabaseImportParseFailedAnalyticsPayload = {
22+
23+
};
24+
25+
export const mockDatabaseImportFailedAnalyticsPayload = {
26+
failed: mockDatabaseImportResponse.errors.length,
27+
errors: ['ValidationError', 'BadRequestException', 'ForbiddenException'],
28+
};
29+
30+
export const mockDatabaseImportSucceededAnalyticsPayload = {
31+
succeed: mockDatabaseImportResponse.success,
32+
};
33+
34+
export const mockDatabaseImportAnalytics = jest.fn(() => ({
35+
sendImportResults: jest.fn(),
36+
sendImportFailed: jest.fn(),
37+
}));

redisinsight/api/src/__mocks__/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ export * from './redis';
1616
export * from './server';
1717
export * from './redis-enterprise';
1818
export * from './redis-sentinel';
19+
export * from './database-import';
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { EventEmitter2 } from '@nestjs/event-emitter';
3+
import {
4+
mockDatabaseImportFailedAnalyticsPayload,
5+
mockDatabaseImportResponse, mockDatabaseImportSucceededAnalyticsPayload,
6+
} from 'src/__mocks__';
7+
import { TelemetryEvents } from 'src/constants';
8+
import { DatabaseImportAnalytics } from 'src/modules/database-import/database-import.analytics';
9+
import {
10+
NoDatabaseImportFileProvidedException, SizeLimitExceededDatabaseImportFileException,
11+
UnableToParseDatabaseImportFileException,
12+
} from 'src/modules/database-import/exceptions';
13+
14+
describe('DatabaseImportAnalytics', () => {
15+
let service: DatabaseImportAnalytics;
16+
let sendEventSpy;
17+
18+
beforeEach(async () => {
19+
jest.clearAllMocks();
20+
21+
const module: TestingModule = await Test.createTestingModule({
22+
providers: [
23+
EventEmitter2,
24+
DatabaseImportAnalytics,
25+
],
26+
}).compile();
27+
28+
service = await module.get(DatabaseImportAnalytics);
29+
sendEventSpy = jest.spyOn(service as any, 'sendEvent');
30+
});
31+
32+
describe('sendImportResults', () => {
33+
it('should emit 2 events with success and failed results', () => {
34+
service.sendImportResults(mockDatabaseImportResponse);
35+
36+
expect(sendEventSpy).toHaveBeenNthCalledWith(
37+
1,
38+
TelemetryEvents.DatabaseImportSucceeded,
39+
mockDatabaseImportSucceededAnalyticsPayload,
40+
);
41+
42+
expect(sendEventSpy).toHaveBeenNthCalledWith(
43+
2,
44+
TelemetryEvents.DatabaseImportFailed,
45+
mockDatabaseImportFailedAnalyticsPayload,
46+
);
47+
});
48+
});
49+
50+
describe('sendImportFailed', () => {
51+
it('should emit 1 event with "Error" cause', () => {
52+
service.sendImportFailed(new Error());
53+
54+
expect(sendEventSpy).toHaveBeenNthCalledWith(
55+
1,
56+
TelemetryEvents.DatabaseImportParseFailed,
57+
{
58+
error: 'Error',
59+
},
60+
);
61+
});
62+
it('should emit 1 event with "UnableToParseDatabaseImportFileException" cause', () => {
63+
service.sendImportFailed(new UnableToParseDatabaseImportFileException());
64+
65+
expect(sendEventSpy).toHaveBeenNthCalledWith(
66+
1,
67+
TelemetryEvents.DatabaseImportParseFailed,
68+
{
69+
error: 'UnableToParseDatabaseImportFileException',
70+
},
71+
);
72+
});
73+
it('should emit 1 event with "NoDatabaseImportFileProvidedException" cause', () => {
74+
service.sendImportFailed(new NoDatabaseImportFileProvidedException());
75+
76+
expect(sendEventSpy).toHaveBeenNthCalledWith(
77+
1,
78+
TelemetryEvents.DatabaseImportParseFailed,
79+
{
80+
error: 'NoDatabaseImportFileProvidedException',
81+
},
82+
);
83+
});
84+
it('should emit 1 event with "SizeLimitExceededDatabaseImportFileException" cause', () => {
85+
service.sendImportFailed(new SizeLimitExceededDatabaseImportFileException());
86+
87+
expect(sendEventSpy).toHaveBeenNthCalledWith(
88+
1,
89+
TelemetryEvents.DatabaseImportParseFailed,
90+
{
91+
error: 'SizeLimitExceededDatabaseImportFileException',
92+
},
93+
);
94+
});
95+
});
96+
});
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { pick } from 'lodash';
2+
import { DatabaseImportService } from 'src/modules/database-import/database-import.service';
3+
import {
4+
mockDatabase,
5+
mockDatabaseImportAnalytics,
6+
mockDatabaseImportFile,
7+
mockDatabaseImportResponse,
8+
MockType,
9+
} from 'src/__mocks__';
10+
import { DatabaseRepository } from 'src/modules/database/repositories/database.repository';
11+
import { DatabaseImportAnalytics } from 'src/modules/database-import/database-import.analytics';
12+
import { Test, TestingModule } from '@nestjs/testing';
13+
import { ConnectionType } from 'src/modules/database/entities/database.entity';
14+
import { BadRequestException, ForbiddenException } from '@nestjs/common';
15+
import { ValidationError } from 'class-validator';
16+
import {
17+
NoDatabaseImportFileProvidedException, SizeLimitExceededDatabaseImportFileException,
18+
UnableToParseDatabaseImportFileException,
19+
} from 'src/modules/database-import/exceptions';
20+
21+
describe('DatabaseImportService', () => {
22+
let service: DatabaseImportService;
23+
let databaseRepository: MockType<DatabaseRepository>;
24+
let analytics: MockType<DatabaseImportAnalytics>;
25+
let validatoSpy;
26+
27+
beforeEach(async () => {
28+
jest.clearAllMocks();
29+
30+
const module: TestingModule = await Test.createTestingModule({
31+
providers: [
32+
DatabaseImportService,
33+
{
34+
provide: DatabaseRepository,
35+
useFactory: jest.fn(() => ({
36+
create: jest.fn().mockResolvedValue(mockDatabase),
37+
})),
38+
},
39+
{
40+
provide: DatabaseImportAnalytics,
41+
useFactory: mockDatabaseImportAnalytics,
42+
},
43+
],
44+
}).compile();
45+
46+
service = await module.get(DatabaseImportService);
47+
databaseRepository = await module.get(DatabaseRepository);
48+
analytics = await module.get(DatabaseImportAnalytics);
49+
validatoSpy = jest.spyOn(service['validator'], 'validateOrReject');
50+
});
51+
52+
describe('importDatabase', () => {
53+
beforeEach(() => {
54+
databaseRepository.create.mockRejectedValueOnce(new BadRequestException());
55+
databaseRepository.create.mockRejectedValueOnce(new ForbiddenException());
56+
validatoSpy.mockRejectedValueOnce([new ValidationError()]);
57+
});
58+
59+
it('should import databases from json', async () => {
60+
const response = await service.import(mockDatabaseImportFile);
61+
62+
expect(response).toEqual({
63+
...mockDatabaseImportResponse,
64+
errors: undefined, // errors omitted from response
65+
});
66+
expect(analytics.sendImportResults).toHaveBeenCalledWith(mockDatabaseImportResponse);
67+
});
68+
69+
it('should import databases from base64', async () => {
70+
const response = await service.import({
71+
...mockDatabaseImportFile,
72+
mimetype: 'binary/octet-stream',
73+
buffer: Buffer.from(mockDatabaseImportFile.buffer.toString('base64')),
74+
});
75+
76+
expect(response).toEqual({
77+
...mockDatabaseImportResponse,
78+
errors: undefined, // errors omitted from response
79+
});
80+
expect(analytics.sendImportResults).toHaveBeenCalledWith(mockDatabaseImportResponse);
81+
});
82+
83+
it('should fail due to file was not provided', async () => {
84+
try {
85+
await service.import(undefined);
86+
fail();
87+
} catch (e) {
88+
expect(e).toBeInstanceOf(NoDatabaseImportFileProvidedException);
89+
expect(e.message).toEqual('No import file provided');
90+
expect(analytics.sendImportFailed)
91+
.toHaveBeenCalledWith(new NoDatabaseImportFileProvidedException('No import file provided'));
92+
}
93+
});
94+
95+
it('should fail due to file exceeded size limitations', async () => {
96+
try {
97+
await service.import({
98+
...mockDatabaseImportFile,
99+
size: 10 * 1024 * 1024 + 1,
100+
});
101+
fail();
102+
} catch (e) {
103+
expect(e).toBeInstanceOf(SizeLimitExceededDatabaseImportFileException);
104+
expect(e.message).toEqual('Import file is too big. Maximum 10mb allowed');
105+
}
106+
});
107+
108+
it('should fail due to incorrect json', async () => {
109+
try {
110+
await service.import({
111+
...mockDatabaseImportFile,
112+
buffer: Buffer.from([0, 21]),
113+
});
114+
fail();
115+
} catch (e) {
116+
expect(e).toBeInstanceOf(UnableToParseDatabaseImportFileException);
117+
expect(e.message).toEqual(`Unable to parse ${mockDatabaseImportFile.originalname}`);
118+
}
119+
});
120+
121+
it('should faile due to incorrect base64 + truncate filename', async () => {
122+
try {
123+
await service.import({
124+
...mockDatabaseImportFile,
125+
originalname: (new Array(1_000).fill(1)).join(''),
126+
mimetype: 'binary/octet-stream',
127+
buffer: Buffer.from([0, 21]),
128+
});
129+
fail();
130+
} catch (e) {
131+
expect(e).toBeInstanceOf(UnableToParseDatabaseImportFileException);
132+
expect(e.message).toEqual(`Unable to parse ${(new Array(50).fill(1)).join('')}...`);
133+
}
134+
});
135+
});
136+
137+
describe('createDatabase', () => {
138+
it('should create standalone database', async () => {
139+
await service['createDatabase']({
140+
...mockDatabase,
141+
});
142+
143+
expect(databaseRepository.create).toHaveBeenCalledWith({
144+
...pick(mockDatabase, ['host', 'port', 'name', 'connectionType']),
145+
});
146+
});
147+
it('should create standalone with created name', async () => {
148+
await service['createDatabase']({
149+
...mockDatabase,
150+
name: undefined,
151+
});
152+
153+
expect(databaseRepository.create).toHaveBeenCalledWith({
154+
...pick(mockDatabase, ['host', 'port', 'name', 'connectionType']),
155+
name: `${mockDatabase.host}:${mockDatabase.port}`,
156+
});
157+
});
158+
it('should create cluster database', async () => {
159+
await service['createDatabase']({
160+
...mockDatabase,
161+
cluster: true,
162+
});
163+
164+
expect(databaseRepository.create).toHaveBeenCalledWith({
165+
...pick(mockDatabase, ['host', 'port', 'name']),
166+
connectionType: ConnectionType.CLUSTER,
167+
});
168+
});
169+
});
170+
});

redisinsight/api/src/modules/database-import/database-import.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export class DatabaseImportService {
9999
* @param item
100100
* @private
101101
*/
102-
private async createDatabase(item: any[]): Promise<Database> {
102+
private async createDatabase(item: any): Promise<Database> {
103103
const data: any = {};
104104

105105
this.fieldsMapSchema.forEach(([field, paths]) => {

redisinsight/api/src/modules/database-import/dto/import.database.dto.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { PickType } from '@nestjs/swagger';
22
import { Database } from 'src/modules/database/models/database';
33
import { Expose, Type } from 'class-transformer';
4-
import { IsInt, IsNotEmpty } from 'class-validator';
4+
import {
5+
IsInt, IsNotEmpty, Max, Min,
6+
} from 'class-validator';
57

68
export class ImportDatabaseDto extends PickType(Database, [
79
'host', 'port', 'name', 'db', 'username', 'password',
@@ -11,5 +13,7 @@ export class ImportDatabaseDto extends PickType(Database, [
1113
@IsNotEmpty()
1214
@IsInt({ always: true })
1315
@Type(() => Number)
16+
@Min(0)
17+
@Max(65536)
1418
port: number;
1519
}

0 commit comments

Comments
 (0)