Skip to content

Commit 0135980

Browse files
author
Artem
committed
#RI-4186 - UTests
1 parent 79f7302 commit 0135980

12 files changed

+655
-9
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { CustomTutorial, CustomTutorialActions } from 'src/modules/custom-tutorial/models/custom-tutorial';
2+
import { CustomTutorialEntity } from 'src/modules/custom-tutorial/entities/custom-tutorial.entity';
3+
import { CustomTutorialManifestType } from 'src/modules/custom-tutorial/models/custom-tutorial.manifest';
4+
import { MemoryStoredFile } from 'nestjs-form-data';
5+
import { UploadCustomTutorialDto } from 'src/modules/custom-tutorial/dto/upload.custom-tutorial.dto';
6+
7+
export const mockCustomTutorialId = 'a77b23c1-7816-4ea4-b61f-d37795a0f805-ct-id';
8+
9+
export const mockCustomTutorialId2 = 'a77b23c1-7816-4ea4-b61f-d37795a0f805-ct-id-2';
10+
11+
export const mockCustomTutorialTmpPath = '/tmp/path';
12+
13+
export const mockCustomTutorial = Object.assign(new CustomTutorial(), {
14+
id: mockCustomTutorialId,
15+
name: 'custom tutorial',
16+
createdAt: new Date(),
17+
});
18+
19+
export const mockCustomTutorialEntity = Object.assign(new CustomTutorialEntity(), {
20+
...mockCustomTutorial,
21+
});
22+
23+
export const mockCustomTutorial2 = Object.assign(new CustomTutorial(), {
24+
id: mockCustomTutorialId2,
25+
name: 'custom tutorial 2',
26+
createdAt: new Date(),
27+
});
28+
29+
export const mockCustomTutorialZipFile = Object.assign(new MemoryStoredFile(), {
30+
size: 100,
31+
buffer: Buffer.from('zip-content', 'utf8'),
32+
});
33+
34+
export const mockUploadCustomTutorialDto = Object.assign(new UploadCustomTutorialDto(), {
35+
name: mockCustomTutorial.name,
36+
file: mockCustomTutorialZipFile,
37+
});
38+
39+
export const mockCustomTutorialManifestManifestJson = {
40+
'ct-folder-1': {
41+
type: 'group',
42+
id: 'ct-folder-1',
43+
label: 'ct-folder-1',
44+
// args: {
45+
// withBorder: true,
46+
// initialIsOpen: true,
47+
// },
48+
children: {
49+
'ct-sub-folder-1': {
50+
type: CustomTutorialManifestType.Group,
51+
id: 'ct-sub-folder-1',
52+
label: 'ct-sub-folder-1',
53+
// args: {
54+
// initialIsOpen: false,
55+
// },
56+
children: {
57+
introduction: {
58+
type: CustomTutorialManifestType.InternalLink,
59+
id: 'introduction',
60+
label: 'introduction',
61+
args: {
62+
path: '/ct-folder-1/ct-sub-folder-1/introduction.md',
63+
},
64+
},
65+
'working-with-hashes': {
66+
type: CustomTutorialManifestType.InternalLink,
67+
id: 'working-with-hashes',
68+
label: 'working-with-hashes',
69+
args: {
70+
path: '/ct-folder-1/ct-sub-folder-1/working-with-hashes.md',
71+
},
72+
},
73+
},
74+
},
75+
'ct-sub-folder-2': {
76+
type: CustomTutorialManifestType.Group,
77+
id: 'ct-sub-folder-2',
78+
label: 'ct-sub-folder-2',
79+
// args: {
80+
// withBorder: true,
81+
// initialIsOpen: false,
82+
// },
83+
children: {
84+
introduction: {
85+
type: CustomTutorialManifestType.InternalLink,
86+
id: 'introduction',
87+
label: 'introduction',
88+
args: {
89+
path: '/ct-folder-1/ct-sub-folder-2/introduction.md',
90+
},
91+
},
92+
'working-with-graphs': {
93+
type: CustomTutorialManifestType.InternalLink,
94+
id: 'working-with-graphs',
95+
label: 'working-with-graphs',
96+
args: {
97+
path: '/ct-folder-1/ct-sub-folder-2/working-with-graphs.md',
98+
},
99+
},
100+
},
101+
},
102+
},
103+
},
104+
};
105+
106+
export const mockCustomTutorialManifestManifest = {
107+
type: CustomTutorialManifestType.Group,
108+
id: mockCustomTutorialId,
109+
label: mockCustomTutorial.name,
110+
_actions: mockCustomTutorial.actions,
111+
_path: mockCustomTutorial.path,
112+
children: mockCustomTutorialManifestManifestJson,
113+
};
114+
115+
export const mockCustomTutorialManifestManifest2 = {
116+
type: CustomTutorialManifestType.Group,
117+
id: mockCustomTutorialId2,
118+
label: mockCustomTutorial2.name,
119+
_actions: mockCustomTutorial2.actions,
120+
_path: mockCustomTutorial2.path,
121+
children: mockCustomTutorialManifestManifestJson,
122+
};
123+
124+
export const globalCustomTutorialManifest = {
125+
'custom-tutorials': {
126+
type: CustomTutorialManifestType.Group,
127+
id: 'custom-tutorials',
128+
label: 'My Tutorials',
129+
_actions: [CustomTutorialActions.CREATE],
130+
args: {
131+
withBorder: true,
132+
initialIsOpen: true,
133+
},
134+
children: {
135+
[mockCustomTutorialManifestManifest.id]: mockCustomTutorialManifestManifest,
136+
[mockCustomTutorialManifestManifest2.id]: mockCustomTutorialManifestManifest2,
137+
},
138+
},
139+
};
140+
141+
export const mockCustomTutorialFsProvider = jest.fn(() => ({
142+
unzipToTmpFolder: jest.fn().mockResolvedValue(mockCustomTutorialTmpPath),
143+
moveFolder: jest.fn(),
144+
removeFolder: jest.fn(),
145+
}));
146+
147+
export const mockCustomTutorialManifestProvider = jest.fn(() => ({
148+
getManifestJson: jest.fn().mockResolvedValue(mockCustomTutorialManifestManifestJson),
149+
generateTutorialManifest: jest.fn().mockResolvedValue(mockCustomTutorialManifestManifest),
150+
}));
151+
152+
export const mockCustomTutorialRepository = jest.fn(() => ({
153+
get: jest.fn().mockResolvedValue(mockCustomTutorial),
154+
create: jest.fn().mockResolvedValue(mockCustomTutorial),
155+
delete: jest.fn(),
156+
list: jest.fn().mockResolvedValue([
157+
mockCustomTutorial,
158+
mockCustomTutorial2,
159+
]),
160+
}));

redisinsight/api/src/__mocks__/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './analytics';
1111
export * from './profiler';
1212
export * from './user';
1313
export * from './databases';
14+
export * from './custom-tutorial';
1415
export * from './autodiscovery';
1516
export * from './redis';
1617
export * from './server';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { HttpException, InternalServerErrorException } from '@nestjs/common';
2+
3+
export const wrapHttpError = (error: Error) => {
4+
if (error instanceof HttpException) {
5+
return error;
6+
}
7+
8+
return new InternalServerErrorException(error.message);
9+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './certificate-import.util';
2+
export * from './errors.util';
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import {
3+
globalCustomTutorialManifest,
4+
mockCustomTutorial,
5+
mockCustomTutorialFsProvider,
6+
mockCustomTutorialId,
7+
mockCustomTutorialManifestManifest, mockCustomTutorialManifestManifest2,
8+
mockCustomTutorialManifestProvider,
9+
mockCustomTutorialRepository,
10+
MockType, mockUploadCustomTutorialDto,
11+
} from 'src/__mocks__';
12+
import * as fs from 'fs-extra';
13+
import { CustomTutorialFsProvider } from 'src/modules/custom-tutorial/providers/custom-tutorial.fs.provider';
14+
import { InternalServerErrorException, NotFoundException } from '@nestjs/common';
15+
import { CustomTutorialService } from 'src/modules/custom-tutorial/custom-tutorial.service';
16+
import { CustomTutorialRepository } from 'src/modules/custom-tutorial/repositories/custom-tutorial.repository';
17+
import {
18+
CustomTutorialManifestProvider,
19+
} from 'src/modules/custom-tutorial/providers/custom-tutorial.manifest.provider';
20+
import ERROR_MESSAGES from 'src/constants/error-messages';
21+
22+
jest.mock('fs-extra');
23+
const mockedFs = fs as jest.Mocked<typeof fs>;
24+
25+
const mockedAdmZip = {
26+
extractAllTo: jest.fn(),
27+
};
28+
jest.mock('adm-zip', () => jest.fn().mockImplementation(() => mockedAdmZip));
29+
30+
describe('CustomTutorialService', () => {
31+
let service: CustomTutorialService;
32+
let customTutorialRepository: MockType<CustomTutorialRepository>;
33+
let customTutorialFsProvider: MockType<CustomTutorialFsProvider>;
34+
let customTutorialManifestProvider: MockType<CustomTutorialManifestProvider>;
35+
36+
beforeEach(async () => {
37+
jest.clearAllMocks();
38+
jest.mock('fs-extra', () => mockedFs);
39+
jest.mock('adm-zip', () => jest.fn().mockImplementation(() => mockedAdmZip));
40+
41+
const module: TestingModule = await Test.createTestingModule({
42+
providers: [
43+
CustomTutorialService,
44+
{
45+
provide: CustomTutorialRepository,
46+
useFactory: mockCustomTutorialRepository,
47+
},
48+
{
49+
provide: CustomTutorialFsProvider,
50+
useFactory: mockCustomTutorialFsProvider,
51+
},
52+
{
53+
provide: CustomTutorialManifestProvider,
54+
useFactory: mockCustomTutorialManifestProvider,
55+
},
56+
],
57+
}).compile();
58+
59+
service = await module.get(CustomTutorialService);
60+
customTutorialRepository = await module.get(CustomTutorialRepository);
61+
customTutorialFsProvider = await module.get(CustomTutorialFsProvider);
62+
customTutorialManifestProvider = await module.get(CustomTutorialManifestProvider);
63+
});
64+
65+
describe('create', () => {
66+
it('Should create custom tutorial', async () => {
67+
const result = await service.create(mockUploadCustomTutorialDto);
68+
69+
expect(result).toEqual(mockCustomTutorialManifestManifest);
70+
});
71+
72+
it('Should throw InternalServerError in case of any non-HttpException error', async () => {
73+
customTutorialRepository.create.mockRejectedValueOnce(new Error('Unable to create'));
74+
75+
try {
76+
await service.create(mockUploadCustomTutorialDto);
77+
} catch (e) {
78+
expect(e).toBeInstanceOf(InternalServerErrorException);
79+
expect(e.message).toEqual('Unable to create');
80+
}
81+
});
82+
});
83+
84+
describe('getGlobalManifest', () => {
85+
it('Should return global manifest with 2 tutorials', async () => {
86+
customTutorialManifestProvider.generateTutorialManifest
87+
.mockResolvedValueOnce(mockCustomTutorialManifestManifest)
88+
.mockResolvedValueOnce(mockCustomTutorialManifestManifest2);
89+
90+
const result = await service.getGlobalManifest();
91+
92+
expect(result).toEqual(globalCustomTutorialManifest);
93+
});
94+
95+
it('Should return global manifest with 1 tutorials since 1 failed to fetch', async () => {
96+
customTutorialManifestProvider.generateTutorialManifest
97+
.mockResolvedValueOnce(null);
98+
99+
const result = await service.getGlobalManifest();
100+
101+
expect(result).toEqual({
102+
'custom-tutorials': {
103+
...globalCustomTutorialManifest['custom-tutorials'],
104+
children: {
105+
[mockCustomTutorialManifestManifest.id]: mockCustomTutorialManifestManifest,
106+
},
107+
},
108+
});
109+
});
110+
111+
it('Should return global manifest without children in case of any error', async () => {
112+
customTutorialRepository.list.mockRejectedValueOnce(new Error('Unable to get list of tutorials'));
113+
114+
const result = await service.getGlobalManifest();
115+
116+
expect(result).toEqual({
117+
'custom-tutorials': {
118+
...globalCustomTutorialManifest['custom-tutorials'],
119+
children: {},
120+
},
121+
});
122+
});
123+
});
124+
125+
describe('delete', () => {
126+
it('Should successfully delete entity and remove related directory', async () => {
127+
await service.delete(mockCustomTutorialId);
128+
129+
expect(customTutorialFsProvider.removeFolder).toHaveBeenCalledWith(mockCustomTutorial.absolutePath);
130+
});
131+
132+
it('Should throw NotFound error when try to delete not existing tutorial', async () => {
133+
customTutorialRepository.get.mockResolvedValueOnce(null);
134+
135+
try {
136+
await service.delete(mockCustomTutorialId);
137+
} catch (e) {
138+
expect(e).toBeInstanceOf(NotFoundException);
139+
expect(e.message).toEqual(ERROR_MESSAGES.CUSTOM_TUTORIAL_NOT_FOUND);
140+
}
141+
});
142+
143+
it('Should throw InternalServerError in case of any non-HttpException error', async () => {
144+
customTutorialRepository.delete.mockRejectedValueOnce(new Error('Unable to delete'));
145+
146+
try {
147+
await service.delete(mockCustomTutorialId);
148+
} catch (e) {
149+
expect(e).toBeInstanceOf(InternalServerErrorException);
150+
expect(e.message).toEqual('Unable to delete');
151+
}
152+
});
153+
});
154+
});

redisinsight/api/src/modules/custom-tutorial/custom-tutorial.service.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
Injectable, InternalServerErrorException, Logger, NotFoundException,
2+
Injectable, Logger, NotFoundException,
33
} from '@nestjs/common';
44
import { v4 as uuidv4 } from 'uuid';
55
import { CustomTutorialRepository } from 'src/modules/custom-tutorial/repositories/custom-tutorial.repository';
@@ -15,6 +15,7 @@ import {
1515
CustomTutorialManifestType,
1616
ICustomTutorialManifest,
1717
} from 'src/modules/custom-tutorial/models/custom-tutorial.manifest';
18+
import { wrapHttpError } from 'src/common/utils';
1819

