Skip to content
46 changes: 46 additions & 0 deletions redisinsight/api/src/__mocks__/browser-history.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,53 @@
import { plainToClass } from "class-transformer";
import { v4 as uuidv4 } from 'uuid';
import {
mockDatabase,
} from 'src/__mocks__';
import { BrowserHistoryMode } from "src/common/constants";
import { RedisDataType } from "src/modules/browser/dto";
import { CreateBrowserHistoryDto } from "src/modules/browser/dto/browser-history/create.browser-history.dto";
import { BrowserHistory, ScanFilter } from "src/modules/browser/dto/browser-history/get.browser-history.dto";
import { BrowserHistoryEntity } from "src/modules/browser/entities/browser-history.entity";

export const mockBrowserHistoryService = () => ({
create: jest.fn(),
get: jest.fn(),
list: jest.fn(),
delete: jest.fn(),
bulkDelete: jest.fn(),
});

export const mockBrowserHistoryProvider = jest.fn(() => ({
create: jest.fn(),
get: jest.fn(),
list: jest.fn(),
delete: jest.fn(),
cleanupDatabaseHistory: jest.fn(),
}));

export const mockCreateBrowserHistoryDto: CreateBrowserHistoryDto = {
mode: BrowserHistoryMode.Pattern,
filter: plainToClass(ScanFilter, {
type: RedisDataType.String,
match: 'key*',
}),
};

export const mockBrowserHistoryEntity = new BrowserHistoryEntity({
id: uuidv4(),
databaseId: mockDatabase.id,
filter: 'ENCRYPTED:filter',
encryption: 'KEYTAR',
createdAt: new Date(),
});

export const mockBrowserHistoryPartial: Partial<BrowserHistory> = {
...mockCreateBrowserHistoryDto,
databaseId: mockDatabase.id,
};

