Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions backend/packages/Upgrade/src/api/controllers/LogController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export class AuditLogController {
* filter:
* type: string
* enum: [experimentCreated, experimentUpdated, experimentStateChanged, experimentDeleted]
* experimentId:
* type: string
* format: uuid
* description: number of audit logs to requests
* tags:
* - Logs
Expand All @@ -64,8 +67,8 @@ export class AuditLogController {
@Body({ validate: true }) logParams: AuditLogParamsValidator
): Promise<ExperimentAuditPaginationInfo> {
const [nodes, total] = await Promise.all([
this.auditService.getAuditLogs(logParams.take, logParams.skip, logParams.filter),
this.auditService.getTotalLogs(logParams.filter),
this.auditService.getAuditLogs(logParams.take, logParams.skip, logParams.filter, logParams.experimentId),
this.auditService.getTotalLogs(logParams.filter, logParams.experimentId),
]);
return {
total,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsNumber, IsNotEmpty, IsEnum, IsOptional } from 'class-validator';
import { IsNumber, IsNotEmpty, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
import { LOG_TYPE } from 'upgrade_types';
export class AuditLogParamsValidator {
@IsNumber()
Expand All @@ -12,4 +12,9 @@ export class AuditLogParamsValidator {
@IsOptional()
@IsEnum(LOG_TYPE)
public filter?: LOG_TYPE;

@IsOptional()
@IsString()
@IsUUID()
public experimentId?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import repositoryError from './utils/repositoryError';

@EntityRepository(ExperimentAuditLog)
export class ExperimentAuditLogRepository extends Repository<ExperimentAuditLog> {
public async paginatedFind(limit: number, offset: number, filter: LOG_TYPE): Promise<ExperimentAuditLog[]> {
public async paginatedFind(
limit: number,
offset: number,
filter?: LOG_TYPE,
experimentId?: string
): Promise<ExperimentAuditLog[]> {
let queryBuilder = this.createQueryBuilder('audit')
.offset(offset)
.limit(limit)
Expand All @@ -17,20 +22,43 @@ export class ExperimentAuditLogRepository extends Repository<ExperimentAuditLog>
if (filter) {
queryBuilder = queryBuilder.where('audit.type = :filter', { filter });
}

if (experimentId) {
const experimentIdCondition = "(audit.data->>'experimentId' = :experimentId)";
queryBuilder = filter
? queryBuilder.andWhere(experimentIdCondition, { experimentId })
: queryBuilder.where(experimentIdCondition, { experimentId });
}

return queryBuilder.getMany().catch((error: any) => {
const errorMsg = repositoryError('ExperimentAuditLogRepository', 'paginatedFind', { limit, offset }, error);
const errorMsg = repositoryError(
'ExperimentAuditLogRepository',
'paginatedFind',
{ limit, offset, filter, experimentId },
error
);
throw errorMsg;
});
}

public getTotalLogs(filter: LOG_TYPE): Promise<number> {
return this.createQueryBuilder('audit')
.where('audit.type = :filter', { filter })
.getCount()
.catch((error: any) => {
const errorMsg = repositoryError('ExperimentAuditLogRepository', 'paginatedFind', { filter }, error);
throw errorMsg;
});
public getTotalLogs(filter?: LOG_TYPE, experimentId?: string): Promise<number> {
let queryBuilder = this.createQueryBuilder('audit');

if (filter) {
queryBuilder = queryBuilder.where('audit.type = :filter', { filter });
}

if (experimentId) {
const experimentIdCondition = "(audit.data->>'experimentId' = :experimentId)";
queryBuilder = filter
? queryBuilder.andWhere(experimentIdCondition, { experimentId })
: queryBuilder.where(experimentIdCondition, { experimentId });
}

return queryBuilder.getCount().catch((error: any) => {
const errorMsg = repositoryError('ExperimentAuditLogRepository', 'getTotalLogs', { filter, experimentId }, error);
throw errorMsg;
});
}

public async saveRawJson(
Expand Down
18 changes: 14 additions & 4 deletions backend/packages/Upgrade/src/api/services/AuditService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,25 @@ export class AuditService {
private experimentAuditLogRepository: ExperimentAuditLogRepository
) {}

public getTotalLogs(filter: LOG_TYPE): Promise<number> {
public getTotalLogs(filter?: LOG_TYPE, experimentId?: string): Promise<number> {
if (filter) {
return this.experimentAuditLogRepository.getTotalLogs(filter);
return this.experimentAuditLogRepository.getTotalLogs(filter, experimentId);
}

if (experimentId) {
return this.experimentAuditLogRepository.count({ where: { id: experimentId } });
}

return this.experimentAuditLogRepository.count();
}

public async getAuditLogs(limit: number, offset: number, filter?: LOG_TYPE): Promise<ExperimentAuditLog[]> {
const logs = await this.experimentAuditLogRepository.paginatedFind(limit, offset, filter);
public async getAuditLogs(
limit: number,
offset: number,
filter?: LOG_TYPE,
experimentId?: string
): Promise<ExperimentAuditLog[]> {
const logs = await this.experimentAuditLogRepository.paginatedFind(limit, offset, filter, experimentId);
return logs.map((log) => this.convertStateStrings(log));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,45 @@ describe('Log Controller Testing', () => {
.expect(200);
});

test('Post request for /api/audit with experimentId', () => {
return request(app)
.post('/api/audit')
.send({
skip: 0,
take: 0,
filter: 'experimentCreated',
experimentId: '550e8400-e29b-41d4-a716-446655440000',
})
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200);
});

test('Post request for /api/audit with only experimentId (no filter)', () => {
return request(app)
.post('/api/audit')
.send({
skip: 0,
take: 10,
experimentId: '550e8400-e29b-41d4-a716-446655440000',
})
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200);
});

test('Post request for /api/audit with invalid experimentId should fail', () => {
return request(app)
.post('/api/audit')
.send({
skip: 0,
take: 0,
experimentId: 'invalid-uuid',
})
.set('Accept', 'application/json')
.expect(400);
});

test('Post request for /api/error', () => {
return request(app)
.post('/api/error')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,32 @@ describe('ExperimentAuditLogRepository Testing', () => {
expect(res).toEqual(5);
});

it('should get total logs with experimentId', async () => {
const experimentId = '550e8400-e29b-41d4-a716-446655440000';
const res = await repo.getTotalLogs(LOG_TYPE.EXPERIMENT_CREATED, experimentId);

expect(repo.createQueryBuilder).toHaveBeenCalledTimes(1);

expect(mock.where).toHaveBeenCalledTimes(1);
expect(mock.andWhere).toHaveBeenCalledTimes(1);
expect(mock.getCount).toHaveBeenCalledTimes(1);

expect(res).toEqual(5);
});

it('should get total logs with only experimentId (no filter)', async () => {
const experimentId = '550e8400-e29b-41d4-a716-446655440000';
const res = await repo.getTotalLogs(undefined, experimentId);

expect(repo.createQueryBuilder).toHaveBeenCalledTimes(1);

expect(mock.where).toHaveBeenCalledTimes(1);
expect(mock.andWhere).not.toHaveBeenCalled();
expect(mock.getCount).toHaveBeenCalledTimes(1);

expect(res).toEqual(5);
});

it('should throw an error when get total logs fails', async () => {
mock.getCount.mockRejectedValue(err);

Expand Down Expand Up @@ -188,6 +214,44 @@ describe('ExperimentAuditLogRepository Testing', () => {
expect(res).toEqual(result);
});

it('should find paginated with experimentId', async () => {
const experimentId = '550e8400-e29b-41d4-a716-446655440000';
const res = await repo.paginatedFind(3, 0, LOG_TYPE.EXPERIMENT_CREATED, experimentId);

expect(repo.createQueryBuilder).toHaveBeenCalledTimes(1);

expect(mock.offset).toHaveBeenCalledTimes(1);
expect(mock.offset).toHaveBeenCalledWith(0);
expect(mock.limit).toHaveBeenCalledTimes(1);
expect(mock.limit).toHaveBeenCalledWith(3);
expect(mock.leftJoinAndSelect).toHaveBeenCalledTimes(1);
expect(mock.orderBy).toHaveBeenCalledTimes(1);
expect(mock.where).toHaveBeenCalledTimes(1);
expect(mock.andWhere).toHaveBeenCalledTimes(1);
expect(mock.getMany).toHaveBeenCalledTimes(1);

expect(res).toEqual(result);
});

it('should find paginated with only experimentId (no filter)', async () => {
const experimentId = '550e8400-e29b-41d4-a716-446655440000';
const res = await repo.paginatedFind(3, 0, undefined, experimentId);

expect(repo.createQueryBuilder).toHaveBeenCalledTimes(1);

expect(mock.offset).toHaveBeenCalledTimes(1);
expect(mock.offset).toHaveBeenCalledWith(0);
expect(mock.limit).toHaveBeenCalledTimes(1);
expect(mock.limit).toHaveBeenCalledWith(3);
expect(mock.leftJoinAndSelect).toHaveBeenCalledTimes(1);
expect(mock.orderBy).toHaveBeenCalledTimes(1);
expect(mock.where).toHaveBeenCalledTimes(1);
expect(mock.andWhere).not.toHaveBeenCalled();
expect(mock.getMany).toHaveBeenCalledTimes(1);

expect(res).toEqual(result);
});

it('should throw an error when find paginated fails', async () => {
mock.getMany.mockRejectedValue(err);

Expand Down
33 changes: 30 additions & 3 deletions backend/packages/Upgrade/test/unit/services/AuditService.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AuditService } from '../../../src/api/services/AuditService';
import { Repository } from 'typeorm';
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ExperimentAuditLogRepository } from '../../../src/api/repositories/ExperimentAuditLogRepository';
Expand All @@ -10,7 +9,7 @@ const auditArr = [1, 2, 3];

describe('Audit Service Testing', () => {
let service: AuditService;
let repo: Repository<ExperimentAuditLogRepository>;
let repo: ExperimentAuditLogRepository;
let module: TestingModule;

beforeAll(() => {
Expand All @@ -35,7 +34,7 @@ describe('Audit Service Testing', () => {
}).compile();

service = module.get<AuditService>(AuditService);
repo = module.get<Repository<ExperimentAuditLogRepository>>(getRepositoryToken(ExperimentAuditLogRepository));
repo = module.get<ExperimentAuditLogRepository>(getRepositoryToken(ExperimentAuditLogRepository));
});

it('should be defined', async () => {
Expand All @@ -56,11 +55,39 @@ describe('Audit Service Testing', () => {
expect(flags).toEqual(auditArr.length);
});

it('should return a count of audit logs with experimentId', async () => {
const experimentId = '550e8400-e29b-41d4-a716-446655440000';
const flags = await service.getTotalLogs(LOG_TYPE.EXPERIMENT_CREATED, experimentId);
expect(flags).toEqual(auditArr.length);
expect(repo.getTotalLogs).toHaveBeenCalledWith(LOG_TYPE.EXPERIMENT_CREATED, experimentId);
});

it('should return a count of audit logs with only experimentId (no filter)', async () => {
const experimentId = '550e8400-e29b-41d4-a716-446655440000';
const flags = await service.getTotalLogs(undefined, experimentId);
expect(flags).toEqual(auditArr.length);
expect(repo.count).toHaveBeenCalledWith({ where: { id: experimentId } });
});

it('should return an array of audit logs', async () => {
const flags = await service.getAuditLogs(1, 0);
expect(flags).toEqual(auditArr);
});

it('should return an array of audit logs with experimentId', async () => {
const experimentId = '550e8400-e29b-41d4-a716-446655440000';
const flags = await service.getAuditLogs(1, 0, undefined, experimentId);
expect(flags).toEqual(auditArr);
expect(repo.paginatedFind).toHaveBeenCalledWith(1, 0, undefined, experimentId);
});

it('should return an array of audit logs with filter and experimentId', async () => {
const experimentId = '550e8400-e29b-41d4-a716-446655440000';
const flags = await service.getAuditLogs(1, 0, LOG_TYPE.EXPERIMENT_CREATED, experimentId);
expect(flags).toEqual(auditArr);
expect(repo.paginatedFind).toHaveBeenCalledWith(1, 0, LOG_TYPE.EXPERIMENT_CREATED, experimentId);
});

it('should return an array of audit logs by type', async () => {
const flags = await service.getAuditLogByType(LOG_TYPE.EXPERIMENT_CREATED);
expect(flags).toEqual(auditArr);
Expand Down
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading