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

ALCS-2325 Tag/Category entities and cruds #1937

Merged
merged 4 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
12 changes: 12 additions & 0 deletions services/apps/alcs/src/alcs/admin/admin.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ import { NoiSubtypeController } from './noi-subtype/noi-subtype.controller';
import { NoiSubtypeService } from './noi-subtype/noi-subtype.service';
import { UnarchiveCardController } from './unarchive-card/unarchive-card.controller';
import { UnarchiveCardService } from './unarchive-card/unarchive-card.service';
import { TagCategoryService } from './tag-category/tag-category.service';
import { TagCategoryController } from './tag-category/tag-category.controller';
import { TagCategory } from './tag-category/tag-category.entity';
import { Tag } from './tag/tag.entity';
import { TagController } from './tag/tag.controller';
import { TagService } from './tag/tag.service';

@Module({
imports: [
Expand All @@ -42,6 +48,8 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service';
NoticeOfIntentSubtype,
ApplicationDecisionConditionType,
Configuration,
TagCategory,
Tag,
]),
ApplicationModule,
NoticeOfIntentModule,
Expand All @@ -62,6 +70,8 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service';
UnarchiveCardController,
NoiSubtypeController,
ApplicationDecisionMakerController,
TagCategoryController,
TagController,
ApplicationDecisionConditionTypesController,
CardStatusController,
BoardManagementController,
Expand All @@ -71,6 +81,8 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service';
HolidayService,
ApplicationCeoCriterionService,
ApplicationDecisionMakerService,
TagCategoryService,
TagService,
UnarchiveCardService,
NoiSubtypeService,
ApplicationDecisionConditionTypesService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TagCategoryController } from './tag-category.controller';
import { TagCategoryService } from './tag-category.service';
import { createMock, DeepMocked } from '@golevelup/nestjs-testing';
import { ClsService } from 'nestjs-cls';
import { initTagCategoryMockEntity } from '../../../../test/mocks/mockEntities';
import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes';
import { TagCategory } from './tag-category.entity';
import { TagCategoryDto } from './tag-category.dto';

describe('TagCategoryController', () => {
let controller: TagCategoryController;
let tagCategoryService: DeepMocked<TagCategoryService>;

const mockCategoryTag = initTagCategoryMockEntity();

beforeEach(async () => {
tagCategoryService = createMock<TagCategoryService>();

const module: TestingModule = await Test.createTestingModule({
controllers: [TagCategoryController],
providers: [
{ provide: TagCategoryService, useValue: tagCategoryService },
{
provide: ClsService,
useValue: {},
},
...mockKeyCloakProviders,
],
}).compile();

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

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

it('should get a tag category', async () => {
tagCategoryService.fetch.mockResolvedValue([[mockCategoryTag], 1]);

const result = await controller.fetch(0, 0);
expect(tagCategoryService.fetch).toHaveBeenCalledTimes(1);
expect(result.data).toEqual([mockCategoryTag]);
expect(result.total).toEqual(1);
});

it('should create a tag category', async () => {
tagCategoryService.create.mockResolvedValue(mockCategoryTag);

const result = await controller.create(mockCategoryTag);
expect(tagCategoryService.create).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockCategoryTag);
});

it('should update tag category', async () => {
tagCategoryService.update.mockResolvedValue({
...mockCategoryTag,
} as TagCategory);
const categoryToUpdate = {
uuid: mockCategoryTag.uuid,
name: mockCategoryTag.name,
} as TagCategoryDto;

const result = await controller.update(
mockCategoryTag.uuid,
categoryToUpdate,
);

expect(tagCategoryService.update).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockCategoryTag);
});

it('should delete a tag category', async () => {
tagCategoryService.delete.mockResolvedValue(mockCategoryTag);

const result = await controller.delete(mockCategoryTag.uuid);
expect(tagCategoryService.delete).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockCategoryTag);
});
});
Abradat marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiOAuth2 } from '@nestjs/swagger';
import * as config from 'config';
import { RolesGuard } from '../../../common/authorization/roles-guard.service';
import { UserRoles } from '../../../common/authorization/roles.decorator';
import { AUTH_ROLE } from '../../../common/authorization/roles';
import { TagCategoryDto } from './tag-category.dto';
import { TagCategoryService } from './tag-category.service';

@Controller('tag-category')
@ApiOAuth2(config.get<string[]>('KEYCLOAK.SCOPES'))
@UseGuards(RolesGuard)
export class TagCategoryController {

constructor(private service: TagCategoryService) {}

@Get('/:pageIndex/:itemsPerPage')
@UserRoles(AUTH_ROLE.ADMIN)
async fetch(
@Param('pageIndex') pageIndex: number,
@Param('itemsPerPage') itemsPerPage: number,
@Query('search') search?: string,
) {
const result = await this.service.fetch(pageIndex, itemsPerPage, search);
return { data: result[0], total: result[1] };
}

@Post('')
@UserRoles(AUTH_ROLE.ADMIN)
async create(@Body() createDto: TagCategoryDto) {
return await this.service.create(createDto);
}

@Patch('/:uuid')
@UserRoles(AUTH_ROLE.ADMIN)
async update(@Param('uuid') uuid: string, @Body() updateDto: TagCategoryDto) {
return await this.service.update(uuid, updateDto);
}

@Delete('/:uuid')
@UserRoles(AUTH_ROLE.ADMIN)
async delete(@Param('uuid') uuid: string) {
return await this.service.delete(uuid);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsString } from 'class-validator';

export class TagCategoryDto {
@IsString()
uuid: string;

@IsString()
name: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AutoMap } from 'automapper-classes';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Base } from '../../../common/entities/base.entity';

