-
Notifications
You must be signed in to change notification settings - Fork 1
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
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
5760bef
add application_tag entity, DTO, controller, and service
Abradat bf4164f
fix tags not being fetched when querying applications and changed con…
Abradat a5fa410
remove logger and cleanup
Abradat c6144d0
add unit tests
Abradat e18bc6b
fix sorting
Abradat edcae3d
Merge branch 'develop' into feature/ALCS-472-backend
Abradat File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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); | ||
}); | ||
}); |
83 changes: 83 additions & 0 deletions
83
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,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()) | ||
: []; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.