Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 23-a-valider-avec-erwan-check-plus-precis-du-cnfs-id-ainsi-que-des-plages-de-rdv-ouvertes
  • 484-statistiques-requete
  • 50-exception-handling
  • 722-envsubst-client-side-conf
  • V2.5
  • client-476-add-localization-filter
  • dependencies/nestjs
  • dev
  • master
  • openshift-test-deploy
  • pnpm
  • renovate/bitnami-mongodb-4.x
  • renovate/devdependencies-(non-major)
  • renovate/elastic-elasticsearch-8.x
  • renovate/elasticsearch-7.x
  • renovate/express-5.x
  • renovate/ghcr.io-browserless-chromium-2.x
  • renovate/ghost-5.x
  • renovate/gouvfr-anct-timetable-to-osm-opening-hours-2.x
  • renovate/jest-junit-16.x
  • renovate/luxon-3.x
  • renovate/major-jest-monorepo
  • renovate/major-nest-monorepo
  • renovate/major-typescript-eslint-monorepo
  • renovate/mongo-express-1.x
  • renovate/mysql-9.x
  • renovate/npm-11.x
  • renovate/tsconfig-paths-4.x
  • send-mail-to-structure-after-orientation
  • tests/users-controllers
  • v1.1.0
  • v1.10.0
  • v1.11.0
  • v1.12.0
  • v1.13.0
  • v1.15.0
  • v1.15.1
  • v1.16.0
  • v1.17.0
  • v1.2.0
  • v1.3.0
  • v1.4.0
  • v1.5.0
  • v1.6.0
  • v1.6.1
  • v1.7.0
  • v1.8.0
  • v1.9.0
  • v1.9.1
  • v2.0.0
  • v2.0.0-beta1
  • v2.0.0-beta1.1
  • v2.0.0-beta2
  • v2.0.0-beta3
  • v2.0.0-beta4
  • v2.0.0-beta5
  • v2.0.1
  • v2.0.2
  • v2.0.3
  • v2.1.0
  • v2.1.1
  • v2.1.2
  • v2.1.3
  • v2.2.0
  • v2.3.0
  • v2.3.1
  • v2.3.2
  • v2.4.0
  • v2.4.1
  • v2.4.2
  • v2.5.0
  • v3.0.0
  • v3.0.1
  • v3.1.0
  • v3.2.0
  • v3.3.0
  • v3.3.1
  • v3.4.0
  • v3.4.1
  • v3.4.2
  • v3.4.3
  • v4.0.0
  • v4.0.1
  • v4.0.3
  • v4.1.0
  • v4.1.1
86 results

Target

Select target project
  • web-et-numerique / Factory / Resin / Server
1 result
Select Git revision
  • 23-a-valider-avec-erwan-check-plus-precis-du-cnfs-id-ainsi-que-des-plages-de-rdv-ouvertes
  • 484-statistiques-requete
  • 50-exception-handling
  • 722-envsubst-client-side-conf
  • V2.5
  • client-476-add-localization-filter
  • dependencies/nestjs
  • dev
  • master
  • openshift-test-deploy
  • pnpm
  • renovate/bitnami-mongodb-4.x
  • renovate/devdependencies-(non-major)
  • renovate/elastic-elasticsearch-8.x
  • renovate/elasticsearch-7.x
  • renovate/express-5.x
  • renovate/ghcr.io-browserless-chromium-2.x
  • renovate/ghost-5.x
  • renovate/gouvfr-anct-timetable-to-osm-opening-hours-2.x
  • renovate/jest-junit-16.x
  • renovate/luxon-3.x
  • renovate/major-jest-monorepo
  • renovate/major-nest-monorepo
  • renovate/major-typescript-eslint-monorepo
  • renovate/mongo-express-1.x
  • renovate/mysql-9.x
  • renovate/npm-11.x
  • renovate/tsconfig-paths-4.x
  • send-mail-to-structure-after-orientation
  • tests/users-controllers
  • v1.1.0
  • v1.10.0
  • v1.11.0
  • v1.12.0
  • v1.13.0
  • v1.15.0
  • v1.15.1
  • v1.16.0
  • v1.17.0
  • v1.2.0
  • v1.3.0
  • v1.4.0
  • v1.5.0
  • v1.6.0
  • v1.6.1
  • v1.7.0
  • v1.8.0
  • v1.9.0
  • v1.9.1
  • v2.0.0
  • v2.0.0-beta1
  • v2.0.0-beta1.1
  • v2.0.0-beta2
  • v2.0.0-beta3
  • v2.0.0-beta4
  • v2.0.0-beta5
  • v2.0.1
  • v2.0.2
  • v2.0.3
  • v2.1.0
  • v2.1.1
  • v2.1.2
  • v2.1.3
  • v2.2.0
  • v2.3.0
  • v2.3.1
  • v2.3.2
  • v2.4.0
  • v2.4.1
  • v2.4.2
  • v2.5.0
  • v3.0.0
  • v3.0.1
  • v3.1.0
  • v3.2.0
  • v3.3.0
  • v3.3.1
  • v3.4.0
  • v3.4.1
  • v3.4.2
  • v3.4.3
  • v4.0.0
  • v4.0.1
  • v4.0.3
  • v4.1.0
  • v4.1.1
86 results
Show changes
34 files
+ 2181
183
Compare changes
  • Side-by-side
  • Inline

Files

+46 −0
Original line number Diff line number Diff line
## Description du problème

_Donnez une briève description du problème_

## L'environnement

#### Utilisez vous l'application sur :

- [ ] Mobile
- [ ] Ordinateur

##### En cas de mobile

###### Quel type de mobile utilisez-vous?

- [ ] Android
- [ ] Iphone

###### Quel navigateur utilisez-vous?

- [ ] Chrome
- [ ] Safari
- [ ] Autre

##### En cas d'ordinateur

###### Quel navigateur utilisez-vous?

- [ ] Chrome
- [ ] Firefox
- [ ] Safari
- [ ] Autre

## Le bug

#### Quelles sont les étapes qui ont menées au problème?

_Donnez une description des étapes, il est fortemment conseillé de l'accompagner par des captures d'écran_

#### Quel est le comportement obtenu?

_Donnez une description du comportement obtenu, il est fortemment conseillé de l'accompagner par des captures d'écran_

#### Quel est le comportement attendu?

_Donnez une description du comportement attendu_
+6 −4
Original line number Diff line number Diff line
@@ -47,7 +47,6 @@ $ npm install

### Base de donnée


```bash
$ docker-compose up -d database-ram
$ docker-compose up -d mongo-express
@@ -70,17 +69,20 @@ $ npm run start:prod

```bash
# Lien vers le swagger
$ http://localhost:3000/api
$ http://localhost:3000/doc

