From 215406ec70060b464c70bb0f73e82f5758f5e95d Mon Sep 17 00:00:00 2001 From: Alberto Baroso <35893959+AlbertoBaroso@users.noreply.github.com> Date: Fri, 12 Apr 2024 19:02:31 +0200 Subject: [PATCH] Availability: controller, service, entity, tests (#21) * Feature/rectuitment session (#20) * feat: session service, controller, entity * feat: update recruitment-session: service, controller, entity * test: mock recruitment session service, insert data mock * fix: relative import of recruitment-session from shared folder * fix: recruitment-session service Delete test * fix: removed lastModified from UpdateRecruitmentSessionDto * fix: ability check on recruitment session creation * feat: check if recruitment session has pending interviews before deleting it * feat: check for conflicts and consistency when updating a recruitment session state * fix: check ability for update recruitment session * refactor: removed unused imports in recruitment-session.controller.ts * fix: use const for unchanged variable in createRecruitmentSession service method * refactor: removed unused code in creatre-recruitment-session.dto.ts * fix: updated Date[] in create and update Recruitment session DTOs * test: Recruitment Session Controller tests * refactor: removed unused imports * feat: Recruitment session module * fix: import of RecruitmentSessionState in recruitment-session.service.ts * fix: find function recreuitment session * fix: set findBy functions * fix: adjustments about array of recruitment session * test: create recruitment session * add: test create RS on service.spec * fix: changed array into scalar value in findBy and findActive --------- Co-authored-by: Alberto Baroso * feat: created availability module * fix: mock data timestamp for midnight * fix: added http exceptions and removed unnecessary request fields * test: Initial tests for availability controller * test: Initial tests for availability service * fix: relationship between timeslot and availability entities * fix: removed relationship fields in entities * test: CRUD unit tests for availability * RecruitmentSession: controller, service, entity, tests (#15) RecruitmentSessionController: - findActive: Retrieve the active recruitment session if it exists. - createRecruitmentSession - updateRecruitmentSession - deleteRecruitmentSession RecruitmentSessionService: - createRecruitmentSession - findAllRecruitmentSessions - findRecruitmentSessionById - findActiveRecruitmentSession - deletRecruitmentSession - updateRecruitmentSession - sessionHasPendingInterviews: Check if a recruitment session has pending interviews (to be implemented). DTOs: - CreateRecruitmentSessionDTO - UpdateRecruitmentSessionDTO - RecruitmentSessionResponseDTO Tests: - Controller Unit tests: recruitment-session.controller.spec.ts - Service Unit tests: recruitment-session.service.spec.ts Commits: * fix: missing dependencies and imports (#9) * fix: added @joi/date library * fix: added missing useState import fix: removed loading screen when auth token is empty * docs: updated project description, useful links, and contributors in README.md (#10) * feat session: service, controller, entity * feat: update recruitment-session: service, controller, entity * feat: update recruitment-session: service, controller, entity * fix: dependencies in shared/abilities * fix: mock shared -> required/optional fields * fix: mock recruitment session service, insert data mock * fix: relative import of recruitment-session from shared folder * fix: recruitment-session service Delete test * fix: removed lastModified from UpdateRecruitmentSessionDto * fix: ability check on recruitment session creation * feat: check if recruitment session has pending interviews before deleting it * feat: check for conflicts and consistency when updating a recruitment session state * fix: check ability for update recruitment session * refactor: removed unused imports in recruitment-session.controller.ts * fix: use const for unchanged variable in createRecruitmentSession service method * refactor: removed unused code in creatre-recruitment-session.dto.ts * fix: updated Date[] in create and update Recruitment session DTOs * fix: added 'state' to recruitmentSession response DTO * test: Recruitment Session Controller tests * refactor: removed unused imports --------- Co-authored-by: Alberto Baroso * fix: updated imports from shared/recruitment-session * Feature: Rectuitment session module (#17) * SonarCloud Analysis (#18) * feat: setup coverageDirectory and coveragePathIgnorePatterns * ci: added SonarCloud Analysis job in GitHub actions * ci: sonar-project.properties configuration * Simplified workflow, single task, maximum gain --------- Co-authored-by: Vincenzo Pellegrini * fix: removed unused avaiability endpoints * feat: added existance checks and conflict check upon availability creation * feat: return 404 when attempting to delete non-existing availabilities refactor: availability.controller.ts using prettier * fix: Availability authorizations and creation schema * fix!: updated AvailabilityState enum values * test: role abilities on Availability test: validate insert Availability schema * feat: additional checks before deleting availability * test: availability controller unit tests * feat: added findByUserAndTimeSlot in Availability service fix: used Relation as type of fields in Availability entity * fix!: removed unnecessary fields in CreateAvailabilityDto * test: Availability service unit tests * fix!: removed create/delete timeslot endpoint * feat: TimeSlot service generateTimeslots() * test: TimeSlot service generateTimeslots() * feat: create recruitment session's timeslots atomically using a transaction * feat: added jest-mock-extended library to auto mock classes * fix: added DbAwareColumn to overcome sqlite column type limitation * fix: apply abilities on TimeSlots * fix: imported missing modules * fix: import Joi in availability controller * test: mock recruitment session for timeslot generation * fix: added coverage exclusions in sonar-project.properties * refactor: format according to prettier rules, reduced code duplication in timeslots.service.spec.ts * feat: User is_board and is_expert flags --------- Co-authored-by: whiitex <89868763+whiitex@users.noreply.github.com> Co-authored-by: Marco De Luca <31864038+markdeluk@users.noreply.github.com> Co-authored-by: whiteOFF <89868763+whiteOFF@users.noreply.github.com> Co-authored-by: Vincenzo Pellegrini Co-authored-by: Mugna0990 <150722467+Mugna0990@users.noreply.github.com> --- api/package.json | 1 + api/src/app.module.ts | 2 + .../applications.controller.spec.ts | 2 +- .../application/applications.service.spec.ts | 8 +- .../availability.controller.spec.ts | 209 +++++++++++++++ .../availability/availability.controller.ts | 135 ++++++++++ api/src/availability/availability.entity.ts | 36 +++ api/src/availability/availability.module.ts | 19 ++ .../availability/availability.service.spec.ts | 178 +++++++++++++ api/src/availability/availability.service.ts | 81 ++++++ .../availability/create-availability.dto.ts | 6 + api/src/mocks/data-sources.ts | 59 +++++ api/src/mocks/data.ts | 56 +++- api/src/mocks/services.ts | 16 +- .../recruitment-session.controller.spec.ts | 21 +- .../recruitment-session.controller.ts | 19 +- .../recruitment-session.module.ts | 7 +- .../recruitment-session.service.spec.ts | 78 +++++- .../recruitment-session.service.ts | 42 ++- api/src/timeslots/create-timeslot.dto.ts | 2 +- api/src/timeslots/timeslot.entity.ts | 12 +- .../timeslots/timeslots.controller.spec.ts | 83 ------ api/src/timeslots/timeslots.controller.ts | 85 +----- api/src/timeslots/timeslots.service.spec.ts | 148 ++++++++++- api/src/timeslots/timeslots.service.ts | 56 +++- api/src/users/user.entity.ts | 12 +- api/src/users/users.controller.spec.ts | 4 + api/src/users/users.controller.ts | 2 + api/src/users/users.e2e-spec.ts | 10 + api/src/utils/database.ts | 33 +++ api/src/utils/db-aware-column.ts | 18 ++ api/test/app.e2e-spec.ts | 13 +- pnpm-lock.yaml | 241 +----------------- shared/src/abilities.ts | 3 +- shared/src/availability.spec.ts | 98 +++++-- shared/src/availability.ts | 30 +-- shared/src/person.ts | 2 + shared/src/recruitment-session.spec.ts | 24 +- shared/src/recruitment-session.ts | 4 +- shared/src/timeslot.ts | 45 +--- sonar-project.properties | 2 +- 41 files changed, 1374 insertions(+), 528 deletions(-) create mode 100644 api/src/availability/availability.controller.spec.ts create mode 100644 api/src/availability/availability.controller.ts create mode 100644 api/src/availability/availability.entity.ts create mode 100644 api/src/availability/availability.module.ts create mode 100644 api/src/availability/availability.service.spec.ts create mode 100644 api/src/availability/availability.service.ts create mode 100644 api/src/availability/create-availability.dto.ts create mode 100644 api/src/mocks/data-sources.ts create mode 100644 api/src/utils/database.ts create mode 100644 api/src/utils/db-aware-column.ts diff --git a/api/package.json b/api/package.json index b4a347b..9dcef79 100644 --- a/api/package.json +++ b/api/package.json @@ -39,6 +39,7 @@ "dotenv": "^16.0.3", "google-auth-library": "^8.7.0", "googleapis": "^118.0.0", + "jest-mock-extended": "^3.0.5", "joi": "^17.7.0", "js-yaml": "^4.1.0", "jwks-rsa": "^3.0.0", diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 271ffd0..607aeb4 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -10,6 +10,7 @@ import { APP_GUARD } from '@nestjs/core'; import { JwtGuard } from './authentication/jwt-guard.guard'; import { AuthorizationModule } from './authorization/authorization.module'; import { AuthorizationGuard } from './authorization/authorization.guard'; +import { AvailabilityModule } from './availability/availability.module'; @Module({ imports: [ @@ -34,6 +35,7 @@ import { AuthorizationGuard } from './authorization/authorization.guard'; ApplicationsModule, AuthenticationModule, AuthorizationModule, + AvailabilityModule, RecruitmentSessionModule, TimeSlotsModule, UsersModule, diff --git a/api/src/application/applications.controller.spec.ts b/api/src/application/applications.controller.spec.ts index 1c04510..e445537 100644 --- a/api/src/application/applications.controller.spec.ts +++ b/api/src/application/applications.controller.spec.ts @@ -16,7 +16,7 @@ import { mockMscApplication, updateApplicationDTO, testDate, -} from '@mocks/data'; +} from 'src/mocks/data'; import { BadRequestException, ConflictException, diff --git a/api/src/application/applications.service.spec.ts b/api/src/application/applications.service.spec.ts index db7ab56..976cfeb 100644 --- a/api/src/application/applications.service.spec.ts +++ b/api/src/application/applications.service.spec.ts @@ -4,8 +4,8 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { Application } from './application.entity'; import { ApplicationState, ApplicationType } from '@hkrecruitment/shared'; import { UsersService } from '../users/users.service'; -import { mockedRepository } from '@mocks/repositories'; -import { mockedUsersService } from '@mocks/services'; +import { mockedRepository } from 'src/mocks/repositories'; +import { mockedUsersService } from 'src/mocks/services'; import { applicant, applicationFiles, @@ -20,7 +20,7 @@ import { folderId, today, testDate, -} from '@mocks/data'; +} from 'src/mocks/data'; import { flattenApplication } from './create-application.dto'; import { InternalServerErrorException } from '@nestjs/common'; @@ -204,7 +204,7 @@ describe('ApplicationsService', () => { const applicantId = 'abc123'; const folderId = 'folder_abc123'; const fileId = 'file_abc123'; - const today = '1/1/2023, 24:00:00'; + const today = '1/1/2023, 10:00:00'; let mockApplication, mockCreateApplicationDTO; switch (applicationType) { case ApplicationType.BSC: diff --git a/api/src/availability/availability.controller.spec.ts b/api/src/availability/availability.controller.spec.ts new file mode 100644 index 0000000..f87083d --- /dev/null +++ b/api/src/availability/availability.controller.spec.ts @@ -0,0 +1,209 @@ +import { + mockAvailability, + mockCreateAvailabilityDto, + mockClerk, + mockTimeSlot, + testDate, +} from 'src/mocks/data'; +import { createMock } from '@golevelup/ts-jest'; +import { AvailabilityController } from './availability.controller'; +import { AvailabilityService } from './availability.service'; +import { TestBed } from '@automock/jest'; +import { AuthenticatedRequest } from 'src/authorization/authenticated-request.types'; +import { UsersService } from 'src/users/users.service'; +import { TimeSlotsService } from 'src/timeslots/timeslots.service'; +import { Action } from '@hkrecruitment/shared'; +import { createMockAbility } from '@hkrecruitment/shared/abilities.spec'; + +describe('AvailabilityController', () => { + let controller: AvailabilityController; + let availabilityService: AvailabilityService; + let userService: UsersService; + let timeslotService: TimeSlotsService; + + /************* Test setup ************/ + + beforeAll(() => { + jest + .spyOn(global, 'Date') + .mockImplementation(() => testDate as unknown as string); + }); + + beforeEach(async () => { + const { unit, unitRef } = TestBed.create(AvailabilityController).compile(); + + controller = unit; + availabilityService = unitRef.get(AvailabilityService); + userService = unitRef.get(UsersService); + timeslotService = unitRef.get(TimeSlotsService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + expect(availabilityService).toBeDefined(); + expect(userService).toBeDefined(); + expect(timeslotService).toBeDefined(); + }); + + // CRUD OPERATIONS + + describe('createAvailability', () => { + it('should allow creating a valid availability', async () => { + const mockRequest = getMockRequest(); + + jest.spyOn(userService, 'findByOauthId').mockResolvedValue(mockClerk); + jest.spyOn(timeslotService, 'findById').mockResolvedValue(mockTimeSlot); + jest + .spyOn(availabilityService, 'createAvailability') + .mockResolvedValue(mockAvailability); + + const result = await controller.createAvailability( + mockCreateAvailabilityDto, + mockRequest, + ); + + expect(result).toEqual(mockAvailability); + }); + + it('should throw an error if the availability is invalid', async () => { + const mockRequest = getMockRequest(); + + jest.spyOn(userService, 'findByOauthId').mockResolvedValue(mockClerk); + jest.spyOn(timeslotService, 'findById').mockResolvedValue(mockTimeSlot); + jest + .spyOn(availabilityService, 'createAvailability') + .mockResolvedValue(undefined); + + await expect( + controller.createAvailability(mockCreateAvailabilityDto, mockRequest), + ).rejects.toThrowError(); + }); + + it('should throw an error if the timeslot does not exist', async () => { + const mockRequest = getMockRequest(); + + jest.spyOn(timeslotService, 'findById').mockResolvedValue(null); + + await expect( + controller.createAvailability(mockCreateAvailabilityDto, mockRequest), + ).rejects.toThrowError('Timeslot not found'); + }); + + it('should throw an error if the user does not exist', async () => { + const mockRequest = getMockRequest(); + + jest.spyOn(timeslotService, 'findById').mockResolvedValue(mockTimeSlot); + jest.spyOn(userService, 'findByOauthId').mockResolvedValue(null); + + await expect( + controller.createAvailability(mockCreateAvailabilityDto, mockRequest), + ).rejects.toThrowError('User not found'); + }); + + it('should throw a conflict error if an availability already exists for the user in the same timeslot', async () => { + const mockRequest = getMockRequest(); + + jest.spyOn(userService, 'findByOauthId').mockResolvedValue(mockClerk); + jest.spyOn(timeslotService, 'findById').mockResolvedValue(mockTimeSlot); + jest + .spyOn(availabilityService, 'findByUserAndTimeSlot') + .mockResolvedValue(mockAvailability); + + await expect( + controller.createAvailability(mockCreateAvailabilityDto, mockRequest), + ).rejects.toThrowError('Availability already exists for this timeslot'); + }); + }); + + // Delete an availability + describe('deleteAvailability', () => { + it('should allow deleting an availability', async () => { + const mockAbility = createMockAbility(({ can }) => { + can(Action.Delete, 'Availability'); + }); + const mockReq = createMock(); + mockReq.user.sub = mockAvailability.user.oauthId; + + jest + .spyOn(availabilityService, 'findById') + .mockResolvedValue(mockAvailability); + jest + .spyOn(availabilityService, 'deleteAvailability') + .mockResolvedValue(mockAvailability); + + await expect( + controller.deleteAvailability( + mockAbility, + mockAvailability.id, + mockReq, + ), + ).resolves.toEqual(mockAvailability); + }); + + it('should throw an error if the user does not have the ability to delete the availability', async () => { + const mockAbility = createMockAbility(({ cannot }) => { + cannot(Action.Delete, 'Availability'); + }); + const mockReq = createMock(); + mockReq.user.sub = '123'; + + jest + .spyOn(availabilityService, 'findById') + .mockResolvedValue(mockAvailability); + await expect( + controller.deleteAvailability( + mockAbility, + mockAvailability.id, + mockReq, + ), + ).rejects.toThrowError('Forbidden'); + }); + + it("should throw an error if the user tries to delete someone else's availability", async () => { + const mockAbility = createMockAbility(({ can }) => { + can(Action.Delete, 'Availability'); + }); + const mockReq = createMock(); + mockReq.user.sub = '345'; + + jest + .spyOn(availabilityService, 'findById') + .mockResolvedValue(mockAvailability); + await expect( + controller.deleteAvailability( + mockAbility, + mockAvailability.id, + mockReq, + ), + ).rejects.toThrowError('Forbidden'); + }); + + it('should throw an error if the availability does not exist', async () => { + const mockAbility = createMockAbility(({ can }) => { + can(Action.Create, 'Availability'); + }); + const mockReq = createMock(); + mockReq.user.sub = '123'; + + jest.spyOn(availabilityService, 'findById').mockResolvedValue(undefined); + + await expect( + controller.deleteAvailability( + mockAbility, + mockAvailability.id, + mockReq, + ), + ).rejects.toThrowError('Not Found'); + }); + + // TODO: Delete an availability that is in use, where the user is optional + // TODO: Delete an availability that is in use, with an existing replacement + // TODO: Delete an availability that is in use, with no existing replacement + }); +}); + +function getMockRequest() { + const mockRequest = createMock(); + mockRequest.user.sub = '123'; + return mockRequest; +} diff --git a/api/src/availability/availability.controller.ts b/api/src/availability/availability.controller.ts new file mode 100644 index 0000000..80bd194 --- /dev/null +++ b/api/src/availability/availability.controller.ts @@ -0,0 +1,135 @@ +import { + Body, + ConflictException, + Controller, + Delete, + ForbiddenException, + NotFoundException, + Param, + Post, + Req, +} from '@nestjs/common'; +import { AvailabilityService } from './availability.service'; +import { TimeSlotsService } from '../timeslots/timeslots.service'; +import { UsersService } from '../users/users.service'; +import { + Action, + AppAbility, + AvailabilityState, + insertAvailabilitySchema, +} from '@hkrecruitment/shared'; +import { JoiValidate } from '../joi-validation/joi-validate.decorator'; +import { + ApiBadRequestResponse, + ApiBearerAuth, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiCreatedResponse, + ApiTags, + ApiNoContentResponse, + ApiBadGatewayResponse, + ApiConflictResponse, + ApiUnprocessableEntityResponse, +} from '@nestjs/swagger'; +import { CheckPolicies } from 'src/authorization/check-policies.decorator'; +import { Availability } from './availability.entity'; +import * as Joi from 'joi'; +import { CreateAvailabilityDto } from './create-availability.dto'; +import { AuthenticatedRequest } from 'src/authorization/authenticated-request.types'; +import { Ability } from 'src/authorization/ability.decorator'; + +@ApiBearerAuth() +@ApiTags('availability') +@Controller('availability') +export class AvailabilityController { + constructor( + private readonly availabilityService: AvailabilityService, + private readonly timeSlotsService: TimeSlotsService, + private readonly userService: UsersService, + ) {} + + @ApiCreatedResponse() + @ApiBadRequestResponse() + @ApiNotFoundResponse() + @ApiForbiddenResponse() + @ApiBadGatewayResponse() + @ApiConflictResponse() + @CheckPolicies((ability) => ability.can(Action.Create, 'Availability')) + @Post() + @JoiValidate({ + param: Joi.number() + .positive() + .integer() + .required() + .label('availability_id'), + body: insertAvailabilitySchema, + }) + async createAvailability( + @Body() availabilityDto: CreateAvailabilityDto, + @Req() req: AuthenticatedRequest, + ): Promise { + /* Verify timeslot exists */ + const timeSlot = await this.timeSlotsService.findById( + availabilityDto.timeSlotId, + ); + if (!timeSlot) throw new NotFoundException('Timeslot not found'); + + /* Verify user exists */ + const user = await this.userService.findByOauthId(req.user.sub); + if (!user) throw new NotFoundException('User not found'); + + /* Verify availability for timeslot does not already exist */ + const existing = await this.availabilityService.findByUserAndTimeSlot( + user, + timeSlot, + ); + if (existing) + throw new ConflictException( + 'Availability already exists for this timeslot', + ); + + const availability = { + timeSlot: timeSlot, + state: AvailabilityState.Free, + } as Availability; + const result = await this.availabilityService.createAvailability( + availability, + ); + if (!result) throw new ForbiddenException(); + return result; + } + + @ApiNotFoundResponse() + @ApiForbiddenResponse() + @ApiNoContentResponse() + @ApiUnprocessableEntityResponse() + @CheckPolicies((ability) => ability.can(Action.Delete, 'Availability')) + @Delete(':availability_id') + @JoiValidate({ + param: Joi.number() + .positive() + .integer() + .required() + .label('availability_id'), + }) + async deleteAvailability( + @Ability() ability: AppAbility, + @Param('availability_id') availabilityId: number, + @Req() req: AuthenticatedRequest, + ): Promise { + // Check if availability exists + const availability = await this.availabilityService.findById( + availabilityId, + ); + if (!availability) throw new NotFoundException(); + + // Check if user has permission to delete availability + if ( + ability.cannot(Action.Delete, 'Availability') || + availability.user.oauthId !== req.user.sub + ) + throw new ForbiddenException(); + + return await this.availabilityService.deleteAvailability(availabilityId); + } +} diff --git a/api/src/availability/availability.entity.ts b/api/src/availability/availability.entity.ts new file mode 100644 index 0000000..384669e --- /dev/null +++ b/api/src/availability/availability.entity.ts @@ -0,0 +1,36 @@ +import { + Column, + Entity, + ManyToOne, + PrimaryGeneratedColumn, + Relation, +} from 'typeorm'; +import { + Availability as AvailabilityInterface, + AvailabilityState, +} from '../../../shared/src/availability'; +import { User } from 'src/users/user.entity'; +import { TimeSlot } from 'src/timeslots/timeslot.entity'; +import { DbAwareColumn } from 'src/utils/db-aware-column'; + +@Entity() +export class Availability implements AvailabilityInterface { + @PrimaryGeneratedColumn('increment') + id: number; + + @Column() + state: AvailabilityState; + + @Column({ name: 'last_modified' }) + lastModified: Date; + + @DbAwareColumn(() => TimeSlot, { name: 'time_slot' }) + @ManyToOne(() => TimeSlot, (timeSlot) => timeSlot.availabilities) + timeSlot: Relation; + + @ManyToOne(() => User, (user) => user.availabilities) + user: Relation; + + // @OneToOne(() => Interview) + // interview: Interview; +} diff --git a/api/src/availability/availability.module.ts b/api/src/availability/availability.module.ts new file mode 100644 index 0000000..a120215 --- /dev/null +++ b/api/src/availability/availability.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { AvailabilityService } from './availability.service'; +import { AvailabilityController } from './availability.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Availability } from './availability.entity'; +import { UsersModule } from 'src/users/users.module'; +import { TimeSlotsModule } from 'src/timeslots/timeslots.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Availability]), + UsersModule, + TimeSlotsModule, + ], + providers: [AvailabilityService], + controllers: [AvailabilityController], + exports: [AvailabilityService], +}) +export class AvailabilityModule {} diff --git a/api/src/availability/availability.service.spec.ts b/api/src/availability/availability.service.spec.ts new file mode 100644 index 0000000..9f62ba2 --- /dev/null +++ b/api/src/availability/availability.service.spec.ts @@ -0,0 +1,178 @@ +import { mockAvailability, testDate } from 'src/mocks/data'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AvailabilityService } from './availability.service'; +import { Availability } from './availability.entity'; +import { getRepositoryToken, getDataSourceToken } from '@nestjs/typeorm'; +import { mockedRepository } from 'src/mocks/repositories'; +import { ConflictException } from '@nestjs/common'; +import { mockDataSource } from 'src/mocks/data-sources'; +import { AvailabilityState } from '@hkrecruitment/shared'; + +describe('AvailabilityService', () => { + let service: AvailabilityService; + + /************* Test setup ************/ + + beforeAll(() => { + jest + .spyOn(global, 'Date') + .mockImplementation(() => testDate as unknown as string); + }); + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AvailabilityService, + { + provide: getRepositoryToken(Availability), + useValue: mockedRepository, + }, + { + provide: getDataSourceToken(), + useValue: mockDataSource, + }, + ], + }).compile(); + + service = module.get(AvailabilityService); + }); + + afterEach(() => jest.clearAllMocks()); + + /*************** Tests ***************/ + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('listAvailabilities', () => { + it('should return all availabilities', async () => { + jest + .spyOn(mockedRepository, 'find') + .mockResolvedValue([mockAvailability]); + const result = await service.listAvailabilities(); + expect(result).toEqual([mockAvailability]); + expect(mockedRepository.find).toHaveBeenCalledTimes(1); + }); + }); + + // CRUD OPERATIONS + + describe('findById', () => { + it('should return the availability with the specified id', async () => { + jest + .spyOn(mockedRepository, 'findBy') + .mockResolvedValue([mockAvailability]); + const result = await service.findById(mockAvailability.id); + expect(result).toEqual(mockAvailability); + expect(mockedRepository.findBy).toHaveBeenCalledTimes(1); + }); + }); + + describe('createAvailability', () => { + it('should create a new availability', async () => { + jest.spyOn(mockedRepository, 'save').mockResolvedValue(mockAvailability); + const result = await service.createAvailability(mockAvailability); + expect(result).toEqual(mockAvailability); + expect(mockedRepository.save).toHaveBeenCalledTimes(1); + }); + }); + + describe('deleteAvailability', () => { + it('should remove the specified availability from the database', async () => { + const mockAvailabilityRepository = { + findOneBy: mockAvailability, + remove: mockAvailability, + }; + const mockedRepositories = mockDataSource.setMockResults({ + Availability: mockAvailabilityRepository, + }); + const result = await service.deleteAvailability(mockAvailability.id); + expect(result).toEqual(mockAvailability); + expect(mockDataSource.createQueryRunner).toHaveBeenCalledTimes(1); + expect( + mockedRepositories['Availability'].findOneBy, + ).toHaveBeenCalledTimes(1); + expect(mockedRepositories['Availability'].findOneBy).toHaveBeenCalledWith( + { id: mockAvailability.id }, + ); + expect(mockedRepositories['Availability'].remove).toHaveBeenCalledTimes( + 1, + ); + expect(mockedRepositories['Availability'].remove).toHaveBeenCalledWith( + mockAvailability, + ); + }); + + it('should throw a conflict error if the availability is in use', async () => { + const mockAvailabilityInUse = { + ...mockAvailability, + state: AvailabilityState.Interviewing, + }; + const mockAvailabilityRepository = { + findOneBy: mockAvailabilityInUse, + remove: mockAvailabilityInUse, + }; + const mockedRepositories = mockDataSource.setMockResults({ + Availability: mockAvailabilityRepository, + }); + jest + .spyOn(mockedRepositories['Availability'], 'findOneBy') + .mockResolvedValue(mockAvailabilityInUse); + jest + .spyOn(mockedRepositories['Availability'], 'remove') + .mockResolvedValue(mockAvailabilityInUse); + const result = service.deleteAvailability(mockAvailabilityInUse.id); + await expect(result).rejects.toThrow(ConflictException); + expect(mockedRepositories['Availability'].findOneBy).toHaveBeenCalledWith( + { id: mockAvailability.id }, + ); + expect( + mockedRepositories['Availability'].findOneBy, + ).toHaveBeenCalledTimes(1); + }); + }); + + describe('findAvailabilityById', () => { + it('should return the availability with the specified id', async () => { + jest + .spyOn(mockedRepository, 'findBy') + .mockResolvedValue([mockAvailability]); + const result = await service.findById(mockAvailability.id); + expect(result).toEqual(mockAvailability); + expect(mockedRepository.findBy).toHaveBeenCalledTimes(1); + }); + }); + + describe('findByUserAndTimeSlot', () => { + it('should return the availability with the specified user and time slot', async () => { + jest + .spyOn(mockedRepository, 'findBy') + .mockResolvedValue([mockAvailability]); + const result = await service.findByUserAndTimeSlot( + mockAvailability.user, + mockAvailability.timeSlot, + ); + expect(result).toEqual(mockAvailability); + expect(mockedRepository.findBy).toHaveBeenCalledTimes(1); + expect(mockedRepository.findBy).toHaveBeenCalledWith({ + user: mockAvailability.user, + timeSlot: mockAvailability.timeSlot, + }); + }); + + it('should return null if no availability is found', async () => { + jest.spyOn(mockedRepository, 'findBy').mockResolvedValue([]); + const result = await service.findByUserAndTimeSlot( + mockAvailability.user, + mockAvailability.timeSlot, + ); + expect(result).toBeNull(); + expect(mockedRepository.findBy).toHaveBeenCalledTimes(1); + expect(mockedRepository.findBy).toHaveBeenCalledWith({ + user: mockAvailability.user, + timeSlot: mockAvailability.timeSlot, + }); + }); + }); +}); diff --git a/api/src/availability/availability.service.ts b/api/src/availability/availability.service.ts new file mode 100644 index 0000000..6f9904d --- /dev/null +++ b/api/src/availability/availability.service.ts @@ -0,0 +1,81 @@ +import { ConflictException, Injectable } from '@nestjs/common'; +import { InjectRepository, InjectDataSource } from '@nestjs/typeorm'; +import { Repository, DataSource, QueryRunner } from 'typeorm'; +import { Availability } from './availability.entity'; +import { TimeSlot } from 'src/timeslots/timeslot.entity'; +import { User } from 'src/users/user.entity'; +import { AvailabilityState } from '@hkrecruitment/shared'; +import { transaction } from 'src/utils/database'; + +@Injectable() +export class AvailabilityService { + constructor( + @InjectRepository(Availability) + private readonly availabilityRepository: Repository, + @InjectDataSource() + private dataSource: DataSource, + ) {} + + async listAvailabilities(): Promise { + return await this.availabilityRepository.find(); + } + + async findById(id: number): Promise { + const matches = await this.availabilityRepository.findBy({ + id: id, + }); + return matches.length > 0 ? matches[0] : null; + } + + async findByUserAndTimeSlot(user: User, timeSlot: TimeSlot) { + const matches = await this.availabilityRepository.findBy({ + user: user, + timeSlot: timeSlot, + }); + return matches.length > 0 ? matches[0] : null; + } + + async createAvailability(availability: Availability): Promise { + return await this.availabilityRepository.save(availability); + } + + async updateAvailability( + oldAvailabilityId: number, + newAvailability: Availability, + ): Promise { + return await this.availabilityRepository.save({ + ...newAvailability, + id: oldAvailabilityId, + }); + } + + async deleteAvailability(availabilityId: number): Promise { + return await transaction( + this.dataSource, + async (queryRunner: QueryRunner) => { + const availability = await queryRunner.manager + .getRepository(Availability) + .findOneBy({ id: availabilityId }); + + // Check if availability is in use + if (availability.state === AvailabilityState.Interviewing) { + const rescheduled = false; + // TODO: If user was optional, just delete it, otherwise: + // Try to assign a different person to the interview + // TODO: Retrieve availability status + id of interviewer of timeslots in range [timeslot-1, timeslot+1] + // TODO: Search for someone with state = AvailabilityState.Available in timesolt, and state != AvailabilityState.interviewing in t-1 and t+1 + // TODO: If it doesn't exist: rescheduled = False + // If no other availability is found, availability cannot be deleted + if (!rescheduled) + throw new ConflictException( + 'Availability is in use and cannot be deleted.', + ); + } + + return await queryRunner.manager + .getRepository(Availability) + .remove(availability); + }, + ); + } +} diff --git a/api/src/availability/create-availability.dto.ts b/api/src/availability/create-availability.dto.ts new file mode 100644 index 0000000..cf90fab --- /dev/null +++ b/api/src/availability/create-availability.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateAvailabilityDto { + @ApiProperty() + timeSlotId: number; +} diff --git a/api/src/mocks/data-sources.ts b/api/src/mocks/data-sources.ts new file mode 100644 index 0000000..4371477 --- /dev/null +++ b/api/src/mocks/data-sources.ts @@ -0,0 +1,59 @@ +import { QueryRunner } from 'typeorm'; +import { mock } from 'jest-mock-extended'; + +export interface QueryResults { + findOneBy?: any; + findOne?: any; + findBy?: any; + find?: any; + save?: any; + query?: any; + remove?: any; +} + +export class MockedDataSource { + results = {}; + + /** + * Sets the mock results for the data source. + * + * @param results - An object containing the query results. + * @returns The mocked results object. + */ + setMockResults(results: { [key: string]: QueryResults }) { + const mockedResults = {}; + for (const key in results) + mockedResults[key] = this.mockResults(results[key]); + this.results = mockedResults; + return mockedResults; + } + + /** + * Creates a mock object with jest.Mock functions for each property of the input object. + * The mock functions will return the corresponding values from the input object. + * + * @param results - The object containing the values to be mocked. + * @returns An object with jest.Mock functions for each property of the input object. + */ + mockResults(results: T): { [K in keyof T]: jest.Mock } { + const mockedResults = {} as { [K in keyof T]: jest.Mock }; + for (const key in results) mockedResults[key] = jest.fn(() => results[key]); + return mockedResults; + } + + /** + * Creates a mock query runner. + * @returns A mock query runner object. + */ + createQueryRunner = jest.fn(() => { + const queryRunner = mock(); + queryRunner.manager.getRepository = jest.fn((entity: any) => { + if (!this.results.hasOwnProperty(entity.name)) + throw new Error(`No results found for entity ${entity.name}`); + return this.results[entity.name]; + }); + return queryRunner; + }); +} + +export const mockDataSource = new MockedDataSource(); diff --git a/api/src/mocks/data.ts b/api/src/mocks/data.ts index be63dcf..64b1c36 100644 --- a/api/src/mocks/data.ts +++ b/api/src/mocks/data.ts @@ -1,23 +1,30 @@ import { CreateApplicationDto } from 'src/application/create-application.dto'; -import { - ApplicationType, - ApplicationState, - LangLevel, - Role, -} from '@hkrecruitment/shared'; import { BscApplication, MscApplication, PhdApplication, } from 'src/application/application.entity'; import { UpdateApplicationDto } from 'src/application/update-application.dto'; -import { RecruitmentSessionState } from '@hkrecruitment/shared/recruitment-session'; +import { + RecruitmentSession, + RecruitmentSessionState, +} from '@hkrecruitment/shared/recruitment-session'; import { CreateRecruitmentSessionDto } from 'src/recruitment-session/create-recruitment-session.dto'; import { UpdateRecruitmentSessionDto } from 'src/recruitment-session/update-recruitment-session.dto'; +import { CreateAvailabilityDto } from 'src/availability/create-availability.dto'; +import { + ApplicationType, + ApplicationState, + LangLevel, + Role, + AvailabilityState, + TimeSlot, +} from '@hkrecruitment/shared'; -export const testDate = new Date(2023, 0, 1); +export const testDate = new Date(2023, 0, 1, 10, 0, 0); export const testDateTimeStart = new Date(2023, 0, 1, 10, 30, 0); export const testDateTime10Minutes = new Date(2023, 0, 1, 10, 40, 0); +export const testDateTime1Hour = new Date(2023, 0, 1, 11, 30, 0); export const testDateTime3Hours = new Date(2023, 0, 1, 13, 30, 0); export const testDateTimeEnd = new Date(2023, 0, 1, 11, 30, 0); @@ -25,7 +32,8 @@ export const mockTimeSlot = { start: testDateTimeStart, end: testDateTimeEnd, id: 1, -}; + availabilities: [], +} as TimeSlot & { availabilities: any[] }; export const testInterviewStart = '11:55' as unknown as Date; export const testInterviewEnd = '20:35' as unknown as Date; @@ -60,6 +68,13 @@ export const mockUpdateRecruitmentSessionDto = { days: [testDay1, testDay2, testDay3], } as UpdateRecruitmentSessionDto; +export const mockGenerateTimeSlots = { + slotDuration: 30, + interviewStart: testDateTimeStart, + interviewEnd: testDateTime1Hour, + days: [testDay1, testDay3], +} as RecruitmentSession; + export const baseFile = { encoding: '7bit', mimetype: 'application/pdf', @@ -169,6 +184,15 @@ export const applicant = { role: Role.Applicant, }; +export const mockClerk = { + firstName: 'Violet', + lastName: 'Red', + oauthId: '321', + sex: 'female', + email: 'email2@example.com', + role: Role.Clerk, +}; + export const applicationFiles = { cv: [ { @@ -191,4 +215,16 @@ export const applicationFiles = { export const applicantId = 'abc123'; export const folderId = 'folder_abc123'; export const fileId = 'file_abc123'; -export const today = '1/1/2023, 24:00:00'; +export const today = '1/1/2023, 10:00:00'; + +export const mockAvailability = { + id: 1, + state: AvailabilityState.Free, + lastModified: new Date(), + timeSlot: mockTimeSlot, + user: applicant, +}; + +export const mockCreateAvailabilityDto = { + timeSlotId: mockTimeSlot.id, +} as CreateAvailabilityDto; diff --git a/api/src/mocks/services.ts b/api/src/mocks/services.ts index 6d7d17c..ae0611f 100644 --- a/api/src/mocks/services.ts +++ b/api/src/mocks/services.ts @@ -1,3 +1,13 @@ -export const mockedUsersService = { - findByOauthId: jest.fn(), -}; +import { TimeSlotsService } from 'src/timeslots/timeslots.service'; +import { UsersService } from 'src/users/users.service'; + +function classToMock(classToMock: any): Object { + const mockedService = {}; + Object.getOwnPropertyNames(classToMock.prototype).forEach((methodName) => { + mockedService[methodName] = jest.fn(); + }); + return mockedService; +} + +export const mockedUsersService = classToMock(UsersService); +export const mockedTimeSlotsService = classToMock(TimeSlotsService); diff --git a/api/src/recruitment-session/recruitment-session.controller.spec.ts b/api/src/recruitment-session/recruitment-session.controller.spec.ts index 719202d..e3fc458 100644 --- a/api/src/recruitment-session/recruitment-session.controller.spec.ts +++ b/api/src/recruitment-session/recruitment-session.controller.spec.ts @@ -67,7 +67,7 @@ describe('RecruitmentSessionController', () => { }); jest .spyOn(service, 'findActiveRecruitmentSession') - .mockResolvedValue({ ...mockRecruitmentSession }); + .mockResolvedValue(mockRecruitmentSession); const result = controller.findActive(mockAbility); await expect(result).rejects.toThrow(ForbiddenException); expect(service.findActiveRecruitmentSession).toHaveBeenCalledTimes(1); @@ -83,6 +83,9 @@ describe('RecruitmentSessionController', () => { jest .spyOn(service, 'createRecruitmentSession') .mockResolvedValue(mockRecruitmentSession); + jest + .spyOn(service, 'findActiveRecruitmentSession') + .mockResolvedValue(null); const result = await controller.createRecruitmentSession( mockCreateRecruitmentSessionDto, ); @@ -137,8 +140,11 @@ describe('RecruitmentSessionController', () => { ); expect(service.updateRecruitmentSession).toHaveBeenCalledTimes(1); expect(service.updateRecruitmentSession).toHaveBeenCalledWith({ - ...mockRecruitmentSession, + ...mockUpdateRecruitmentSessionDto, + createdAt: mockRecruitmentSession.createdAt, lastModified: testDate, + id: mockRecruitmentSession.id, + state: mockRecruitmentSession.state, }); }); @@ -214,7 +220,7 @@ describe('RecruitmentSessionController', () => { expect(service.findActiveRecruitmentSession).toHaveBeenCalledTimes(1); }); - it("shouldn't throw a ConflictException when updating the currentyl active RecruitmentSection state to 'Active'", async () => { + it("shouldn't throw a ConflictException when updating the currently active RecruitmentSection state to 'Active'", async () => { const mockAbility = createMockAbility(({ can }) => { can(Action.Update, 'RecruitmentSession'); }); @@ -294,11 +300,14 @@ describe('RecruitmentSessionController', () => { state: mockDeletedRecruitmentSession.state, createdAt: mockDeletedRecruitmentSession.createdAt, } as RecruitmentSessionResponseDto; + jest + .spyOn(global, 'Date') + .mockImplementation(() => testDate as unknown as string); jest .spyOn(service, 'findRecruitmentSessionById') .mockResolvedValue(mockRecruitmentSession); jest - .spyOn(service, 'deletRecruitmentSession') + .spyOn(service, 'deleteRecruitmentSession') .mockResolvedValue(mockDeletedRecruitmentSession); const result = await controller.deleteRecruitmentSession( mockRecruitmentSession.id, @@ -308,8 +317,8 @@ describe('RecruitmentSessionController', () => { expect(service.findRecruitmentSessionById).toHaveBeenCalledWith( mockRecruitmentSession.id, ); - expect(service.deletRecruitmentSession).toHaveBeenCalledTimes(1); - expect(service.deletRecruitmentSession).toHaveBeenCalledWith( + expect(service.deleteRecruitmentSession).toHaveBeenCalledTimes(1); + expect(service.deleteRecruitmentSession).toHaveBeenCalledWith( mockRecruitmentSession, ); }); diff --git a/api/src/recruitment-session/recruitment-session.controller.ts b/api/src/recruitment-session/recruitment-session.controller.ts index bc85757..72324fd 100644 --- a/api/src/recruitment-session/recruitment-session.controller.ts +++ b/api/src/recruitment-session/recruitment-session.controller.ts @@ -56,7 +56,7 @@ export class RecruitmentSessionController { @CheckPolicies((ability) => ability.can(Action.Read, 'RecruitmentSession')) async findActive( @Ability() ability: AppAbility, - ): Promise { + ): Promise { const recruitmentSession = await this.recruitmentSessionService.findActiveRecruitmentSession(); if (recruitmentSession === null) { @@ -81,7 +81,7 @@ export class RecruitmentSessionController { @ApiBadRequestResponse() @ApiForbiddenResponse() @ApiConflictResponse({ - description: 'The recruitment session cannot be created', // + description: 'The recruitment session cannot be created', }) @ApiCreatedResponse() @JoiValidate({ @@ -95,14 +95,14 @@ export class RecruitmentSessionController { // there should be only one active recruitment session at a time const hasActiveRecruitmentSession = await this.recruitmentSessionService.findActiveRecruitmentSession(); - if (hasActiveRecruitmentSession) + if (hasActiveRecruitmentSession != null) throw new ConflictException( 'There is already an active recruitment session', ); - return this.recruitmentSessionService.createRecruitmentSession({ - ...recruitmentSession, - }); + return this.recruitmentSessionService.createRecruitmentSession( + recruitmentSession, + ); } // UPDATE A RECRUITMENT SESSION @@ -149,7 +149,7 @@ export class RecruitmentSessionController { const currentlyActiveRecruitmentSession = await this.recruitmentSessionService.findActiveRecruitmentSession(); if ( - currentlyActiveRecruitmentSession && + currentlyActiveRecruitmentSession != null && currentlyActiveRecruitmentSession.id !== recruitmentSession.id // It's ok to set 'Active' to the (already) active recruitment session ) throw new ConflictException( @@ -207,7 +207,8 @@ export class RecruitmentSessionController { await this.recruitmentSessionService.findRecruitmentSessionById( recruitmentSessionId, ); - if (!toRemove) throw new NotFoundException('Recruitment session not found'); + if (toRemove === null) + throw new NotFoundException('Recruitment session not found'); // Check if recruitment session has pending interviews if (toRemove.state !== RecruitmentSessionState.Concluded) { @@ -223,7 +224,7 @@ export class RecruitmentSessionController { // Delete recruitment session const deletedRecruitmentSession = - await this.recruitmentSessionService.deletRecruitmentSession(toRemove); + await this.recruitmentSessionService.deleteRecruitmentSession(toRemove); return plainToClass( RecruitmentSessionResponseDto, diff --git a/api/src/recruitment-session/recruitment-session.module.ts b/api/src/recruitment-session/recruitment-session.module.ts index b53e481..48200b0 100644 --- a/api/src/recruitment-session/recruitment-session.module.ts +++ b/api/src/recruitment-session/recruitment-session.module.ts @@ -4,9 +4,14 @@ import { RecruitmentSessionController } from './recruitment-session.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { RecruitmentSession } from './recruitment-session.entity'; import { UsersModule } from 'src/users/users.module'; +import { TimeSlotsModule } from 'src/timeslots/timeslots.module'; @Module({ - imports: [TypeOrmModule.forFeature([RecruitmentSession]), UsersModule], + imports: [ + TypeOrmModule.forFeature([RecruitmentSession]), + UsersModule, + TimeSlotsModule, + ], providers: [RecruitmentSessionService], controllers: [RecruitmentSessionController], exports: [RecruitmentSessionService], diff --git a/api/src/recruitment-session/recruitment-session.service.spec.ts b/api/src/recruitment-session/recruitment-session.service.spec.ts index d6dfc9e..6df16dc 100644 --- a/api/src/recruitment-session/recruitment-session.service.spec.ts +++ b/api/src/recruitment-session/recruitment-session.service.spec.ts @@ -1,12 +1,17 @@ -import { mockRecruitmentSession, testDate } from '@mocks/data'; -import { mockedRepository } from '@mocks/repositories'; +import { mockRecruitmentSession, testDate } from 'src/mocks/data'; +import { mockedRepository } from 'src/mocks/repositories'; import { TestingModule, Test } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; +import { getRepositoryToken, getDataSourceToken } from '@nestjs/typeorm'; import { RecruitmentSession } from './recruitment-session.entity'; import { RecruitmentSessionService } from './recruitment-session.service'; +import { mockedTimeSlotsService as mockedTimeSlotsServiceClass } from '@mocks/services'; +import { mockDataSource } from 'src/mocks/data-sources'; +import { TimeSlotsService } from 'src/timeslots/timeslots.service'; +import { RecruitmentSessionState } from '@hkrecruitment/shared'; describe('Recruitment Session Service', () => { let recruitmentSessionService: RecruitmentSessionService; + let mockedTimeSlotsService: TimeSlotsService; beforeAll(() => { jest @@ -22,9 +27,18 @@ describe('Recruitment Session Service', () => { provide: getRepositoryToken(RecruitmentSession), useValue: mockedRepository, }, + { + provide: TimeSlotsService, + useValue: mockedTimeSlotsServiceClass, + }, + { + provide: getDataSourceToken(), + useValue: mockDataSource, + }, ], }).compile(); + mockedTimeSlotsService = module.get(TimeSlotsService); recruitmentSessionService = module.get( RecruitmentSessionService, ); @@ -36,16 +50,72 @@ describe('Recruitment Session Service', () => { expect(recruitmentSessionService).toBeDefined(); }); + describe('createRecruitmentSession', () => { + it('should create a new recruitment session', async () => { + const mockRecruitmentSessionRepository = { + save: mockRecruitmentSession, + }; + const mockedRepositories = mockDataSource.setMockResults({ + RecruitmentSession: mockRecruitmentSessionRepository, + }); + jest + .spyOn(mockedTimeSlotsService, 'createRecruitmentSessionTimeSlots') + .mockResolvedValue([]); + const result = await recruitmentSessionService.createRecruitmentSession( + mockRecruitmentSession, + ); + const expectedRecruitmentSession = { + ...mockRecruitmentSession, + createdAt: testDate, + lastModified: testDate, + }; + expect(result).toEqual(mockRecruitmentSession); + expect( + mockedRepositories['RecruitmentSession'].save, + ).toHaveBeenCalledTimes(1); + expect( + mockedRepositories['RecruitmentSession'].save, + ).toHaveBeenCalledWith(expectedRecruitmentSession); + expect( + mockedTimeSlotsService.createRecruitmentSessionTimeSlots, + ).toHaveBeenCalledTimes(1); + }); + }); + describe('deleteRecruitmentSession', () => { it('should remove the specified recruitment session from the database', async () => { jest .spyOn(mockedRepository, 'remove') .mockResolvedValue(mockRecruitmentSession); - const result = await recruitmentSessionService.deletRecruitmentSession( + const result = await recruitmentSessionService.deleteRecruitmentSession( mockRecruitmentSession, ); expect(result).toEqual(mockRecruitmentSession); expect(mockedRepository.remove).toHaveBeenCalledTimes(1); + expect(mockedRepository.remove).toHaveBeenCalledWith( + mockRecruitmentSession, + ); + }); + }); + + describe('updateRecruitmentSession', () => { + it('should update and return an existing recruitment session', async () => { + const updatedRecruitmentSession: RecruitmentSession = { + ...mockRecruitmentSession, + state: RecruitmentSessionState.Concluded, + }; + jest + .spyOn(mockedRepository, 'save') + .mockResolvedValue(updatedRecruitmentSession); + const result = await recruitmentSessionService.updateRecruitmentSession( + mockRecruitmentSession, + ); + + expect(result).toEqual(updatedRecruitmentSession); + expect(mockedRepository.save).toHaveBeenCalledTimes(1); + expect(mockedRepository.save).toHaveBeenCalledWith( + mockRecruitmentSession, + ); }); }); }); diff --git a/api/src/recruitment-session/recruitment-session.service.ts b/api/src/recruitment-session/recruitment-session.service.ts index 2b77ee5..670a9e6 100644 --- a/api/src/recruitment-session/recruitment-session.service.ts +++ b/api/src/recruitment-session/recruitment-session.service.ts @@ -1,15 +1,20 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { InjectRepository, InjectDataSource } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; import { RecruitmentSession } from './recruitment-session.entity'; import { CreateRecruitmentSessionDto } from './create-recruitment-session.dto'; import { RecruitmentSessionState } from '@hkrecruitment/shared'; +import { transaction } from 'src/utils/database'; +import { TimeSlotsService } from 'src/timeslots/timeslots.service'; @Injectable() export class RecruitmentSessionService { constructor( @InjectRepository(RecruitmentSession) private readonly recruitmentSessionRepository: Repository, + private readonly timeslotService: TimeSlotsService, + @InjectDataSource() + private dataSource: DataSource, ) {} async createRecruitmentSession( @@ -21,26 +26,41 @@ export class RecruitmentSessionService { state: RecruitmentSessionState.Active, createdAt: now, lastModified: now, - } as unknown as RecruitmentSession; - await this.recruitmentSessionRepository.save(rs); - return rs; + } as RecruitmentSession; + + return transaction(this.dataSource, async (queryRunner) => { + const recruitmentSession = await queryRunner.manager + .getRepository(RecruitmentSession) + .save(rs); + await this.timeslotService.createRecruitmentSessionTimeSlots( + queryRunner, + recruitmentSession, + ); + return recruitmentSession; + }); } async findAllRecruitmentSessions(): Promise { return await this.recruitmentSessionRepository.find(); } - async findRecruitmentSessionById(id: number): Promise { - return await this.recruitmentSessionRepository.findOne({ where: { id } }); + async findRecruitmentSessionById( + RSid: number, + ): Promise { + const matches = await this.recruitmentSessionRepository.findBy({ + id: RSid, + }); + return matches.length > 0 ? matches[0] : null; } - async findActiveRecruitmentSession(): Promise { - return await this.recruitmentSessionRepository.findOne({ - where: { state: RecruitmentSessionState.Active }, + async findActiveRecruitmentSession(): Promise { + const matches = await this.recruitmentSessionRepository.findBy({ + state: RecruitmentSessionState.Active, }); + return matches.length > 0 ? matches[0] : null; } - async deletRecruitmentSession( + async deleteRecruitmentSession( recruitmentSession: RecruitmentSession, ): Promise { return await this.recruitmentSessionRepository.remove(recruitmentSession); diff --git a/api/src/timeslots/create-timeslot.dto.ts b/api/src/timeslots/create-timeslot.dto.ts index 2b9aaea..2e7836d 100644 --- a/api/src/timeslots/create-timeslot.dto.ts +++ b/api/src/timeslots/create-timeslot.dto.ts @@ -1,7 +1,7 @@ import { TimeSlot } from '@hkrecruitment/shared'; import { ApiProperty } from '@nestjs/swagger'; -export class CreateTimeSlotDto implements TimeSlot { +export class CreateTimeSlotDto implements Partial { @ApiProperty() start: Date; diff --git a/api/src/timeslots/timeslot.entity.ts b/api/src/timeslots/timeslot.entity.ts index 959f85f..54911ca 100644 --- a/api/src/timeslots/timeslot.entity.ts +++ b/api/src/timeslots/timeslot.entity.ts @@ -1,5 +1,12 @@ -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { + Column, + Entity, + OneToMany, + PrimaryGeneratedColumn, + Relation, +} from 'typeorm'; import { TimeSlot as TimeSlotInterface } from '@hkrecruitment/shared'; +import { Availability } from 'src/availability/availability.entity'; @Entity() export class TimeSlot implements TimeSlotInterface { @@ -11,4 +18,7 @@ export class TimeSlot implements TimeSlotInterface { @Column() end: Date; + + @OneToMany(() => Availability, (availability) => availability.timeSlot) + availabilities: Relation; } diff --git a/api/src/timeslots/timeslots.controller.spec.ts b/api/src/timeslots/timeslots.controller.spec.ts index 0c2d34b..0c1c16d 100644 --- a/api/src/timeslots/timeslots.controller.spec.ts +++ b/api/src/timeslots/timeslots.controller.spec.ts @@ -1,14 +1,6 @@ import { TestBed } from '@automock/jest'; -import { - mockTimeSlot, - testDate, - testDateTimeEnd, - testDateTimeStart, -} from '@mocks/data'; import { TimeSlotsController } from './timeslots.controller'; import { TimeSlotsService } from './timeslots.service'; -import { testDateTime10Minutes } from '@mocks/data'; -import { testDateTime3Hours } from '@mocks/data'; describe('TimeSlotController', () => { let controller: TimeSlotsController; @@ -26,79 +18,4 @@ describe('TimeSlotController', () => { expect(controller).toBeDefined(); expect(service).toBeDefined(); }); - - // Create a time slot - describe('createTimeSlot', () => { - it('should allow creating a valid time slot', async () => { - const timeSlot = { - start: mockTimeSlot.start, - end: mockTimeSlot.end, - }; - - jest.spyOn(service, 'countOverlappingTimeSlots').mockResolvedValue(0); - jest.spyOn(service, 'createTimeSlot').mockResolvedValue(mockTimeSlot); - - const result = await controller.createTimeSlot(timeSlot); - - expect(result).toEqual(mockTimeSlot); - }); - - it('should throw an error if the duration is less than 30 minutes', async () => { - const timeSlot = { - start: testDateTimeStart, - end: testDateTime10Minutes, - }; - - await expect(controller.createTimeSlot(timeSlot)).rejects.toThrow( - 'The duration of the time slot must be at least 30 minutes', - ); - }); - - it('should throw an error if the duration is more than 60 minutes', async () => { - const timeSlot = { - start: testDateTimeStart, - end: testDateTime3Hours, - }; - - await expect(controller.createTimeSlot(timeSlot)).rejects.toThrow( - 'The duration of the time slot must be at most 60 minutes', - ); - }); - - it('should throw an error if the time slot overlaps with an existing time slot', async () => { - const timeSlot = { - start: testDateTimeStart, - end: testDateTimeEnd, - }; - - jest.spyOn(service, 'countOverlappingTimeSlots').mockResolvedValue(1); - - await expect(controller.createTimeSlot(timeSlot)).rejects.toThrow( - 'The time slot overlaps with existing time slots', - ); - }); - }); - - describe('deleteTimeSlot', () => { - it('should allow deleting an existing time slot', async () => { - jest.spyOn(service, 'findById').mockResolvedValue(mockTimeSlot); - jest.spyOn(service, 'deleteTimeSlot').mockResolvedValue(mockTimeSlot); - - await expect(controller.deleteTimeSlot(mockTimeSlot.id)).resolves.toEqual( - mockTimeSlot, - ); - expect(service.deleteTimeSlot).toHaveBeenCalledWith(mockTimeSlot); - expect(service.deleteTimeSlot).toHaveBeenCalledTimes(1); - }); - - it('should throw an error if the time slot does not exist', async () => { - jest.spyOn(service, 'findById').mockResolvedValue(null); - jest.spyOn(service, 'deleteTimeSlot').mockResolvedValue(mockTimeSlot); - - await expect( - controller.deleteTimeSlot(mockTimeSlot.id), - ).rejects.toThrowError('Time slot not found'); - expect(service.deleteTimeSlot).toHaveBeenCalledTimes(0); - }); - }); }); diff --git a/api/src/timeslots/timeslots.controller.ts b/api/src/timeslots/timeslots.controller.ts index 3202071..9580b8d 100644 --- a/api/src/timeslots/timeslots.controller.ts +++ b/api/src/timeslots/timeslots.controller.ts @@ -1,91 +1,10 @@ -import { - Body, - Controller, - BadRequestException, - NotFoundException, - ConflictException, - Param, - Post, - Delete, -} from '@nestjs/common'; +import { Controller } from '@nestjs/common'; import { TimeSlotsService } from './timeslots.service'; -import { Action, createTimeSlotSchema, TimeSlot } from '@hkrecruitment/shared'; -import { JoiValidate } from '../joi-validation/joi-validate.decorator'; -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiCreatedResponse, - ApiOkResponse, - ApiTags, - ApiConflictResponse, - ApiNoContentResponse, -} from '@nestjs/swagger'; -import { CheckPolicies } from 'src/authorization/check-policies.decorator'; -import { CreateTimeSlotDto } from './create-timeslot.dto'; -import * as Joi from 'joi'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; @ApiBearerAuth() @ApiTags('timeslots') @Controller('timeslots') export class TimeSlotsController { constructor(private readonly timeSlotsService: TimeSlotsService) {} - - @ApiBadRequestResponse() - @ApiForbiddenResponse() - @ApiConflictResponse({ - description: 'The time slot overlaps with existing time slots', - }) - @ApiCreatedResponse() - @JoiValidate({ - body: createTimeSlotSchema, - }) - @CheckPolicies((ability) => ability.can(Action.Create, 'TimeSlot')) - @Post() - async createTimeSlot(@Body() timeSlot: CreateTimeSlotDto): Promise { - const startDate = new Date(timeSlot.start); - const endDate = new Date(timeSlot.end); - - // Check duration - const durationInMinutes = - (endDate.getTime() - startDate.getTime()) / (1000 * 60); - if (durationInMinutes < 30) { - throw new BadRequestException( - 'The duration of the time slot must be at least 30 minutes', - ); - } else if (durationInMinutes > 60) { - throw new BadRequestException( - 'The duration of the time slot must be at most 60 minutes', - ); - } - - // Check overlapping timeslots - const overlappingTimeSlots = - await this.timeSlotsService.countOverlappingTimeSlots(startDate, endDate); - if (overlappingTimeSlots > 0) - throw new ConflictException( - 'The time slot overlaps with existing time slots', - ); - - return await this.timeSlotsService.createTimeSlot(timeSlot); - } - - @ApiBadRequestResponse() - @ApiForbiddenResponse() - @ApiNotFoundResponse() - @ApiOkResponse() - @ApiNoContentResponse() - @CheckPolicies((ability) => ability.can(Action.Delete, 'TimeSlot')) - @Delete('/:time_slot_id') - @JoiValidate({ - param: Joi.number().positive().integer().required().label('time_slot_id'), - }) - async deleteTimeSlot( - @Param('time_slot_id') timeSlotId: number, - ): Promise { - const timeSlot = await this.timeSlotsService.findById(timeSlotId); - if (!timeSlot) throw new NotFoundException('Time slot not found'); - return await this.timeSlotsService.deleteTimeSlot(timeSlot); - } } diff --git a/api/src/timeslots/timeslots.service.spec.ts b/api/src/timeslots/timeslots.service.spec.ts index fde00f1..6284724 100644 --- a/api/src/timeslots/timeslots.service.spec.ts +++ b/api/src/timeslots/timeslots.service.spec.ts @@ -1,17 +1,19 @@ -import { mockTimeSlot, testDate } from '@mocks/data'; -import { mockedRepository } from '@mocks/repositories'; +import { mockGenerateTimeSlots, mockTimeSlot, testDate } from 'src/mocks/data'; +import { mockedRepository } from 'src/mocks/repositories'; import { TestingModule, Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { TimeSlot } from './timeslot.entity'; import { TimeSlotsService } from './timeslots.service'; +import { mockDataSource } from 'src/mocks/data-sources'; describe('TimeSlotsService', () => { let timeSlotService: TimeSlotsService; + let mockDate: jest.SpyInstance; /************* Test setup ************/ beforeAll(() => { - jest + mockDate = jest .spyOn(global, 'Date') .mockImplementation(() => testDate as unknown as string); }); @@ -73,4 +75,144 @@ describe('TimeSlotsService', () => { expect(mockedRepository.save).toHaveBeenCalledTimes(1); }); }); + + describe('createRecruitmentSessionTimeSlots', () => { + it('should create recruitment session time slots', async () => { + const expectedTimeSlots: Partial[] = [ + { + start: new Date('2023-01-01T10:30:00'), + end: new Date('2023-01-01T11:00:00'), + }, + { + start: new Date('2023-01-01T11:00:00'), + end: new Date('2023-01-01T11:30:00'), + }, + { + start: new Date('2023-01-01T10:00:00'), + end: new Date('2022-01-01T11:00:00'), + }, + { + start: new Date('2023-01-01T10:00:00'), + end: new Date('2022-01-01T11:00:00'), + }, + ]; + + mockDataSource.setMockResults({ TimeSlot: { save: expectedTimeSlots } }); + const queryRunner = mockDataSource.createQueryRunner(); + const result = await timeSlotService.createRecruitmentSessionTimeSlots( + queryRunner, + mockGenerateTimeSlots, + ); + + expect(result).toEqual(expectedTimeSlots); + expect( + queryRunner.manager.getRepository(TimeSlot).save, + ).toHaveBeenCalledTimes(1); + }); + }); + + describe('generateTimeSlots', () => { + it('should generate time slots', () => { + mockDate.mockRestore(); + const slotDuration = 60; + const interviewStart = new Date('2022-01-01T09:00:00'); + const interviewEnd = new Date('2022-01-01T12:00:00'); + const days = [new Date('2022-01-01'), new Date('2022-01-03')]; + const expectedTimeSlots: Partial[] = [ + { + start: new Date('2022-01-01T09:00:00'), + end: new Date('2022-01-01T10:00:00'), + }, + { + start: new Date('2022-01-03T09:00:00'), + end: new Date('2022-01-03T10:00:00'), + }, + { + start: new Date('2022-01-01T10:00:00'), + end: new Date('2022-01-01T11:00:00'), + }, + { + start: new Date('2022-01-03T10:00:00'), + end: new Date('2022-01-03T11:00:00'), + }, + { + start: new Date('2022-01-01T11:00:00'), + end: new Date('2022-01-01T12:00:00'), + }, + { + start: new Date('2022-01-03T11:00:00'), + end: new Date('2022-01-03T12:00:00'), + }, + ]; + testTimeSlotsGeneration( + timeSlotService, + slotDuration, + interviewStart, + interviewEnd, + days, + expectedTimeSlots, + ); + }); + + it("shouldn't generate time slots that overflow interviewEnd time", () => { + mockDate.mockRestore(); + const slotDuration = 50; + const interviewStart = new Date('2022-02-02T09:00:00'); + const interviewEnd = new Date('2022-02-02T11:00:00'); + const days = [new Date('2022-02-02'), new Date('2022-02-04')]; + const expectedTimeSlots: Partial[] = [ + { + start: new Date('2022-02-02T09:00:00'), + end: new Date('2022-02-02T09:50:00'), + }, + { + start: new Date('2022-02-04T09:00:00'), + end: new Date('2022-02-04T09:50:00'), + }, + { + start: new Date('2022-02-02T09:50:00'), + end: new Date('2022-02-02T10:40:00'), + }, + { + start: new Date('2022-02-04T09:50:00'), + end: new Date('2022-02-04T10:40:00'), + }, + ]; + testTimeSlotsGeneration( + timeSlotService, + slotDuration, + interviewStart, + interviewEnd, + days, + expectedTimeSlots, + ); + }); + }); }); + +function testTimeSlotsGeneration( + timeSlotService: TimeSlotsService, + slotDuration: number, + interviewStart: Date, + interviewEnd: Date, + days: Date[], + expectedResult: Partial[], +) { + const expectedTimeSlots: Partial[] = expectedResult.map( + (timeSlot) => + ({ + ...timeSlot, + id: undefined, + availabilities: undefined, + } as TimeSlot), + ); + + const result = timeSlotService.generateTimeSlots( + slotDuration, + interviewStart, + interviewEnd, + days, + ); + + expect(result).toEqual(expectedTimeSlots); +} diff --git a/api/src/timeslots/timeslots.service.ts b/api/src/timeslots/timeslots.service.ts index ec5e1b9..7eb37c0 100644 --- a/api/src/timeslots/timeslots.service.ts +++ b/api/src/timeslots/timeslots.service.ts @@ -1,7 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, LessThan, MoreThan } from 'typeorm'; +import { Repository, LessThan, MoreThan, QueryRunner } from 'typeorm'; import { TimeSlot } from './timeslot.entity'; +import { RecruitmentSession } from '@hkrecruitment/shared'; import { CreateTimeSlotDto } from './create-timeslot.dto'; @Injectable() @@ -51,4 +52,57 @@ export class TimeSlotsService { async createTimeSlot(timeSlot: CreateTimeSlotDto): Promise { return await this.timeSlotRepository.save(timeSlot); } + + async createRecruitmentSessionTimeSlots( + queryRunner: QueryRunner, + recruitmentSession: RecruitmentSession, + ): Promise { + const { slotDuration, interviewStart, interviewEnd, days } = + recruitmentSession; + const timeSlots = this.generateTimeSlots( + slotDuration, + interviewStart, + interviewEnd, + days, + ); + return await queryRunner.manager.getRepository(TimeSlot).save(timeSlots); + } + + generateTimeSlots( + slotDuration: number, + interviewStart: Date, + interviewEnd: Date, + days: Date[], + ): TimeSlot[] { + const timeSlots: TimeSlot[] = []; + const interviewStartMinutes = + interviewStart.getHours() * 60 + interviewStart.getMinutes(); + const interviewEndMinutes = + interviewEnd.getHours() * 60 + interviewEnd.getMinutes(); + const dailySlots = Math.floor( + (interviewEndMinutes - interviewStartMinutes) / slotDuration, + ); + + for (let i = 0; i < dailySlots; i++) { + for (let day of days) { + const timeSlotStart = new Date(day); + timeSlotStart.setHours( + interviewStart.getHours() + Math.floor((i * slotDuration) / 60), + interviewStart.getMinutes() + ((i * slotDuration) % 60), + 0, + 0, + ); + const timeSlotEnd = new Date( + timeSlotStart.getTime() + slotDuration * 1000 * 60, + ); + + const timeSlot = new TimeSlot(); + timeSlot.start = timeSlotStart; + timeSlot.end = timeSlotEnd; + timeSlots.push(timeSlot); + } + } + + return timeSlots; + } } diff --git a/api/src/users/user.entity.ts b/api/src/users/user.entity.ts index f797bca..81bd96c 100644 --- a/api/src/users/user.entity.ts +++ b/api/src/users/user.entity.ts @@ -1,5 +1,6 @@ -import { Column, Entity, PrimaryColumn } from 'typeorm'; +import { Column, Entity, OneToMany, PrimaryColumn, Relation } from 'typeorm'; import { Person, Role } from '@hkrecruitment/shared'; +import { Availability } from 'src/availability/availability.entity'; @Entity() export class User implements Person { @@ -26,4 +27,13 @@ export class User implements Person { @Column() role: Role; + + @OneToMany(() => Availability, (availability) => availability.user) + availabilities?: Relation; + + @Column() + is_board: boolean; + + @Column() + is_expert: boolean; } diff --git a/api/src/users/users.controller.spec.ts b/api/src/users/users.controller.spec.ts index 90f5ca7..845f281 100644 --- a/api/src/users/users.controller.spec.ts +++ b/api/src/users/users.controller.spec.ts @@ -31,6 +31,8 @@ describe('UsersController', () => { sex: 'M', email: 'example@example.com', role: Role.Applicant, + is_board: false, + is_expert: false, }; const mockMember: Person = { @@ -40,6 +42,8 @@ describe('UsersController', () => { sex: 'F', email: 'jane@hknpolito.org', role: Role.Member, + is_board: false, + is_expert: false, }; const mockUsers = [mockApplicant, mockMember]; diff --git a/api/src/users/users.controller.ts b/api/src/users/users.controller.ts index d3af4d9..21e986b 100644 --- a/api/src/users/users.controller.ts +++ b/api/src/users/users.controller.ts @@ -96,6 +96,8 @@ export class UsersController { return this.usersService.create({ ...user, role: !!user.role ? user.role : defaultRole, + is_board: false, + is_expert: false, }); } diff --git a/api/src/users/users.e2e-spec.ts b/api/src/users/users.e2e-spec.ts index 2291a5a..06f35b3 100644 --- a/api/src/users/users.e2e-spec.ts +++ b/api/src/users/users.e2e-spec.ts @@ -27,6 +27,8 @@ describe('UsersController (e2e)', () => { sex: 'F', email: 'known-superuser-test@example.com', role: Role.Admin, + is_board: true, + is_expert: true, }, { oauthId: getSub(newApplicantToken), @@ -35,6 +37,8 @@ describe('UsersController (e2e)', () => { sex: 'F', email: 'test-applicant@example.com', role: Role.Applicant, + is_board: false, + is_expert: false, }, { oauthId: getSub(newMemberToken), @@ -43,6 +47,8 @@ describe('UsersController (e2e)', () => { sex: 'M', email: 'hknrecruitment-test@hknpolito.org', role: Role.Member, + is_board: false, + is_expert: true, }, ]; }); @@ -138,6 +144,8 @@ describe('UsersController (e2e)', () => { role: Role.Applicant, phone_no: null, telegramId: null, + is_board: false, + is_expert: false, }; await request(app.getHttpServer()) @@ -165,6 +173,8 @@ describe('UsersController (e2e)', () => { role: Role.Member, phone_no: null, telegramId: null, + is_board: false, + is_expert: false, }; await request(app.getHttpServer()) diff --git a/api/src/utils/database.ts b/api/src/utils/database.ts new file mode 100644 index 0000000..f9bdeb9 --- /dev/null +++ b/api/src/utils/database.ts @@ -0,0 +1,33 @@ +import { DataSource, QueryRunner } from 'typeorm'; + +/** + * Executes a transaction using the provided data source and actions. + * @param dataSource The connection configuration to a specific database to use for the transaction. + * @param actions The actions to perform within the transaction. + * @returns The result of the executed actions. + */ +export async function transaction( + dataSource: DataSource, + actions: (queryRunner: QueryRunner) => Promise, +): Promise { + // BEGIN TRANSACTION + const queryRunner = dataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + let result: any; + + try { + result = await actions(queryRunner); + // COMMIT TRANSACTION + await queryRunner.commitTransaction(); + } catch (err) { + // ROLLBACK TRANSACTION + await queryRunner.rollbackTransaction(); + throw err; + } finally { + await queryRunner.release(); + } + + return result; +} diff --git a/api/src/utils/db-aware-column.ts b/api/src/utils/db-aware-column.ts new file mode 100644 index 0000000..85aca90 --- /dev/null +++ b/api/src/utils/db-aware-column.ts @@ -0,0 +1,18 @@ +import { Column, ColumnOptions } from 'typeorm'; + +/** + * Decorator function that uses 'varchar' as the column type in the testing environment. + * This is necessary as sqlite does not support custom classes as column types. + * If not in a test environment, it returns a column with its original type. + * + * @param type - The type of the column. + * @param columnOptions - The options for the column. + * @returns The column definition. + */ +export function DbAwareColumn( + type: (t?: any) => Function, + columnOptions: ColumnOptions, +) { + if (process.env.NODE_ENV === 'test') return Column('varchar', columnOptions); + return Column(type, columnOptions); +} diff --git a/api/test/app.e2e-spec.ts b/api/test/app.e2e-spec.ts index 80144af..db63ae9 100644 --- a/api/test/app.e2e-spec.ts +++ b/api/test/app.e2e-spec.ts @@ -4,6 +4,7 @@ import * as dotenv from 'dotenv'; dotenv.config({ path: '.env.test' }); import { readFileSync } from 'fs'; import { decodeJwt } from 'jose'; +import { INestApplication } from '@nestjs/common'; export const createApp = async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ @@ -19,10 +20,12 @@ export const createApp = async () => { const allCredentials = JSON.parse( readFileSync('test/user-credentials.json').toString(), ); + const auth0_issuer = process.env.AUTH0_ISSUER_URL; const auth0_client_id = process.env.AUTH0_CLIENT_ID; const auth0_client_secret = process.env.AUTH0_CLIENT_SECRET; const auth0_audience = process.env.AUTH0_AUDIENCE; + export const getAccessToken = async (key: string): Promise => { const credentials: { mail: string; @@ -38,21 +41,25 @@ export const getAccessToken = async (key: string): Promise => { client_id: auth0_client_id, client_secret: auth0_client_secret, }; + const response = await fetch(`${auth0_issuer}oauth/token`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), - }).then((res) => res.json()); + }); + + const json = await response.json(); - return response.access_token; + return json.access_token; }; + export const getSub = (accessToken: string): string => { const decoded = decodeJwt(accessToken); return decoded.sub; }; describe('e2e build test', () => { - let app; + let app: INestApplication; beforeEach(async () => { app = await createApp(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbb0a77..1b5024d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,6 +61,9 @@ importers: googleapis: specifier: ^118.0.0 version: 118.0.0 + jest-mock-extended: + specifier: ^3.0.5 + version: 3.0.5(jest@28.1.3)(typescript@4.5.2) joi: specifier: ^17.7.0 version: 17.7.0 @@ -286,7 +289,6 @@ packages: dependencies: '@jridgewell/gen-mapping': 0.1.1 '@jridgewell/trace-mapping': 0.3.17 - dev: true /@angular-devkit/core@14.2.1(chokidar@3.5.3): resolution: {integrity: sha512-lW8oNGuJqr4r31FWBjfWQYkSXdiOHBGOThIEtHvUVBKfPF/oVrupLueCUgBPel+NvxENXdo93uPsqHN7bZbmsQ==} @@ -401,12 +403,10 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/highlight': 7.18.6 - dev: true /@babel/compat-data@7.20.5: resolution: {integrity: sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==} engines: {node: '>=6.9.0'} - dev: true /@babel/core@7.20.5: resolution: {integrity: sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==} @@ -429,7 +429,6 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color - dev: true /@babel/core@7.21.3: resolution: {integrity: sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw==} @@ -452,7 +451,6 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color - dev: true /@babel/generator@7.20.5: resolution: {integrity: sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==} @@ -461,7 +459,6 @@ packages: '@babel/types': 7.20.5 '@jridgewell/gen-mapping': 0.3.2 jsesc: 2.5.2 - dev: true /@babel/generator@7.21.3: resolution: {integrity: sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==} @@ -471,7 +468,6 @@ packages: '@jridgewell/gen-mapping': 0.3.2 '@jridgewell/trace-mapping': 0.3.17 jsesc: 2.5.2 - dev: true /@babel/helper-compilation-targets@7.20.0(@babel/core@7.20.5): resolution: {integrity: sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==} @@ -484,7 +480,6 @@ packages: '@babel/helper-validator-option': 7.18.6 browserslist: 4.21.4 semver: 6.3.0 - dev: true /@babel/helper-compilation-targets@7.20.7(@babel/core@7.21.3): resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==} @@ -498,12 +493,10 @@ packages: browserslist: 4.21.5 lru-cache: 5.1.1 semver: 6.3.0 - dev: true /@babel/helper-environment-visitor@7.18.9: resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-function-name@7.19.0: resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} @@ -511,7 +504,6 @@ packages: dependencies: '@babel/template': 7.18.10 '@babel/types': 7.20.5 - dev: true /@babel/helper-function-name@7.21.0: resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==} @@ -519,21 +511,18 @@ packages: dependencies: '@babel/template': 7.20.7 '@babel/types': 7.21.3 - dev: true /@babel/helper-hoist-variables@7.18.6: resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.20.5 - dev: true /@babel/helper-module-imports@7.18.6: resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.20.5 - dev: true /@babel/helper-module-transforms@7.20.2: resolution: {integrity: sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==} @@ -549,7 +538,6 @@ packages: '@babel/types': 7.20.5 transitivePeerDependencies: - supports-color - dev: true /@babel/helper-module-transforms@7.21.2: resolution: {integrity: sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==} @@ -565,46 +553,38 @@ packages: '@babel/types': 7.21.3 transitivePeerDependencies: - supports-color - dev: true /@babel/helper-plugin-utils@7.20.2: resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-simple-access@7.20.2: resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.20.5 - dev: true /@babel/helper-split-export-declaration@7.18.6: resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.20.5 - dev: true /@babel/helper-string-parser@7.19.4: resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-identifier@7.19.1: resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-option@7.18.6: resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-option@7.21.0: resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} engines: {node: '>=6.9.0'} - dev: true /@babel/helpers@7.20.6: resolution: {integrity: sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==} @@ -615,7 +595,6 @@ packages: '@babel/types': 7.20.5 transitivePeerDependencies: - supports-color - dev: true /@babel/helpers@7.21.0: resolution: {integrity: sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==} @@ -626,7 +605,6 @@ packages: '@babel/types': 7.21.3 transitivePeerDependencies: - supports-color - dev: true /@babel/highlight@7.18.6: resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} @@ -635,7 +613,6 @@ packages: '@babel/helper-validator-identifier': 7.19.1 chalk: 2.4.2 js-tokens: 4.0.0 - dev: true /@babel/parser@7.20.5: resolution: {integrity: sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==} @@ -643,7 +620,6 @@ packages: hasBin: true dependencies: '@babel/types': 7.20.5 - dev: true /@babel/parser@7.21.3: resolution: {integrity: sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==} @@ -651,7 +627,6 @@ packages: hasBin: true dependencies: '@babel/types': 7.21.3 - dev: true /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.21.3): resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} @@ -660,7 +635,6 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - dev: true /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} @@ -669,7 +643,6 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - dev: true /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.21.3): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} @@ -678,7 +651,6 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - dev: true /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.21.3): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} @@ -687,7 +659,6 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - dev: true /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} @@ -696,7 +667,6 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - dev: true /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.21.3): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} @@ -705,7 +675,6 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - dev: true /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} @@ -714,7 +683,6 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - dev: true /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.21.3): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} @@ -723,7 +691,6 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - dev: true /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} @@ -732,7 +699,6 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - dev: true /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} @@ -741,7 +707,6 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - dev: true /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} @@ -750,7 +715,6 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - dev: true /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.21.3): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} @@ -760,7 +724,6 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - dev: true /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.21.3): resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} @@ -770,7 +733,6 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - dev: true /@babel/plugin-transform-react-jsx-self@7.21.0(@babel/core@7.21.3): resolution: {integrity: sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==} @@ -806,7 +768,6 @@ packages: '@babel/code-frame': 7.18.6 '@babel/parser': 7.20.5 '@babel/types': 7.20.5 - dev: true /@babel/template@7.20.7: resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} @@ -815,7 +776,6 @@ packages: '@babel/code-frame': 7.18.6 '@babel/parser': 7.21.3 '@babel/types': 7.21.3 - dev: true /@babel/traverse@7.20.5: resolution: {integrity: sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==} @@ -833,7 +793,6 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color - dev: true /@babel/traverse@7.21.3: resolution: {integrity: sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==} @@ -851,7 +810,6 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color - dev: true /@babel/types@7.20.5: resolution: {integrity: sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==} @@ -860,7 +818,6 @@ packages: '@babel/helper-string-parser': 7.19.4 '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 - dev: true /@babel/types@7.21.3: resolution: {integrity: sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==} @@ -869,11 +826,9 @@ packages: '@babel/helper-string-parser': 7.19.4 '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 - dev: true /@bcoe/v8-coverage@0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - dev: true /@casl/ability@6.3.3: resolution: {integrity: sha512-UzbqsE9etu6QzZrRmqIyVun2kztAzJ46Tz7lC/2P2buCE6B6Ll7Vptz7JTQtGwapLbeKo2jS7dL966TVOQ7x4g==} @@ -1180,12 +1135,10 @@ packages: get-package-type: 0.1.0 js-yaml: 3.14.1 resolve-from: 5.0.0 - dev: true /@istanbuljs/schema@0.1.3: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} - dev: true /@jest/console@28.1.3: resolution: {integrity: sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==} @@ -1197,7 +1150,6 @@ packages: jest-message-util: 28.1.3 jest-util: 28.1.3 slash: 3.0.0 - dev: true /@jest/core@28.1.3(ts-node@10.7.0): resolution: {integrity: sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==} @@ -1240,7 +1192,6 @@ packages: transitivePeerDependencies: - supports-color - ts-node - dev: true /@jest/create-cache-key-function@27.5.1: resolution: {integrity: sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==} @@ -1257,14 +1208,12 @@ packages: '@jest/types': 28.1.3 '@types/node': 16.18.4 jest-mock: 28.1.3 - dev: true /@jest/expect-utils@28.1.3: resolution: {integrity: sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: jest-get-type: 28.0.2 - dev: true /@jest/expect@28.1.3: resolution: {integrity: sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==} @@ -1274,7 +1223,6 @@ packages: jest-snapshot: 28.1.3 transitivePeerDependencies: - supports-color - dev: true /@jest/fake-timers@28.1.3: resolution: {integrity: sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==} @@ -1286,7 +1234,6 @@ packages: jest-message-util: 28.1.3 jest-mock: 28.1.3 jest-util: 28.1.3 - dev: true /@jest/globals@28.1.3: resolution: {integrity: sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==} @@ -1297,7 +1244,6 @@ packages: '@jest/types': 28.1.3 transitivePeerDependencies: - supports-color - dev: true /@jest/reporters@28.1.3: resolution: {integrity: sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==} @@ -1335,14 +1281,12 @@ packages: v8-to-istanbul: 9.0.1 transitivePeerDependencies: - supports-color - dev: true /@jest/schemas@28.1.3: resolution: {integrity: sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: '@sinclair/typebox': 0.24.51 - dev: true /@jest/source-map@28.1.2: resolution: {integrity: sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==} @@ -1351,7 +1295,6 @@ packages: '@jridgewell/trace-mapping': 0.3.17 callsites: 3.1.0 graceful-fs: 4.2.10 - dev: true /@jest/test-result@28.1.3: resolution: {integrity: sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==} @@ -1361,7 +1304,6 @@ packages: '@jest/types': 28.1.3 '@types/istanbul-lib-coverage': 2.0.4 collect-v8-coverage: 1.0.1 - dev: true /@jest/test-sequencer@28.1.3: resolution: {integrity: sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==} @@ -1371,7 +1313,6 @@ packages: graceful-fs: 4.2.10 jest-haste-map: 28.1.3 slash: 3.0.0 - dev: true /@jest/transform@28.1.3: resolution: {integrity: sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==} @@ -1394,7 +1335,6 @@ packages: write-file-atomic: 4.0.2 transitivePeerDependencies: - supports-color - dev: true /@jest/types@27.5.1: resolution: {integrity: sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==} @@ -1417,7 +1357,6 @@ packages: '@types/node': 16.18.4 '@types/yargs': 17.0.15 chalk: 4.1.2 - dev: true /@joi/date@2.1.0: resolution: {integrity: sha512-2zN5m0LgxZp/cynHGbzEImVmFIa+n+IOb/Nlw5LX/PLJneeCwG1NbiGw7MvPjsAKUGQK8z31Nn6V6lEN+4fZhg==} @@ -1431,7 +1370,6 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.14 - dev: true /@jridgewell/gen-mapping@0.3.2: resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} @@ -1842,19 +1780,16 @@ packages: /@sinclair/typebox@0.24.51: resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} - dev: true /@sinonjs/commons@1.8.6: resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} dependencies: type-detect: 4.0.8 - dev: true /@sinonjs/fake-timers@9.1.2: resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} dependencies: '@sinonjs/commons': 1.8.6 - dev: true /@sqltools/formatter@1.2.5: resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} @@ -2004,26 +1939,22 @@ packages: '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 '@types/babel__traverse': 7.18.3 - dev: true /@types/babel__generator@7.6.4: resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} dependencies: '@babel/types': 7.20.5 - dev: true /@types/babel__template@7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: '@babel/parser': 7.20.5 '@babel/types': 7.20.5 - dev: true /@types/babel__traverse@7.18.3: resolution: {integrity: sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==} dependencies: '@babel/types': 7.20.5 - dev: true /@types/body-parser@1.19.2: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} @@ -2074,23 +2005,19 @@ packages: resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} dependencies: '@types/node': 16.18.4 - dev: true /@types/istanbul-lib-coverage@2.0.4: resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} - dev: true /@types/istanbul-lib-report@3.0.0: resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} dependencies: '@types/istanbul-lib-coverage': 2.0.4 - dev: true /@types/istanbul-reports@3.0.1: resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} dependencies: '@types/istanbul-lib-report': 3.0.0 - dev: true /@types/jest@28.1.8: resolution: {integrity: sha512-8TJkV++s7B6XqnDrzR1m/TT0A0h948Pnl/097veySPN67VRAgQ4gZ7n2KfJo2rVq6njQjdxU3GCCyDvAeuHoiw==} @@ -2151,7 +2078,6 @@ packages: /@types/prettier@2.7.1: resolution: {integrity: sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==} - dev: true /@types/prop-types@15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} @@ -2193,7 +2119,6 @@ packages: /@types/stack-utils@2.0.1: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} - dev: true /@types/superagent@4.1.16: resolution: {integrity: sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ==} @@ -2214,7 +2139,6 @@ packages: /@types/yargs-parser@21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} - dev: true /@types/yargs@16.0.5: resolution: {integrity: sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==} @@ -2226,7 +2150,6 @@ packages: resolution: {integrity: sha512-ZHc4W2dnEQPfhn06TBEdWaiUHEZAocYaiVMfwOipY5jcJt/251wVrKCBWBetGZWO5CF8tdb7L3DmdxVlZ2BOIg==} dependencies: '@types/yargs-parser': 21.0.0 - dev: true /@typescript-eslint/eslint-plugin@5.45.0(@typescript-eslint/parser@5.45.0)(eslint@8.29.0)(typescript@4.5.2): resolution: {integrity: sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==} @@ -2613,7 +2536,6 @@ packages: engines: {node: '>=8'} dependencies: type-fest: 0.21.3 - dev: true /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -2624,7 +2546,6 @@ packages: engines: {node: '>=4'} dependencies: color-convert: 1.9.3 - dev: true /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} @@ -2635,7 +2556,6 @@ packages: /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - dev: true /any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -2647,7 +2567,6 @@ packages: dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 - dev: true /app-root-path@3.1.0: resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} @@ -2683,7 +2602,6 @@ packages: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: sprintf-js: 1.0.3 - dev: true /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2729,7 +2647,6 @@ packages: slash: 3.0.0 transitivePeerDependencies: - supports-color - dev: true /babel-plugin-istanbul@6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} @@ -2742,7 +2659,6 @@ packages: test-exclude: 6.0.0 transitivePeerDependencies: - supports-color - dev: true /babel-plugin-jest-hoist@28.1.3: resolution: {integrity: sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==} @@ -2752,7 +2668,6 @@ packages: '@babel/types': 7.20.5 '@types/babel__core': 7.1.20 '@types/babel__traverse': 7.18.3 - dev: true /babel-preset-current-node-syntax@1.0.1(@babel/core@7.21.3): resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} @@ -2772,7 +2687,6 @@ packages: '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.3) '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.3) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.21.3) - dev: true /babel-preset-jest@28.1.3(@babel/core@7.21.3): resolution: {integrity: sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==} @@ -2783,7 +2697,6 @@ packages: '@babel/core': 7.21.3 babel-plugin-jest-hoist: 28.1.3 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.21.3) - dev: true /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2855,7 +2768,6 @@ packages: engines: {node: '>=8'} dependencies: fill-range: 7.0.1 - dev: true /browser-tabs-lock@1.2.15: resolution: {integrity: sha512-J8K9vdivK0Di+b8SBdE7EZxDr88TnATing7XoLw6+nFkXMQ6sVBh92K3NQvZlZU91AIkFRi0w3sztk5Z+vsswA==} @@ -2883,7 +2795,6 @@ packages: electron-to-chromium: 1.4.284 node-releases: 2.0.10 update-browserslist-db: 1.0.10(browserslist@4.21.5) - dev: true /bs-logger@0.2.6: resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} @@ -2896,7 +2807,6 @@ packages: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: node-int64: 0.4.0 - dev: true /buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -2970,7 +2880,6 @@ packages: /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - dev: true /camel-case@4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} @@ -2982,19 +2891,16 @@ packages: /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - dev: true /camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - dev: true /caniuse-lite@1.0.30001436: resolution: {integrity: sha512-ZmWkKsnC2ifEPoWUvSAIGyOYwT+keAaaWPHiQ9DfMqS1t6tfuyFYoWR78TeZtznkEQ64+vGXH9cZrElwR2Mrxg==} /caniuse-lite@1.0.30001468: resolution: {integrity: sha512-zgAo8D5kbOyUcRAgSmgyuvBkjrGk5CGYG5TYgFdpQv+ywcyEpo1LOWoG8YmoflGnh+V+UsNuKYedsoYs0hzV5A==} - dev: true /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -3003,7 +2909,6 @@ packages: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - dev: true /chalk@3.0.0: resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} @@ -3023,7 +2928,6 @@ packages: /char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} - dev: true /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} @@ -3055,11 +2959,9 @@ packages: /ci-info@3.7.0: resolution: {integrity: sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==} engines: {node: '>=8'} - dev: true /cjs-module-lexer@1.2.2: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} - dev: true /class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -3144,17 +3046,14 @@ packages: /co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - dev: true /collect-v8-coverage@1.0.1: resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} - dev: true /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: color-name: 1.1.3 - dev: true /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} @@ -3164,7 +3063,6 @@ packages: /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -3236,7 +3134,6 @@ packages: /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - dev: true /cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} @@ -3285,7 +3182,6 @@ packages: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - dev: true /css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} @@ -3346,7 +3242,6 @@ packages: /dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} - dev: true /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -3355,7 +3250,6 @@ packages: /deepmerge@4.2.2: resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} engines: {node: '>=0.10.0'} - dev: true /defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -3391,7 +3285,6 @@ packages: /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} - dev: true /dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} @@ -3403,7 +3296,6 @@ packages: /diff-sequences@28.1.1: resolution: {integrity: sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dev: true /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} @@ -3500,7 +3392,6 @@ packages: /emittery@0.10.2: resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} engines: {node: '>=12'} - dev: true /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -3548,7 +3439,6 @@ packages: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: is-arrayish: 0.2.1 - dev: true /es-cookie@1.3.2: resolution: {integrity: sha512-UTlYYhXGLOy05P/vKVT2Ui7WtC7NiRzGtJyAKKn32g5Gvcjn7KAClLPWlipCtxIus934dFg9o9jXiBL0nP+t9Q==} @@ -3597,12 +3487,10 @@ packages: /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - dev: true /escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} - dev: true /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} @@ -3731,7 +3619,6 @@ packages: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - dev: true /esquery@1.4.0: resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} @@ -3804,12 +3691,10 @@ packages: onetime: 5.1.2 signal-exit: 3.0.7 strip-final-newline: 2.0.0 - dev: true /exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} - dev: true /expect@28.1.3: resolution: {integrity: sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==} @@ -3820,7 +3705,6 @@ packages: jest-matcher-utils: 28.1.3 jest-message-util: 28.1.3 jest-util: 28.1.3 - dev: true /express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} @@ -3919,7 +3803,6 @@ packages: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} dependencies: bser: 2.1.1 - dev: true /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} @@ -3946,7 +3829,6 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 - dev: true /finalhandler@1.2.0: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} @@ -3968,7 +3850,6 @@ packages: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 - dev: true /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} @@ -4070,7 +3951,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true - dev: true optional: true /function-bind@1.1.1: @@ -4132,7 +4012,6 @@ packages: /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} - dev: true /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} @@ -4148,7 +4027,6 @@ packages: /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} - dev: true /get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} @@ -4160,7 +4038,6 @@ packages: /get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - dev: true /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -4203,7 +4080,6 @@ packages: /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} - dev: true /globals@13.18.0: resolution: {integrity: sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==} @@ -4298,7 +4174,6 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -4333,7 +4208,6 @@ packages: /html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - dev: true /html-minifier-terser@6.1.0: resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} @@ -4393,7 +4267,6 @@ packages: /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - dev: true /humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} @@ -4443,7 +4316,6 @@ packages: dependencies: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 - dev: true /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} @@ -4531,7 +4403,6 @@ packages: /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} @@ -4544,7 +4415,6 @@ packages: resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} dependencies: has: 1.0.3 - dev: true /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} @@ -4558,7 +4428,6 @@ packages: /is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} - dev: true /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} @@ -4580,7 +4449,6 @@ packages: /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - dev: true /is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} @@ -4606,7 +4474,6 @@ packages: /istanbul-lib-coverage@3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} - dev: true /istanbul-lib-instrument@5.2.1: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} @@ -4619,7 +4486,6 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color - dev: true /istanbul-lib-report@3.0.0: resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} @@ -4628,7 +4494,6 @@ packages: istanbul-lib-coverage: 3.2.0 make-dir: 3.1.0 supports-color: 7.2.0 - dev: true /istanbul-lib-source-maps@4.0.1: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} @@ -4639,7 +4504,6 @@ packages: source-map: 0.6.1 transitivePeerDependencies: - supports-color - dev: true /istanbul-reports@3.1.5: resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} @@ -4647,7 +4511,6 @@ packages: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.0 - dev: true /iterare@1.2.1: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} @@ -4670,7 +4533,6 @@ packages: dependencies: execa: 5.1.1 p-limit: 3.1.0 - dev: true /jest-circus@28.1.3: resolution: {integrity: sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow==} @@ -4697,7 +4559,6 @@ packages: stack-utils: 2.0.6 transitivePeerDependencies: - supports-color - dev: true /jest-cli@28.1.3(@types/node@16.18.4)(ts-node@10.7.0): resolution: {integrity: sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==} @@ -4725,7 +4586,6 @@ packages: - '@types/node' - supports-color - ts-node - dev: true /jest-config@28.1.3(@types/node@16.18.4)(ts-node@10.7.0): resolution: {integrity: sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==} @@ -4765,7 +4625,6 @@ packages: ts-node: 10.7.0(@swc/core@1.3.56)(@types/node@16.18.4)(typescript@4.5.2) transitivePeerDependencies: - supports-color - dev: true /jest-diff@28.1.3: resolution: {integrity: sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==} @@ -4775,14 +4634,12 @@ packages: diff-sequences: 28.1.1 jest-get-type: 28.0.2 pretty-format: 28.1.3 - dev: true /jest-docblock@28.1.1: resolution: {integrity: sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: detect-newline: 3.1.0 - dev: true /jest-each@28.1.3: resolution: {integrity: sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g==} @@ -4793,7 +4650,6 @@ packages: jest-get-type: 28.0.2 jest-util: 28.1.3 pretty-format: 28.1.3 - dev: true /jest-environment-node@28.1.3: resolution: {integrity: sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==} @@ -4805,12 +4661,10 @@ packages: '@types/node': 16.18.4 jest-mock: 28.1.3 jest-util: 28.1.3 - dev: true /jest-get-type@28.0.2: resolution: {integrity: sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dev: true /jest-haste-map@28.1.3: resolution: {integrity: sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==} @@ -4829,7 +4683,6 @@ packages: walker: 1.0.8 optionalDependencies: fsevents: 2.3.2 - dev: true /jest-leak-detector@28.1.3: resolution: {integrity: sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA==} @@ -4837,7 +4690,6 @@ packages: dependencies: jest-get-type: 28.0.2 pretty-format: 28.1.3 - dev: true /jest-matcher-utils@28.1.3: resolution: {integrity: sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==} @@ -4847,7 +4699,6 @@ packages: jest-diff: 28.1.3 jest-get-type: 28.0.2 pretty-format: 28.1.3 - dev: true /jest-message-util@28.1.3: resolution: {integrity: sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==} @@ -4862,7 +4713,6 @@ packages: pretty-format: 28.1.3 slash: 3.0.0 stack-utils: 2.0.6 - dev: true /jest-mock-extended@2.0.9(jest@28.1.3)(typescript@4.5.2): resolution: {integrity: sha512-eRZq7/FgwHbxOMm3Lo4DpQX6S2zi4OvwMVFHEb3FgDLp0Xy3P1WARkF93xxO5uD4nAHiEPYHZ25qVU9mAVxoLQ==} @@ -4875,13 +4725,23 @@ packages: typescript: 4.5.2 dev: true + /jest-mock-extended@3.0.5(jest@28.1.3)(typescript@4.5.2): + resolution: {integrity: sha512-/eHdaNPUAXe7f65gHH5urc8SbRVWjYxBqmCgax2uqOBJy8UUcCBMN1upj1eZ8y/i+IqpyEm4Kq0VKss/GCCTdw==} + peerDependencies: + jest: ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0 + typescript: ^3.0.0 || ^4.0.0 || ^5.0.0 + dependencies: + jest: 28.1.3(@types/node@16.18.4)(ts-node@10.7.0) + ts-essentials: 7.0.3(typescript@4.5.2) + typescript: 4.5.2 + dev: false + /jest-mock@28.1.3: resolution: {integrity: sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: '@jest/types': 28.1.3 '@types/node': 16.18.4 - dev: true /jest-pnp-resolver@1.2.3(jest-resolve@28.1.3): resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} @@ -4893,12 +4753,10 @@ packages: optional: true dependencies: jest-resolve: 28.1.3 - dev: true /jest-regex-util@28.0.2: resolution: {integrity: sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dev: true /jest-resolve-dependencies@28.1.3: resolution: {integrity: sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==} @@ -4908,7 +4766,6 @@ packages: jest-snapshot: 28.1.3 transitivePeerDependencies: - supports-color - dev: true /jest-resolve@28.1.3: resolution: {integrity: sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ==} @@ -4923,7 +4780,6 @@ packages: resolve: 1.22.1 resolve.exports: 1.1.0 slash: 3.0.0 - dev: true /jest-runner@28.1.3: resolution: {integrity: sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==} @@ -4952,7 +4808,6 @@ packages: source-map-support: 0.5.13 transitivePeerDependencies: - supports-color - dev: true /jest-runtime@28.1.3: resolution: {integrity: sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw==} @@ -4982,7 +4837,6 @@ packages: strip-bom: 4.0.0 transitivePeerDependencies: - supports-color - dev: true /jest-snapshot@28.1.3: resolution: {integrity: sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==} @@ -5013,7 +4867,6 @@ packages: semver: 7.3.8 transitivePeerDependencies: - supports-color - dev: true /jest-util@28.1.3: resolution: {integrity: sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==} @@ -5025,7 +4878,6 @@ packages: ci-info: 3.7.0 graceful-fs: 4.2.10 picomatch: 2.3.1 - dev: true /jest-validate@28.1.3: resolution: {integrity: sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA==} @@ -5037,7 +4889,6 @@ packages: jest-get-type: 28.0.2 leven: 3.1.0 pretty-format: 28.1.3 - dev: true /jest-watcher@28.1.3: resolution: {integrity: sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==} @@ -5051,7 +4902,6 @@ packages: emittery: 0.10.2 jest-util: 28.1.3 string-length: 4.0.2 - dev: true /jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} @@ -5068,7 +4918,6 @@ packages: '@types/node': 16.18.4 merge-stream: 2.0.0 supports-color: 8.1.1 - dev: true /jest@28.1.3(@types/node@16.18.4)(ts-node@10.7.0): resolution: {integrity: sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==} @@ -5088,7 +4937,6 @@ packages: - '@types/node' - supports-color - ts-node - dev: true /joi@17.7.0: resolution: {integrity: sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==} @@ -5116,7 +4964,6 @@ packages: dependencies: argparse: 1.0.10 esprima: 4.0.1 - dev: true /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} @@ -5128,7 +4975,6 @@ packages: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} hasBin: true - dev: true /json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} @@ -5160,7 +5006,6 @@ packages: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true - dev: true /jsonc-parser@3.1.0: resolution: {integrity: sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==} @@ -5241,12 +5086,10 @@ packages: /kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - dev: true /leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} - dev: true /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} @@ -5262,7 +5105,6 @@ packages: /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true /loader-runner@4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} @@ -5273,7 +5115,6 @@ packages: engines: {node: '>=8'} dependencies: p-locate: 4.1.0 - dev: true /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} @@ -5357,7 +5198,6 @@ packages: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: yallist: 3.1.1 - dev: true /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} @@ -5430,7 +5270,6 @@ packages: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} dependencies: tmpl: 1.0.5 - dev: true /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} @@ -5464,7 +5303,6 @@ packages: dependencies: braces: 3.0.2 picomatch: 2.3.1 - dev: true /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} @@ -5490,7 +5328,6 @@ packages: /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - dev: true /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -5633,7 +5470,6 @@ packages: /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true /negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} @@ -5708,11 +5544,9 @@ packages: /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - dev: true /node-releases@2.0.10: resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} - dev: true /node-releases@2.0.6: resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==} @@ -5751,14 +5585,12 @@ packages: /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - dev: true /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} dependencies: path-key: 3.1.1 - dev: true /npmlog@5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} @@ -5812,7 +5644,6 @@ packages: engines: {node: '>=6'} dependencies: mimic-fn: 2.1.0 - dev: true /optionator@0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} @@ -5859,7 +5690,6 @@ packages: engines: {node: '>=6'} dependencies: p-try: 2.2.0 - dev: true /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} @@ -5872,7 +5702,6 @@ packages: engines: {node: '>=8'} dependencies: p-limit: 2.3.0 - dev: true /p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} @@ -5892,7 +5721,6 @@ packages: /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - dev: true /packet-reader@1.0.0: resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} @@ -5920,7 +5748,6 @@ packages: error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - dev: true /parse5-htmlparser2-tree-adapter@6.0.1: resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} @@ -5971,7 +5798,6 @@ packages: /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - dev: true /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} @@ -5980,11 +5806,9 @@ packages: /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - dev: true /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} @@ -6067,19 +5891,16 @@ packages: /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - dev: true /pirates@4.0.5: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} - dev: true /pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} dependencies: find-up: 4.1.0 - dev: true /pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} @@ -6143,7 +5964,6 @@ packages: ansi-regex: 5.0.1 ansi-styles: 5.2.0 react-is: 18.2.0 - dev: true /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -6182,7 +6002,6 @@ packages: dependencies: kleur: 3.0.3 sisteransi: 1.0.5 - dev: true /prop-types-extra@1.1.1(react@18.2.0): resolution: {integrity: sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==} @@ -6298,7 +6117,6 @@ packages: /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - dev: true /react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} @@ -6439,7 +6257,6 @@ packages: engines: {node: '>=8'} dependencies: resolve-from: 5.0.0 - dev: true /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} @@ -6449,12 +6266,10 @@ packages: /resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - dev: true /resolve.exports@1.1.0: resolution: {integrity: sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==} engines: {node: '>=10'} - dev: true /resolve@1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} @@ -6463,7 +6278,6 @@ packages: is-core-module: 2.11.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true /restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} @@ -6623,12 +6437,10 @@ packages: engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 - dev: true /shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - dev: true /shelljs@0.8.5: resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} @@ -6659,12 +6471,10 @@ packages: /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - dev: true /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - dev: true /smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} @@ -6703,7 +6513,6 @@ packages: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 - dev: true /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -6731,7 +6540,6 @@ packages: /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: true /sqlite3@5.1.6: resolution: {integrity: sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==} @@ -6763,7 +6571,6 @@ packages: engines: {node: '>=10'} dependencies: escape-string-regexp: 2.0.0 - dev: true /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} @@ -6779,7 +6586,6 @@ packages: dependencies: char-regex: 1.0.2 strip-ansi: 6.0.1 - dev: true /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -6813,17 +6619,14 @@ packages: /strip-bom@4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} - dev: true /strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - dev: true /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - dev: true /superagent@8.0.5: resolution: {integrity: sha512-lQVE0Praz7nHiSaJLKBM/cZyi7J0E4io8tWnGSBdBrqAzhzrjQ/F5iGP9Zr29CJC8N5zYdhG2kKaNcB6dKxp7g==} @@ -6858,7 +6661,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -6878,12 +6680,10 @@ packages: dependencies: has-flag: 4.0.0 supports-color: 7.2.0 - dev: true /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: true /swagger-ui-dist@4.15.1: resolution: {integrity: sha512-DlZARu6ckUFqDe0j5IPayO4k0gQvYQw9Un02MhxAgaMtVnTH2vmyyDe+yKeV0r1LiiPx3JbasdS/5Yyb/AV3iw==} @@ -6915,7 +6715,6 @@ packages: dependencies: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - dev: true /terser-webpack-plugin@5.3.6(@swc/core@1.3.56)(webpack@5.74.0): resolution: {integrity: sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==} @@ -6983,7 +6782,6 @@ packages: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 minimatch: 3.1.2 - dev: true /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -7015,19 +6813,16 @@ packages: /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - dev: true /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} - dev: true /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 - dev: true /toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} @@ -7054,7 +6849,6 @@ packages: typescript: '>=3.7.0' dependencies: typescript: 4.5.2 - dev: true /ts-jest@28.0.8(@babel/core@7.21.3)(jest@28.1.3)(typescript@4.5.2): resolution: {integrity: sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg==} @@ -7215,7 +7009,6 @@ packages: /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} - dev: true /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} @@ -7225,7 +7018,6 @@ packages: /type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - dev: true /type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} @@ -7407,7 +7199,6 @@ packages: browserslist: 4.21.5 escalade: 3.1.1 picocolors: 1.0.0 - dev: true /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -7444,7 +7235,6 @@ packages: '@jridgewell/trace-mapping': 0.3.17 '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 - dev: true /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} @@ -7507,7 +7297,6 @@ packages: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: makeerror: 1.0.12 - dev: true /warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} @@ -7670,7 +7459,6 @@ packages: dependencies: imurmurhash: 0.1.4 signal-exit: 3.0.7 - dev: true /xml2js@0.4.23: resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} @@ -7699,7 +7487,6 @@ packages: /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: true /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} diff --git a/shared/src/abilities.ts b/shared/src/abilities.ts index 23fd5bf..dd277ee 100644 --- a/shared/src/abilities.ts +++ b/shared/src/abilities.ts @@ -8,7 +8,7 @@ import { import { applyAbilitiesForPerson, Person, Role } from "./person"; import { Application, applyAbilitiesOnApplication } from "./application"; import { applyAbilitiesOnAvailability, Availability } from "./availability"; -import { TimeSlot } from "./timeslot"; +import { applyAbilitiesOnTimeSlot, TimeSlot } from "./timeslot"; import { RecruitmentSession } from "./recruitment-session"; import { applyAbilitiesOnRecruitmentSession } from "./recruitment-session"; @@ -53,6 +53,7 @@ export const abilityForUser = (user: UserAuth): AppAbility => { applyAbilitiesOnApplication(user, builder); applyAbilitiesOnAvailability(user, builder); applyAbilitiesOnRecruitmentSession(user, builder); + applyAbilitiesOnTimeSlot(user, builder); const { build } = builder; return build(); diff --git a/shared/src/availability.spec.ts b/shared/src/availability.spec.ts index 212a245..53f5939 100644 --- a/shared/src/availability.spec.ts +++ b/shared/src/availability.spec.ts @@ -1,39 +1,93 @@ import { AvailabilityState, - AvailabilityType, Availability, - updateAvailabilitySchema, + insertAvailabilitySchema, + applyAbilitiesOnAvailability, } from "./availability"; import { createMockAbility } from "./abilities.spec"; import { Action, UserAuth, checkAbility } from "./abilities"; import { Role } from "./person"; +const mockAbilityForAvailability = (user: UserAuth) => + createMockAbility((builder) => { + applyAbilitiesOnAvailability(user, builder); + }); + +const mockAvailability = { + id: 1, + state: AvailabilityState.Free, + lastModified: new Date(), + timeSlot: { + start: new Date(2024, 0, 1, 10, 0, 0), + end: new Date(2024, 0, 1, 11, 0, 0), + id: 1, + availabilities: [], + }, + user: { + firstName: "John", + lastName: "Doe", + oauthId: "123", + role: Role.Member, + }, +}; + describe("Availability", () => { - describe("updateAvailabilitySchema", () => { - it("should allow a valid update", () => { - const updateAvailability = { - state: AvailabilityState.Confirmed, - timeSlotId: 123, - }; - const { error } = updateAvailabilitySchema.validate(updateAvailability); - expect(error).toBeDefined(); + Object.values(Role).forEach((role: Role) => { + describe("should allow only HKN members to read, create, and delete availabilities", () => { + const mockAbility = mockAbilityForAvailability({ role, sub: "123" }); + it(`should allow only Admin and Supervisors to update availabilities [${role}]`, () => { + expect( + checkAbility( + mockAbility, + Action.Update, + mockAvailability, + "Availability" + ) + ).toBe([Role.Admin, Role.Supervisor].includes(role)); + }); + it(`should allow only HKN members to read, create, and delete availabilities [${role}]`, () => { + expect( + checkAbility( + mockAbility, + Action.Read, + mockAvailability, + "Availability" + ) + ).toBe(![Role.Applicant, Role.None].includes(role)); + expect( + checkAbility( + mockAbility, + Action.Create, + mockAvailability, + "Availability" + ) + ).toBe(![Role.Applicant, Role.None].includes(role)); + expect( + checkAbility( + mockAbility, + Action.Delete, + mockAvailability, + "Availability" + ) + ).toBe(![Role.Applicant, Role.None].includes(role)); + }); }); + }); - it("should not allow updating with an invalid state", () => { - const updateAvailability = { - state: "Non_Existent_State", - timeSlotId: 123, + describe("insertAvailabilitySchema", () => { + it("should allow creating a valid availability", () => { + const validAvailability = { + timeSlotId: 1, }; - const { error } = updateAvailabilitySchema.validate(updateAvailability); - expect(error).toBeDefined(); + + const { error } = insertAvailabilitySchema.validate(validAvailability); + expect(error).toBeUndefined(); }); - it("should not allow updating with an invalid timeSlotId", () => { - const updateAvailability = { - state: AvailabilityState.Confirmed, - timeSlotId: -321, - }; - const { error } = updateAvailabilitySchema.validate(updateAvailability); + it("should not allow creating an availability without a timeSlotId", () => { + const invalidAvailability = {}; + + const { error } = insertAvailabilitySchema.validate(invalidAvailability); expect(error).toBeDefined(); }); }); diff --git a/shared/src/availability.ts b/shared/src/availability.ts index 1cd0e38..f21bb2b 100644 --- a/shared/src/availability.ts +++ b/shared/src/availability.ts @@ -1,11 +1,12 @@ +import { TimeSlot } from "./timeslot"; import { Action, ApplyAbilities } from "./abilities"; import { Person, Role } from "./person"; import * as Joi from "joi"; export enum AvailabilityState { - Subscribed = "subscribed", - Confirmed = "confirmed", - Cancelled = "cancelled", + Free = "free", + Interviewing = "interviewing", + Recovering = "recovering", } export enum AvailabilityType { @@ -14,21 +15,17 @@ export enum AvailabilityType { } export interface Availability { + id: number; state: AvailabilityState; - timeSlotId: number; - member: Person; - // assignedAt?: Date; - // confirmedAt?: Date; - // cancelledAt?: Date; + lastModified: Date; + timeSlot: TimeSlot; + user: Person; } /* Validation schemas */ -export const updateAvailabilitySchema = Joi.object({ - state: Joi.string() - .valid(...Object.values(AvailabilityType)) - .required(), - timeSlotId: Joi.number().positive().required(), +export const insertAvailabilitySchema = Joi.object({ + timeSlotId: Joi.number().required(), }).options({ stripUnknown: true, abortEarly: false, @@ -48,13 +45,12 @@ export const applyAbilitiesOnAvailability: ApplyAbilities = ( break; case Role.Member: case Role.Clerk: + can(Action.Create, "Availability"); can(Action.Read, "Availability"); - can(Action.Update, "Availability", { userId: user.sub }); + can(Action.Delete, "Availability"); + cannot(Action.Update, "Availability"); break; case Role.Applicant: - can(Action.Read, "Availability", { userId: user.sub }); - can(Action.Update, "Availability", { userId: user.sub }); - break; default: cannot(Action.Manage, "Availability"); } diff --git a/shared/src/person.ts b/shared/src/person.ts index b393cd4..ceeca23 100644 --- a/shared/src/person.ts +++ b/shared/src/person.ts @@ -19,6 +19,8 @@ export interface Person { phone_no?: string; telegramId?: string; role: Role; + is_board: boolean; + is_expert: boolean; } export const createUserSchema = Joi.object({ diff --git a/shared/src/recruitment-session.spec.ts b/shared/src/recruitment-session.spec.ts index e86cf48..b0c9a34 100644 --- a/shared/src/recruitment-session.spec.ts +++ b/shared/src/recruitment-session.spec.ts @@ -3,6 +3,7 @@ import { RecruitmentSessionState, createRecruitmentSessionSchema, applyAbilitiesOnRecruitmentSession, + updateRecruitmentSessionSchema, } from "./recruitment-session"; import { createMockAbility } from "./abilities.spec"; import { Action, UserAuth, checkAbility } from "./abilities"; @@ -13,8 +14,8 @@ describe("Recruitment Session", () => { const mockRecSess: Partial = { state: RecruitmentSessionState.Active, slotDuration: 5, - interviewStart: "11:55" as unknown as Date, - interviewEnd: "16:30" as unknown as Date, + interviewStart: "2024-10-05 11:55" as unknown as Date, + interviewEnd: "2024-10-29 16:30" as unknown as Date, days: [new Date("2024-12-23"), new Date("2024-12-23")], lastModified: new Date("2023-10-20 15:10"), }; @@ -80,4 +81,23 @@ describe("Recruitment Session", () => { expect(mockRecSess.interviewStart).toMatch("11:55"); }); }); + + describe("updateRecruitmentSessionSchema", () => { + it("should allow a valild update", () => { + const mockUpdate: Partial = { + state: RecruitmentSessionState.Concluded, + interviewEnd: "2023-10-26 19:30" as unknown as Date, + }; + expect( + updateRecruitmentSessionSchema.validate(mockUpdate) + ).not.toHaveProperty("error"); + }); + + it("should allow not to set optional fields", () => { + const mockUpdate: Partial = {}; + expect( + updateRecruitmentSessionSchema.validate(mockUpdate) + ).not.toHaveProperty("error"); + }); + }); }); diff --git a/shared/src/recruitment-session.ts b/shared/src/recruitment-session.ts index a232e18..ca1890c 100644 --- a/shared/src/recruitment-session.ts +++ b/shared/src/recruitment-session.ts @@ -27,8 +27,8 @@ export interface RecruitmentSession { export const createRecruitmentSessionSchema = Joi.object({ state: Joi.string().valid("active", "concluded").required(), slotDuration: Joi.number().integer().optional(), - interviewStart: JoiDate.date().format("HH:mm").required(), - interviewEnd: JoiDate.date().format("HH:mm").required(), + interviewStart: JoiDate.date().format("YYYY-MM-DD HH:mm").required(), + interviewEnd: JoiDate.date().format("YYYY-MM-DD HH:mm").required(), days: Joi.array().items(JoiDate.date().format("YYYY-MM-DD")).optional(), lastModified: JoiDate.date().format("YYYY-MM-DD HH:mm").required(), }).options({ diff --git a/shared/src/timeslot.ts b/shared/src/timeslot.ts index fea1951..6343754 100644 --- a/shared/src/timeslot.ts +++ b/shared/src/timeslot.ts @@ -1,28 +1,12 @@ import { Action, ApplyAbilities } from "./abilities"; import { Role } from "./person"; -import DateExtension from "@joi/date"; -import * as Joi from "joi"; -const JoiDate = Joi.extend(DateExtension); - -// import BaseJoi from "joi"; -// const Joi = BaseJoi.extend(JoiDate); export interface TimeSlot { + id: number; start: Date; end: Date; } -/* Validation schemas */ - -export const createTimeSlotSchema = Joi.object({ - start: JoiDate.date().format("YYYY-MM-DD HH:mm").required(), - end: JoiDate.date().format("YYYY-MM-DD HH:mm").required(), -}).options({ - stripUnknown: true, - abortEarly: false, - presence: "required", -}); - /* Abilities */ export const applyAbilitiesOnTimeSlot: ApplyAbilities = ( @@ -30,18 +14,17 @@ export const applyAbilitiesOnTimeSlot: ApplyAbilities = ( { can, cannot } ) => { can(Action.Manage, "TimeSlot"); - // switch (user.role) { - // case Role.Admin: - // case Role.Supervisor: - // case Role.Clerk: - // // TODO: Decide who can create/delete timeslots - // can(Action.Manage, "TimeSlot"); - // break; - // case Role.Member: - // case Role.Applicant: - // can(Action.Read, "TimeSlot"); - // break; - // default: - // cannot(Action.Manage, "TimeSlot"); - // } + switch (user.role) { + case Role.Admin: + case Role.Supervisor: + can(Action.Manage, "TimeSlot"); + break; + case Role.Clerk: + case Role.Member: + case Role.Applicant: + can(Action.Read, "TimeSlot"); + break; + default: + cannot(Action.Manage, "TimeSlot"); + } }; diff --git a/sonar-project.properties b/sonar-project.properties index 895f2fc..edb0d90 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,4 +4,4 @@ sonar.projectName=HKrecruitment sonar.javascript.lcov.reportPaths=./coverage/shared/lcov.info, ./coverage/api/lcov.info, ./coverage/api-e2e/lcov.info sonar.sources=api/, frontend/, shared/ -sonar.coverage.exclusions=**/node_modules/**/*, **/*.spec.*, **/test/**/*, **/tests/**/*, **/*.json, **/*.yaml, **/*.yml, **/*.md +sonar.coverage.exclusions=**/node_modules/**/*, **/*.spec.*, **/*.e2e-spec.*, **/documentation/**/*, **/frontend/**/*, **/test/**/*, **/tests/**/*, **/*.json, **/*.yaml, **/*.yml, **/*.md