@Entity({ comment: 'Tag category.' })
export class TagCategory extends Base {
constructor(data?: Partial<TagCategory>) {
super();
if (data) {
Object.assign(this, data);
}
}

@AutoMap()
@PrimaryGeneratedColumn('uuid')
uuid: string;

@AutoMap()
@Column()
name: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { CONFIG_TOKEN } from '@app/common/config/config.module';
import { Test, TestingModule } from '@nestjs/testing';
import { Repository } from 'typeorm';
import { createMock, DeepMocked } from '@golevelup/nestjs-testing';
import { TagCategory } from '../tag-category/tag-category.entity';
import { initTagCategoryMockEntity } from '../../../../test/mocks/mockEntities';
import { AutomapperModule } from 'automapper-nestjs';
import { classes } from 'automapper-classes';
import { getRepositoryToken } from '@nestjs/typeorm';
import * as config from 'config';
import { TagCategoryDto } from './tag-category.dto';
import { ServiceValidationException } from '@app/common/exceptions/base.exception';
import { TagCategoryService } from './tag-category.service';

describe('TagCategoryService', () => {
let service: TagCategoryService;
let tagCategoryRepositoryMock: DeepMocked<Repository<TagCategory>>;
let mockTagCategoryEntity;

beforeEach(async () => {
tagCategoryRepositoryMock = createMock<Repository<TagCategory>>();
mockTagCategoryEntity = initTagCategoryMockEntity();

const module: TestingModule = await Test.createTestingModule({
imports: [
AutomapperModule.forRoot({
strategyInitializer: classes(),
}),
],
providers: [
TagCategoryService,
{
provide: getRepositoryToken(TagCategory),
useValue: tagCategoryRepositoryMock,
},
{
provide: CONFIG_TOKEN,
useValue: config,
},
],
}).compile();

service = module.get<TagCategoryService>(TagCategoryService);
tagCategoryRepositoryMock.findOne.mockResolvedValue(mockTagCategoryEntity);
tagCategoryRepositoryMock.findOneOrFail.mockResolvedValue(mockTagCategoryEntity);
tagCategoryRepositoryMock.save.mockResolvedValue(mockTagCategoryEntity);
tagCategoryRepositoryMock = module.get(getRepositoryToken(TagCategory));
});

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

it('should get a tag category', async () => {
expect(await service.getOneOrFail('fake')).toStrictEqual(
mockTagCategoryEntity,
);
});

it('should call save when an tag category is updated', async () => {
const payload: TagCategoryDto = {
uuid: mockTagCategoryEntity.uuid,
name: mockTagCategoryEntity.name,
};

const result = await service.update(mockTagCategoryEntity.uuid, payload);
expect(result).toStrictEqual(mockTagCategoryEntity);
expect(tagCategoryRepositoryMock.save).toHaveBeenCalledTimes(1);
expect(tagCategoryRepositoryMock.save).toHaveBeenCalledWith(
mockTagCategoryEntity,
);
});

it('should fail update if tag category does not exist', async () => {
const payload: TagCategoryDto = {
uuid: mockTagCategoryEntity.uuid,
name: mockTagCategoryEntity.name,
};

tagCategoryRepositoryMock.findOneOrFail.mockRejectedValue(
new ServiceValidationException(
`Tag Category for with ${mockTagCategoryEntity.uuid} not found`,
),
);

await expect(
service.update(mockTagCategoryEntity.uuid, payload),
).rejects.toMatchObject(
new ServiceValidationException(
`Tag Category for with ${mockTagCategoryEntity.uuid} not found`,
),
);
expect(tagCategoryRepositoryMock.save).toBeCalledTimes(0);
});

it('should call save when tag successfully create', async () => {
const payload: TagCategoryDto = {
uuid: mockTagCategoryEntity.uuid,
name: mockTagCategoryEntity.name,
};

await service.create(payload);

expect(tagCategoryRepositoryMock.save).toBeCalledTimes(1);
});
});
Abradat marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import { TagCategory } from './tag-category.entity';
import { FindOptionsWhere, Like, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { TagCategoryDto } from './tag-category.dto';

@Injectable()
export class TagCategoryService {
constructor(
@InjectRepository(TagCategory)
private repository: Repository<TagCategory>,
) {}

async fetch(pageIndex: number, itemsPerPage: number, search?: string) {
let searchExpression: FindOptionsWhere<TagCategory> | undefined = undefined;

if (search) {
searchExpression = {
name: Like(`%${search}%`),
};
}

return (
(await this.repository.findAndCount({
where: searchExpression,
order: { name: 'DESC' },
take: itemsPerPage,
skip: pageIndex * itemsPerPage,
})) || [[], 0]
);
}

async create(dto: TagCategoryDto) {
const newTagCategory = new TagCategory();
newTagCategory.name = dto.name;
return this.repository.save(newTagCategory);
}

async getOneOrFail(uuid: string) {
return await this.repository.findOneOrFail({
where: { uuid },
});
}

async update(uuid: string, updateDto: TagCategoryDto) {
const tagCategory = await this.getOneOrFail(uuid);
tagCategory.name = updateDto.name;
return await this.repository.save(tagCategory);
}

async delete(uuid: string) {
const tagCategory = await this.getOneOrFail(uuid);

return await this.repository.remove(tagCategory);
}
}
Loading