Skip to content

Commit

Permalink
Merge pull request #1945 from bcgov/feature/ALCS-472-backend
Browse files Browse the repository at this point in the history
Add Application Tag Resource to Backend
  • Loading branch information
Abradat authored Oct 31, 2024
2 parents 718a10e + edcae3d commit 8588367
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 50 deletions.
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,81 @@
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 : [];
}
}
Loading

0 comments on commit 8588367

Please sign in to comment.