-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1945 from bcgov/feature/ALCS-472-backend
Add Application Tag Resource to Backend
- Loading branch information
Showing
10 changed files
with
389 additions
and
50 deletions.
There are no files selected for viewing
69 changes: 69 additions & 0 deletions
69
services/apps/alcs/src/alcs/application/application-tag/application-tag.controller.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
44 changes: 44 additions & 0 deletions
44
services/apps/alcs/src/alcs/application/application-tag/application-tag.controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
services/apps/alcs/src/alcs/application/application-tag/application-tag.dto.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
122 changes: 122 additions & 0 deletions
122
services/apps/alcs/src/alcs/application/application-tag/application-tag.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
81 changes: 81 additions & 0 deletions
81
services/apps/alcs/src/alcs/application/application-tag/application-tag.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 : []; | ||
} | ||
} |
Oops, something went wrong.