export const mockBrowserHistory = {
...mockBrowserHistoryPartial,
id: mockBrowserHistoryEntity.id,
createdAt: mockBrowserHistoryEntity.createdAt,
} as BrowserHistory;
2 changes: 2 additions & 0 deletions redisinsight/api/src/__mocks__/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export const mockCreateQueryBuilder = jest.fn(() => ({
select: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
having: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
leftJoin: jest.fn().mockReturnThis(),
offset: jest.fn().mockReturnThis(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Test, TestingModule } from '@nestjs/testing';
import { when } from 'jest-when';
import { getRepositoryToken } from '@nestjs/typeorm';
import { InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { Repository } from 'typeorm';
import {
mockEncryptionService,
mockEncryptResult,
mockRepository,
mockDatabase,
MockType,
mockQueryBuilderGetMany,
mockQueryBuilderGetManyRaw,
mockBrowserHistory,
mockBrowserHistoryEntity,
mockBrowserHistoryPartial,
} from 'src/__mocks__';
import { EncryptionService } from 'src/modules/encryption/encryption.service';
import { BrowserHistoryProvider } from 'src/modules/browser/providers/history/browser-history.provider';
import { BrowserHistoryEntity } from 'src/modules/browser/entities/browser-history.entity';
import ERROR_MESSAGES from 'src/constants/error-messages';
import { KeytarDecryptionErrorException } from 'src/modules/encryption/exceptions';
import { BrowserHistory } from 'src/modules/browser/dto/browser-history/get.browser-history.dto';
import { BrowserHistoryMode } from 'src/common/constants';

describe('BrowserHistoryProvider', () => {
let service: BrowserHistoryProvider;
let repository: MockType<Repository<BrowserHistory>>;
let encryptionService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
BrowserHistoryProvider,
{
provide: getRepositoryToken(BrowserHistoryEntity),
useFactory: mockRepository,
},
{
provide: EncryptionService,
useFactory: mockEncryptionService,
},
],
}).compile();

service = module.get<BrowserHistoryProvider>(BrowserHistoryProvider);
repository = module.get(getRepositoryToken(BrowserHistoryEntity));
encryptionService = module.get<EncryptionService>(EncryptionService);

// encryption mocks
['filter',].forEach((field) => {
when(encryptionService.encrypt)
.calledWith(JSON.stringify(mockBrowserHistory[field]))
.mockReturnValue({
...mockEncryptResult,
data: mockBrowserHistoryEntity[field],
});
when(encryptionService.decrypt)
.calledWith(mockBrowserHistoryEntity[field], mockEncryptResult.encryption)
.mockReturnValue(JSON.stringify(mockBrowserHistory[field]));
});
});

describe('create', () => {
it('should process new entity', async () => {
repository.save.mockReturnValueOnce(mockBrowserHistoryEntity);
expect(await service.create(mockBrowserHistoryPartial)).toEqual(mockBrowserHistory);
});
});

describe('get', () => {
it('should get browser history item', async () => {
repository.findOneBy.mockReturnValueOnce(mockBrowserHistoryEntity);

expect(await service.get(mockBrowserHistory.id)).toEqual(mockBrowserHistory);
});
it('should return null fields in case of decryption errors', async () => {
when(encryptionService.decrypt)
.calledWith(mockBrowserHistoryEntity['filter'], mockEncryptResult.encryption)
.mockRejectedValueOnce(new KeytarDecryptionErrorException());
repository.findOneBy.mockReturnValueOnce(mockBrowserHistoryEntity);

expect(await service.get(mockBrowserHistory.id)).toEqual({
...mockBrowserHistory,
filter: null,
});
});
it('should throw an error', async () => {
repository.findOneBy.mockReturnValueOnce(null);

try {
await service.get(mockBrowserHistory.id);
fail();
} catch (e) {
expect(e).toBeInstanceOf(NotFoundException);
expect(e.message).toEqual(ERROR_MESSAGES.BROWSER_HISTORY_ITEM_NOT_FOUND);
}
});
});

describe('list', () => {
it('should get list of browser history', async () => {
mockQueryBuilderGetMany.mockReturnValueOnce([{
id: mockBrowserHistory.id,
createdAt: mockBrowserHistory.createdAt,
notExposed: 'field',
}]);
expect(await service.list(mockBrowserHistory.databaseId, BrowserHistoryMode.Pattern)).toEqual([{
id: mockBrowserHistory.id,
createdAt: mockBrowserHistory.createdAt,
mode: mockBrowserHistory.mode,
}]);
});
});

describe('delete', () => {
it('Should not return anything on cleanup', async () => {
repository.delete.mockReturnValueOnce(mockBrowserHistoryEntity);
expect(await service.delete(mockBrowserHistory.databaseId, mockBrowserHistory.id)).toEqual(undefined);
});
it('Should throw InternalServerErrorException when error during delete', async () => {
repository.delete.mockRejectedValueOnce(new Error());
await expect(service.delete(mockBrowserHistory.databaseId, mockBrowserHistory.id)).rejects.toThrowError(InternalServerErrorException);
});
});

describe('cleanupDatabaseHistory', () => {
it('Should not return anything on cleanup', async () => {
mockQueryBuilderGetManyRaw.mockReturnValue([
{ id: mockBrowserHistoryEntity.id },
{ id: mockBrowserHistoryEntity.id },
]);

expect(await service.cleanupDatabaseHistory(mockDatabase.id, BrowserHistoryMode.Pattern)).toEqual(undefined);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import {
mockDatabase, MockType, mockDatabaseConnectionService, mockDatabaseId, mockBrowserHistoryProvider, mockBrowserHistoryEntity, mockBrowserHistory, mockIORedisClient,
} from 'src/__mocks__';
import { BrowserHistoryProvider } from 'src/modules/browser/providers/history/browser-history.provider';
import { DatabaseConnectionService } from 'src/modules/database/database-connection.service';
import { BrowserHistoryService } from './browser-history.service';
import { BrowserHistoryMode } from 'src/common/constants';

describe('BrowserHistoryService', () => {
let service: BrowserHistoryService;
let browserHistoryProvider: MockType<BrowserHistoryProvider>;
let databaseConnectionService: MockType<DatabaseConnectionService>;

beforeEach(async () => {
jest.clearAllMocks();

const module: TestingModule = await Test.createTestingModule({
providers: [
BrowserHistoryService,
{
provide: BrowserHistoryProvider,
useFactory: mockBrowserHistoryProvider,
},
{
provide: DatabaseConnectionService,
useFactory: mockDatabaseConnectionService,
},
],
}).compile();

service = await module.get(BrowserHistoryService);
browserHistoryProvider = await module.get(BrowserHistoryProvider);
databaseConnectionService = await module.get(DatabaseConnectionService);
});


describe('create', () => {
it('should create new database and send analytics event', async () => {
browserHistoryProvider.create.mockResolvedValue(mockBrowserHistory);
expect(await service.create(mockIORedisClient, mockBrowserHistory)).toEqual(mockBrowserHistory);
});
it('should throw NotFound if no browser history?', async () => {
databaseConnectionService.createClient.mockRejectedValueOnce(new Error());
await expect(service.create(mockIORedisClient, mockBrowserHistory)).rejects.toThrow(InternalServerErrorException);
});
});

describe('get', () => {
it('should return browser history by id', async () => {
browserHistoryProvider.get.mockResolvedValue(mockBrowserHistoryEntity);
expect(await service.get(mockDatabase.id)).toEqual(mockBrowserHistoryEntity);
});
});

describe('list', () => {
it('should return browser history items', async () => {
browserHistoryProvider.list.mockResolvedValue([mockBrowserHistory, mockBrowserHistory]);
expect(await service.list(mockDatabaseId, BrowserHistoryMode.Pattern)).toEqual([mockBrowserHistory, mockBrowserHistory]);
});
it('should throw Error?', async () => {
browserHistoryProvider.list.mockRejectedValueOnce(new Error());
await expect(service.list(mockDatabaseId, BrowserHistoryMode.Pattern)).rejects.toThrow(Error);
});
});

describe('delete', () => {
it('should remove existing browser history item', async () => {
browserHistoryProvider.delete.mockResolvedValue(mockBrowserHistory);
expect(await service.delete(mockBrowserHistory.databaseId, BrowserHistoryMode.Pattern)).toEqual(mockBrowserHistory);
});
it('should throw NotFoundException? on any error during deletion', async () => {
browserHistoryProvider.delete.mockRejectedValueOnce(new NotFoundException());
await expect(service.delete(mockBrowserHistory.databaseId, BrowserHistoryMode.Pattern)).rejects.toThrow(NotFoundException);
});
});

describe('bulkDelete', () => {
it('should remove multiple browser history items', async () => {
expect(await service.bulkDelete(mockBrowserHistory.databaseId,[mockDatabase.id])).toEqual({ affected: 1 });
});
it('should ignore errors and do not count affected', async () => {
browserHistoryProvider.delete.mockRejectedValueOnce(new NotFoundException());
expect(await service.bulkDelete(mockBrowserHistory.databaseId,[mockDatabase.id])).toEqual({ affected: 0 });
});
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { plainToClass } from 'class-transformer';
import { DatabaseConnectionService } from 'src/modules/database/database-connection.service';
import { ClientMetadata } from 'src/common/models';
import { BrowserHistoryMode } from 'src/common/constants';
import { BrowserHistoryProvider } from '../../providers/history/browser-history.provider';
import { BrowserHistory } from '../../dto/browser-history/get.browser-history.dto';
import { CreateBrowserHistoryDto } from '../../dto/browser-history/create.browser-history.dto';
import { DeleteBrowserHistoryItemsResponse } from '../../dto/browser-history/delete.browser-history.response.dto';
import { BrowserHistoryProvider } from 'src/modules/browser/providers/history/browser-history.provider';
import { BrowserHistory } from 'src/modules/browser/dto/browser-history/get.browser-history.dto';
import { CreateBrowserHistoryDto } from 'src/modules/browser/dto/browser-history/create.browser-history.dto';
import { DeleteBrowserHistoryItemsResponse } from 'src/modules/browser/dto/browser-history/delete.browser-history.response.dto';

@Injectable()
export class BrowserHistoryService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,4 +356,63 @@ describe('StreamService', () => {
}
});
});
describe('deleteEntries', () => {
const mockEntriesIds = mockStreamEntries.map(({ id}) => (id))
beforeEach(() => {
when(browserTool.execCommand)
.calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything())
.mockResolvedValue(true);
when(browserTool.execCommand)
.calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XInfoStream, expect.anything())
.mockResolvedValue(mockStreamInfoReply);
when(browserTool.execCommand)
.calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XRevRange, expect.anything())
.mockResolvedValue(mockStreamEntriesReply);
when(browserTool.execCommand)
.calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XRange, expect.anything())
.mockResolvedValue(mockStreamEntriesReply);
when(browserTool.execCommand)
.calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XDel, expect.anything())
.mockResolvedValue(mockStreamEntries.length);
});
it('delete entries', async () => {

const result = await service.deleteEntries(mockBrowserClientMetadata, {
keyName: mockAddStreamEntriesDto.keyName,
entries: mockEntriesIds,
});
expect(result).toEqual({affected: mockStreamEntries.length});
});
it('should throw Not Found when key does not exists', async () => {
when(browserTool.execCommand)
.calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName])
.mockResolvedValueOnce(false);

try {
await service.deleteEntries(mockBrowserClientMetadata, {
keyName: mockAddStreamEntriesDto.keyName,
entries: mockEntriesIds,
});
fail();
} catch (e) {
expect(e).toBeInstanceOf(NotFoundException);
expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);
}
});
it('should throw Wrong Type error', async () => {
when(browserTool.execCommand)
.calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XInfoStream, [mockAddStreamEntriesDto.keyName])
.mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType));

try {
await service.getEntries(mockBrowserClientMetadata, {
...mockAddStreamEntriesDto,
});
fail();
} catch (e) {
expect(e).toBeInstanceOf(BadRequestException);
expect(e.message).toEqual(RedisErrorCodes.WrongType);
}
});
});
});
Loading