# Lien vers le mongo-express
$ http://localhost:8081
```

## Documentation

A documentation is generated with compodoc in addition of the existing documentation on the wiki.

```sh
npm run doc:serve
```

You can now visualize it at : `localhost:8080`

## Test
+1 −1
Original line number Diff line number Diff line
@@ -18,7 +18,7 @@
    "release": "standard-version",
    "init-db": "node ./scripts/init-db.js",
    "test": "jest",
    "test:watch": "jest --config ./test/jest.json --watch",
    "test:watch": "jest --config ./test/jest.json --watch --coverage",
    "test:cov": "jest --config ./test/jest.json --coverage --ci --reporters=default --reporters=jest-junit",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json",
Original line number Diff line number Diff line
import { HttpModule } from '@nestjs/common';
import { getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { Types } from 'mongoose';
import { mockJwtAuthGuard } from '../../test/mock/guards/jwt-auth.mock.guard';
import { mockRoleGuard } from '../../test/mock/guards/role.mock.guard';
import { NewsletterServiceMock } from '../../test/mock/services/newsletter.mock.service';
import { StructuresServiceMock } from '../../test/mock/services/structures.mock.service';
import { UsersServiceMock } from '../../test/mock/services/user.mock.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { ConfigurationModule } from '../configuration/configuration.module';
import { MailerService } from '../mailer/mailer.service';
import { NewsletterSubscription } from '../newsletter/newsletter-subscription.schema';
import { NewsletterService } from '../newsletter/newsletter.service';
import { SearchModule } from '../search/search.module';
import { Structure } from '../structures/schemas/structure.schema';
import { StructuresService } from '../structures/services/structures.service';
import { StructuresSearchService } from '../structures/services/structures-search.service';
import { StructuresService } from '../structures/services/structures.service';
import { RolesGuard } from '../users/guards/roles.guard';
import { User } from '../users/schemas/user.schema';
import { EmployerSearchService } from '../users/services/employer-search.service';
import { EmployerService } from '../users/services/employer.service';
import { JobsService } from '../users/services/jobs.service';
import { UsersService } from '../users/services/users.service';
import { AdminController } from './admin.controller';
import { mockJwtAuthGuard } from '../../test/mock/guards/jwt-auth.mock.guard';
import { mockRoleGuard } from '../../test/mock/guards/role.mock.guard';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../users/guards/roles.guard';
import { UsersServiceMock } from '../../test/mock/services/user.mock.service';
import { StructuresServiceMock } from '../../test/mock/services/structures.mock.service';
import { NewsletterServiceMock } from '../../test/mock/services/newsletter.mock.service';

describe('AdminController', () => {
  let controller: AdminController;
  let userService: UsersService;

  const mockEmployerSearchService = {
    indexEmployer: jest.fn(),
    search: jest.fn(),
    dropIndex: jest.fn(),
    createEmployerIndex: jest.fn(),
    deleteIndex: jest.fn(),
  };

  const mockEmployerService = {
    findOne: jest.fn(),
    deleteOneId: jest.fn(),
    findByName: jest.fn(),
    findAllValidated: jest.fn(),
    findAllUnvalidated: jest.fn(),
    createEmployerFromAdmin: jest.fn(),
    validate: jest.fn(),
    update: jest.fn(),
    mergeEmployer: jest.fn(),
    deleteInvalidEmployer: jest.fn(),
  };

  const mockJobService = {
    findOne: jest.fn(),
    findByName: jest.fn(),
    deleteOneId: jest.fn(),
    findAll: jest.fn(),
    findAllUnvalidated: jest.fn(),
    createJobFromAdmin: jest.fn(),
    validate: jest.fn(),
    update: jest.fn(),
    mergeJob: jest.fn(),
    deleteInvalidJob: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
@@ -41,6 +80,18 @@ describe('AdminController', () => {
        },
        StructuresSearchService,
        MailerService,
        {
          provide: EmployerService,
          useValue: mockEmployerService,
        },
        {
          provide: JobsService,
          useValue: mockJobService,
        },
        {
          provide: EmployerSearchService,
          useValue: mockEmployerSearchService,
        },
        {
          provide: getModelToken('User'),
          useValue: User,
@@ -63,6 +114,7 @@ describe('AdminController', () => {
      .compile();

    controller = module.get<AdminController>(AdminController);
    userService = module.get<UsersService>(UsersService);
  });

  it('should be defined', () => {
@@ -71,7 +123,7 @@ describe('AdminController', () => {

  it('should get pending attachments', async () => {
    expect((await controller.getPendingAttachments()).length).toBe(2);
    expect(Object.keys((await controller.getPendingAttachments())[0]).length).toBe(3);
    expect(Object.keys((await controller.getPendingAttachments())[0]).length).toBe(4);
  });

  describe('Pending structures validation', () => {
@@ -80,9 +132,10 @@ describe('AdminController', () => {
        structureId: '6093ba0e2ab5775cfc01ed3e',
        structureName: 'test',
        userEmail: 'jean.paul@mii.com',
        updatedAt: '2021-03-02T10:07:48.000Z',
      };
      expect((await controller.validatePendingStructure(pendingStructureTest)).length).toBe(2);
      expect(Object.keys((await controller.validatePendingStructure(pendingStructureTest))[0]).length).toBe(3);
      expect(Object.keys((await controller.validatePendingStructure(pendingStructureTest))[0]).length).toBe(4);
    });

    it('should get structure does not exist', async () => {
@@ -90,6 +143,7 @@ describe('AdminController', () => {
        structureId: '1093ba0e2ab5775cfc01z2ki',
        structureName: 'test',
        userEmail: 'jean.paul@mii.com',
        updatedAt: '2021-03-02T10:07:48.000Z',
      };
      try {
        await controller.validatePendingStructure(pendingStructureTest);
@@ -106,9 +160,10 @@ describe('AdminController', () => {
        structureId: '6093ba0e2ab5775cfc01ed3e',
        structureName: 'test',
        userEmail: 'jean.paul@mii.com',
        updatedAt: '2021-03-02T10:07:48.000Z',
      };
      expect((await controller.refusePendingStructure(pendingStructureTest)).length).toBe(2);
      expect(Object.keys((await controller.refusePendingStructure(pendingStructureTest))[0]).length).toBe(3);
      expect(Object.keys((await controller.refusePendingStructure(pendingStructureTest))[0]).length).toBe(4);
    });

    it('should get structure does not exist', async () => {
@@ -116,6 +171,7 @@ describe('AdminController', () => {
        structureId: '1093ba0e2ab5775cfc01z2ki',
        structureName: 'test',
        userEmail: 'jean.paul@mii.com',
        updatedAt: '2021-03-02T10:07:48.000Z',
      };
      try {
        await controller.refusePendingStructure(pendingStructureTest);
@@ -140,6 +196,134 @@ describe('AdminController', () => {
    });
  });

  describe('setUserEmployer', () => {
    it('should set a new employer to the user', async () => {
      const spyer = jest.spyOn(userService, 'updateUserEmployer');
      const mockUserId = '6231aefe76598527c8d0b5bc';
      const mockEmployer = {
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Sopra',
        validated: true,
      };
      mockEmployerService.findOne.mockResolvedValueOnce(mockEmployer);
      const reply = await controller.setUserEmployer({
        userId: mockUserId,
        employerId: String(mockEmployer._id),
      });
      expect(spyer).toBeCalledTimes(1);
      expect(spyer).toBeCalledWith(mockUserId, mockEmployer);
      expect(reply).toBeTruthy();
    });

    it('should not set a new employer to the user if the employer does not exist', async () => {
      const spyer = jest.spyOn(userService, 'updateUserEmployer');
      const mockUserId = '6231aefe76598527c8d0b5bc';
      const mockEmployer = {
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Sopra',
        validated: true,
      };
      mockEmployerService.findOne.mockResolvedValueOnce(null);
      try {
        await controller.setUserEmployer({
          userId: mockUserId,
          employerId: String(mockEmployer._id),
        });
        expect(true).toBe(false);
      } catch (e) {
        expect(spyer).toBeCalledTimes(0);
        expect(e.message).toBe('Employer does not exist');
        expect(e.status).toBe(400);
      }
    });

    it('should not set a new employer to the user if the user does not exist', async () => {
      const spyer = jest.spyOn(userService, 'updateUserEmployer');
      const mockUserId = 'thisuserdoesnotexist';
      const mockEmployer = {
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Sopra',
        validated: true,
      };
      mockEmployerService.findOne.mockResolvedValueOnce(mockEmployer);
      try {
        await controller.setUserEmployer({
          userId: mockUserId,
          employerId: String(mockEmployer._id),
        });
        expect(true).toBe(false);
      } catch (e) {
        expect(spyer).toBeCalledTimes(0);
        expect(e.message).toBe('User does not exist');
        expect(e.status).toBe(400);
      }
    });
  });

  describe('setUserJob', () => {
    it('should set a new job to the user', async () => {
      const spyer = jest.spyOn(userService, 'updateUserJob');
      const mockUserId = '6231aefe76598527c8d0b5bc';
      const mockJob = {
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Toto',
        validated: true,
      };
      mockJobService.findOne.mockResolvedValueOnce(mockJob);
      const reply = await controller.setUserJob({
        userId: mockUserId,
        jobId: String(mockJob._id),
      });
      expect(spyer).toBeCalledTimes(1);
      expect(spyer).toBeCalledWith(mockUserId, mockJob);
      expect(reply).toBeTruthy();
    });

    it('should not set a new job to the user if the job does not exist', async () => {
      const spyer = jest.spyOn(userService, 'updateUserJob');
      const mockUserId = '6231aefe76598527c8d0b5bc';
      const mockJob = {
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Dev',
        validated: true,
      };
      mockJobService.findOne.mockResolvedValueOnce(null);
      try {
        await controller.setUserJob({
          userId: mockUserId,
          jobId: String(mockJob._id),
        });
        expect(true).toBe(false);
      } catch (e) {
        expect(spyer).toBeCalledTimes(0);
        expect(e.message).toBe('Job does not exist');
        expect(e.status).toBe(400);
      }
    });

    it('should not set a new job to the user if the user does not exist', async () => {
      const spyer = jest.spyOn(userService, 'updateUserJob');
      const mockUserId = 'thisuserdoesntexist';
      const mockJob = {
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Dev',
        validated: true,
      };
      mockJobService.findOne.mockResolvedValueOnce(mockJob);
      try {
        await controller.setUserJob({
          userId: mockUserId,
          jobId: String(mockJob._id),
        });
        expect(true).toBe(false);
      } catch (e) {
        expect(spyer).toBeCalledTimes(0);
        expect(e.message).toBe('User does not exist');
        expect(e.status).toBe(400);
      }
    });
  });

  describe('Search user', () => {
    it('should return all users, empty string', async () => {
      expect((await controller.searchUsers({ searchString: '' })).length).toBe(2);
@@ -156,7 +340,7 @@ describe('AdminController', () => {
    });
  });

  describe('Search user newleetter subscription', () => {
  describe('Search user newsletter subscription', () => {
    it('should return all subscribed users, empty string', async () => {
      expect((await controller.getNewsletterSubscriptions({ searchString: '' })).length).toBe(3);
    });
Original line number Diff line number Diff line
import { ApiOperation, ApiParam } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import {
  Body,
  Delete,
  Param,
  Controller,
  Delete,
  Get,
  Post,
  UseGuards,
  HttpStatus,
  HttpException,
  HttpStatus,
  Logger,
  Param,
  Post,
  Put,
  UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import { validate } from 'class-validator';
import { DateTime, Interval } from 'luxon';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { NewsletterSubscription } from '../newsletter/newsletter-subscription.schema';
import { NewsletterService } from '../newsletter/newsletter.service';
import { Structure } from '../structures/schemas/structure.schema';
import { StructuresService } from '../structures/services/structures.service';
import { Roles } from '../users/decorators/roles.decorator';
import { RolesGuard } from '../users/guards/roles.guard';
import { IUser } from '../users/interfaces/user.interface';
import { EmployerService } from '../users/services/employer.service';
import { JobsService } from '../users/services/jobs.service';
import { UsersService } from '../users/services/users.service';
import { PendingStructureDto } from './dto/pending-structure.dto';
import { validate } from 'class-validator';
import { Structure } from '../structures/schemas/structure.schema';
import { IUser } from '../users/interfaces/user.interface';
import { SetUserEmployerDto } from './dto/set-user-employer.dto';
import { SetUserJobDto } from './dto/set-user-job.dto';

@Controller('admin')
export class AdminController {
@@ -29,18 +35,22 @@ export class AdminController {
  constructor(
    private usersService: UsersService,
    private structuresService: StructuresService,
    private jobsService: JobsService,
    private employerService: EmployerService,
    private newsletterService: NewsletterService
  ) {}

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @Get('pendingStructures')
  @ApiOperation({ description: 'Get pending structre for validation' })
  @ApiOperation({ description: 'Get pending structures for validation' })
  public async getPendingAttachments(): Promise<PendingStructureDto[]> {
    const pendingStructure = await this.usersService.getPendingStructures();
    return Promise.all(
      pendingStructure.map(async (structure) => {
        structure.structureName = (await this.structuresService.findOne(structure.structureId)).structureName;
        const structureDocument = await this.structuresService.findOne(structure.structureId);
        structure.structureName = structureDocument.structureName;
        structure.updatedAt = structureDocument.updatedAt;
        return structure;
      })
    );
@@ -49,13 +59,30 @@ export class AdminController {
  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @Get('adminStructuresList')
  @ApiOperation({ description: 'Get pending structre for validation' })
  @ApiOperation({ description: 'Get pending structures for validation' })
  public async getAdminStructuresList(): Promise<any> {
    const structuresList = { claimed: [], inClaim: [], toClaim: [], incomplete: [] };
    structuresList.inClaim = await this.getPendingAttachments();
    structuresList.toClaim = (await this.structuresService.findAllUnclaimed()).filter(
      (demand) => !structuresList.inClaim.find((elem) => elem.structureId == demand.structureId)
    );
    const today = DateTime.local().setZone('utc', { keepLocalTime: true });
    const inClaimStructures = await this.getPendingAttachments();
    structuresList.inClaim = inClaimStructures.map((structure) => {
      return {
        structureId: structure.structureId,
        structureName: structure.structureName,
        updatedAt: structure.updatedAt,
        isOutdated: Interval.fromDateTimes(DateTime.fromISO(structure.updatedAt), today).length('months') > 6,
      };
    });
    const toClaimStructures = await this.structuresService.findAllUnclaimed();
    structuresList.toClaim = toClaimStructures
      .filter((demand) => !structuresList.inClaim.find((elem) => elem.structureId == demand.structureId))
      .map((structure) => {
        return {
          structureId: structure.structureId,
          structureName: structure.structureName,
          updatedAt: structure.updatedAt,
          isOutdated: Interval.fromDateTimes(DateTime.fromISO(structure.updatedAt), today).length('months') > 6,
        };
      });
    const allStructures = await this.structuresService.findAll();
    structuresList.claimed = allStructures
      .filter(
@@ -64,14 +91,25 @@ export class AdminController {
          !structuresList.toClaim.find((elem) => elem.structureId == demand.id)
      )
      .map((structure) => {
        return { structureId: structure.id, structureName: structure.structureName };
        return {
          structureId: structure.id,
          structureName: structure.structureName,
          updatedAt: structure.updatedAt,
          isOutdated: Interval.fromDateTimes(DateTime.fromISO(structure.updatedAt), today).length('months') > 6,
        };
      });
    structuresList.incomplete = await Promise.all(
      allStructures.map(async (struct) => {
        const validity = await validate(new Structure(struct));
        if (validity.length > 0) {
          this.logger.debug(`getAdminStructuresList - validation failed. errors: ${validity.toString()}`);
          return { structureId: struct.id, structureName: struct.structureName };

          return {
            structureId: struct.id,
            structureName: struct.structureName,
            updatedAt: struct.updatedAt,
            isOutdated: Interval.fromDateTimes(DateTime.fromISO(struct.updatedAt), today).length('months') > 6,
          };
        } else {
          this.logger.debug('getAdminStructuresList - validation succeed');
          return null;
@@ -141,6 +179,7 @@ export class AdminController {
  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @Delete('user/:id')
  @ApiBearerAuth('JWT')
  @ApiParam({ name: 'id', type: String, required: true })
  public async deleteUser(@Param() params) {
    const user = await this.usersService.deleteOneId(params.id);
@@ -156,6 +195,7 @@ export class AdminController {

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @ApiBearerAuth('JWT')
  @Post('searchUsers')
  public async searchUsers(@Body() searchString: { searchString: string }) {
    if (searchString && searchString.searchString && searchString.searchString.length > 0)
@@ -165,14 +205,28 @@ export class AdminController {

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @Get('getUnAttachedUsers')
  @ApiBearerAuth('JWT')
  @Get('unAttachedUsers')
  public async findUnattachedUsers() {
    return this.usersService.findAllUnattached();
    return this.usersService.findAllUnattached().then((formatUsers) => {
      return formatUsers.map((user) => {
        return {
          id: user._id,
          surname: user.surname,
          name: user.name,
          email: user.email,
          phone: user.phone,
          job: user.job,
          employer: user.employer,
        };
      });
    });
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @Get('getAttachedUsers')
  @ApiBearerAuth('JWT')
  @Get('attachedUsers')
  public async findAttachedUsers() {
    return this.usersService.findAllAttached().then(async (users: IUser[]) => {
      return this.structuresService.getAllUserCompletedStructures(users);
@@ -180,8 +234,9 @@ export class AdminController {
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @ApiBearerAuth('JWT')
  @Roles('admin')
  @Get('getUnVerifiedUsers')
  @Get('unVerifiedUsers')
  public async findUnVerifiedUsers() {
    return this.usersService.findAllUnVerified().then(async (users: IUser[]) => {
      return this.structuresService.getAllUserCompletedStructures(users);
@@ -190,6 +245,7 @@ export class AdminController {

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @ApiBearerAuth('JWT')
  @Post('searchNewsletterSubscriptions')
  public async getNewsletterSubscriptions(@Body() searchString: { searchString: string }) {
    if (searchString && searchString.searchString && searchString.searchString.length > 0)
@@ -199,6 +255,7 @@ export class AdminController {

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @ApiBearerAuth('JWT')
  @Get('countNewsletterSubscriptions')
  public async countNewsletterSubscriptions(): Promise<number> {
    return this.newsletterService.countNewsletterSubscriptions();
@@ -206,9 +263,60 @@ export class AdminController {

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @ApiBearerAuth('JWT')
  @Delete('newsletterSubscription/:email')
  @ApiParam({ name: 'email', type: String, required: true })
  public async unsubscribeUserFromNewsletter(@Param() params): Promise<NewsletterSubscription> {
    return this.newsletterService.newsletterUnsubscribe(params.email);
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @ApiBearerAuth('JWT')
  @ApiOperation({ description: 'Set user job' })
  @ApiResponse({ status: HttpStatus.OK, description: 'Return user profile' })
  @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'User does not exist' })
  @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Job does not exist' })
  @Put('setUserJob')
  public async setUserJob(@Body() setUserJob: SetUserJobDto): Promise<IUser> {
    this.logger.debug(`setUserJob`);
    const jobDocument = await this.jobsService.findOne(setUserJob.jobId);
    if (!jobDocument) {
      this.logger.warn(`Job does not exist: ${setUserJob.jobId}`);
      throw new HttpException('Job does not exist', HttpStatus.BAD_REQUEST);
    }

    const userDocument = await this.usersService.findById(setUserJob.userId);
    if (!userDocument) {
      this.logger.warn(`User does not exist: ${setUserJob.userId}`);
      throw new HttpException('User does not exist', HttpStatus.BAD_REQUEST);
    }
    await this.usersService.updateUserJob(userDocument._id, jobDocument);
    return this.usersService.findById(setUserJob.userId);
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @ApiBearerAuth('JWT')
  @ApiOperation({ description: 'Set user employer' })
  @ApiResponse({ status: HttpStatus.OK, description: 'Return user profile' })
  @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'User does not exist' })
  @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Employer does not exist' })
  @Put('setUserEmployer')
  public async setUserEmployer(@Body() setUserEmployer: SetUserEmployerDto): Promise<IUser> {
    this.logger.debug(`setUserEmployer`);
    const employerDocument = await this.employerService.findOne(setUserEmployer.employerId);
    if (!employerDocument) {
      this.logger.warn(`Employer does not exist: ${setUserEmployer.employerId}`);
      throw new HttpException('Employer does not exist', HttpStatus.BAD_REQUEST);
    }

    const userDocument = await this.usersService.findById(setUserEmployer.userId);
    if (!userDocument) {
      this.logger.warn(`User does not exist: ${setUserEmployer.userId}`);
      throw new HttpException('User does not exist', HttpStatus.BAD_REQUEST);
    }
    await this.usersService.updateUserEmployer(userDocument._id, employerDocument);
    return this.usersService.findById(setUserEmployer.userId);
  }
}
+12 −0
Original line number Diff line number Diff line
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';

export class MergeEmployerDto {
  @IsNotEmpty()
  @ApiProperty({ type: String })
  sourceEmployerId: string;

  @IsNotEmpty()
  @ApiProperty({ type: String })
  targetEmployerId: string;
}
+12 −0
Original line number Diff line number Diff line
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';

export class MergeJobDto {
  @IsNotEmpty()
  @ApiProperty({ type: String })
  sourceJobId: string;

  @IsNotEmpty()
  @ApiProperty({ type: String })
  targetJobId: string;
}
Original line number Diff line number Diff line
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsMongoId, IsNotEmpty, IsString } from 'class-validator';
import { IsDate, IsEmail, IsMongoId, IsNotEmpty, IsString } from 'class-validator';

export class PendingStructureDto {
  @IsNotEmpty()
@@ -16,4 +16,9 @@ export class PendingStructureDto {
  @IsString()
  @ApiProperty({ type: String })
  structureName: string;

  @IsNotEmpty()
  @IsDate()
  @ApiProperty({ type: String })
  updatedAt: string;
}
+12 −0
Original line number Diff line number Diff line
import { IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class SetUserEmployerDto {
  @IsNotEmpty()
  @ApiProperty({ type: String })
  userId: string;

  @IsNotEmpty()
  @ApiProperty({ type: String })
  employerId: string;
}
+12 −0
Original line number Diff line number Diff line
import { IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class SetUserJobDto {
  @IsNotEmpty()
  @ApiProperty({ type: String })
  userId: string;

  @IsNotEmpty()
  @ApiProperty({ type: String })
  jobId: string;
}
Original line number Diff line number Diff line
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
import { IsDate, IsNotEmpty, IsString } from 'class-validator';

export class UnclaimedStructureDto {
  @IsNotEmpty()
@@ -11,4 +11,9 @@ export class UnclaimedStructureDto {
  @IsString()
  @ApiProperty({ type: String })
  structureName: string;

  @IsNotEmpty()
  @IsDate()
  @ApiProperty({ type: String })
  updatedAt: string;
}
Original line number Diff line number Diff line
@@ -61,6 +61,14 @@ export const config = {
      ejs: 'structureDeletionNotification.ejs',
      json: 'structureDeletionNotification.json',
    },
    adminJobCreate: {
      ejs: 'adminJobCreate.ejs',
      json: 'adminJobCreate.json',
    },
    adminEmployerCreate: {
      ejs: 'adminEmployerCreate.ejs',
      json: 'adminEmployerCreate.json',
    },
    contactMessage: {
      ejs: 'contactMessage.ejs',
      json: 'contactMessage.json',
Original line number Diff line number Diff line
Bonjour,<br />
<br />
Un nouvel employeur <%= employerName %> vient d'être créé,
<a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/admin>"
  >cliquez ici pour le valider.</a
>
Original line number Diff line number Diff line
Bonjour,<br />
<br />
Une nouvelle fonction <%= jobName %> vient d'être créée,
<a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/admin>"
  >cliquez ici pour la valider.</a
>
Original line number Diff line number Diff line
import { ConfigurationService } from '../configuration/configuration.service';
import { Page } from '../pages/schemas/page.schema';
import { Post } from '../posts/schemas/post.schema';
import { UserRole } from '../users/enum/user-role.enum';
import { User } from '../users/schemas/user.schema';

export function rewriteGhostImgUrl(configService: ConfigurationService, itemData: Page | Post): Page | Post {
  // Handle image display. Rewrite image URL to fit ghost infra issue.
@@ -15,3 +17,7 @@ export function rewriteGhostImgUrl(configService: ConfigurationService, itemData
  }
  return itemData;
}

export function hasAdminRole(user: User): boolean {
  return user.role === UserRole.admin;
}
Original line number Diff line number Diff line
@@ -240,7 +240,11 @@ export class StructuresService {
    await Promise.all(
      structures.map(async (structure: StructureDocument) => {
        if (!(await this.userService.isStructureClaimed(structure.id))) {
          unclaimedStructures.push({ structureId: structure.id, structureName: structure.structureName });
          unclaimedStructures.push({
            structureId: structure.id,
            structureName: structure.structureName,
            updatedAt: structure.updatedAt,
          });
        }
      })
    );
@@ -778,6 +782,8 @@ export class StructuresService {
          name: user.name,
          email: user.email,
          phone: user.phone,
          job: user.job,
          employer: user.employer,
          structures: await Promise.all(
            user.structuresLink.map(async (id) => {
              return this.findOne(id.toHexString());
Original line number Diff line number Diff line
import { HttpModule } from '@nestjs/common';
import { HttpModule, HttpStatus } from '@nestjs/common';
import { getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { Types } from 'mongoose';
import { UsersServiceMock } from '../../../test/mock/services/user.mock.service';
import { ConfigurationModule } from '../../configuration/configuration.module';
import { CreateEmployerDto } from '../dto/create-employer.dto';
import { Employer } from '../schemas/employer.schema';
import { EmployerService } from '../services/employer.service';
import { UsersService } from '../services/users.service';
import { EmployerController } from './employer.controller';

describe('EmployerController', () => {
  let controller: EmployerController;
  let userService: UsersService;

  const employerServiceMock = {
    findAll: jest.fn(),
@@ -18,12 +20,24 @@ describe('EmployerController', () => {
    create: jest.fn(),
    deleteByName: jest.fn(),
    initEmployerIndex: jest.fn(),
    findOne: jest.fn(),
    deleteOneId: jest.fn(),
    findAllValidated: jest.fn(),
    findAllUnvalidated: jest.fn(),
    validate: jest.fn(),
    update: jest.fn(),
    mergeEmployer: jest.fn(),
    deleteInvalidEmployer: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [ConfigurationModule, HttpModule],
      providers: [
        {
          provide: UsersService,
          useClass: UsersServiceMock,
        },
        {
          provide: EmployerService,
          useValue: employerServiceMock,
@@ -37,6 +51,7 @@ describe('EmployerController', () => {
    }).compile();

    controller = module.get<EmployerController>(EmployerController);
    userService = module.get<UsersService>(UsersService);
  });

  it('should be defined', () => {
@@ -77,89 +92,208 @@ describe('EmployerController', () => {
      expect(findAllReply.length).toBe(2);
    });
  });
  describe('createEmployer', () => {
    it('should create new employer `Metro`', async () => {
      const newEmployer: CreateEmployerDto = {
        name: 'Metro',
      };
      const newCreatedEmployer: Employer = {
        name: 'Metro',
        validated: false,
      };

  // describe('deleteEmployer', () => {
  //   it('should delete employer `Metro`', async () => {
  //     const employer: Employer = {
  //       name: 'Metro',
  //       validated: true,
  //     };
  //     const employerToRemove: CreateEmployerDto = {
  //       name: 'Metro',
  //     };
  //     employerServiceMock.findByName.mockResolvedValueOnce(employer);
  //     employerServiceMock.deleteByName.mockResolvedValueOnce({
  //       ok: 1,
  //       n: 1,
  //       deletedCount: 1,
  //     });
  //     const deleteEmployer = await controller.deleteEmployer(employerToRemove);
  //     expect(deleteEmployer.name).toBe('Metro');
  //     expect(deleteEmployer.validated).toBe(true);
  //   });

  //   it('should throw error on unexisting employer `Metro`', async () => {
  //     const employerToRemove: CreateEmployerDto = {
  //       name: 'Metro',
  //     };
  //     employerServiceMock.deleteByName.mockResolvedValueOnce(null);
  //     try {
  //       await controller.deleteEmployer(employerToRemove);
  //       expect(true).toBe(false);
  //     } catch (e) {
  //       expect(e.message).toBe('Employer does not exist');
  //       expect(e.status).toBe(404);
  //     }
  //   });
  // });
  describe('resetES', () => {
    it('should reset search index', async () => {
      employerServiceMock.initEmployerIndex.mockResolvedValueOnce([
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5a7'),
          name: 'CAF',
          validated: true,
        },
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5a7'),
          name: 'CARSAT',
          validated: true,
        },
      ]);
      const index = await controller.resetES();
      expect(index.length).toBe(2);
    });
  });

  describe('Create Employer', () => {
    it('should create a employer', async () => {
      employerServiceMock.findByName.mockResolvedValueOnce(null);
      employerServiceMock.create.mockResolvedValueOnce(newCreatedEmployer);
      const createReply = await controller.createEmployer(newEmployer);
      expect(createReply).toEqual(newCreatedEmployer);
      employerServiceMock.create.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Sopra',
        validated: true,
      });
    it('should throw error on already existing employer `Metro`', async () => {
      const newEmployer: CreateEmployerDto = {
        name: 'Metro',
      };
      const req = { user: { _id: '6036721022462b001334c4bb', role: 0 } };
      const reply = await controller.createEmployer({ name: 'Sopra' }, req);
      expect(reply).toBeTruthy();
    });
    it('should not create if employer already exists', async () => {
      employerServiceMock.findByName.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5a7'),
        name: 'Metro',
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Sopra',
        validated: true,
      });
      const req = { user: { _id: '6036721022462b001334c4bb' }, role: 0 };
      try {
        await controller.createEmployer(newEmployer);
        expect(true).toBe(false);
        await controller.createEmployer({ name: 'Sopra' }, req);
        expect;
      } catch (e) {
        expect(e.message).toBe('Employer already exist');
        expect(e.status).toBe(422);
        expect(e.status).toBe(HttpStatus.UNPROCESSABLE_ENTITY);
      }
    });
  });
    it('should call create with send notification to true if admin', async () => {
      employerServiceMock.findByName.mockResolvedValueOnce(null);
      const employer = {
        name: 'Sopra',
      };
      employerServiceMock.create.mockResolvedValueOnce({ ...employer, validated: true });
      const req = { user: { _id: '6036721022462b001334c4bb', role: 1 } };

  describe('deleteEmployer', () => {
    it('should delete employer `Metro`', async () => {
      const employer: Employer = {
      const reply = await controller.createEmployer(employer, req);
      expect(reply).toBeTruthy();
      expect(employerServiceMock.create).toHaveBeenCalledWith(employer, true, false);
    });
  });
  describe('Validate Employer', () => {
    it('should validate an employer', async () => {
      employerServiceMock.validate.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Sopra',
        validated: true,
      });
      expect(await controller.validateEmployer({ id: '6231aefe76598527c8d0b5bc' })).toBeTruthy();
    });
  });
  describe('Get Employers', () => {
    it('should call all validated employers and populate them with users attached to it', async () => {
      const spyer = jest.spyOn(userService, 'populateEmployerswithUsers');
      employerServiceMock.findAllValidated.mockResolvedValueOnce([
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
          name: 'Metro',
          validated: true,
      };
      const employerToRemove: CreateEmployerDto = {
        },
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
          name: 'Sopra',
          validated: true,
        },
      ]);
      await controller.findValidatedEmployers();
      expect(employerServiceMock.findAllValidated.mock.calls.length).toBe(1);
      expect(spyer.mock.calls.length).toBe(1);
    });

    it('should call all unvalidated employers and populate them with users attached to it', async () => {
      const spyer = jest.spyOn(userService, 'populateEmployerswithUsers');
      employerServiceMock.findAllUnvalidated.mockResolvedValueOnce([
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
          name: 'Metro',
      };
      employerServiceMock.findByName.mockResolvedValueOnce(employer);
      employerServiceMock.deleteByName.mockResolvedValueOnce({
          validated: false,
        },
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
          name: 'Sopra',
          validated: false,
        },
      ]);
      await controller.findUnvalidatedEmployers();
      expect(employerServiceMock.findAllUnvalidated.mock.calls.length).toBe(1);
      expect(spyer.mock.calls.length).toBe(1);
    });
  });
  describe('Edit Employer', () => {
    it('should update employer', async () => {
      employerServiceMock.update.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'SopraMod',
        validated: true,
      });
      expect(await controller.updateEmployer('6231aefe76598527c8d0b5bc', { name: 'SopraMod' })).toBeTruthy();
    });

    it('should delete an unvalidated employer and replace all its occurence with a chosen validated employer', async () => {
      employerServiceMock.mergeEmployer.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Sopra',
        validated: true,
      });
      employerServiceMock.deleteInvalidEmployer.mockResolvedValueOnce({
        n: 1,
        ok: 1,
        deletedCount: 1,
      });

      const reply = await controller.mergeEmployer({
        sourceEmployerId: '6231aefe76598527c8d0b5ba',
        targetEmployerId: '6231aefe76598527c8d0b5bc',
      });
      expect(reply).toBeTruthy();
    });
  });
  describe('Delete Employer', () => {
    it('should delete employer', async () => {
      employerServiceMock.deleteOneId.mockResolvedValueOnce({
        n: 1,
        ok: 1,
        deletedCount: 1,
      });
      const deleteEmployer = await controller.deleteEmployer(employerToRemove);
      expect(deleteEmployer.name).toBe('Metro');
      expect(deleteEmployer.validated).toBe(true);

      employerServiceMock.findOne.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Sopra',
        validated: true,
      });
      const reply = await controller.deleteEmployer({ id: '6231aefe76598527c8d0b5ba' });
      expect(reply).toBeTruthy();
    });

    it('should throw error on unexisting employer `Metro`', async () => {
      const employerToRemove: CreateEmployerDto = {
        name: 'Metro',
      };
      employerServiceMock.deleteByName.mockResolvedValueOnce(null);
    it('should not delete employer if a user is linked', async () => {
      employerServiceMock.findOne.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'Sopra',
        validated: true,
      });
      try {
        await controller.deleteEmployer(employerToRemove);
        await controller.deleteEmployer({ id: '6231aefe76598527c8d0b5bc' });
        expect(true).toBe(false);
      } catch (e) {
        expect(e.message).toBe('Employer does not exist');
        expect(e.status).toBe(404);
        expect(e.message).toEqual('Cannot delete employer. It has user(s) attached to it.');
        expect(e.status).toEqual(HttpStatus.FORBIDDEN);
      }
    });
  });
  describe('resetES', () => {
    it('should reset search index', async () => {
      employerServiceMock.initEmployerIndex.mockResolvedValueOnce([
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5a7'),
          name: 'CAF',
          validated: true,
        },
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5a7'),
          name: 'CARSAT',
          validated: true,
        },
      ]);
      const index = await controller.resetES();
      expect(index.length).toBe(2);
    });
  });
});
Original line number Diff line number Diff line
@@ -6,23 +6,30 @@ import {
  HttpException,
  HttpStatus,
  Logger,
  Param,
  Post,
  Put,
  Query,
  Request,
  UseGuards,
} from '@nestjs/common';
import { ApiParam } from '@nestjs/swagger';
import { ApiBearerAuth, ApiOperation, ApiParam } from '@nestjs/swagger';
import { MergeEmployerDto } from '../../admin/dto/merge-employer.dto';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { hasAdminRole } from '../../shared/utils';
import { Roles } from '../decorators/roles.decorator';
import { CreateEmployerDto } from '../dto/create-employer.dto';
import { RolesGuard } from '../guards/roles.guard';
import { IEmployer } from '../interfaces/employer.interface';
import { Employer } from '../schemas/employer.schema';
import { EmployerService } from '../services/employer.service';
import { UsersService } from '../services/users.service';

@Controller('employer')
export class EmployerController {
  private readonly logger = new Logger(EmployerController.name);

  constructor(private employerService: EmployerService) {}
  constructor(private employerService: EmployerService, private usersService: UsersService) {}

  /**
   * Find all employer. If search is given as param, filter on it. Otherwise return everything
@@ -45,47 +52,111 @@ export class EmployerController {
   * @returns {Employer}
   */
  @Post()
  @UseGuards(JwtAuthGuard)
  @ApiBearerAuth('JWT')
  @ApiParam({ name: 'newEmployer', type: CreateEmployerDto, required: true })
  public async createEmployer(@Body() newEmployer: CreateEmployerDto): Promise<Employer> {
  public async createEmployer(@Body() newEmployer: CreateEmployerDto, @Request() req): Promise<Employer> {
    this.logger.debug(`createEmployer: ${newEmployer.name}`);
    const existingEmployer = await this.employerService.findByName(newEmployer.name);
    if (existingEmployer) {
      this.logger.warn(`Employer already exist: ${newEmployer.name}`);
      throw new HttpException('Employer already exist', HttpStatus.UNPROCESSABLE_ENTITY);
    }
    // if user is admin, do not send notification
    if (hasAdminRole(req.user)) {
      return this.employerService.create(newEmployer, true, false);
    }
    return this.employerService.create(newEmployer);
  }

  // SEARCH

  /**
   * Delete Employer if exist
   * @param employer {CreateEmployerDto} - Employer to delete
   * @returns {Employer}
   * Init or reset search index for employere
   * @returns {Employer[]}
   */
  @Delete()
  @Post('searchIndex')
  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @ApiParam({ name: 'employer', type: CreateEmployerDto, required: true })
  public async deleteEmployer(@Body() employer: CreateEmployerDto): Promise<Employer> {
    this.logger.debug(`deleteEmployer: ${employer.name}`);
    const existingEmployer = await this.employerService.findByName(employer.name);
    if (!existingEmployer) {
      this.logger.warn(`Employer does not exist: ${employer.name}`);
      throw new HttpException('Employer does not exist', HttpStatus.NOT_FOUND);
  public async resetES(): Promise<Employer[]> {
    return this.employerService.initEmployerIndex();
  }
    await this.employerService.deleteByName(employer.name);
    return existingEmployer;

  @UseGuards(JwtAuthGuard, RolesGuard)
  @ApiBearerAuth('JWT')
  @Roles('admin')
  @Get('validated')
  @ApiOperation({ description: 'Get validated employers populated with users attached to it' })
  public async findValidatedEmployers() {
    return this.employerService.findAllValidated().then(async (employers: IEmployer[]) => {
      return this.usersService.populateEmployerswithUsers(employers);
    });
  }

  // SEARCH
  @UseGuards(JwtAuthGuard, RolesGuard)
  @ApiBearerAuth('JWT')
  @Roles('admin')
  @Get('unvalidated')
  @ApiOperation({ description: 'Get unvalidated employers for validation or merge' })
  public async findUnvalidatedEmployers() {
    return this.employerService.findAllUnvalidated().then(async (employers: IEmployer[]) => {
      return this.usersService.populateEmployerswithUsers(employers);
    });
  }

  /*
   ** All users attached to this employer will be affected to the targeted employer
   ** The original unvalidated employer will be deleted
   */
  @UseGuards(JwtAuthGuard, RolesGuard)
  @ApiBearerAuth('JWT')
  @Roles('admin')
  @Put('merge')
  public async mergeEmployer(@Body() mergeEmployer: MergeEmployerDto) {
    return this.employerService.mergeEmployer(mergeEmployer);
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @ApiBearerAuth('JWT')
  @Roles('admin')
  @Post('validate/:id')
  @ApiParam({ name: 'id', type: String, required: true })
  @ApiOperation({ description: 'Validate employer' })
  public async validateEmployer(@Param() params) {
    return this.employerService.validate(params.id);
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @ApiBearerAuth('JWT')
  @Roles('admin')
  @Put(':id')
  public async updateEmployer(@Param('id') id: string, @Body() body: CreateEmployerDto) {
    return this.employerService.update(id, body);
  }

  /**
   * Init or reset search index for employere
   * @returns {Employer[]}
   * Delete Employer if exist
   * @param employer {CreateEmployerDto} - Employer to delete
   * @returns {Employer}
   */
  @Post('searchIndex')
  @Delete(':id')
  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  public async resetES(): Promise<Employer[]> {
    return this.employerService.initEmployerIndex();
  @ApiParam({ name: 'employer', type: CreateEmployerDto, required: true })
  public async deleteEmployer(@Param() params): Promise<Employer> {
    this.logger.debug(`deleteEmployer: ${params.id}`);
    // look for employer
    const researchedEmployer = await this.employerService.findOne(params.id);
    // look for any relations within user collection, reject action if found
    if (researchedEmployer !== null) {
      const isEmployerLinked = await this.usersService.isEmployerLinkedtoUser(researchedEmployer._id);
      if (!isEmployerLinked) {
        return this.employerService.deleteOneId(params.id);
      } else {
        throw new HttpException('Cannot delete employer. It has user(s) attached to it.', HttpStatus.FORBIDDEN);
      }
    } else {
      throw new HttpException('Employer does not exists', HttpStatus.NOT_FOUND);
    }
  }
}
Original line number Diff line number Diff line
import { HttpModule } from '@nestjs/common';
import { HttpModule, HttpStatus } from '@nestjs/common';
import { getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { Types } from 'mongoose';
import { ConfigurationModule } from '../../configuration/configuration.module';
import { CreateJobDto } from '../dto/create-job.dto';
import { Job } from '../schemas/job.schema';
import { User } from '../schemas/user.schema';
import { JobsService } from '../services/jobs.service';
import { UsersService } from '../services/users.service';
import { JobsController } from './jobs.controller';

describe('JobsController', () => {
@@ -15,6 +17,19 @@ describe('JobsController', () => {
    findAll: jest.fn(),
    findByName: jest.fn(),
    create: jest.fn(),
    findOne: jest.fn(),
    deleteOneId: jest.fn(),
    findAllUnvalidated: jest.fn(),
    createJobFromAdmin: jest.fn(),
    validate: jest.fn(),
    update: jest.fn(),
    mergeJob: jest.fn(),
    deleteInvalidJob: jest.fn(),
  };

  const userServiceMock = {
    populateJobswithUsers: jest.fn(),
    isJobLinkedtoUser: jest.fn(),
  };

  beforeEach(async () => {
@@ -25,10 +40,18 @@ describe('JobsController', () => {
          provide: JobsService,
          useValue: jobServiceMock,
        },
        {
          provide: UsersService,
          useValue: userServiceMock,
        },
        {
          provide: getModelToken('Job'),
          useValue: Job,
        },
        {
          provide: getModelToken('User'),
          useValue: User,
        },
      ],
      controllers: [JobsController],
    }).compile();
@@ -65,6 +88,7 @@ describe('JobsController', () => {
    it('should create job `Dev`', async () => {
      const newJob: CreateJobDto = {
        name: 'Dev',
        hasPersonalOffer: false,
      };
      const newCreatedJob: Job = {
        name: 'Dev',
@@ -73,12 +97,14 @@ describe('JobsController', () => {
      };
      jobServiceMock.findByName.mockResolvedValueOnce(null);
      jobServiceMock.create.mockResolvedValueOnce(newCreatedJob);
      const createReply = await controller.createJob(newJob);
      const req = { user: { _id: '6036721022462b001334c4bb' } };
      const createReply = await controller.createJob(req, newJob);
      expect(createReply).toEqual(newCreatedJob);
    });
    it('should throw error on already existing job `Dev`', async () => {
      const newJob: CreateJobDto = {
        name: 'Dev',
        hasPersonalOffer: false,
      };
      jobServiceMock.findByName.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5a7'),
@@ -87,7 +113,8 @@ describe('JobsController', () => {
        hasPersonalOffer: false,
      });
      try {
        await controller.createJob(newJob);
        const req = { user: { _id: '6036721022462b001334c4bb' } };
        await controller.createJob(req, newJob);
        expect(true).toBe(false);
      } catch (e) {
        expect(e.message).toBe('Job already exist');
@@ -95,4 +122,159 @@ describe('JobsController', () => {
      }
    });
  });

  describe('Create Job', () => {
    it('should create a job', async () => {
      jobServiceMock.findByName.mockResolvedValueOnce(null);
      jobServiceMock.create.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Dev',
        validated: true,
      });
      const req = { user: { _id: '6036721022462b001334c4bb', role: 0 } };
      const reply = await controller.createJob(req, { name: 'Dev', hasPersonalOffer: true });
      expect(reply).toBeTruthy();
    });
    it('should not create if job already exists', async () => {
      jobServiceMock.findByName.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Dev',
        validated: true,
      });
      const req = { user: { _id: '6036721022462b001334c4bb', role: 0 } };
      try {
        await controller.createJob(req, { name: 'Dev', hasPersonalOffer: true });
      } catch (e) {
        expect(e.message).toBe('Job already exist');
        expect(e.status).toBe(HttpStatus.UNPROCESSABLE_ENTITY);
      }
    });
    it('should call create with send notification to true if admin', async () => {
      jobServiceMock.findByName.mockResolvedValueOnce(null);
      jobServiceMock.create.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Dev',
        validated: true,
      });
      const req = { user: { _id: '6036721022462b001334c4bb', role: 1 } };
      const job = { name: 'Dev', hasPersonalOffer: true };
      const reply = await controller.createJob(req, job);
      expect(reply).toBeTruthy();
      expect(jobServiceMock.create).toHaveBeenCalledWith(job, true, true, false);
    });
  });
  describe('Validate Job', () => {
    it('should validate a given job', async () => {
      jobServiceMock.validate.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Dev',
        validated: true,
      });
      expect(await controller.validateJob({ id: '6231aefe76598527c8d0b5bc' })).toBeTruthy();
    });
  });
  describe('Get Jobs', () => {
    it('should call all validated jobs and populate them with users attached to it', async () => {
      const spyer = jest.spyOn(userServiceMock, 'populateJobswithUsers');
      jobServiceMock.findAll.mockResolvedValueOnce([
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
          name: 'Dev',
          validated: true,
        },
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
          name: 'Scrum',
          validated: true,
        },
      ]);
      await controller.findValidatedJobs();
      expect(jobServiceMock.findAll.mock.calls.length).toBe(2);
      expect(spyer.mock.calls.length).toBe(1);
    });

    it('should call all unvalidated jobs and populate them with users attached to it', async () => {
      const spyer = jest.spyOn(userServiceMock, 'populateJobswithUsers');
      jobServiceMock.findAllUnvalidated.mockResolvedValueOnce([
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
          name: 'Dev',
          validated: false,
        },
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
          name: 'Scrum',
          validated: false,
        },
      ]);
      await controller.findUnvalidatedJobs();
      expect(jobServiceMock.findAllUnvalidated.mock.calls.length).toBe(1);
      expect(spyer.mock.calls.length).toBe(2);
    });
  });
  describe('Edit Job', () => {
    it('should update job', async () => {
      jobServiceMock.update.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'DevMod',
        validated: true,
      });
      expect(
        await controller.updateJob('6231aefe76598527c8d0b5bc', { name: 'DevMod', hasPersonalOffer: false })
      ).toBeTruthy();
    });

    it('should delete an unvalidated job and replace all its occurence with a chosen validated job', async () => {
      jobServiceMock.mergeJob.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Dev',
        validated: true,
      });
      jobServiceMock.deleteInvalidJob.mockResolvedValueOnce({
        n: 1,
        ok: 1,
        deletedCount: 1,
      });

      const reply = await controller.mergeJob({
        sourceJobId: '6231aefe76598527c8d0b5ba',
        targetJobId: '6231aefe76598527c8d0b5bc',
      });
      expect(reply).toBeTruthy();
    });
  });
  describe('Delete Job', () => {
    jobServiceMock.deleteOneId.mockResolvedValueOnce({
      n: 1,
      ok: 1,
      deletedCount: 1,
    });

    it('should delete job', async () => {
      jobServiceMock.findOne.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5ba'),
        name: 'Dev',
        validated: true,
      });
      userServiceMock.isJobLinkedtoUser.mockResolvedValueOnce(false);
      const reply = await controller.deleteJob({ id: '6231aefe76598527c8d0b5ba' });
      expect(reply).toBeTruthy();
    });

    it('should not delete job if a user is linked', async () => {
      jobServiceMock.findOne.mockResolvedValueOnce({
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'Dev',
        validated: true,
      });
      userServiceMock.isJobLinkedtoUser.mockResolvedValueOnce(true);
      try {
        await controller.deleteJob({ id: '6231aefe76598527c8d0b5bc' });
        expect(true).toBe(false);
      } catch (e) {
        expect(e.message).toEqual('Cannot delete job. It has user(s) attached to it.');
        expect(e.status).toEqual(HttpStatus.FORBIDDEN);
      }
    });
  });
});
Original line number Diff line number Diff line
import { Body, Controller, Get, HttpException, HttpStatus, Logger, Post } from '@nestjs/common';
import { ApiParam } from '@nestjs/swagger';
import { CreateEmployerDto } from '../dto/create-employer.dto';
import {
  Body,
  Controller,
  Delete,
  Get,
  HttpException,
  HttpStatus,
  Logger,
  Param,
  Post,
  Put,
  Request,
  UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiParam } from '@nestjs/swagger';
import { MergeJobDto } from '../../admin/dto/merge-job.dto';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { hasAdminRole } from '../../shared/utils';
import { Roles } from '../decorators/roles.decorator';
import { CreateJobDto } from '../dto/create-job.dto';
import { RolesGuard } from '../guards/roles.guard';
import { IJob } from '../interfaces/job.interface';
import { Job } from '../schemas/job.schema';
import { JobsService } from '../services/jobs.service';
import { UsersService } from '../services/users.service';

@Controller('jobs')
export class JobsController {
  private readonly logger = new Logger(JobsController.name);

  constructor(private jobsService: JobsService) {}
  constructor(private jobsService: JobsService, private usersService: UsersService) {}

  /**
   * Return every jobs
@@ -22,18 +41,95 @@ export class JobsController {

  /**
   * Create a new job
   * @param job {CreateEmployerDto}
   * @param job {CreateJobDto}
   * @returns {Job}
   */
  @Post()
  @UseGuards(JwtAuthGuard)
  @ApiBearerAuth('JWT')
  @ApiParam({ name: 'job', type: CreateJobDto, required: true })
  public async createJob(@Body() job: CreateEmployerDto): Promise<Job> {
  public async createJob(@Request() req, @Body() job: CreateJobDto): Promise<Job> {
    this.logger.debug(`createJob: ${job.name}`);
    const existingJob = await this.jobsService.findByName(job.name);
    if (existingJob) {
      this.logger.warn(`Job already exist: ${job.name}`);
      throw new HttpException('Job already exist', HttpStatus.UNPROCESSABLE_ENTITY);
    }
    // if user is admin, do not send notification
    if (hasAdminRole(req.user)) {
      return this.jobsService.create(job, true, job.hasPersonalOffer, false);
    }
    return this.jobsService.create(job);
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @ApiBearerAuth('JWT')
  @Roles('admin')
  @Get('validated')
  @ApiOperation({ description: 'Get validated jobs populated with users attached to it' })
  public async findValidatedJobs() {
    return this.jobsService.findAll().then(async (jobs: IJob[]) => {
      return this.usersService.populateJobswithUsers(jobs);
    });
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @ApiBearerAuth('JWT')
  @Roles('admin')
  @Get('unvalidated')
  @ApiOperation({ description: 'Get unvalidated jobs populated with users attached to it' })
  public async findUnvalidatedJobs() {
    return this.jobsService.findAllUnvalidated().then(async (jobs: IJob[]) => {
      return this.usersService.populateJobswithUsers(jobs);
    });
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @ApiBearerAuth('JWT')
  @Roles('admin')
  @Post('validate/:id')
  @ApiParam({ name: 'id', type: String, required: true })
  @ApiOperation({ description: 'Validate job' })
  public async validateJob(@Param() params) {
    return this.jobsService.validate(params.id);
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @ApiBearerAuth('JWT')
  @Roles('admin')
  @Put('merge')
  public async mergeJob(@Body() mergeJob: MergeJobDto) {
    this.logger.debug('mergeJob');
    return this.jobsService.mergeJob(mergeJob);
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @ApiBearerAuth('JWT')
  @Roles('admin')
  @Put(':id')
  public async updateJob(@Param('id') id: string, @Body() body: CreateJobDto) {
    this.logger.debug('updateJob');
    return this.jobsService.update(id, body);
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @Delete(':id')
  @ApiBearerAuth('JWT')
  @ApiParam({ name: 'id', type: String, required: true })
  public async deleteJob(@Param() params) {
    // look for job
    const researchedJob = await this.jobsService.findOne(params.id);
    // look for any relations within user collection, reject action if found
    if (researchedJob !== null) {
      const isJobLinked = await this.usersService.isJobLinkedtoUser(researchedJob._id);
      if (!isJobLinked) {
        return this.jobsService.deleteOneId(params.id);
      } else {
        throw new HttpException('Cannot delete job. It has user(s) attached to it.', HttpStatus.FORBIDDEN);
      }
    } else {
      throw new HttpException('Job does not exists', HttpStatus.NOT_FOUND);
    }
  }
}
Original line number Diff line number Diff line
import { IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';

export class CreateEmployerDto {
  @ApiProperty({ type: String, example: 'PIMMS Vaise' })
Original line number Diff line number Diff line
import { IsNotEmpty, IsString } from 'class-validator';
import { IsBoolean, IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class CreateJobDto {
@@ -6,4 +6,8 @@ export class CreateJobDto {
  @IsNotEmpty()
  @IsString()
  readonly name: string;
  @ApiProperty({ type: Boolean, example: false })
  @IsNotEmpty()
  @IsBoolean()
  readonly hasPersonalOffer: boolean;
}
Original line number Diff line number Diff line
import { Document } from 'mongoose';
import { Employer } from '../schemas/employer.schema';

export type IEmployer = Employer & Document;
+4 −0
Original line number Diff line number Diff line
import { Document } from 'mongoose';
import { Job } from '../schemas/job.schema';

export type IJob = Job & Document;
Original line number Diff line number Diff line
import { HttpModule, HttpStatus } from '@nestjs/common';
import { getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { AxiosResponse } from 'axios';
import { Types } from 'mongoose';
import { ConfigurationModule } from '../../configuration/configuration.module';
import { MailerService } from '../../mailer/mailer.service';
import { CreateEmployerDto } from '../dto/create-employer.dto';
import { EmployerDocument } from '../schemas/employer.schema';
import { User } from '../schemas/user.schema';
import { EmployerSearchService } from './employer-search.service';
import { EmployerService } from './employer.service';
import { UsersService } from './users.service';

describe('EmployerService', () => {
  let service: EmployerService;
  let mailer: MailerService;

  const mockEmployerSearchService = {
    indexEmployer: jest.fn(),
@@ -20,13 +26,18 @@ describe('EmployerService', () => {
  const mockEmployerModel = {
    create: jest.fn(),
    findOne: jest.fn(),
    findById: jest.fn(),
    deleteOne: jest.fn(),
    find: jest.fn(() => mockEmployerModel),
    sort: jest.fn(() => mockEmployerModel),
  };
  const mockUserService = {
    replaceEmployers: jest.fn(),
    getAdmins: jest.fn(),
  };
  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [ConfigurationModule],
      imports: [ConfigurationModule, HttpModule],
      providers: [
        EmployerService,
        {
@@ -37,9 +48,19 @@ describe('EmployerService', () => {
          provide: getModelToken('Employer'),
          useValue: mockEmployerModel,
        },
        {
          provide: UsersService,
          useValue: mockUserService,
        },
        {
          provide: getModelToken('User'),
          useValue: User,
        },
        MailerService,
      ],
    }).compile();
    service = module.get<EmployerService>(EmployerService);
    mailer = module.get<MailerService>(MailerService);
  });

  it('should be defined', () => {
@@ -62,6 +83,67 @@ describe('EmployerService', () => {
    const reply = await service.findAll();
    expect(reply.length).toBe(2);
  });
  it('findOne', async () => {
    mockEmployerModel.findById.mockResolvedValueOnce({
      _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
      name: 'Sopra',
      validated: true,
    });
    const reply = await service.findOne('6231aefe76598527c8d0b5bc');
    expect(reply).toBeTruthy();
  });
  it('findAllValidated', async () => {
    mockEmployerModel.find.mockResolvedValueOnce([
      {
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'Sopra',
        validated: true,
      },
    ]);
    const reply = await service.findAllValidated();
    expect(reply.length).toBe(1);
  });

  it('findAllUnvalidated', async () => {
    mockEmployerModel.find.mockResolvedValueOnce([
      {
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'Sopra',
        validated: false,
      },
    ]);
    const reply = await service.findAllValidated();
    expect(reply.length).toBe(1);
  });

  it('finds all unvalidated employers', async () => {
    mockEmployerModel.find.mockResolvedValueOnce([
      {
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'Metro',
        validated: false,
      },
    ]);
    const reply = await service.findAllUnvalidated();
    expect(reply[0].validated).toBeFalsy;
  });

  it('finds all validated employers', async () => {
    mockEmployerModel.find.mockResolvedValueOnce([
      {
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'Metro',
        validated: true,
      },
      {
        _id: Types.ObjectId('6231aefe76598527c8d0b5b2'),
        name: 'Sopra',
        validated: true,
      },
    ]);
    const reply = await service.findAllUnvalidated();
    expect(reply.length).toBe(2);
  });

  it('findByName', async () => {
    mockEmployerModel.findOne.mockResolvedValue({
@@ -73,23 +155,45 @@ describe('EmployerService', () => {
    expect(reply).toBeTruthy();
  });

  describe('createEmployer', () => {
    it('create', async () => {
      mockEmployerModel.create.mockResolvedValue({
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'Sopra',
      validated: true,
        validated: false,
      });
      mockEmployerModel.findOne.mockResolvedValue({
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'Sopra',
      validated: true,
        validated: false,
      });
      const createEmployer: CreateEmployerDto = {
        name: 'Sopra',
      };
      jest.spyOn(service, 'sendAdminCreateNotification').mockResolvedValueOnce();
      const reply = await service.create(createEmployer);
      expect(reply).toBeTruthy();
      expect(service.sendAdminCreateNotification).toBeCalledTimes(1);
    });

    it('should create validated employer and not send email to admins', async () => {
      mockEmployerModel.create.mockResolvedValue({
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'Sopra',
        validated: false,
      });
    const createJob: CreateEmployerDto = {
      mockEmployerModel.findOne.mockResolvedValue({
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'Sopra',
        validated: false,
      });
      const createEmployer: CreateEmployerDto = {
        name: 'Sopra',
      };
    const reply = await service.create(createJob);
      const reply = await service.create(createEmployer, true, false);
      expect(reply).toBeTruthy();
    });
  });

  it('delete', async () => {
    mockEmployerModel.findOne.mockResolvedValueOnce({
@@ -145,4 +249,218 @@ describe('EmployerService', () => {
      expect(reply.length).toBe(2);
    });
  });

  describe('mergeEmployer', () => {
    it('should delete source employer', async () => {
      const reply = {
        _id: Types.ObjectId('623aed68c5d45b6fbbaa7e61'),
        name: 'Metro',
        validated: true,
      };
      mockEmployerModel.findById
        .mockResolvedValueOnce({
          _id: Types.ObjectId('623aed68c5d45b6fbbaa7e60'),
          name: 'Sopra',
          validated: false,
        })
        .mockResolvedValueOnce(reply);
      jest.spyOn(service, 'deleteInvalidEmployer').mockRejectedValueOnce({});
      mockUserService.replaceEmployers.mockResolvedValueOnce(null);
      expect(
        await service.mergeEmployer({
          sourceEmployerId: '623aed68c5d45b6fbbaa7e60',
          targetEmployerId: '623aed68c5d45b6fbbaa7e61',
        })
      ).toEqual(reply);
    });
    it('should delete source employer', async () => {
      mockEmployerModel.findById.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
      jest.spyOn(service, 'deleteInvalidEmployer').mockRejectedValueOnce({});
      mockUserService.replaceEmployers.mockResolvedValueOnce(null);
      try {
        await service.mergeEmployer({
          sourceEmployerId: '623aed68c5d45b6fbbaa7e60',
          targetEmployerId: '623aed68c5d45b6fbbaa7e61',
        });
      } catch (e) {
        expect(e.message).toBe('Cannot operate on employer.');
        expect(e.status).toBe(HttpStatus.NOT_FOUND);
      }
    });
  });

  describe('sendAdminCreateNotification', () => {
    it('should sendAdminCreateNotification', async () => {
      const employer = {
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'Metro',
        validated: true,
      };
      const result: AxiosResponse = {
        data: {
          status: 200,
          content: {
            success: true,
            response: [7010],
          },
        },
        status: 200,
        statusText: 'OK',
        headers: {},
        config: {},
      };
      jest.spyOn(mailer, 'send').mockResolvedValueOnce(result);
      const spyer = jest.spyOn(mailer, 'send');
      mockUserService.getAdmins.mockResolvedValueOnce([
        {
          _id: '6231aefe76598527c8d0b5bc',
          validationToken:
            'cf1c74c22cedb6b575945098db42d2f493fb759c9142c6aff7980f252886f36ee086574ee99a06bc99119079257116c959c8ec870949cebdef2b293666dbca42',
          emailVerified: true,
          email: 'admin@admin.com',
          password: '$2a$12$vLQjJ9zAWyUwiFLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu',
          role: 0,
          name: 'admin',
          surname: 'admin',
          personalOffers: [],
        },
      ]);
      await service.sendAdminCreateNotification(
        employer as EmployerDocument,
        'adminEmployerCreate.ejs',
        'adminEmployerCreate.json'
      );
      expect(spyer.mock.calls.length).toBe(1);
    });
  });

  describe('validate', () => {
    it('should validate employer', async () => {
      mockEmployerModel.findById.mockResolvedValueOnce({
        _id: Types.ObjectId('623aed68c5d45b6fbbaa7e60'),
        name: 'Sopra',
        validated: false,
        save: jest.fn().mockResolvedValueOnce(null),
      });
      const employer = await service.validate('623aed68c5d45b6fbbaa7e60');
      expect(employer.validated).toBe(true);
    });
    it('should throw exception', async () => {
      mockEmployerModel.findById.mockResolvedValueOnce(null);
      try {
        await service.validate('623aed68c5d45b6fbbaa7e60');
        expect(true).toBe(false);
      } catch (e) {
        expect(e.message).toBe('Cannot validate employer. It might have been already validate');
        expect(e.status).toBe(HttpStatus.NOT_FOUND);
      }
    });
  });

  describe('update', () => {
    it('should update employer', async () => {
      mockEmployerModel.findById.mockResolvedValueOnce({
        _id: Types.ObjectId('623aed68c5d45b6fbbaa7e60'),
        name: 'Sopraaa',
        validated: false,
        save: jest.fn().mockResolvedValueOnce(null),
      });
      const employer = await service.update('623aed68c5d45b6fbbaa7e60', { name: 'Sopra' });
      expect(employer.name).toBe('Sopra');
    });
    it('should throw exception', async () => {
      mockEmployerModel.findById.mockResolvedValueOnce(null);
      try {
        await service.update('623aed68c5d45b6fbbaa7e60', { name: 'Sopra' });
        expect(true).toBe(false);
      } catch (e) {
        expect(e.message).toBe('Cannot edit employer. It was not found in database.');
        expect(e.status).toBe(HttpStatus.NOT_FOUND);
      }
    });
  });

  describe('deleteInvalidEmployer', () => {
    it('should delete invalid employer', async () => {
      mockEmployerModel.findById.mockResolvedValueOnce({
        _id: Types.ObjectId('623aed68c5d45b6fbbaa7e60'),
        name: 'Sopra',
        validated: false,
        save: jest.fn().mockResolvedValueOnce(null),
      });
      mockEmployerSearchService.deleteIndex.mockResolvedValueOnce(null);
      mockEmployerModel.deleteOne.mockResolvedValueOnce({
        _id: Types.ObjectId('623aed68c5d45b6fbbaa7e60'),
        name: 'Sopra',
        validated: false,
        save: jest.fn().mockResolvedValueOnce(null),
      });
      await service.deleteInvalidEmployer('6231aefe76598527c8d0b5bc');
      expect(mockEmployerSearchService.deleteIndex).toBeCalled();
      expect(mockEmployerModel.deleteOne).toBeCalled();
    });
  });

  describe('deleteOneId', () => {
    it('should delete ', async () => {
      mockEmployerModel.findOne.mockResolvedValueOnce({
        _id: '6231aefe76598527c8d0b5bc',
        validationToken:
          'cf1c74c22cedb6b575945098db42d2f493fb759c9142c6aff7980f252886f36ee086574ee99a06bc99119079257116c959c8ec870949cebdef2b293666dbca42',
        emailVerified: true,
        email: 'admin@admin.com',
        password: '$2a$12$vLQjJ9zAWyUwiFLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu',
        role: 0,
        name: 'admin',
        surname: 'admin',
        personalOffers: [],
        deleteOne: jest.fn().mockResolvedValueOnce({
          _id: '6231aefe76598527c8d0b5bc',
          validationToken:
            'cf1c74c22cedb6b575945098db42d2f493fb759c9142c6aff7980f252886f36ee086574ee99a06bc99119079257116c959c8ec870949cebdef2b293666dbca42',
          emailVerified: true,
          email: 'admin@admin.com',
          password: '$2a$12$vLQjJ9zAWyUwiFLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu',
          role: 0,
          name: 'admin',
          surname: 'admin',
          personalOffers: [],
        }),
      });
      expect(await service.deleteOneId('6231aefe76598527c8d0b5bc')).toStrictEqual({
        _id: '6231aefe76598527c8d0b5bc',
        validationToken:
          'cf1c74c22cedb6b575945098db42d2f493fb759c9142c6aff7980f252886f36ee086574ee99a06bc99119079257116c959c8ec870949cebdef2b293666dbca42',
        emailVerified: true,
        email: 'admin@admin.com',
        password: '$2a$12$vLQjJ9zAWyUwiFLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu',
        role: 0,
        name: 'admin',
        surname: 'admin',
        personalOffers: [],
      });
    });
    it('should throw an error ', async () => {
      mockEmployerModel.findOne.mockResolvedValueOnce(null);

      try {
        expect(await service.deleteOneId('6231aefe76598527c8d0b5bc')).toStrictEqual({
          _id: '6231aefe76598527c8d0b5bc',
          validationToken:
            'cf1c74c22cedb6b575945098db42d2f493fb759c9142c6aff7980f252886f36ee086574ee99a06bc99119079257116c959c8ec870949cebdef2b293666dbca42',
          emailVerified: true,
          email: 'admin@admin.com',
          password: '$2a$12$vLQjJ9zAWyUwiFLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu',
          role: 0,
          name: 'admin',
          surname: 'admin',
          personalOffers: [],
        });
        expect(true).toBe(false);
      } catch (e) {
        expect(e.message).toBe('Invalid employer id');
        expect(e.status).toBe(HttpStatus.BAD_REQUEST);
      }
    });
  });
});
Original line number Diff line number Diff line
import { Injectable, Logger } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Query } from 'mongoose';
import * as ejs from 'ejs';
import { Model, Query, Types } from 'mongoose';
import { MergeEmployerDto } from '../../admin/dto/merge-employer.dto';
import { MailerService } from '../../mailer/mailer.service';
import { CreateEmployerDto } from '../dto/create-employer.dto';
import { IEmployer } from '../interfaces/employer.interface';
import { Employer, EmployerDocument } from '../schemas/employer.schema';
import { EmployerSearchService } from './employer-search.service';
import { UsersService } from './users.service';

@Injectable()
export class EmployerService {
@@ -11,6 +16,8 @@ export class EmployerService {

  constructor(
    @InjectModel(Employer.name) private employerModel: Model<EmployerDocument>,
    private readonly userService: UsersService,
    private readonly mailerService: MailerService,
    private readonly employerSearchService: EmployerSearchService
  ) {}

@@ -19,19 +26,124 @@ export class EmployerService {
    return this.employerModel.find().sort({ name: 1 });
  }

  public async findOne(idParam: string): Promise<EmployerDocument> {
    this.logger.debug('findOne');
    return await this.employerModel.findById(Types.ObjectId(idParam));
  }

  public async findAllUnvalidated(): Promise<Employer[]> {
    this.logger.debug('findAllUnvalidated');
    return this.employerModel.find({ validated: false });
  }

  public async findAllValidated(): Promise<IEmployer[]> {
    this.logger.debug(`findAllValidated`);
    return this.employerModel.find({ validated: true });
  }

  public async findByName(name: string): Promise<EmployerDocument> {
    this.logger.debug('findByName');
    return this.employerModel.findOne({ name });
  }

  public async create(employer: CreateEmployerDto, validated = false): Promise<Employer> {
  public async create(employer: CreateEmployerDto, validated = false, sendAdminNotification = true): Promise<Employer> {
    this.logger.debug(`createEmployer: ${employer.name}`);
    await this.employerModel.create({ name: employer.name, validated: validated });
    await this.employerModel.create({ name: employer.name, validated });
    const document = await this.findByName(employer.name);
    this.employerSearchService.indexEmployer(document);
    if (sendAdminNotification) {
      this.sendAdminCreateNotification(
        document,
        this.mailerService.config.templates.adminEmployerCreate.ejs,
        this.mailerService.config.templates.adminEmployerCreate.json
      );
    }
    return document;
  }

  public async sendAdminCreateNotification(
    employer: EmployerDocument,
    templateLocation: any,
    jsonConfigLocation: any
  ): Promise<void> {
    const config = this.mailerService.config;
    const ejsPath = this.mailerService.getTemplateLocation(templateLocation);
    const jsonConfig = this.mailerService.loadJsonConfig(jsonConfigLocation);
    const html = await ejs.renderFile(ejsPath, {
      config,
      employerName: employer ? employer.name : '',
    });
    const admins = await this.userService.getAdmins();
    admins.forEach((admin) => {
      this.mailerService.send(admin.email, jsonConfig.subject, html);
    });
  }

  public async validate(employerId: string): Promise<Employer> {
    this.logger.debug(`validateEmployer: ${employerId}`);
    const employer = await this.employerModel.findById(Types.ObjectId(employerId));
    if (employer) {
      employer.validated = true;
      employer.save();
      return employer;
    } else {
      throw new HttpException('Cannot validate employer. It might have been already validate', HttpStatus.NOT_FOUND);
    }
  }

  public async update(employerId: string, newEmployer: CreateEmployerDto): Promise<Employer> {
    this.logger.debug(`editEmployer: ${employerId}`);
    const employer = await this.employerModel.findById(Types.ObjectId(employerId));
    if (employer) {
      employer.name = newEmployer.name;
      employer.save();
      return employer;
    } else {
      throw new HttpException('Cannot edit employer. It was not found in database.', HttpStatus.NOT_FOUND);
    }
  }

  public async mergeEmployer({
    sourceEmployerId,
    targetEmployerId,
  }: MergeEmployerDto): Promise<Employer | HttpException> {
    this.logger.debug(`mergeEmployer: ${sourceEmployerId} into ${targetEmployerId}`);
    const sourceEmployer = await this.employerModel.findById(Types.ObjectId(sourceEmployerId));
    const targetEmployer = await this.employerModel.findById(Types.ObjectId(targetEmployerId));
    if (targetEmployer && sourceEmployer) {
      this.userService.replaceEmployers(sourceEmployer, targetEmployer);
      if (!sourceEmployer.validated) {
        this.deleteInvalidEmployer(sourceEmployerId);
      }
      return targetEmployer;
    } else {
      throw new HttpException('Cannot operate on employer.', HttpStatus.NOT_FOUND);
    }
  }

  public async deleteInvalidEmployer(
    id: string
  ): Promise<
    Query<{
      ok?: number;
      n?: number;
      deletedCount?: number;
    }>
  > {
    this.logger.debug(`deleteInvalidEmployer: ${id}`);
    const document = await this.employerModel.findById(Types.ObjectId(id));
    this.employerSearchService.deleteIndex(document, document._id);
    return this.employerModel.deleteOne({ _id: id });
  }

  public async deleteOneId(id: string): Promise<Employer> {
    const employer = await this.employerModel.findOne({ _id: id });
    if (!employer) {
      throw new HttpException('Invalid employer id', HttpStatus.BAD_REQUEST);
    }
    return employer.deleteOne();
  }

  public async deleteByName(
    name: string
  ): Promise<
Original line number Diff line number Diff line
import { HttpModule, HttpService, HttpStatus } from '@nestjs/common';
import { getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { AxiosResponse } from 'axios';
import { Types } from 'mongoose';
import { of } from 'rxjs';
import { ConfigurationModule } from '../../configuration/configuration.module';
import { MailerService } from '../../mailer/mailer.service';
import { CreateJobDto } from '../dto/create-job.dto';
import { JobDocument } from '../schemas/job.schema';
import { JobsService } from './jobs.service';
import { UsersService } from './users.service';

describe('JobsService', () => {
  let service: JobsService;
  let httpService: HttpService;
  let mailer: MailerService;

  const mockJobModel = {
    create: jest.fn(),
    find: jest.fn(),
    findOne: jest.fn(),
    findById: jest.fn(),
    deleteOne: jest.fn(),
  };

  const mockUserModel = {
    find: jest.fn(),
    findOne: jest.fn(),
    findById: jest.fn(),
  };
  const mockUserService = {
    replaceJobs: jest.fn(),
    getAdmins: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [ConfigurationModule],
      imports: [ConfigurationModule, HttpModule],
      providers: [
        JobsService,
        {
          provide: getModelToken('Job'),
          useValue: mockJobModel,
        },
        {
          provide: UsersService,
          useValue: mockUserService,
        },
        {
          provide: getModelToken('User'),
          useValue: mockUserModel,
        },
        MailerService,
      ],
    }).compile();
    service = module.get<JobsService>(JobsService);
    mailer = module.get<MailerService>(MailerService);
    httpService = module.get<HttpService>(HttpService);
  });

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

  it('findAll', async () => {
  describe('findAll', () => {
    it('should findAll validated jobs', async () => {
      mockJobModel.find.mockResolvedValue([
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
@@ -44,6 +76,32 @@ describe('JobsService', () => {
      const reply = await service.findAll();
      expect(reply.length).toBe(1);
    });
    it('should findAll all jobs', async () => {
      mockJobModel.find.mockResolvedValue([
        {
          _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
          name: 'CNFSssss',
          validated: false,
          hasPersonalOffer: true,
        },
      ]);
      const reply = await service.findAll(false);
      expect(reply.length).toBe(1);
    });
  });

  it('findUnvalidated', async () => {
    mockJobModel.find.mockResolvedValue([
      {
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'CNFS',
        validated: false,
        hasPersonalOffer: true,
      },
    ]);
    const reply = await service.findAllUnvalidated();
    expect(reply[0].validated).toBeFalsy;
  });

  it('findByName', async () => {
    mockJobModel.findOne.mockResolvedValue({
@@ -55,18 +113,255 @@ describe('JobsService', () => {
    const reply = await service.findByName('CNFS');
    expect(reply).toBeTruthy();
  });
  it('findOne', async () => {
    mockJobModel.findById.mockResolvedValue({
      _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
      name: 'CNFS',
      validated: true,
      hasPersonalOffer: true,
    });
    const reply = await service.findOne('6231aefe76598527c8d0b5bc');
    expect(reply).toBeTruthy();
  });

  describe('createJob', () => {
    it('create', async () => {
      mockJobModel.create.mockResolvedValue({
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
      name: 'CNFS',
        name: 'Dev',
        validated: false,
        hasPersonalOffer: false,
      });
      const createJob: CreateJobDto = {
        name: 'Dev',
        hasPersonalOffer: false,
      };
      const reply = await service.create(createJob);
      expect(reply).toBeTruthy();
    });

    it('should send an email to admins', async () => {
      //todo fetch admin infos and check httpservice is called further
      const result: AxiosResponse = {
        data: {
          status: 200,
          content: {
            success: true,
            response: [7010],
          },
        },
        status: 200,
        statusText: 'OK',
        headers: {},
        config: {},
      };
      jest.spyOn(httpService, 'post').mockImplementationOnce(() => of(result));
      expect(await mailer.send('a@a.com', 'test', '<p>This is a test</p>')).toBe(result);
    });
  });

  it('createFromAdmin', async () => {
    mockJobModel.create.mockResolvedValue({
      _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
      name: 'Dev',
      validated: true,
      hasPersonalOffer: true,
      hasPersonalOffer: false,
    });
    const createJob: CreateJobDto = {
      name: 'Dev',
      hasPersonalOffer: false,
    };
    const reply = await service.create(createJob);
    expect(reply).toBeTruthy();
  });

  describe('sendAdminCreateNotification', () => {
    it('should sendAdminCreateNotification', async () => {
      const job = {
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'Metro',
        validated: true,
      };
      const result: AxiosResponse = {
        data: {
          status: 200,
          content: {
            success: true,
            response: [7010],
          },
        },
        status: 200,
        statusText: 'OK',
        headers: {},
        config: {},
      };
      jest.spyOn(mailer, 'send').mockResolvedValueOnce(result);
      const spyer = jest.spyOn(mailer, 'send');
      mockUserService.getAdmins.mockResolvedValueOnce([
        {
          _id: '6231aefe76598527c8d0b5bc',
          validationToken:
            'cf1c74c22cedb6b575945098db42d2f493fb759c9142c6aff7980f252886f36ee086574ee99a06bc99119079257116c959c8ec870949cebdef2b293666dbca42',
          emailVerified: true,
          email: 'admin@admin.com',
          password: '$2a$12$vLQjJ9zAWyUwiFLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu',
          role: 0,
          name: 'admin',
          surname: 'admin',
          personalOffers: [],
        },
      ]);
      await service.sendAdminCreateNotification(job as JobDocument, 'adminJobCreate.ejs', 'adminJobCreate.json');
      expect(spyer.mock.calls.length).toBe(1);
    });
  });

  describe('validate', () => {
    it('should validate job', async () => {
      mockJobModel.findById.mockResolvedValueOnce({
        _id: Types.ObjectId('623aed68c5d45b6fbbaa7e60'),
        name: 'Dev',
        validated: false,
        save: jest.fn().mockResolvedValueOnce(null),
      });
      const job = await service.validate('623aed68c5d45b6fbbaa7e60');
      expect(job.validated).toBe(true);
    });
    it('should throw exception', async () => {
      mockJobModel.findById.mockResolvedValueOnce(null);
      try {
        await service.validate('623aed68c5d45b6fbbaa7e60');
        expect(true).toBe(false);
      } catch (e) {
        expect(e.message).toBe('Cannot validate job. It might have been already validate');
        expect(e.status).toBe(HttpStatus.NOT_FOUND);
      }
    });
  });

  describe('mergeJob', () => {
    it('should delete source job', async () => {
      const reply = {
        _id: Types.ObjectId('623aed68c5d45b6fbbaa7e61'),
        name: 'Dev 2',
        validated: true,
        hasPersonalOffer: false,
      };
      mockJobModel.findById
        .mockResolvedValueOnce({
          _id: Types.ObjectId('623aed68c5d45b6fbbaa7e60'),
          name: 'Dev',
          validated: false,
          hasPersonalOffer: false,
        })
        .mockResolvedValueOnce(reply);
      jest.spyOn(service, 'deleteInvalidJob').mockRejectedValueOnce({});

      expect(
        await service.mergeJob({ sourceJobId: '623aed68c5d45b6fbbaa7e60', targetJobId: '623aed68c5d45b6fbbaa7e61' })
      ).toEqual(reply);
    });
    it('should throw error if target job is not validated', async () => {
      mockJobModel.findById
        .mockResolvedValueOnce({
          _id: Types.ObjectId('623aed68c5d45b6fbbaa7e60'),
          name: 'Dev',
          validated: false,
          hasPersonalOffer: false,
        })
        .mockResolvedValueOnce(null);
      try {
        await service.mergeJob({ sourceJobId: '623aed68c5d45b6fbbaa7e60', targetJobId: '623aed68c5d45b6fbbaa7e61' });
        expect(true).toBe(false);
      } catch (e) {
        expect(e.message).toBe('Cannot operate on job.');
        expect(e.status).toBe(HttpStatus.NOT_FOUND);
      }
    });
    it('should throw error if no source or target job', async () => {
      mockJobModel.findById.mockResolvedValueOnce(null).mockResolvedValueOnce({
        _id: Types.ObjectId('623aed68c5d45b6fbbaa7e60'),
        name: 'Dev',
        validated: false,
        hasPersonalOffer: false,
      });
      try {
        await service.mergeJob({ sourceJobId: '623aed68c5d45b6fbbaa7e60', targetJobId: '623aed68c5d45b6fbbaa7e61' });
        expect(true).toBe(false);
      } catch (e) {
        expect(e.message).toBe('Cannot operate on job.');
        expect(e.status).toBe(HttpStatus.NOT_FOUND);
      }
    });
  });

  it('deleteInvalidJob', async () => {
    mockJobModel.deleteOne.mockResolvedValueOnce(null);
    const spyer = jest.spyOn(service, 'deleteInvalidJob');
    await service.deleteInvalidJob('623aed68c5d45b6fbbaa7e60');
    expect(spyer.mock.calls.length).toEqual(1);
  });

  describe('deleteOneId', () => {
    it('should delete ', async () => {
      mockJobModel.findOne.mockResolvedValueOnce({
        _id: Types.ObjectId('623aed68c5d45b6fbbaa7e60'),
        name: 'Dev',
        validated: false,
        deleteOne: jest.fn().mockResolvedValueOnce('toto'),
      });
      const reply = await service.deleteOneId('623aed68c5d45b6fbbaa7e60');
      expect(reply).toBe('toto');
    });
    it('should delete ', async () => {
      mockJobModel.findOne.mockResolvedValueOnce(null);
      try {
        await service.deleteOneId('623aed68c5d45b6fbbaa7e60');
        expect(true).toBe(false);
      } catch (e) {
        expect(e.message).toBe('Invalid job id');
        expect(e.status).toBe(HttpStatus.BAD_REQUEST);
      }
    });
  });

  describe('update', () => {
    it('should update', async () => {
      const newJobDto = {
        name: 'CNFS',
        hasPersonalOffer: true,
      };
      const newJob = {
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'CNFS',
        validated: true,
        hasPersonalOffer: true,
      };
      mockJobModel.findById.mockResolvedValue({
        _id: Types.ObjectId('6231aefe76598527c8d0b5bc'),
        name: 'CNFS old',
        validated: true,
        hasPersonalOffer: false,
        save: jest.fn().mockResolvedValueOnce(newJob),
      });
      const reply = await service.update('623aed68c5d45b6fbbaa7e60', newJobDto);
      expect(reply.name).toBe('CNFS');
      expect(reply.hasPersonalOffer).toBe(true);
    });
  });

  it('should throw error', async () => {
    const newJobDto = {
      name: 'CNFS',
      hasPersonalOffer: true,
    };
    mockJobModel.findById.mockResolvedValue(null);
    try {
      await service.update('623aed68c5d45b6fbbaa7e60', newJobDto);
      expect(true).toBe(false);
    } catch (e) {
      expect(e.message).toBe('Cannot edit job. It was not found in database.');
      expect(e.status).toBe(HttpStatus.NOT_FOUND);
    }
  });
});
Original line number Diff line number Diff line
import { Injectable, Logger } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import * as ejs from 'ejs';
import { Model, Query, Types } from 'mongoose';
import { MergeJobDto } from '../../admin/dto/merge-job.dto';
import { MailerService } from '../../mailer/mailer.service';
import { CreateJobDto } from '../dto/create-job.dto';
import { IJob } from '../interfaces/job.interface';
import { Job, JobDocument } from '../schemas/job.schema';
import { UsersService } from './users.service';

@Injectable()
export class JobsService {
  private readonly logger = new Logger(JobsService.name);

  constructor(@InjectModel(Job.name) private jobModel: Model<JobDocument>) {}
  constructor(
    @InjectModel(Job.name) private jobModel: Model<JobDocument>,
    private readonly userService: UsersService,
    private readonly mailerService: MailerService
  ) {}

  public async findAll(filterValidetedJobs = true): Promise<Job[]> {
    this.logger.debug(`findAll with filterValidetedJobs: ${filterValidetedJobs}`);
    if (!filterValidetedJobs) {
  public async findAll(filterValidatedJobs = true): Promise<IJob[]> {
    this.logger.debug(`findAll with filterValidetedJobs: ${filterValidatedJobs}`);
    if (!filterValidatedJobs) {
      return this.jobModel.find();
    } else {
      return this.jobModel.find({ validated: true });
@@ -24,8 +33,115 @@ export class JobsService {
    return this.jobModel.findOne({ name });
  }

  public async create(job: CreateJobDto, validated = false, hasPersonalOffer = true): Promise<Job> {
    this.logger.debug(`createJob: ${job.name}`);
    return this.jobModel.create({ name: job.name, validated, hasPersonalOffer });
  public async findOne(idParam: string): Promise<JobDocument> {
    this.logger.debug('findOne');
    return this.jobModel.findById(Types.ObjectId(idParam));
  }

  public async findAllUnvalidated(): Promise<IJob[]> {
    this.logger.debug('findAllUnvalidated');
    return this.jobModel.find({ validated: false });
  }

  public async create(
    job: CreateJobDto,
    validated = false,
    hasPersonalOffer = false,
    sendNotification = true
  ): Promise<Job> {
    this.logger.debug(`createJob: ${job.name} | notificationSending ${sendNotification}`);
    const result = this.jobModel
      .create({ name: job.name, validated, hasPersonalOffer })
      .then(async (postCreate: JobDocument) => {
        if (sendNotification) {
          this.sendAdminCreateNotification(
            postCreate,
            this.mailerService.config.templates.adminJobCreate.ejs,
            this.mailerService.config.templates.adminJobCreate.json
          );
        }
        return postCreate;
      });
    return result;
  }

  public async sendAdminCreateNotification(
    job: JobDocument,
    templateLocation: any,
    jsonConfigLocation: any
  ): Promise<void> {
    const config = this.mailerService.config;
    const ejsPath = this.mailerService.getTemplateLocation(templateLocation);
    const jsonConfig = this.mailerService.loadJsonConfig(jsonConfigLocation);
    const html = await ejs.renderFile(ejsPath, {
      config,
      jobName: job ? job.name : '',
    });
    const admins = await this.userService.getAdmins();
    admins.forEach((admin) => {
      this.mailerService.send(admin.email, jsonConfig.subject, html);
    });
  }

  public async validate(jobId: string): Promise<Job> {
    this.logger.debug(`validateJob: ${jobId}`);
    const job = await this.jobModel.findById(Types.ObjectId(jobId));
    if (job) {
      job.validated = true;
      job.save();
      return job;
    } else {
      throw new HttpException('Cannot validate job. It might have been already validate', HttpStatus.NOT_FOUND);
    }
  }

  public async mergeJob({ sourceJobId, targetJobId }: MergeJobDto): Promise<Job | HttpException> {
    this.logger.debug(`mergeJob: ${sourceJobId} into ${targetJobId}`);
    const sourceJob = await this.jobModel.findById(Types.ObjectId(sourceJobId));
    const targetJob = await this.jobModel.findById(Types.ObjectId(targetJobId));
    if (targetJob && sourceJob) {
      this.logger.debug(`Both jobs : ${sourceJob}, ${targetJob}`);
      this.userService.replaceJobs(sourceJob, targetJob);
      if (!sourceJob.validated) {
        this.deleteInvalidJob(sourceJobId);
      }
      return targetJob;
    } else {
      throw new HttpException('Cannot operate on job.', HttpStatus.NOT_FOUND);
    }
  }

  public async deleteInvalidJob(
    id: string
  ): Promise<
    Query<{
      ok?: number;
      n?: number;
      deletedCount?: number;
    }>
  > {
    this.logger.debug(`deleteInvalidJob: ${id}`);
    return this.jobModel.deleteOne({ _id: id });
  }

  public async deleteOneId(id: string): Promise<Job> {
    const job = await this.jobModel.findOne({ _id: id });
    if (!job) {
      throw new HttpException('Invalid job id', HttpStatus.BAD_REQUEST);
    }
    return job.deleteOne();
  }

  public async update(jobId: string, newJob: CreateJobDto): Promise<Job> {
    this.logger.debug(`editJob: ${jobId}`);
    const job = await this.jobModel.findById(Types.ObjectId(jobId));
    if (job) {
      job.name = newJob.name;
      job.hasPersonalOffer = newJob.hasPersonalOffer;
      job.save();
      return job;
    } else {
      throw new HttpException('Cannot edit job. It was not found in database.', HttpStatus.NOT_FOUND);
    }
  }
}
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ import * as bcrypt from 'bcrypt';
import { personalOffersDataMock } from '../../../test/mock/data/personalOffers.mock.data';
import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-offer.schema';
import { usersMockData } from '../../../test/mock/data/users.mock.data';
import { employersMockData } from '../../../test/mock/data/employers.mock.data';
import { LoginDto } from '../../auth/login-dto';
import { ConfigurationModule } from '../../configuration/configuration.module';
import { MailerModule } from '../../mailer/mailer.module';
@@ -19,6 +20,17 @@ function hashPassword() {
  return bcrypt.hashSync(process.env.USER_PWD, process.env.SALT);
}

const mockUserModel = {
  create: jest.fn(),
  findOne: jest.fn(),
  findById: jest.fn(),
  deleteOne: jest.fn(),
  updateMany: jest.fn(),
  exec: jest.fn(),
  find: jest.fn(() => mockUserModel),
  sort: jest.fn(() => mockUserModel),
};

describe('UsersService', () => {
  let service: UsersService;

@@ -29,7 +41,7 @@ describe('UsersService', () => {
        UsersService,
        {
          provide: getModelToken('User'),
          useValue: User,
          useValue: mockUserModel,
        },
      ],
    }).compile();
@@ -405,4 +417,29 @@ describe('UsersService', () => {
      }
    });
  });

  describe('user employer', () => {
    it('should replace employer with a new one', async () => {
      const spyer = jest.spyOn(mockUserModel, 'updateMany');

      mockUserModel.updateMany.mockReturnThis();
      mockUserModel.exec.mockResolvedValueOnce([usersMockData[0]]);
      await service.replaceEmployers(employersMockData[0], employersMockData[1]);
      expect(spyer).toBeCalledTimes(1);
    });

    it('should return true if a user is linked to a given employer', async () => {});
    it('should fetch users attached to a given employer', async () => {});
    it('should populate an employer with a list of attached users', async () => {});
    it("should update user's employer ", async () => {});
  });

  describe('user job', () => {
    it('should replace job with a new one', async () => {});

    it('should return true if a user is linked to a given job', async () => {});
    it('should fetch users attached to a given job', async () => {});
    it('should populate an job with a list of attached users', async () => {});
    it("should update user's job ", async () => {});
  });
});
Original line number Diff line number Diff line
@@ -14,8 +14,8 @@ import { PendingStructureDto } from '../../admin/dto/pending-structure.dto';
import { OwnerDto } from '../dto/owner.dto';
import { StructureDocument } from '../../structures/schemas/structure.schema';
import { ConfigurationService } from '../../configuration/configuration.service';
import { EmployerDocument } from '../schemas/employer.schema';
import { JobDocument } from '../schemas/job.schema';
import { Employer, EmployerDocument } from '../schemas/employer.schema';
import { Job, JobDocument } from '../schemas/job.schema';
import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-offer.schema';

@Injectable()
@@ -87,7 +87,7 @@ export class UsersService {
  }

  public findAll(): Promise<User[]> {
    return this.userModel.find().exec();
    return this.userModel.find().populate('employer').populate('job').select('-password').exec();
  }

  public findAllUnattached(): Promise<IUser[]> {
@@ -98,6 +98,9 @@ export class UsersService {
      .where('structuresLink')
      .size(0)
      .sort({ surname: 1 })
      .populate('employer')
      .populate('job')
      .select('-password')
      .exec();
  }

@@ -107,18 +110,29 @@ export class UsersService {
      .where('emailVerified')
      .equals(true)
      .sort({ surname: 1 })
      .populate('employer')
      .populate('job')
      .select('-password')
      .exec();
  }

  public findAllUnVerified(): Promise<IUser[]> {
    return this.userModel.find().where('emailVerified').equals(false).sort({ surname: 1 }).exec();
    return this.userModel
      .find()
      .where('emailVerified')
      .equals(false)
      .sort({ surname: 1 })
      .populate('employer')
      .populate('job')
      .select('-password')
      .exec();
  }

  public async findById(id: string, passwordQuery?: boolean): Promise<IUser | undefined> {
    if (passwordQuery) {
      return this.userModel.findById(id).exec();
      return this.userModel.findById(id).populate('employer').populate('job').exec();
    }
    return this.userModel.findById(id).select('-password').exec();
    return this.userModel.findById(id).populate('employer').populate('job').select('-password').exec();
  }

  public async removeStructureIdFromUsers(structureId: Types.ObjectId): Promise<IUser[] | undefined> {
@@ -127,6 +141,19 @@ export class UsersService {
      .exec();
  }

  public async replaceEmployers(
    sourceEmployer: EmployerDocument,
    targetEmployer: EmployerDocument
  ): Promise<IUser[] | undefined> {
    return this.userModel
      .updateMany({ employer: sourceEmployer._id }, { $set: { employer: targetEmployer._id } })
      .exec();
  }

  public async replaceJobs(sourceJob: JobDocument, targetJob: JobDocument): Promise<IUser[] | undefined> {
    return this.userModel.updateMany({ job: sourceJob._id }, { $set: { job: targetJob._id } }).exec();
  }

  /**
   * Return a user after credential checking.
   * Use for login action
@@ -359,6 +386,63 @@ export class UsersService {
    return this.userModel.findOne({ structuresLink: Types.ObjectId(structureId) }).exec();
  }

  public async isEmployerLinkedtoUser(id: string): Promise<boolean> {
    const users = await this.userModel.find().exec();
    let hasLinkedUser = false;
    users.map((x) => {
      if (String(x.employer) === id.toString()) {
        hasLinkedUser = true;
      }
    });
    return hasLinkedUser;
  }

  public async isJobLinkedtoUser(id: string): Promise<boolean> {
    const users = await this.userModel.find().exec();
    let hasLinkedUser = false;
    users.map((x) => {
      if (String(x.job) === id.toString()) {
        hasLinkedUser = true;
      }
    });
    return hasLinkedUser;
  }

  public getJobAttachedUsers(jobId: JobDocument): Promise<IUser[]> {
    return this.userModel.find({ job: jobId._id }).select('-password -job').exec();
  }

  public getEmployerAttachedUsers(employerId: EmployerDocument): Promise<IUser[]> {
    return this.userModel.find({ employer: employerId._id }).select('-password -employer').exec();
  }

  public async populateJobswithUsers(jobs: JobDocument[]): Promise<any[]> {
    return Promise.all(
      jobs.map(async (job) => {
        return {
          _id: job._id,
          name: job.name,
          validated: job.validated,
          hasPersonalOffer: job.hasPersonalOffer,
          users: await this.getJobAttachedUsers(job),
        };
      })
    );
  }

  public async populateEmployerswithUsers(employers: EmployerDocument[]): Promise<any[]> {
    return Promise.all(
      employers.map(async (employer) => {
        return {
          _id: employer._id,
          name: employer.name,
          validated: employer.validated,
          users: await this.getEmployerAttachedUsers(employer),
        };
      })
    );
  }

  public getStructureOwners(structureId: string): Promise<IUser[]> {
    return this.userModel.find({ structuresLink: Types.ObjectId(structureId) }).exec();
  }
@@ -529,7 +613,7 @@ export class UsersService {
      return this.getPendingStructures();
    } else {
      throw new HttpException(
        'Cannot validate strucutre. It might have been already validate, or the structure does`nt belong to the user',
        'Cannot validate strucutre. It might have been already validate, or the structure doesn`t belong to the user',
        HttpStatus.NOT_FOUND
      );
    }
@@ -608,6 +692,7 @@ export class UsersService {
  }

  /**
   *
   * @param profile
   * @param userId
   */
@@ -615,4 +700,24 @@ export class UsersService {
    this.logger.debug(`updateUserProfile | ${userId}`);
    return this.userModel.updateOne({ _id: userId }, { $set: { employer: employer._id, job: job._id } });
  }

  /**
   *
   * @param job
   * @param userId
   */
  public async updateUserJob(userId: Types.ObjectId, job: JobDocument): Promise<any> {
    this.logger.debug(`updateUserProfile - Job | ${userId}`);
    return this.userModel.updateOne({ _id: userId }, { $set: { job: job._id } });
  }

  /**
   *
   * @param employer
   * @param userId
   */
  public async updateUserEmployer(userId: Types.ObjectId, employer: EmployerDocument): Promise<any> {
    this.logger.debug(`updateUserProfile - Employer | ${userId}`);
    return this.userModel.updateOne({ _id: userId }, { $set: { employer: employer._id } });
  }
}
+14 −0
Original line number Diff line number Diff line
import { IEmployer } from '../../../src/users/interfaces/employer.interface';

export const employersMockData: IEmployer[] = [
  {
    _id: '6036721022462b001334c4ba',
    name: 'Metro',
    validated: true,
  } as any,
  {
    _id: '6036721022462b001334c4bb',
    name: 'Sopra',
    validated: true,
  } as any,
] as IEmployer[];
Original line number Diff line number Diff line
import { HttpException, HttpStatus } from '@nestjs/common';
import { isValidObjectId } from 'mongoose';
import { PendingStructureDto } from '../../../src/admin/dto/pending-structure.dto';
import { LoginDto } from '../../../src/auth/login-dto';
import { PersonalOffer, PersonalOfferDocument } from '../../../src/personal-offers/schemas/personal-offer.schema';
@@ -66,17 +67,66 @@ export class UsersServiceMock {
    return user;
  }

  findById(id: string) {
    if (isValidObjectId(id)) {
      return {
        _id: id,
        validationToken:
          'cf1c74c22cedb6b575945098db42d2f493fb759c9142c6aff7980f252886f36ee086574ee99a06bc99119079257116c959c8ec870949cebdef2b293666dbca42',
        emailVerified: true,
        email: 'pauline.dupont@mii.com',
        password: '$2a$12$vLQjJ9zAWyUwiFLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu',
        role: 0,
        name: 'DUPONT',
        surname: 'Pauline',
        personalOffers: [],
      };
    } else {
      return null;
    }
  }

  isEmployerLinkedtoUser(id: string) {
    if (id == '6231aefe76598527c8d0b5bc') return true;
    else {
      return false;
    }
  }

  isJobLinkedtoUser(id: string) {
    if (id == '6231aefe76598527c8d0b5bc') return true;
    else {
      return false;
    }
  }
  getAdmins() {
    return {
      _id: '6231aefe76598527c8d0b5bc',
      validationToken:
        'cf1c74c22cedb6b575945098db42d2f493fb759c9142c6aff7980f252886f36ee086574ee99a06bc99119079257116c959c8ec870949cebdef2b293666dbca42',
      emailVerified: true,
      email: 'admin@admin.com',
      password: '$2a$12$vLQjJ9zAWyUwiFLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu',
      role: 0,
      name: 'admin',
      surname: 'admin',
      personalOffers: [],
    };
  }

  getPendingStructures() {
    return [
      {
        structureId: '6093ba0e2ab5775cfc01ed3e',
        structureName: 'a',
        userEmail: 'paula.dubois@mii.com',
        updatedAt: '2021-03-02T10:07:48.000Z',
      },
      {
        structureId: '6903ba0e2ab5775cfc01ed4d',
        structureName: "L'Atelier Numérique",
        userEmail: 'jacques.dupont@mii.com',
        updatedAt: '2021-03-02T10:07:48.000Z',
      },
    ];
  }
@@ -211,6 +261,18 @@ export class UsersServiceMock {
    return await [];
  }

  async populateEmployerswithUsers(): Promise<User[]> {
    return await [];
  }

  async populateJobswithUsers(): Promise<User[]> {
    return await [];
  }

  replaceEmployers(source, tar): IUser[] {
    return [];
  }

  public async addPersonalOffer(userId: string, personalOfferDocument: PersonalOfferDocument): Promise<IUser> {
    if (userId === '6036721022462b001334c4bb') {
      const personalOffer: PersonalOffer = { ...personalOfferDocument } as PersonalOffer;
@@ -240,4 +302,12 @@ export class UsersServiceMock {
    }
    throw new HttpException('User not found for the personal offer attachment', HttpStatus.BAD_REQUEST);
  }

  async updateUserJob() {
    return await [];
  }

  async updateUserEmployer() {
    return await [];
  }
}