Skip to content

Commit f25bee3

Browse files
committed
Merge branch 'main' into bugfix/RI-4132-logical-db-control-is-not-displayed
2 parents 36b9992 + 3e50d94 commit f25bee3

File tree

21 files changed

+590
-83
lines changed

21 files changed

+590
-83
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,53 @@
1+
import { plainToClass } from "class-transformer";
2+
import { v4 as uuidv4 } from 'uuid';
3+
import {
4+
mockDatabase,
5+
} from 'src/__mocks__';
6+
import { BrowserHistoryMode } from "src/common/constants";
7+
import { RedisDataType } from "src/modules/browser/dto";
8+
import { CreateBrowserHistoryDto } from "src/modules/browser/dto/browser-history/create.browser-history.dto";
9+
import { BrowserHistory, ScanFilter } from "src/modules/browser/dto/browser-history/get.browser-history.dto";
10+
import { BrowserHistoryEntity } from "src/modules/browser/entities/browser-history.entity";
11+
112
export const mockBrowserHistoryService = () => ({
213
create: jest.fn(),
314
get: jest.fn(),
415
list: jest.fn(),
516
delete: jest.fn(),
617
bulkDelete: jest.fn(),
718
});
19+
20+
export const mockBrowserHistoryProvider = jest.fn(() => ({
21+
create: jest.fn(),
22+
get: jest.fn(),
23+
list: jest.fn(),
24+
delete: jest.fn(),
25+
cleanupDatabaseHistory: jest.fn(),
26+
}));
27+
28+
export const mockCreateBrowserHistoryDto: CreateBrowserHistoryDto = {
29+
mode: BrowserHistoryMode.Pattern,
30+
filter: plainToClass(ScanFilter, {
31+
type: RedisDataType.String,
32+
match: 'key*',
33+
}),
34+
};
35+
36+
export const mockBrowserHistoryEntity = new BrowserHistoryEntity({
37+
id: uuidv4(),
38+
databaseId: mockDatabase.id,
39+
filter: 'ENCRYPTED:filter',
40+
encryption: 'KEYTAR',
41+
createdAt: new Date(),
42+
});
43+
44+
export const mockBrowserHistoryPartial: Partial<BrowserHistory> = {
45+
...mockCreateBrowserHistoryDto,
46+
databaseId: mockDatabase.id,
47+
};
48+
49+
export const mockBrowserHistory = {
50+
...mockBrowserHistoryPartial,
51+
id: mockBrowserHistoryEntity.id,
52+
createdAt: mockBrowserHistoryEntity.createdAt,
53+
} as BrowserHistory;