1920
@Injectable()
2021
export class CustomTutorialService {
@@ -49,7 +50,8 @@ export class CustomTutorialService {
4950

5051
return await this.customTutorialManifestProvider.generateTutorialManifest(tutorial);
5152
} catch (e) {
52-
throw new InternalServerErrorException(e.message);
53+
this.logger.error('Unable to create custom tutorials', e);
54+
throw wrapHttpError(e);
5355
}
5456
}
5557

@@ -62,6 +64,7 @@ export class CustomTutorialService {
6264

6365
try {
6466
const tutorials = await this.customTutorialRepository.list();
67+
6568
const manifests = await Promise.all(
6669
tutorials.map(
6770
this.customTutorialManifestProvider.generateTutorialManifest.bind(this.customTutorialManifestProvider),
@@ -105,12 +108,12 @@ export class CustomTutorialService {
105108

106109
public async delete(id: string): Promise<void> {
107110
try {
108-
const tutorial = await this.customTutorialRepository.get(id);
111+
const tutorial = await this.get(id);
109112
await this.customTutorialRepository.delete(id);
110113
await this.customTutorialFsProvider.removeFolder(tutorial.absolutePath);
111114
} catch (e) {
112115
this.logger.error('Unable to delete custom tutorial', e);
113-
throw e;
116+
throw wrapHttpError(e);
114117
}
115118
}
116119
}

redisinsight/api/src/modules/custom-tutorial/dto/upload.custom-tutorial.dto.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ApiProperty } from '@nestjs/swagger';
22
import { Expose } from 'class-transformer';
3-
import { IsString } from 'class-validator';
3+
import { IsNotEmpty, IsString } from 'class-validator';
44
import { HasMimeType, IsFile, MemoryStoredFile } from 'nestjs-form-data';
55

66
export class UploadCustomTutorialDto {
@@ -18,5 +18,6 @@ export class UploadCustomTutorialDto {
1818
})
1919
@Expose()
2020
@IsString()
21+
@IsNotEmpty()
2122
name: string;
2223
}

0 commit comments

Comments
 (0)