Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Application Tag Resource to Backend #1945

Merged
merged 6 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ApplicationTagController } from './application-tag.controller';
import { ApplicationTagService } from './application-tag.service';
import { createMock, DeepMocked } from '@golevelup/nestjs-testing';
import {
initApplicationMockEntity,
initApplicationWithTagsMockEntity,
initTagMockEntity,
} from '../../../../test/mocks/mockEntities';
import { ClsService } from 'nestjs-cls';
import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes';
import { TagModule } from '../../tag/tag.module';
import { ApplicationTagDto } from './application-tag.dto';

describe('ApplicationTagController', () => {
let controller: ApplicationTagController;
let applicationTagService: DeepMocked<ApplicationTagService>;

const mockApplicationEntityWithoutTags = initApplicationMockEntity();
mockApplicationEntityWithoutTags.tags = [];
const mockApplicationEntityWithTags = initApplicationWithTagsMockEntity();
const mockTagEntity = initTagMockEntity();

beforeEach(async () => {
applicationTagService = createMock();
const module: TestingModule = await Test.createTestingModule({
controllers: [ApplicationTagController],
providers: [
{ provide: ApplicationTagService, useValue: applicationTagService },
{ provide: ClsService, useValue: {} },
...mockKeyCloakProviders,
],
imports: [],
}).compile();

controller = module.get<ApplicationTagController>(ApplicationTagController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

it('should return tags for the application', async () => {
applicationTagService.getApplicationTags.mockResolvedValue([mockTagEntity]);

const result = await controller.getApplicationTags('app_1');
expect(applicationTagService.getApplicationTags).toHaveBeenCalledTimes(1);
expect(result[0].name).toEqual('tag-name');
});

it('should create tags', async () => {
applicationTagService.addTagToApplication.mockResolvedValue(mockApplicationEntityWithTags);

const mockTagDto = new ApplicationTagDto();
mockTagDto.tagName = 'tag-name';

const result = await controller.addTagToApplication('app_1', mockTagDto);
expect(applicationTagService.addTagToApplication).toHaveBeenCalledTimes(1);
expect(result.tags[0].name).toEqual('tag-name');
});

it('should remove tags', async () => {
applicationTagService.removeTagFromApplication.mockResolvedValue(mockApplicationEntityWithoutTags);

const result = await controller.removeTagFromApplication('app_1', 'tag-name');
expect(applicationTagService.removeTagFromApplication).toHaveBeenCalledTimes(1);
expect(result.tags.length).toEqual(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
HttpStatus,
Logger,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { ApiOAuth2 } from '@nestjs/swagger';
import { RolesGuard } from '../../../common/authorization/roles-guard.service';
import * as config from 'config';
import { ApplicationTagService } from './application-tag.service';
import { ApplicationTagDto } from './application-tag.dto';
import { UserRoles } from '../../../common/authorization/roles.decorator';
import { ROLES_ALLOWED_APPLICATIONS } from '../../../common/authorization/roles';

@Controller('application/:fileNumber/tag')
@ApiOAuth2(config.get<string[]>('KEYCLOAK.SCOPES'))
@UseGuards(RolesGuard)
export class ApplicationTagController {
constructor(private service: ApplicationTagService) {}

@Get('')
@UserRoles(...ROLES_ALLOWED_APPLICATIONS)
async getApplicationTags(@Param('fileNumber') fileNumber: string) {
return await this.service.getApplicationTags(fileNumber);
}

@Post('')
@UserRoles(...ROLES_ALLOWED_APPLICATIONS)
async addTagToApplication(@Param('fileNumber') fileNumber: string, @Body() dto: ApplicationTagDto) {
return await this.service.addTagToApplication(fileNumber, dto.tagName);
}

@Delete('/:tagName')
@UserRoles(...ROLES_ALLOWED_APPLICATIONS)
async removeTagFromApplication(@Param('fileNumber') fileNumber: string, @Param('tagName') tagName: string) {
return await this.service.removeTagFromApplication(fileNumber, tagName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsString } from 'class-validator';

export class ApplicationTagDto {
@IsString()
tagName: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ApplicationTagService } from './application-tag.service';
import { TagModule } from '../../tag/tag.module';
import { createMock, DeepMocked } from '@golevelup/nestjs-testing';
import { Repository } from 'typeorm';
import { Tag } from '../../tag/tag.entity';
import { Application } from '../application.entity';
import {
initApplicationMockEntity,
initApplicationWithTagsMockEntity,
initTagMockEntity,
} from '../../../../test/mocks/mockEntities';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ServiceNotFoundException, ServiceValidationException } from '@app/common/exceptions/base.exception';

describe('ApplicationTagService', () => {
let service: ApplicationTagService;
let tagRepositoryMock: DeepMocked<Repository<Tag>>;
let applicationRepositoryMock: DeepMocked<Repository<Application>>;

let mockApplicationEntityWithoutTags: Application;
let mockApplicationEntityWithTags: Application;
let mockApplicationEntityWithDifferentTags: Application;
let mockTagEntity: Tag;

beforeEach(async () => {
tagRepositoryMock = createMock();
applicationRepositoryMock = createMock();

mockApplicationEntityWithoutTags = initApplicationMockEntity();
mockApplicationEntityWithTags = initApplicationWithTagsMockEntity();
mockApplicationEntityWithDifferentTags = initApplicationWithTagsMockEntity();
mockApplicationEntityWithDifferentTags.tags[0].name = 'tag-name-2';
mockApplicationEntityWithDifferentTags.tags[0].uuid = 'tag-uuid-2';
mockTagEntity = initTagMockEntity();

const module: TestingModule = await Test.createTestingModule({
providers: [
ApplicationTagService,
{ provide: getRepositoryToken(Application), useValue: applicationRepositoryMock },
{ provide: getRepositoryToken(Tag), useValue: tagRepositoryMock },
],
}).compile();

service = module.get<ApplicationTagService>(ApplicationTagService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

it('should add tag to the application if not existing', async () => {
applicationRepositoryMock.findOne.mockResolvedValue(mockApplicationEntityWithoutTags);
tagRepositoryMock.findOne.mockResolvedValue(mockTagEntity);
applicationRepositoryMock.save.mockResolvedValue(mockApplicationEntityWithTags);

await service.addTagToApplication('app_1', 'tag-name');
expect(mockApplicationEntityWithoutTags.tags.length).toEqual(1);
expect(applicationRepositoryMock.save).toHaveBeenCalledTimes(1);
});

it('should raise an error if application is not found', async () => {
applicationRepositoryMock.findOne.mockResolvedValue(null);

await expect(service.addTagToApplication('app-1', 'tag-name')).rejects.toThrow(ServiceNotFoundException);
});

it('should throw an error if tag is not found', async () => {
applicationRepositoryMock.findOne.mockResolvedValue(mockApplicationEntityWithoutTags);
tagRepositoryMock.findOne.mockResolvedValue(null);

await expect(service.addTagToApplication('app-1', 'tag-name')).rejects.toThrow(ServiceNotFoundException);
});

it('should raise an error if the application already has the tag', async () => {
applicationRepositoryMock.findOne.mockResolvedValue(mockApplicationEntityWithTags);
tagRepositoryMock.findOne.mockResolvedValue(mockTagEntity);

await expect(service.addTagToApplication('app-1', 'tag-name')).rejects.toThrow(ServiceValidationException);
});

it('should throw an error if the application does not have any tags when deleting', async () => {
applicationRepositoryMock.findOne.mockResolvedValue(mockApplicationEntityWithoutTags);
tagRepositoryMock.findOne.mockResolvedValue(mockTagEntity);

await expect(service.removeTagFromApplication('app-1', 'tag-name')).rejects.toThrow(ServiceValidationException);
});

it('should throw an error if the application does not have the tag requested when deleting', async () => {
applicationRepositoryMock.findOne.mockResolvedValue(mockApplicationEntityWithDifferentTags);
tagRepositoryMock.findOne.mockResolvedValue(mockTagEntity);

await expect(service.removeTagFromApplication('app-1', 'tag-name')).rejects.toThrow(ServiceValidationException);
});

it('should delete the tag from application if exists', async () => {
applicationRepositoryMock.findOne.mockResolvedValue(mockApplicationEntityWithTags);
tagRepositoryMock.findOne.mockResolvedValue(mockTagEntity);
applicationRepositoryMock.save.mockResolvedValue(mockApplicationEntityWithoutTags);

await service.removeTagFromApplication('app-1', 'tag-name');
expect(mockApplicationEntityWithTags.tags.length).toEqual(0);
expect(applicationRepositoryMock.save).toHaveBeenCalledTimes(1);
});

it('should return application tags', async () => {
applicationRepositoryMock.findOne.mockResolvedValue(mockApplicationEntityWithTags);
tagRepositoryMock.findOne.mockResolvedValue(mockTagEntity);

const result = await service.getApplicationTags('app-1');
expect(result).toBeTruthy();
expect(result.length).toEqual(1);
});

it('should return empty array if application does not have tags', async () => {
applicationRepositoryMock.findOne.mockResolvedValue(mockApplicationEntityWithoutTags);

const result = await service.getApplicationTags('app-1');
expect(result).toEqual([]);
expect(result.length).toEqual(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Tag } from '../../tag/tag.entity';
import { Application } from '../application.entity';
import { ServiceNotFoundException, ServiceValidationException } from '@app/common/exceptions/base.exception';

@Injectable()
export class ApplicationTagService {
constructor(
@InjectRepository(Tag) private tagRepository: Repository<Tag>,
@InjectRepository(Application) private applicationRepository: Repository<Application>,
) {}

async addTagToApplication(fileNumber: string, tagName: string) {
const application = await this.applicationRepository.findOne({
where: { fileNumber: fileNumber },
relations: ['tags'],
});
if (!application) {
throw new ServiceNotFoundException(`Application not found with number ${fileNumber}`);
}

const tag = await this.tagRepository.findOne({ where: { name: tagName } });
if (!tag) {
throw new ServiceNotFoundException(`Tag not found with name ${tagName}`);
}

if (!application.tags) {
application.tags = [];
}

const tagExists = application.tags.some((t) => t.uuid === tag.uuid);
if (tagExists) {
throw new ServiceValidationException(`Tag ${tagName} already exists`);
}

application.tags.push(tag);
return this.applicationRepository.save(application);
}

async removeTagFromApplication(fileNumber: string, tagName: string) {
const application = await this.applicationRepository.findOne({
where: { fileNumber: fileNumber },
relations: ['tags'],
});
if (!application) {
throw new ServiceNotFoundException(`Application not found with number ${fileNumber}`);
}

const tag = await this.tagRepository.findOne({ where: { name: tagName } });
if (!tag) {
throw new ServiceNotFoundException(`Tag not found with name ${tagName}`);
}

if (!application.tags) {
application.tags = [];
}

const tagExists = application.tags.some((t) => t.uuid === tag.uuid);
if (!tagExists) {
throw new ServiceValidationException(`Tag ${tagName} does not exist in the application`);
}

application.tags = application.tags.filter((t) => t.uuid !== tag.uuid);
return this.applicationRepository.save(application);
}

async getApplicationTags(fileNumber: string) {
const application = await this.applicationRepository.findOne({
where: { fileNumber: fileNumber },
relations: ['tags'],
});

if (!application) {
throw new ServiceNotFoundException(`Application not found with number ${fileNumber}`);
}

return application.tags && application.tags.length > 0
? application.tags.sort((a, b) => a.auditCreatedAt.getTime() - b.auditCreatedAt.getTime())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally think sorting should be done on the database.

: [];
}
}
Loading