redisinsight/api/src/__mocks__/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export const mockCreateQueryBuilder = jest.fn(() => ({
3737
select: jest.fn().mockReturnThis(),
3838
set: jest.fn().mockReturnThis(),
3939
orderBy: jest.fn().mockReturnThis(),
40+
groupBy: jest.fn().mockReturnThis(),
41+
having: jest.fn().mockReturnThis(),
4042
limit: jest.fn().mockReturnThis(),
4143
leftJoin: jest.fn().mockReturnThis(),
4244
offset: jest.fn().mockReturnThis(),
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { when } from 'jest-when';
3+
import { getRepositoryToken } from '@nestjs/typeorm';
4+
import { InternalServerErrorException, NotFoundException } from '@nestjs/common';
5+
import { Repository } from 'typeorm';
6+
import {
7+
mockEncryptionService,
8+
mockEncryptResult,
9+
mockRepository,
10+
mockDatabase,
11+
MockType,
12+
mockQueryBuilderGetMany,
13+
mockQueryBuilderGetManyRaw,
14+
mockBrowserHistory,
15+
mockBrowserHistoryEntity,
16+
mockBrowserHistoryPartial,
17+
} from 'src/__mocks__';
18+
import { EncryptionService } from 'src/modules/encryption/encryption.service';
19+
import { BrowserHistoryProvider } from 'src/modules/browser/providers/history/browser-history.provider';
20+
import { BrowserHistoryEntity } from 'src/modules/browser/entities/browser-history.entity';
21+
import ERROR_MESSAGES from 'src/constants/error-messages';
22+
import { KeytarDecryptionErrorException } from 'src/modules/encryption/exceptions';
23+
import { BrowserHistory } from 'src/modules/browser/dto/browser-history/get.browser-history.dto';
24+
import { BrowserHistoryMode } from 'src/common/constants';
25+
26+
describe('BrowserHistoryProvider', () => {
27+
let service: BrowserHistoryProvider;
28+
let repository: MockType<Repository<BrowserHistory>>;
29+
let encryptionService;
30+
31+
beforeEach(async () => {
32+
const module: TestingModule = await Test.createTestingModule({
33+
providers: [
34+
BrowserHistoryProvider,
35+
{
36+
provide: getRepositoryToken(BrowserHistoryEntity),
37+
useFactory: mockRepository,
38+
},
39+
{
40+
provide: EncryptionService,
41+
useFactory: mockEncryptionService,
42+
},
43+
],
44+
}).compile();
45+
46+
service = module.get<BrowserHistoryProvider>(BrowserHistoryProvider);
47+
repository = module.get(getRepositoryToken(BrowserHistoryEntity));
48+
encryptionService = module.get<EncryptionService>(EncryptionService);
49+
50+
// encryption mocks
51+
['filter',].forEach((field) => {
52+
when(encryptionService.encrypt)
53+
.calledWith(JSON.stringify(mockBrowserHistory[field]))
54+
.mockReturnValue({
55+
...mockEncryptResult,
56+
data: mockBrowserHistoryEntity[field],
57+
});
58+
when(encryptionService.decrypt)
59+
.calledWith(mockBrowserHistoryEntity[field], mockEncryptResult.encryption)
60+
.mockReturnValue(JSON.stringify(mockBrowserHistory[field]));
61+
});
62+
});
63+
64+
describe('create', () => {
65+
it('should process new entity', async () => {
66+
repository.save.mockReturnValueOnce(mockBrowserHistoryEntity);
67+
expect(await service.create(mockBrowserHistoryPartial)).toEqual(mockBrowserHistory);
68+
});
69+
});
70+
71+
describe('get', () => {
72+
it('should get browser history item', async () => {
73+
repository.findOneBy.mockReturnValueOnce(mockBrowserHistoryEntity);
74+
75+
expect(await service.get(mockBrowserHistory.id)).toEqual(mockBrowserHistory);
76+
});
77+
it('should return null fields in case of decryption errors', async () => {
78+
when(encryptionService.decrypt)
79+
.calledWith(mockBrowserHistoryEntity['filter'], mockEncryptResult.encryption)
80+
.mockRejectedValueOnce(new KeytarDecryptionErrorException());
81+
repository.findOneBy.mockReturnValueOnce(mockBrowserHistoryEntity);
82+
83+
expect(await service.get(mockBrowserHistory.id)).toEqual({
84+
...mockBrowserHistory,
85+
filter: null,
86+
});
87+
});
88+
it('should throw an error', async () => {
89+
repository.findOneBy.mockReturnValueOnce(null);
90+
91+
try {
92+
await service.get(mockBrowserHistory.id);
93+
fail();
94+
} catch (e) {
95+
expect(e).toBeInstanceOf(NotFoundException);
96+
expect(e.message).toEqual(ERROR_MESSAGES.BROWSER_HISTORY_ITEM_NOT_FOUND);
97+
}
98+
});
99+
});
100+
101+
describe('list', () => {
102+
it('should get list of browser history', async () => {
103+
mockQueryBuilderGetMany.mockReturnValueOnce([{
104+
id: mockBrowserHistory.id,
105+
createdAt: mockBrowserHistory.createdAt,
106+
notExposed: 'field',
107+
}]);
108+
expect(await service.list(mockBrowserHistory.databaseId, BrowserHistoryMode.Pattern)).toEqual([{
109+
id: mockBrowserHistory.id,
110+
createdAt: mockBrowserHistory.createdAt,
111+
mode: mockBrowserHistory.mode,
112+
}]);
113+
});
114+
});
115+
116+
describe('delete', () => {
117+
it('Should not return anything on cleanup', async () => {
118+
repository.delete.mockReturnValueOnce(mockBrowserHistoryEntity);
119+
expect(await service.delete(mockBrowserHistory.databaseId, mockBrowserHistory.id)).toEqual(undefined);
120+
});
121+
it('Should throw InternalServerErrorException when error during delete', async () => {
122+
repository.delete.mockRejectedValueOnce(new Error());
123+
await expect(service.delete(mockBrowserHistory.databaseId, mockBrowserHistory.id)).rejects.toThrowError(InternalServerErrorException);
124+
});
125+
});
126+
127+
describe('cleanupDatabaseHistory', () => {
128+
it('Should not return anything on cleanup', async () => {
129+
mockQueryBuilderGetManyRaw.mockReturnValue([
130+
{ id: mockBrowserHistoryEntity.id },
131+
{ id: mockBrowserHistoryEntity.id },
132+
]);
133+
134+
expect(await service.cleanupDatabaseHistory(mockDatabase.id, BrowserHistoryMode.Pattern)).toEqual(undefined);
135+
});
136+
});
137+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { InternalServerErrorException, NotFoundException } from '@nestjs/common';
2+
import { Test, TestingModule } from '@nestjs/testing';
3+
import {
4+
mockDatabase, MockType, mockDatabaseConnectionService, mockDatabaseId, mockBrowserHistoryProvider, mockBrowserHistoryEntity, mockBrowserHistory, mockIORedisClient,
5+
} from 'src/__mocks__';
6+
import { BrowserHistoryProvider } from 'src/modules/browser/providers/history/browser-history.provider';
7+
import { DatabaseConnectionService } from 'src/modules/database/database-connection.service';
8+
import { BrowserHistoryService } from './browser-history.service';
9+
import { BrowserHistoryMode } from 'src/common/constants';
10+
11+
describe('BrowserHistoryService', () => {
12+
let service: BrowserHistoryService;
13+
let browserHistoryProvider: MockType<BrowserHistoryProvider>;
14+
let databaseConnectionService: MockType<DatabaseConnectionService>;
15+
16+
beforeEach(async () => {
17+
jest.clearAllMocks();
18+
19+
const module: TestingModule = await Test.createTestingModule({
20+
providers: [
21+
BrowserHistoryService,
22+
{
23+
provide: BrowserHistoryProvider,
24+
useFactory: mockBrowserHistoryProvider,
25+
},
26+
{
27+
provide: DatabaseConnectionService,
28+
useFactory: mockDatabaseConnectionService,
29+
},
30+
],
31+
}).compile();
32+
33+
service = await module.get(BrowserHistoryService);
34+
browserHistoryProvider = await module.get(BrowserHistoryProvider);
35+
databaseConnectionService = await module.get(DatabaseConnectionService);
36+
});
37+
38+
39+
describe('create', () => {
40+
it('should create new database and send analytics event', async () => {
41+
browserHistoryProvider.create.mockResolvedValue(mockBrowserHistory);
42+
expect(await service.create(mockIORedisClient, mockBrowserHistory)).toEqual(mockBrowserHistory);
43+
});
44+
it('should throw NotFound if no browser history?', async () => {
45+
databaseConnectionService.createClient.mockRejectedValueOnce(new Error());
46+
await expect(service.create(mockIORedisClient, mockBrowserHistory)).rejects.toThrow(InternalServerErrorException);
47+
});
48+
});
49+
50+
describe('get', () => {
51+
it('should return browser history by id', async () => {
52+
browserHistoryProvider.get.mockResolvedValue(mockBrowserHistoryEntity);
53+
expect(await service.get(mockDatabase.id)).toEqual(mockBrowserHistoryEntity);
54+
});
55+
});
56+
57+
describe('list', () => {
58+
it('should return browser history items', async () => {
59+
browserHistoryProvider.list.mockResolvedValue([mockBrowserHistory, mockBrowserHistory]);
60+
expect(await service.list(mockDatabaseId, BrowserHistoryMode.Pattern)).toEqual([mockBrowserHistory, mockBrowserHistory]);
61+
});
62+
it('should throw Error?', async () => {
63+
browserHistoryProvider.list.mockRejectedValueOnce(new Error());
64+
await expect(service.list(mockDatabaseId, BrowserHistoryMode.Pattern)).rejects.toThrow(Error);
65+
});
66+
});
67+
68+
describe('delete', () => {
69+
it('should remove existing browser history item', async () => {
70+
browserHistoryProvider.delete.mockResolvedValue(mockBrowserHistory);
71+
expect(await service.delete(mockBrowserHistory.databaseId, BrowserHistoryMode.Pattern)).toEqual(mockBrowserHistory);
72+
});
73+
it('should throw NotFoundException? on any error during deletion', async () => {
74+
browserHistoryProvider.delete.mockRejectedValueOnce(new NotFoundException());
75+
await expect(service.delete(mockBrowserHistory.databaseId, BrowserHistoryMode.Pattern)).rejects.toThrow(NotFoundException);
76+
});
77+
});
78+
79+
describe('bulkDelete', () => {
80+
it('should remove multiple browser history items', async () => {
81+
expect(await service.bulkDelete(mockBrowserHistory.databaseId,[mockDatabase.id])).toEqual({ affected: 1 });
82+
});
83+
it('should ignore errors and do not count affected', async () => {
84+
browserHistoryProvider.delete.mockRejectedValueOnce(new NotFoundException());
85+
expect(await service.bulkDelete(mockBrowserHistory.databaseId,[mockDatabase.id])).toEqual({ affected: 0 });
86+
});
87+
});
88+
89+
});

redisinsight/api/src/modules/browser/services/browser-history/browser-history.service.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import { plainToClass } from 'class-transformer';
55
import { DatabaseConnectionService } from 'src/modules/database/database-connection.service';
66
import { ClientMetadata } from 'src/common/models';
77
import { BrowserHistoryMode } from 'src/common/constants';
8-
import { BrowserHistoryProvider } from '../../providers/history/browser-history.provider';
9-
import { BrowserHistory } from '../../dto/browser-history/get.browser-history.dto';
10-
import { CreateBrowserHistoryDto } from '../../dto/browser-history/create.browser-history.dto';
11-
import { DeleteBrowserHistoryItemsResponse } from '../../dto/browser-history/delete.browser-history.response.dto';
8+
import { BrowserHistoryProvider } from 'src/modules/browser/providers/history/browser-history.provider';
9+
import { BrowserHistory } from 'src/modules/browser/dto/browser-history/get.browser-history.dto';
10+
import { CreateBrowserHistoryDto } from 'src/modules/browser/dto/browser-history/create.browser-history.dto';
11+
import { DeleteBrowserHistoryItemsResponse } from 'src/modules/browser/dto/browser-history/delete.browser-history.response.dto';
1212

1313
@Injectable()
1414
export class BrowserHistoryService {

redisinsight/api/src/modules/browser/services/stream/stream.service.spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,4 +356,63 @@ describe('StreamService', () => {
356356
}
357357
});
358358
});
359+
describe('deleteEntries', () => {
360+
const mockEntriesIds = mockStreamEntries.map(({ id}) => (id))
361+
beforeEach(() => {
362+
when(browserTool.execCommand)
363+
.calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything())
364+
.mockResolvedValue(true);
365+
when(browserTool.execCommand)
366+
.calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XInfoStream, expect.anything())
367+
.mockResolvedValue(mockStreamInfoReply);
368+
when(browserTool.execCommand)
369+
.calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XRevRange, expect.anything())
370+
.mockResolvedValue(mockStreamEntriesReply);
371+
when(browserTool.execCommand)
372+
.calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XRange, expect.anything())
373+
.mockResolvedValue(mockStreamEntriesReply);
374+
when(browserTool.execCommand)
375+
.calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XDel, expect.anything())
376+
.mockResolvedValue(mockStreamEntries.length);
377+
});
378+
it('delete entries', async () => {
379+
380+
const result = await service.deleteEntries(mockBrowserClientMetadata, {
381+
keyName: mockAddStreamEntriesDto.keyName,
382+
entries: mockEntriesIds,
383+
});
384+
expect(result).toEqual({affected: mockStreamEntries.length});
385+
});
386+
it('should throw Not Found when key does not exists', async () => {
387+
when(browserTool.execCommand)
388+
.calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName])
389+
.mockResolvedValueOnce(false);
390+
391+
try {
392+
await service.deleteEntries(mockBrowserClientMetadata, {
393+
keyName: mockAddStreamEntriesDto.keyName,
394+
entries: mockEntriesIds,
395+
});
396+
fail();
397+
} catch (e) {
398+
expect(e).toBeInstanceOf(NotFoundException);
399+
expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);
400+
}
401+
});
402+
it('should throw Wrong Type error', async () => {
403+
when(browserTool.execCommand)
404+
.calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XInfoStream, [mockAddStreamEntriesDto.keyName])
405+
.mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType));
406+
407+
try {
408+
await service.getEntries(mockBrowserClientMetadata, {
409+
...mockAddStreamEntriesDto,
410+
});
411+
fail();
412+
} catch (e) {
413+
expect(e).toBeInstanceOf(BadRequestException);
414+
expect(e.message).toEqual(RedisErrorCodes.WrongType);
415+
}
416+
});
417+
});
359418
});

0 commit comments

Comments
 (0)