diff --git a/CHANGELOG.md b/CHANGELOG.md index f1ff84ac97d5cb04685d4412269930e0a3d75349..4c4ba5b7e2e9406aa7e24bd5f473067f4c6fcdfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.11.0](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/compare/v1.10.0...v1.11.0) (2022-01-12) + + +### Features + +* add structure and user information on structure claim ([92e0099](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/92e00990779c3149057cb51decbd262c40600e34)) +* **aptic-structures:** send mail when structure is created via aptic ([afdae30](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/afdae30a9c657c7fbfc17fbd2a229daa82d8d543)) +* **dev:** add auto import of images in ghost at init ([a9cec09](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/a9cec09e3be83728c9725190d9e07e3597b3356a)) +* migrate null values to 0 for equipments ([f0b382c](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/f0b382ce2cfd45a8a4f19ca661b1e97d1f5371e8)) +* **TU:** add various TU ([9f82093](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/9f82093997d66c3a3469f21fa1eb7bfca2310e04)) + + +### Bug Fixes + +* mail spelling ([7af3cd8](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/7af3cd86cbaa14af316c98896e4ea87660742a04)) +* mail spelling and typo ([b2ff1e3](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/b2ff1e3bdbaaa7a32640a3cdafa0323fe339884d)) +* **post:** post image broken links ([a50f133](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/a50f1334871e81e5f9d5e64d46c19d55a4a854ef)) +* **post:** rollback of unecesary localconf protection remove ([8d6056a](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/8d6056af95b41626ee589000416d51d67eb49380)) + ## [1.10.0](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/compare/v1.9.1...v1.10.0) (2021-12-22) diff --git a/docker-compose.yml b/docker-compose.yml index e28e86d0f70ccc389b2cf30f2424f71a81ecfc20..ff23e61fc8f9ba00a175ce3211043d0d01616c55 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '2' services: service-ram: - image: registry.forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server:master + image: registry.forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server:dev ports: - ${SERVICE_API_BIND_PORT}:3000 extra_hosts: @@ -87,7 +87,7 @@ services: - db-ghost:/var/lib/mysql es01: - image: elasticsearch:7.6.1 + image: elasticsearch:7.16.2 restart: unless-stopped environment: node.name: es01 diff --git a/package-lock.json b/package-lock.json index c32dba743fa033561364730f0db3a6b29cb20137..26f6823102671c944e40ab31097f71212cc5cb61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ram_server", - "version": "1.10.0", + "version": "1.11.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4448,6 +4448,12 @@ "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", "dev": true }, + "@golevelup/ts-jest": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@golevelup/ts-jest/-/ts-jest-0.3.2.tgz", + "integrity": "sha512-hv+j/vau2oj5CuhY1CrTs48Qu+ZVXpm/56OzKcL2KVN8+yc0ZIMbvTIld1wbrT1RrvAuwfDmUM2s3aSy7veuwg==", + "dev": true + }, "@hutson/parse-repository-url": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", diff --git a/package.json b/package.json index 7bcfb722c57763dc38d71e4e785d5e0630e1cbc5..a42098352a937e0fb37708dbf79ad87b49ca9351 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ram_server", "private": true, - "version": "1.10.0", + "version": "1.11.0", "description": "Nest TypeScript starter repository", "license": "MIT", "scripts": { @@ -60,6 +60,7 @@ }, "devDependencies": { "@compodoc/compodoc": "^1.1.16", + "@golevelup/ts-jest": "^0.3.2", "@nestjs/cli": "^7.5.1", "@nestjs/schematics": "^7.1.3", "@nestjs/testing": "^7.5.1", diff --git a/scripts/ghost/migrations/init/posts.json b/scripts/ghost/migrations/init/posts.json index 7d672a8ecd8ad4af6054f95851be52505330be70..b4da27c9af0a6de7de127faf09ca8352a7d6f38d 100644 --- a/scripts/ghost/migrations/init/posts.json +++ b/scripts/ghost/migrations/init/posts.json @@ -227,7 +227,7 @@ "slug": "le-reseau-de-mediation-numerique-metropolitain-quest-ce-que-cest", "mobiledoc": "{\"version\":\"0.3.1\",\"atoms\":[],\"cards\":[],\"markups\":[],\"sections\":[[1,\"p\",[[0,[],0,\"La Métropole s’est engagée depuis plusieurs mois maintenant dans la mise en place d’un réseau des acteurs de la méditation numérique.\"]]],[1,\"p\",[[0,[],0,\"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\"]]],[1,\"p\",[[0,[],0,\"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\"]]],[1,\"p\",[[0,[],0,\"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\"]]]]}", "comment_id": "603f6835adff2300014659ee", - "feature_image": "http://localhost:2368/content/images/2021/03/folder.png", + "feature_image": "scripts/data/folder.png", "featured": false, "status": "published", "visibility": "public", @@ -349,7 +349,7 @@ "slug": "une-plateforme-daccompagnement-nationale", "mobiledoc": "{\"version\":\"0.3.1\",\"atoms\":[],\"cards\":[],\"markups\":[],\"sections\":[[1,\"p\",[[0,[],0,\"Le site solidarite-numerique.fr développé par la Mednum en partenariat avec le secrétariat d’État a la culture.\"]]],[1,\"p\",[]],[1,\"p\",[[0,[],0,\"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\"]]],[1,\"p\",[[0,[],0,\"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\"]]],[1,\"p\",[[0,[],0,\"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\"]]]]}", "comment_id": "603f67d9adff2300014659e0", - "feature_image": "http://localhost:2368/content/images/2021/03/etude.png", + "feature_image": "scripts/data/etude.png", "featured": false, "status": "published", "visibility": "public", @@ -571,7 +571,7 @@ "slug": "the-editor", "mobiledoc": "{\"version\":\"0.3.1\",\"atoms\":[],\"cards\":[],\"markups\":[[\"a\",[\"href\",\"https://resin.grandlyon.com/home\"]]],\"sections\":[[1,\"p\",[]],[1,\"p\",[[0,[],0,\"Prévus pour se dérouler sur la métropole cette année, les deux événements Superdemain et Numérique en Commun[s] ont dû évidemment repenser l'organisation de cette édition 2020, année si particulière.\"]]],[1,\"p\",[[0,[],0,\"Les formations des professionnels prévus en présentiel sont annulées en raison du confinement. Mais une troisième vague de parcours en ligne est ouverte jusqu'au 13 novembre.\"]]],[1,\"p\",[[0,[0],1,\"Inscriptions\"]]],[1,\"p\",[[0,[],0,\"Les super-shows (conférences) sont maintenus en format 100% distancielPlus d'infos\"]]],[1,\"p\",[[0,[],0,\"Également prévu sur la métropole de Lyon, l'évènement Numérique en Commun[s], évènement national autour du numérique et ses enjeux a lui aussi basculé en 100% distanciel.\"]]],[1,\"p\",[[0,[],0,\"Au programme : 2h30 d'échanges le 17 novembre prochain avec de nombreux intervenants nationaux et 9 parcours en ligne à destination des professionnels, agents de collectivités ou élus sur des sujets d'actualités : inclusion numérique, communs, impact environnemental du numérique...\"]]],[1,\"p\",[[0,[0],1,\"Informations et inscription\"]]],[1,\"p\",[]]]}", "comment_id": "601425dc7775f400017ea02d", - "feature_image": "http://localhost:2368/content/images/2021/03/nec-1.png", + "feature_image": "scripts/data/nec.png", "featured": false, "status": "published", "visibility": "public", @@ -793,7 +793,7 @@ "slug": "admin-settings", "mobiledoc": "{\"version\":\"0.3.1\",\"atoms\":[[\"soft-return\",\"\",{}],[\"soft-return\",\"\",{}],[\"soft-return\",\"\",{}],[\"soft-return\",\"\",{}],[\"soft-return\",\"\",{}],[\"soft-return\",\"\",{}],[\"soft-return\",\"\",{}]],\"cards\":[[\"image\",{\"src\":\"https://static.ghost.org/v1.0.0/images/private.png\"}]],\"markups\":[[\"em\"],[\"strong\"],[\"a\",[\"href\",\"https://ghost.org/pricing/\"]],[\"a\",[\"href\",\"http://localhost:2368/organising-content/\"]]],\"sections\":[[1,\"h2\",[[0,[],0,\"Make your site private\"]]],[1,\"p\",[[0,[],0,\"Le centre socio-culturel la Carnière situé à Saint-Priest possède un important pôle numérique.\"]]],[10,0],[1,\"p\",[[0,[],0,\"Ghost will give you a short, randomly generated pass-phrase which you can share with anyone who needs access to the site while you're working on it. While this setting is enabled, all search engine optimisation features will be switched off to help keep your site under the radar.\"]]],[1,\"p\",[[0,[],0,\"Do remember though, this is \"],[0,[0],1,\"not\"],[0,[],0,\" secure authentication. You shouldn't rely on this feature for protecting important private data. It's just a simple, shared pass-phrase for some very basic privacy.\"]]],[1,\"h2\",[[0,[],0,\"Invite your team \"]]],[1,\"p\",[[0,[],0,\"Ghost has a number of different user roles for your team:\"]]],[1,\"p\",[[0,[1],1,\"Contributors\"],[1,[],0,0],[0,[],0,\"This is the base user level in Ghost. Contributors can create and edit their own draft posts, but they are unable to edit drafts of others or publish posts. Contributors are \"],[0,[1],1,\"untrusted\"],[0,[],0,\" users with the most basic access to your publication.\"]]],[1,\"p\",[[0,[1],1,\"Authors\"],[1,[],0,1],[0,[],0,\"Authors are the 2nd user level in Ghost. Authors can write, edit and publish their own posts. Authors are \"],[0,[1],1,\"trusted\"],[0,[],0,\" users. If you don't trust users to be allowed to publish their own posts, they should be set as Contributors.\"]]],[1,\"p\",[[0,[1],1,\"Editors\"],[1,[],0,2],[0,[],0,\"Editors are the 3rd user level in Ghost. Editors can do everything that an Author can do, but they can also edit and publish the posts of others - as well as their own. Editors can also invite new Contributors & Authors to the site.\"]]],[1,\"p\",[[0,[1],1,\"Administrators\"],[1,[],0,3],[0,[],0,\"The top user level in Ghost is Administrator. Again, administrators can do everything that Authors and Editors can do, but they can also edit all site settings and data, not just content. Additionally, administrators have full access to invite, manage or remove any other user of the site.\"],[1,[],0,4],[1,[],0,5],[0,[1],1,\"The Owner\"],[1,[],0,6],[0,[],0,\"There is only ever one owner of a Ghost site. The owner is a special user which has all the same permissions as an Administrator, but with two exceptions: The Owner can never be deleted. And in some circumstances the owner will have access to additional special settings if applicable. For example: billing details, if using \"],[0,[2,1],2,\"Ghost(Pro)\"],[0,[],0,\".\"]]],[1,\"blockquote\",[[0,[0],1,\"It's a good idea to ask all of your users to fill out their user profiles, including bio and social links. These will populate rich structured data for posts and generally create more opportunities for themes to fully populate their design.\"]]],[1,\"h2\",[[0,[],0,\"Next: Organising content\"]]],[1,\"p\",[[0,[],0,\"Find out how to \"],[0,[3],1,\"organise your content\"],[0,[],0,\" with sensible tags and authors, or for more advanced configurations, how to create custom content structures using dynamic routing.\"]]]]}", "comment_id": "601425dc7775f400017ea029", - "feature_image": "http://localhost:2368/content/images/2021/03/la_carniere.png", + "feature_image": "scripts/data/la_carniere.png", "featured": false, "status": "published", "visibility": "public", diff --git a/scripts/init-ghost.js b/scripts/init-ghost.js index 345ed027d3792a0966ff63452cb05fddeb406fe1..4ecc42cb5c8af6ff8afbcb25e2caebd34150efc0 100644 --- a/scripts/init-ghost.js +++ b/scripts/init-ghost.js @@ -100,8 +100,23 @@ function processImagesInHTML(html) { }); } +async function uploadPostImage(imagePath) +{ + let imagePromise = api.images.upload({ + ref: imagePath, + file: path.resolve(imagePath) + }); + + return Promise.resolve(imagePromise).then((url) => { + return url.url; + }).catch((error) => { + console.error(error); + return null + }) +} + + async function createPosts(deleteOnly) { - // Get existing posts api.posts .browse({ limit: 'all' }) .then(async (existingPosts) => { @@ -120,7 +135,11 @@ async function createPosts(deleteOnly) { // Creating new posts if (!deleteOnly) { console.log('-- Creating ' + postsData.length + ' posts --'); - _.forEach(postsData, (post) => { + _.forEach(postsData, async (post) => { + //upload de l'image en featured_image + if (post.feature_image) { + post.feature_image = await uploadPostImage(post.feature_image); + } api.posts .add(post, { source: 'html' }) .then((res) => { diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index ad0a493f351f0a25a9b66e18930f632f3c9f4966..3fae73b8f3970e7786f21eba848bef4ca20db68a 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -183,6 +183,6 @@ export class AdminController { @Delete('newsletterSubscription/:email') @ApiParam({ name: 'email', type: String, required: true }) public async unsubscribeUserFromNewsletter(@Param() params): Promise<NewsletterSubscription> { - return this.newsletterService.deleteOneEmail(params.email); + return this.newsletterService.newsletterUnsubscribe(params.email); } } diff --git a/src/categories/services/categories-accompagnement.service.spec.ts b/src/categories/services/categories-accompagnement.service.spec.ts index 2690850884d95e003009cd4395d8d7125889d03b..a889a46f2b6744ab3caed6d07a947a5c8f3399a2 100644 --- a/src/categories/services/categories-accompagnement.service.spec.ts +++ b/src/categories/services/categories-accompagnement.service.spec.ts @@ -1,11 +1,16 @@ import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; -import { CategoriesAccompagnement } from '../schemas/categoriesAccompagnement.schema'; +import { CreateCategoriesAccompagnement } from '../dto/create-categoriesAccompagnement.dto'; import { CategoriesAccompagnementService } from './categories-accompagnement.service'; describe('CategoriesAccompagnementService', () => { let service: CategoriesAccompagnementService; + const mockCategoriesAccompagnementModel = { + create: jest.fn(), + find: jest.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [], @@ -13,7 +18,7 @@ describe('CategoriesAccompagnementService', () => { CategoriesAccompagnementService, { provide: getModelToken('CategoriesAccompagnement'), - useValue: CategoriesAccompagnement, + useValue: mockCategoriesAccompagnementModel, }, ], }).compile(); @@ -24,4 +29,20 @@ describe('CategoriesAccompagnementService', () => { it('should be defined', () => { expect(service).toBeDefined(); }); + + it('should create categorie accompagnement', async () => { + const data: CreateCategoriesAccompagnement = { name: 'test', modules: [{ id: 'Test', text: 'Text du test' }] }; + const _doc = { _id: '5fbb92ef80a5c257dc0161f2', ...data }; + mockCategoriesAccompagnementModel.create.mockResolvedValueOnce(_doc); + expect(await service.create(data)).toEqual(_doc); + }); + + it('should findAll structures', async () => { + const data: CreateCategoriesAccompagnement[] = [ + { name: 'test', modules: [{ id: 'Test', text: 'Text du test' }] }, + { name: 'test2', modules: [{ id: 'Test2', text: 'Text du test test 2' }] }, + ]; + mockCategoriesAccompagnementModel.find.mockResolvedValueOnce(data); + expect(await service.findAll()).toEqual(data); + }); }); diff --git a/src/categories/services/categories-accompagnement.service.ts b/src/categories/services/categories-accompagnement.service.ts index 830ad0c4552e0d68ccdccacda1f5bbbe0d63c3de..a8a41d17713196746f749b3e88c8dd9be0dbc328 100644 --- a/src/categories/services/categories-accompagnement.service.ts +++ b/src/categories/services/categories-accompagnement.service.ts @@ -11,11 +11,10 @@ export class CategoriesAccompagnementService { ) {} public async create(createDto: CreateCategoriesAccompagnement): Promise<CategoriesAccompagnement> { - const createdStructure = new this.structureModel(createDto); - return createdStructure.save(); + return this.structureModel.create(createDto); } public async findAll(): Promise<CategoriesAccompagnement[]> { - return this.structureModel.find().exec(); + return this.structureModel.find(); } } diff --git a/src/categories/services/categories-formations.service.spec.ts b/src/categories/services/categories-formations.service.spec.ts index 4f079fdd4611597a976174206557113ead3b3194..6f7a63bdf1bd3e9bd2639fe6475d87cacd5bdffc 100644 --- a/src/categories/services/categories-formations.service.spec.ts +++ b/src/categories/services/categories-formations.service.spec.ts @@ -1,11 +1,16 @@ import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; -import { CategoriesFormations } from '../schemas/categoriesFormations.schema'; +import { CreateCategoriesFormations } from '../dto/create-categoriesFormations.dto'; import { CategoriesFormationsService } from './categories-formations.service'; describe('CategoriesFormationsService', () => { let service: CategoriesFormationsService; + const mockCategoriesFormationModel = { + create: jest.fn(), + find: jest.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [], @@ -13,7 +18,7 @@ describe('CategoriesFormationsService', () => { CategoriesFormationsService, { provide: getModelToken('CategoriesFormations'), - useValue: CategoriesFormations, + useValue: mockCategoriesFormationModel, }, ], }).compile(); @@ -24,4 +29,53 @@ describe('CategoriesFormationsService', () => { it('should be defined', () => { expect(service).toBeDefined(); }); + + it('should create categorie formation', async () => { + const data: CreateCategoriesFormations = { + name: 'test', + modules: [ + { + id: '2', + display_id: '2', + display_name: 'Modules APTIC - n°2', + text: 'Les conduites à risques et les bons usages du numérique', + digest: 'Les conduites à risques et les bons usages du numérique - 02', + }, + ], + }; + const _doc = { _id: '5fbb92ef80a5c257dc0161f2', ...data }; + mockCategoriesFormationModel.create.mockResolvedValueOnce(_doc); + expect(await service.create(data)).toEqual(_doc); + }); + + it('should findAll structures', async () => { + const data: CreateCategoriesFormations[] = [ + { + name: 'test', + modules: [ + { + id: '2', + display_id: '2', + display_name: 'Modules APTIC - n°2', + text: 'Les conduites à risques et les bons usages du numérique', + digest: 'Les conduites à risques et les bons usages du numérique - 02', + }, + ], + }, + { + name: 'test', + modules: [ + { + id: '1', + display_id: '1', + display_name: 'Modules APTIC - n°1', + text: 'Les conduites à risques et les bons usages', + digest: 'Les conduites à risques et les bons usages - 01', + }, + ], + }, + ]; + mockCategoriesFormationModel.find.mockResolvedValueOnce(data); + expect(await service.findAll()).toEqual(data); + }); }); diff --git a/src/categories/services/categories-formations.service.ts b/src/categories/services/categories-formations.service.ts index 71a6c1073ad5245541c651836b272195dd96397c..fab26f580d98135ce25f086a37136de2e2f9e095 100644 --- a/src/categories/services/categories-formations.service.ts +++ b/src/categories/services/categories-formations.service.ts @@ -9,15 +9,14 @@ export class CategoriesFormationsService { constructor(@InjectModel(CategoriesFormations.name) private structureModel: Model<CategoriesFormationsDocument>) {} public async create(createDto: CreateCategoriesFormations): Promise<CategoriesFormations> { - const createdStructure = new this.structureModel(createDto); - return createdStructure.save(); + return this.structureModel.create(createDto); } public async findAll(): Promise<CategoriesFormations[]> { - return this.structureModel.find().exec(); + return this.structureModel.find(); } - public findOne(categoryId: string): Promise<any> { + public findOne(categoryId: string): Promise<CategoriesFormations> { return this.structureModel.findOne({ id: categoryId }).select({ 'modules.id': 1 }).exec(); } } diff --git a/src/categories/services/categories-others.service.spec.ts b/src/categories/services/categories-others.service.spec.ts index 7c25eb3618f8bd75287bb29c7ebc52ba3256f33c..5de85c62c3669768f6064a8a2d54b2eb6a4a740e 100644 --- a/src/categories/services/categories-others.service.spec.ts +++ b/src/categories/services/categories-others.service.spec.ts @@ -1,11 +1,16 @@ import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; -import { CategoriesOthers } from '../schemas/categoriesOthers.schema'; +import { CreateCategoriesOthers } from '../dto/create-categoriesOthers.dto'; import { CategoriesOthersService } from './categories-others.service'; describe('CategoriesFormationsService', () => { let service: CategoriesOthersService; + const mockCategoriesOthersModel = { + create: jest.fn(), + find: jest.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [], @@ -13,7 +18,7 @@ describe('CategoriesFormationsService', () => { CategoriesOthersService, { provide: getModelToken('CategoriesOthers'), - useValue: CategoriesOthers, + useValue: mockCategoriesOthersModel, }, ], }).compile(); @@ -24,4 +29,20 @@ describe('CategoriesFormationsService', () => { it('should be defined', () => { expect(service).toBeDefined(); }); + + it('should create categorie accompagnement', async () => { + const data: CreateCategoriesOthers = { name: 'test', modules: [{ id: 'Test', text: 'Text du test' }] }; + const _doc = { _id: '5fbb92ef80a5c257dc0161f2', ...data }; + mockCategoriesOthersModel.create.mockResolvedValueOnce(_doc); + expect(await service.create(data)).toEqual(_doc); + }); + + it('should findAll structures', async () => { + const data: CreateCategoriesOthers[] = [ + { name: 'test', modules: [{ id: 'Test', text: 'Text du test' }] }, + { name: 'test2', modules: [{ id: 'Test2', text: 'Text du test test 2' }] }, + ]; + mockCategoriesOthersModel.find.mockResolvedValueOnce(data); + expect(await service.findAll()).toEqual(data); + }); }); diff --git a/src/categories/services/categories-others.service.ts b/src/categories/services/categories-others.service.ts index d229648e6d1b6fd8c8d1ad1dd07d059048c9c1c3..fd8b1b68d36d720784c98be2211a2e94dc088fdf 100644 --- a/src/categories/services/categories-others.service.ts +++ b/src/categories/services/categories-others.service.ts @@ -9,11 +9,10 @@ export class CategoriesOthersService { constructor(@InjectModel(CategoriesOthers.name) private structureModel: Model<CategoriesOthersDocument>) {} public async create(createDto: CreateCategoriesOthers): Promise<CategoriesOthers> { - const createdStructure = new this.structureModel(createDto); - return createdStructure.save(); + return this.structureModel.create(createDto); } public async findAll(): Promise<CategoriesOthers[]> { - return this.structureModel.find().exec(); + return this.structureModel.find(); } } diff --git a/src/configuration/config.ts b/src/configuration/config.ts index 97ab675e89c54d4e224c3ebec86ed372bbb45ce9..de4534cc695b6e734ed862ee60d283319a03253e 100644 --- a/src/configuration/config.ts +++ b/src/configuration/config.ts @@ -61,5 +61,9 @@ export const config = { ejs: 'structureDeletionNotification.ejs', json: 'structureDeletionNotification.json', }, + newApticStructure: { + ejs: 'newApticStructure.ejs', + json: 'newApticStructure.json', + }, }, }; diff --git a/src/mailer/mail-templates/adminStructureClaim.ejs b/src/mailer/mail-templates/adminStructureClaim.ejs index 1e7e162f39e671fdf5d6beb2e1765a7d284e37e5..91f347bac1cfa065a9e27a049e518796852e5f14 100644 --- a/src/mailer/mail-templates/adminStructureClaim.ejs +++ b/src/mailer/mail-templates/adminStructureClaim.ejs @@ -1,4 +1,11 @@ Bonjour,<br /> <br /> -Une nouvelle structure a été revendiquée. Pour valider ou refuser la demande, merci de vous rendre sur +La structure <%= structureName %> a été revendiquée par <%= user.name %> <%= user.surname %>. +<br /> +Voici les informations de la structure : <br /> +<%= structureAdress %><br /> +<%= structureDescription %><br /> +Et du demandeur : <br /> +<%= user.email %><br /> +Pour valider ou refuser la demande, merci de vous rendre sur <a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/admin">ce lien</a>. diff --git a/src/mailer/mail-templates/newApticStructure.ejs b/src/mailer/mail-templates/newApticStructure.ejs new file mode 100644 index 0000000000000000000000000000000000000000..0ac70f82b8eaf07d195cb07c9f64aeed6138b8c8 --- /dev/null +++ b/src/mailer/mail-templates/newApticStructure.ejs @@ -0,0 +1,6 @@ +Bonjour,<br /> +<br /> +Une nouvelle structure a été crée via Aptic. Ses caractéristiques sont : <br /> +- Nom : <%= name %><br /> +- Adresse : <%= address %><br /> +- Description : <%= description %><br /> diff --git a/src/mailer/mail-templates/newApticStructure.json b/src/mailer/mail-templates/newApticStructure.json new file mode 100644 index 0000000000000000000000000000000000000000..60799afdae187f6af93b9f977b52990466399a29 --- /dev/null +++ b/src/mailer/mail-templates/newApticStructure.json @@ -0,0 +1,3 @@ +{ + "subject": "Nouvelle structure Aptic" +} diff --git a/src/migrations/scripts/1640095148120-remove-equipments-null-values.ts b/src/migrations/scripts/1640095148120-remove-equipments-null-values.ts new file mode 100644 index 0000000000000000000000000000000000000000..400d666b2fca7e362e0ef725da32cdfc0aff521c --- /dev/null +++ b/src/migrations/scripts/1640095148120-remove-equipments-null-values.ts @@ -0,0 +1,56 @@ +import { Db } from 'mongodb'; +import { getDb } from '../migrations-utils/db'; + +export const up = async () => { + const db: Db = await getDb(); + + const cursor = db.collection('structures').find({}); + let document; + while ((document = await cursor.next())) { + const newDoc = document; + if (document.nbComputers === null) { + newDoc.nbComputers = 0; + } + if (document.nbPrinters === null) { + newDoc.nbPrinters = 0; + } + if (document.nbScanners === null) { + newDoc.nbScanners = 0; + } + if (document.nbTablets === null) { + newDoc.nbTablets = 0; + } + if (document.nbNumericTerminal === null) { + newDoc.nbNumericTerminal = 0; + } + await db.collection('structures').updateOne({ _id: document._id }, [{ $set: newDoc }]); + } + console.log(`Update done`); +}; + +export const down = async () => { + const db: Db = await getDb(); + const cursor = db.collection('structures').find({}); + + let document; + while ((document = await cursor.next())) { + const newDoc = document; + if (document.nbComputers === 0) { + newDoc.nbComputers = null; + } + if (document.nbPrinters === 0) { + newDoc.nbPrinters = null; + } + if (document.nbScanners === 0) { + newDoc.nbScanners = null; + } + if (document.nbTablets === 0) { + newDoc.nbTablets = null; + } + if (document.nbNumericTerminal === 0) { + newDoc.nbNumericTerminal = null; + } + await db.collection('structures').updateOne({ _id: document._id }, [{ $set: newDoc }]); + } + console.log(`Update done`); +}; diff --git a/src/newsletter/newsletter.service.spec.ts b/src/newsletter/newsletter.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..15cfedd88581e257b359f3297886181982df19a3 --- /dev/null +++ b/src/newsletter/newsletter.service.spec.ts @@ -0,0 +1,137 @@ +import { HttpModule, HttpStatus } from '@nestjs/common'; +import { getModelToken } from '@nestjs/mongoose'; +import { Test, TestingModule } from '@nestjs/testing'; +import { INewsletterSubscription } from './interface/newsletter-subscription.interface'; +import { NewsletterSubscription } from './newsletter-subscription.schema'; +import { NewsletterService } from './newsletter.service'; +describe('NewsletterService', () => { + let service: NewsletterService; + + const mockNewsletterModel = { + create: jest.fn(), + deleteOne: jest.fn(), + countDocuments: jest.fn(), + findOne: jest.fn(), + exec: jest.fn(), + find: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [HttpModule], + providers: [ + NewsletterService, + { + provide: getModelToken(NewsletterSubscription.name), + useValue: mockNewsletterModel, + }, + ], + }).compile(); + + service = module.get<NewsletterService>(NewsletterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('newsletterSubscribe', () => { + it('it should not add subscription for email test2@test.com : already exist', async () => { + const result = { email: 'test2@test.com' } as INewsletterSubscription; + jest + .spyOn(service, 'findOne') + .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => result); + try { + await service.newsletterSubscribe('test2@test.com'); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toEqual('Email already exists'); + expect(e.status).toEqual(HttpStatus.BAD_REQUEST); + } + }); + it('it should add a subscription for email test2@test.com', async () => { + const result: INewsletterSubscription = { email: 'test2@test.com' } as INewsletterSubscription; + const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' }; + jest + .spyOn(service, 'findOne') + .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined) + .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => result); + mockNewsletterModel.create.mockResolvedValueOnce(_doc); + + const subscription = await service.newsletterSubscribe('test2@test.com'); + expect(subscription).toEqual({ email: 'test2@test.com' }); + }); + }); + describe('newsletterUnsubscribe', () => { + it('it should not remove subscription for email test@test.com : does not exist', async () => { + const result: INewsletterSubscription = undefined; + jest + .spyOn(service, 'findOne') + .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => result); + try { + await service.newsletterUnsubscribe('test@test.com'); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toEqual('Invalid email'); + expect(e.status).toEqual(HttpStatus.BAD_REQUEST); + } + }); + it('it should remove a subscription for email test2@test.com', async () => { + const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' }; + const result = { + email: 'test2@test.com', + deleteOne: async () => _doc, + } as INewsletterSubscription; + jest + .spyOn(service, 'findOne') + .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => result); + + const subscription = await service.newsletterUnsubscribe('test2@test.com'); + expect(subscription).toEqual(_doc); + }); + }); + + describe('countNewsletterSubscriptions', () => { + it('it should count subscriptions', async () => { + mockNewsletterModel.countDocuments.mockResolvedValueOnce(69); + const count = await service.countNewsletterSubscriptions(); + + expect(count).toEqual(69); + }); + }); + + describe('findOne', () => { + it('it should not find a subscription with email test@test.com', async () => { + mockNewsletterModel.findOne.mockResolvedValueOnce(undefined); + const findOneEmail = await service.findOne('test@test.com'); + expect(findOneEmail).toEqual(undefined); + }); + it('it should find a subscription with email test2@test.com', async () => { + const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' } as INewsletterSubscription; + mockNewsletterModel.findOne.mockResolvedValueOnce(_doc); + const findOneEmail = await service.findOne('test2@test.com'); + expect(findOneEmail).toEqual(_doc); + }); + }); + describe('findAll', () => { + it('it should find all', async () => { + const _docs = [{ _id: 'a1aaaaa1a1', email: 'test2@test.com' } as INewsletterSubscription]; + mockNewsletterModel.find.mockResolvedValueOnce(_docs); + const findOneEmail = await service.findAll(); + expect(findOneEmail).toEqual(_docs); + }); + }); + describe('searchNewsletterSubscription', () => { + it('it should find 2 search result', async () => { + const _docs = [ + { _id: 'a1aaaaa1a1', email: 'test2@test.com' } as INewsletterSubscription, + { _id: 'bbbbb', email: 'test@test.com' } as INewsletterSubscription, + ]; + mockNewsletterModel.find.mockResolvedValueOnce(_docs); + const findOneEmail = await service.searchNewsletterSubscription('test'); + expect(findOneEmail.length).toBe(2); + }); + }); +}); diff --git a/src/newsletter/newsletter.service.ts b/src/newsletter/newsletter.service.ts index 89020eddbaa0808f25f0cedd9c30fba7bc29d1f0..c5f8363fbfc83d175b1fe5377e0de76386010e27 100644 --- a/src/newsletter/newsletter.service.ts +++ b/src/newsletter/newsletter.service.ts @@ -15,40 +15,31 @@ export class NewsletterService { if (existingEmail) { throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST); } - const createSubscription = new this.newsletterSubscriptionModel({ email: email }); - createSubscription.save(); - return await this.findOne(email); + await this.newsletterSubscriptionModel.create({ email: email }); + return this.findOne(email); } public async newsletterUnsubscribe(email: string): Promise<NewsletterSubscription> { - const subscription = await this.newsletterSubscriptionModel.findOne({ email: email }).exec(); + const subscription = await this.findOne(email); if (!subscription) { throw new HttpException('Invalid email', HttpStatus.BAD_REQUEST); } return subscription.deleteOne(); } - public async findOne(mail: string): Promise<NewsletterSubscription | undefined> { - return this.newsletterSubscriptionModel.findOne({ email: mail }).exec(); + public async findOne(mail: string): Promise<INewsletterSubscription | undefined> { + return this.newsletterSubscriptionModel.findOne({ email: mail }); } public async searchNewsletterSubscription(searchString: string): Promise<NewsletterSubscriptionDocument[]> { - return this.newsletterSubscriptionModel.find({ email: new RegExp(searchString, 'i') }).exec(); + return this.newsletterSubscriptionModel.find({ email: new RegExp(searchString, 'i') }); } public async countNewsletterSubscriptions(): Promise<number> { - return this.newsletterSubscriptionModel.countDocuments({}).exec(); - } - - public async deleteOneEmail(mail: string): Promise<NewsletterSubscription | undefined> { - const subscription = await this.newsletterSubscriptionModel.findOne({ email: mail }).exec(); - if (!subscription) { - throw new HttpException('Invalid email', HttpStatus.BAD_REQUEST); - } - return subscription.deleteOne(); + return this.newsletterSubscriptionModel.countDocuments({}); } public async findAll(): Promise<NewsletterSubscription[]> { - return await this.newsletterSubscriptionModel.find().exec(); + return this.newsletterSubscriptionModel.find(); } } diff --git a/src/posts/posts.controller.spec.ts b/src/posts/posts.controller.spec.ts index 39b091029861448339fa4a8c755dd634664a6d77..56371cebb1628a0cb17190a7e65f98ea7e199903 100644 --- a/src/posts/posts.controller.spec.ts +++ b/src/posts/posts.controller.spec.ts @@ -1,17 +1,38 @@ -import { HttpModule } from '@nestjs/common'; +import { HttpModule, HttpService } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { of } from 'rxjs'; import { ConfigurationModule } from '../configuration/configuration.module'; import { PostsController } from './posts.controller'; import { PostsService } from './posts.service'; -import { PostWithMeta } from './schemas/postWithMeta.schema'; +import { AxiosResponse } from 'axios'; describe('PostsController', () => { let controller: PostsController; + const httpServiceMock = { + get: jest.fn(), + }; + + const postServiceMock = { + getLocationTags: jest.fn(), + getPublicTags: jest.fn(), + getRegularTags: jest.fn(), + formatPosts: jest.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ConfigurationModule, HttpModule], - providers: [PostsService], + providers: [ + { + provide: PostsService, + useValue: postServiceMock, + }, + { + provide: HttpService, + useValue: httpServiceMock, + }, + ], controllers: [PostsController], }).compile(); @@ -22,23 +43,383 @@ describe('PostsController', () => { expect(controller).toBeDefined(); }); - it('should get pending attachments', async () => { - const result:PostWithMeta = {posts:[], meta:{pagination: null}}; - const query = ""; - jest.spyOn(controller, 'findAll').mockImplementation(async (): Promise<any> => result); - expect(await controller.findAll(query)).toBe(result); + describe('findAll', () => { + it('should get all posts', async () => { + const query = ''; + const result: AxiosResponse = { + data: { + posts: [ + { + id: '61c4847b0ff4550001505090', + uuid: 'f4ee5a37-a343-4cad-8a32-3f6cf87f9569', + title: 'Only feature image', + slug: 'only-feature-image', + html: '<p>Test</p>', + comment_id: '61c4847b0ff4550001505090', + feature_image: 'http://localhost:2368/content/images/2021/12/dacc-4.png', + featured: false, + visibility: 'public', + email_recipient_filter: 'none', + created_at: '2021-12-23T14:15:23.000+00:00', + updated_at: '2021-12-23T14:15:45.000+00:00', + published_at: '2021-12-23T14:15:45.000+00:00', + custom_excerpt: null, + codeinjection_head: null, + codeinjection_foot: null, + custom_template: null, + canonical_url: null, + tags: [Array], + authors: [Array], + primary_author: [Object], + primary_tag: [Object], + url: 'http://localhost:2368/only-feature-image/', + excerpt: + '« Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.\n' + + 'Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,\n' + + 'dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper\n' + + 'congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est\n' + + 'eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu\n' + + 'massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut\n' + + 'in risus volutpat libero pharetra tem', + reading_time: 2, + access: true, + send_email_when_published: false, + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + email_subject: null, + frontmatter: null, + }, + { + id: '61c4847b0ff4550001505090', + uuid: 'f4ee5a37-a343-4cad-8a32-3f6cf87f9569', + title: 'Only test image', + slug: 'only-test-image', + html: '<p>Test 2</p>', + comment_id: '61c4847b0ff4550001505090', + feature_image: 'http://localhost:2368/content/images/2021/12/test.png', + featured: false, + visibility: 'public', + email_recipient_filter: 'none', + created_at: '2021-12-23T14:15:23.000+00:00', + updated_at: '2021-12-23T14:15:45.000+00:00', + published_at: '2021-12-23T14:15:45.000+00:00', + custom_excerpt: null, + codeinjection_head: null, + codeinjection_foot: null, + custom_template: null, + canonical_url: null, + tags: [Array], + authors: [Array], + primary_author: [Object], + primary_tag: [Object], + url: 'http://localhost:2368/only-feature-image/', + excerpt: + '« Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.\n' + + 'Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,\n' + + 'dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper\n' + + 'congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est\n' + + 'eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu\n' + + 'massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut\n' + + 'in risus volutpat libero pharetra tem', + reading_time: 2, + access: true, + send_email_when_published: false, + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + email_subject: null, + frontmatter: null, + }, + { + id: '61c4847b0ff4550001505090', + uuid: 'f4ee5a37-a343-4cad-8a32-3f6cf87f9569', + title: 'Only toto image', + slug: 'only-toto-image', + html: '<p>Test 3</p>', + comment_id: '61c4847b0ff4550001505090', + feature_image: 'http://localhost:2368/content/images/2021/12/dacc-4.png', + featured: false, + visibility: 'public', + email_recipient_filter: 'none', + created_at: '2021-12-23T14:15:23.000+00:00', + updated_at: '2021-12-23T14:15:45.000+00:00', + published_at: '2021-12-23T14:15:45.000+00:00', + custom_excerpt: null, + codeinjection_head: null, + codeinjection_foot: null, + custom_template: null, + canonical_url: null, + tags: [Array], + authors: [Array], + primary_author: [Object], + primary_tag: [Object], + url: 'http://localhost:2368/only-feature-image/', + excerpt: + '« Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.\n' + + 'Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,\n' + + 'dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper\n' + + 'congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est\n' + + 'eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu\n' + + 'massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut\n' + + 'in risus volutpat libero pharetra tem', + reading_time: 2, + access: true, + send_email_when_published: false, + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + email_subject: null, + frontmatter: null, + }, + ], + meta: { pagination: { page: 1, limit: 15, pages: 1, total: 2, next: null, prev: null } }, + }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + }; + jest.spyOn(httpServiceMock, 'get').mockImplementationOnce(() => of(result)); + const response = await controller.findAll(query); + expect(response.posts.length).toEqual(3); + expect(response.meta.pagination.limit).toEqual(15); + expect(response.meta.pagination.page).toEqual(1); + expect(response.meta.pagination.total).toEqual(2); + }); }); - it('should get pending attachments', async () => { - const result = { posts:[] }; - jest.spyOn(controller, 'findAllTags').mockImplementation(async (): Promise<any> => result); - expect(await controller.findAllTags()).toBe(result); + describe('findAllTags', () => { + it('should get all tags', async () => { + postServiceMock.getLocationTags.mockImplementationOnce(() => [ + { + id: '61b74b2c0d3b9800018ca3df', + name: 'oullins', + slug: 'oullins', + description: 'commune', + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:24.000Z', + updated_at: '2021-12-13T13:31:25.000Z', + url: 'http://localhost:2368/tag/oullins/', + }, + { + id: '61b74b2c0d3b9800018ca3df', + name: 'oullins', + slug: 'oullins', + description: 'commune', + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:24.000Z', + updated_at: '2021-12-13T13:31:25.000Z', + url: 'http://localhost:2368/tag/oullins/', + }, + { + id: '61b74b2c0d3b9800018ca3e0', + name: 'Bron', + slug: 'bron', + description: 'commune', + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:24.000Z', + updated_at: '2021-12-13T13:31:24.000Z', + url: 'http://localhost:2368/404/', + }, + ]); + postServiceMock.getPublicTags.mockImplementationOnce(() => [ + { + id: '61b74b2d0d3b9800018ca3f6', + name: 'Séniors (+ de 65ans)', + slug: 'seniors-de-65ans', + description: 'public', + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:25.000Z', + updated_at: '2021-12-13T13:31:25.000Z', + url: 'http://localhost:2368/404/', + }, + { + id: '61b74b2d0d3b9800018ca3f7', + name: 'Allophones', + slug: 'allophones', + description: 'public', + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:25.000Z', + updated_at: '2021-12-13T13:31:25.000Z', + url: 'http://localhost:2368/404/', + }, + ]); + postServiceMock.getRegularTags.mockImplementationOnce(() => [ + { + id: '61b74b2d0d3b9800018ca3fa', + name: 'Études', + slug: 'etudes', + description: null, + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:25.000Z', + updated_at: '2021-12-13T13:31:25.000Z', + url: 'http://localhost:2368/tag/etudes/', + }, + ]); + const result = await controller.findAllTags(); + expect(result.commune.length).toBe(3); + expect(result.others.length).toBe(1); + expect(result.public.length).toBe(2); + }); }); - it('should get pending attachments', async () => { - const result = { public:[], comune:[], others:[] }; - const id = "78945945" - jest.spyOn(controller, 'getPostbyId').mockImplementation(async (): Promise<any> => result); - expect(await controller.getPostbyId(id)).toBe(result); + describe('getPostbyId', () => { + it('should get post Hello by id 61c4847b0ff4550001505090', async () => { + const data = [ + { + id: '61c4847b0ff4550001505090', + uuid: 'f4ee5a37-a343-4cad-8a32-3f6cf87f9569', + title: 'Hello', + slug: 'hello', + html: '<p>Test</p>', + comment_id: '61c4847b0ff4550001505090', + feature_image: 'http://localhost:2368/content/images/2021/12/dacc-4.png', + featured: false, + visibility: 'public', + email_recipient_filter: 'none', + created_at: '2021-12-23T14:15:23.000+00:00', + updated_at: '2021-12-23T14:15:45.000+00:00', + published_at: '2021-12-23T14:15:45.000+00:00', + custom_excerpt: null, + codeinjection_head: null, + codeinjection_foot: null, + custom_template: null, + canonical_url: null, + tags: [Array], + authors: [Array], + primary_author: [Object], + primary_tag: [Object], + url: 'http://localhost:2368/hello/', + excerpt: + '« Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.\n' + + 'Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,\n' + + 'dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper\n' + + 'congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est\n' + + 'eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu\n' + + 'massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut\n' + + 'in risus volutpat libero pharetra tem', + reading_time: 2, + access: true, + send_email_when_published: false, + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + email_subject: null, + frontmatter: null, + }, + ]; + const axiosResult: AxiosResponse = { + data: { + posts: data, + }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + }; + httpServiceMock.get.mockImplementationOnce(() => of(axiosResult)); + postServiceMock.formatPosts.mockImplementationOnce(() => data); + const result = await (await controller.getPostbyId('61c4847b0ff4550001505090')).toPromise(); + expect(result).toStrictEqual({ posts: [data] }); + }); }); }); diff --git a/src/posts/posts.controller.ts b/src/posts/posts.controller.ts index d947390c55e7a81214f5228c8d558d23dab0b2a8..0652fc5ae92355154d2778545387241f17ac6a53 100644 --- a/src/posts/posts.controller.ts +++ b/src/posts/posts.controller.ts @@ -9,6 +9,7 @@ import { PostWithMeta } from './schemas/postWithMeta.schema'; @Controller('posts') export class PostsController { + private readonly logger = new Logger(PostsController.name); constructor(private readonly httpService: HttpService, private readonly postsService: PostsService) {} @Get() @@ -36,7 +37,7 @@ export class PostsController { .pipe( map((response) => response.data), catchError((err) => { - Logger.error(err); + this.logger.error(err); throw new HttpException('Invalid ghost configuration', HttpStatus.BAD_REQUEST); }) ); diff --git a/src/posts/posts.service.spec.ts b/src/posts/posts.service.spec.ts index 5b01e555650ff0b0f58f1a33e8f3f352846d4fbe..90d17e16278895ed92a474666fda924c18c171d9 100644 --- a/src/posts/posts.service.spec.ts +++ b/src/posts/posts.service.spec.ts @@ -1,10 +1,210 @@ import { HttpModule } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ConfigurationModule } from '../configuration/configuration.module'; +import { TagEnum } from './enums/tag.enum'; import { PostsService } from './posts.service'; +import { Tag } from './schemas/tag.schema'; +import * as _ from 'lodash'; +import { Post } from './schemas/post.schema'; describe('PostsService', () => { let service: PostsService; + // let configService: ConfigurationServiceMock; + + const locationtags = [ + { + id: '61b74b2c0b9800018ca3df', + name: 'oullins', + slug: 'oullins', + description: 'commune', + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:24.000Z', + updated_at: '2021-12-13T13:31:25.000Z', + url: 'http://localhost:2368/tag/oullins/', + }, + { + id: '61b74b2c0d9800018ca3df', + name: 'oullins', + slug: 'oullins', + description: 'commune', + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:24.000Z', + updated_at: '2021-12-13T13:31:25.000Z', + url: 'http://localhost:2368/tag/oullins/', + }, + { + id: '61b74b2d3b9800018ca3e0', + name: 'Bron', + slug: 'bron', + description: 'commune', + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:24.000Z', + updated_at: '2021-12-13T13:31:24.000Z', + url: 'http://localhost:2368/404/', + }, + ]; + + const publictags = [ + { + id: '61b74b2d0d3b9800018ca3f6', + name: 'Séniors (+ de 65ans)', + slug: 'seniors-de-65ans', + description: 'public', + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:25.000Z', + updated_at: '2021-12-13T13:31:25.000Z', + url: 'http://localhost:2368/404/', + }, + { + id: '61b74b2d0d3b98000ca3f7', + name: 'Allophones', + slug: 'allophones', + description: 'public', + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:25.000Z', + updated_at: '2021-12-13T13:31:25.000Z', + url: 'http://localhost:2368/404/', + }, + ]; + + const otherTags = [ + { + id: '61b74b2d0d3b980001a3fa', + name: 'Études', + slug: 'etudes', + description: null, + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:25.000Z', + updated_at: '2021-12-13T13:31:25.000Z', + url: 'http://localhost:2368/tag/etudes/', + }, + { + id: '61b74b2d0d3b980001a3fa', + name: 'A la une', + slug: 'a-la-une', + description: null, + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:25.000Z', + updated_at: '2021-12-13T13:31:25.000Z', + url: 'http://localhost:2368/tag/a-la-une/', + }, + { + id: '61b74b2d0d3b980001a3fa', + name: 'Infos', + slug: 'infos', + description: null, + feature_image: null, + visibility: 'public', + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + created_at: '2021-12-13T13:31:25.000Z', + updated_at: '2021-12-13T13:31:25.000Z', + url: 'http://localhost:2368/tag/infos/', + }, + ]; + + const tagsData = [...locationtags, ...publictags, ...otherTags]; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -13,9 +213,114 @@ describe('PostsService', () => { }).compile(); service = module.get<PostsService>(PostsService); + jest.spyOn(service, 'getTags').mockImplementationOnce(async (): Promise<Tag[]> => tagsData); }); it('should be defined', () => { expect(service).toBeDefined(); }); + + it('should get tags', async () => { + expect(await service.getTags()).toEqual(tagsData); + }); + + it('should get location tags', async () => { + expect(await service.getLocationTags()).toEqual(locationtags); + }); + + it('should get public tags', async () => { + expect(await service.getPublicTags()).toEqual(publictags); + }); + + it('should get regular tags', async () => { + expect(await service.getRegularTags()).toEqual([otherTags[1], otherTags[2], otherTags[0]]); + }); + describe('arraymove', () => { + it('should order tags for display: A La Une tag should be set to first index', () => { + const dataCopy = [...otherTags]; + service.arraymove(dataCopy, _.findIndex(dataCopy, { slug: TagEnum.aLaUne }), 0); + expect(dataCopy).toEqual([otherTags[1], otherTags[0], otherTags[2]]); + }); + it('should order tags for display: Info tag should be set to second index', () => { + const dataCopy = [...otherTags]; + expect(service.arraymove(dataCopy, _.findIndex(dataCopy, { slug: TagEnum.infos }), 1)).toEqual([ + otherTags[0], + otherTags[2], + otherTags[1], + ]); + }); + it('should order tags for display: A La Une tag should be set to first index and Info to second', () => { + const dataCopy = [...otherTags]; + service.arraymove(dataCopy, _.findIndex(dataCopy, { slug: TagEnum.aLaUne }), 0); + expect(service.arraymove(dataCopy, _.findIndex(dataCopy, { slug: TagEnum.infos }), 1)).toEqual([ + otherTags[1], + otherTags[2], + otherTags[0], + ]); + }); + }); + + describe('formatPosts', () => { + const postToFormat: Post = { + id: '61c4847b0ff4550001505090', + uuid: 'f4ee5a37-a343-4cad-8a32-3f6cf87f9569', + title: 'Only feature image', + slug: 'only-feature-image', + html: '<p>Test</p>', + comment_id: '61c4847b0ff4550001505090', + feature_image: 'http://localhost:2368/content/images/2021/12/dacc-4.png', + featured: false, + visibility: 'public', + email_recipient_filter: 'none', + created_at: '2021-12-23T14:15:23.000+00:00', + updated_at: '2021-12-23T14:15:45.000+00:00', + published_at: '2021-12-23T14:15:45.000+00:00', + custom_excerpt: null, + codeinjection_head: null, + codeinjection_foot: null, + custom_template: null, + canonical_url: null, + tags: [], + authors: [], + primary_author: [], + primary_tag: [], + url: 'http://localhost:2368/only-feature-image/', + excerpt: + '« Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.\n' + + 'Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,\n' + + 'dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper\n' + + 'congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est\n' + + 'eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu\n' + + 'massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut\n' + + 'in risus volutpat libero pharetra tem', + reading_time: '2', + access: true, + send_email_when_published: false, + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + email_subject: null, + frontmatter: null, + }; + it('should format post with no custom expert', () => { + const objCp = { ...postToFormat }; + let result = { ...postToFormat }; + result.excerpt = 'Inconnu'; + result.feature_image = 'https://localhost/blog/content/images/2021/12/dacc-4.png'; + expect(service.formatPosts(objCp)).toEqual(result); + }); + it('should format post with custom expert', () => { + let objCp = { ...postToFormat }; + objCp.custom_excerpt = 'Toto'; + let result = { ...postToFormat }; + result.custom_excerpt = 'Toto'; + result.feature_image = 'https://localhost/blog/content/images/2021/12/dacc-4.png'; + expect(service.formatPosts(objCp)).toEqual(result); + }); + }); }); diff --git a/src/posts/posts.service.ts b/src/posts/posts.service.ts index f239e7282af4f5d490de5648d9d3c10ebb731ace..18adcffd400647853778ec2a4675834f3864d95c 100644 --- a/src/posts/posts.service.ts +++ b/src/posts/posts.service.ts @@ -72,13 +72,14 @@ export class PostsService { if (!postData.custom_excerpt) { postData.excerpt = 'Inconnu'; } + const test = `<p>Test qui va bien Test qui va bienTest qui va bienTest qui va bienTest qui va bienTest qui va bienTest qui va bienTest qui va bienTest qui va bienTest qui va bienTest qui va bienTest qui va bienTest qui va bienTest qui va bienTest qui va bienTest qui va bienTest qui va bien.</p><figure class=\"kg-card kg-image-card\"><img src=\"https://resin-dev.grandlyon.com/content/images/2022/01/resin-logo-1200x630.png\" class=\"kg-image\" alt loading=\"lazy\" width=\"1200\" height=\"630\" srcset=\"https://resin-dev.grandlyon.com/content/images/size/w600/2022/01/resin-logo-1200x630.png 600w, https://resin-dev.grandlyon.com/content/images/size/w1000/2022/01/resin-logo-1200x630.png 1000w, https://resin-dev.grandlyon.com/content/images/2022/01/resin-logo-1200x630.png 1200w\" sizes=\"(min-width: 720px) 720px\"></figure>`; // Handle image display. Rewrite image URL to fit ghost infra issue. if (postData.feature_image && !this.configService.isLocalConf()) { postData.feature_image = `https://${this.configService.config.host}/blog/content${ postData.feature_image.split('/content')[1] }`; const regex = /(https?:\/\/ghost):(\d*)?/g; - postData.html = postData.html.replace(regex, `https://${this.configService.config.host}`); + postData.html = postData.html.replace(regex, `https://${this.configService.config.host}/blog`); } return postData; } diff --git a/src/posts/schemas/post.schema.ts b/src/posts/schemas/post.schema.ts index 3e2ac2ca992fc0be11e97e73b2e13f6730a6aa75..6e2e8cc22f9bd07f2d0c75f0c3327d7e2a91b10a 100644 --- a/src/posts/schemas/post.schema.ts +++ b/src/posts/schemas/post.schema.ts @@ -27,4 +27,13 @@ export class Post { meta_title: string; meta_description: string; email_subject: string; + codeinjection_head: string; + codeinjection_foot: string; + custom_template: string; + canonical_url: string; + tags: []; + authors: []; + primary_author: []; + primary_tag: []; + frontmatter: []; } diff --git a/src/structures/services/aptic-structures.service.ts b/src/structures/services/aptic-structures.service.ts index 766a7513fbdfe2cfeb5c88a2d6d86de0acdebbd4..978a2ecf9d3093ec35e75498b9c863db7cfd6113 100644 --- a/src/structures/services/aptic-structures.service.ts +++ b/src/structures/services/aptic-structures.service.ts @@ -17,6 +17,7 @@ import { StructuresSearchService } from './structures-search.service'; @Injectable() export class ApticStructuresService { + private readonly logger = new Logger(ApticStructuresService.name); constructor( private readonly httpService: HttpService, private readonly userService: UsersService, @@ -46,13 +47,15 @@ export class ApticStructuresService { this.createApticStructures(structure); }, (err) => { - Logger.log(err); + this.logger.log(err); } ); }); }, (err) => { - Logger.log(`getApticStructures error on postal code: ${postalCode}. Code: ${err}`); + this.logger.log( + `formatApticStructures | getApticStructures error on postal code: ${postalCode}. Code: ${err}` + ); } ); }); @@ -65,7 +68,7 @@ export class ApticStructuresService { private async createApticStructures(structure: ApticStructure): Promise<any> { this.structureAlreadyExist(structure).then(async (exist) => { if (!exist) { - Logger.log(`Create structure : ${structure.name}`, 'ApticStructuresService - createApticStructures'); + this.logger.log(`createApticStructures | Create structure : ${structure.name}`); const createdStructure = new this.structureModel(); // Known fields createdStructure.structureName = structure.name; @@ -80,11 +83,11 @@ export class ApticStructuresService { createdStructure.publics = ['toutPublic']; createdStructure.accountVerified = true; createdStructure.freeWorkShop = false; - createdStructure.nbComputers = null; - createdStructure.nbPrinters = null; - createdStructure.nbScanners = null; - createdStructure.nbTablets = null; - createdStructure.nbNumericTerminal = null; + createdStructure.nbComputers = 0; + createdStructure.nbPrinters = 0; + createdStructure.nbScanners = 0; + createdStructure.nbTablets = 0; + createdStructure.nbNumericTerminal = 0; // Address createdStructure.coord = [structure.address.gpsLng, structure.address.gpsLat]; createdStructure.address = this.formatAddress(structure); @@ -97,6 +100,7 @@ export class ApticStructuresService { createdStructure.save(); this.structuresSearchService.indexStructure(createdStructure); // Send admin weird structure mail + this.userService.sendAdminApticNewStructureMail(createdStructure); this.verifyDuplication(createdStructure); } }); @@ -182,7 +186,7 @@ export class ApticStructuresService { public getMetopoleMunicipality(): void { const req = 'https://download.data.grandlyon.com/ws/grandlyon/adr_voie_lieu.adrcomgl/all.json?maxfeatures=-1&start=1'; - Logger.log(`Request : ${req}`, 'ApticStructuresService - getMetopoleMunicipality'); + this.logger.log(`getMetopoleMunicipality | Request : ${req}`, ''); this.httpService.get(encodeURI(req)).subscribe( (data) => { const inseeArray = data.data.values.map((municipality) => { @@ -195,19 +199,19 @@ export class ApticStructuresService { this.formatApticStructures(postalCodeArray); }); }, - (err) => Logger.error(err) + (err) => this.logger.error(err) ); } public getPostalCodeWithINSEE(inseeCode: string): Observable<AxiosResponse<any>> { const req = `https://geo.api.gouv.fr/communes/${inseeCode}?fields=codesPostaux&format=json`; - Logger.log(`Request : ${req}`, 'ApticStructuresService - getMetopoleMunicipality'); + this.logger.debug(`getMetopoleMunicipality | Request : ${req}`); return this.httpService.get(encodeURI(req)); } public getApticStructures(postalCodeData: string): Observable<AxiosResponse<{ presencePoints: ApticStructure[] }>> { const req = `https://aptisearch-api.aptic.fr/v1/postal-code/${postalCodeData}`; - Logger.log(`Request : ${req}`, 'ApticStructuresService'); + this.logger.debug(`getApticStructures | Request : ${req}`); return this.httpService.get(req, { headers: { api_key: process.env.APTIC_TOKEN, @@ -220,7 +224,7 @@ export class ApticStructuresService { rejectUnauthorized: false, }); const req = `https://aptisearch-api.aptic.fr/v1/catalog/${catalogId}/services`; - Logger.log(`Request : ${req}`, 'ApticStructuresService'); + this.logger.log(`getApticStructureOffer | Request : ${req}`); return this.httpService.get(req, { httpsAgent: agent, headers: { diff --git a/src/structures/services/structure.service.spec.ts b/src/structures/services/structure.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..82128e4105f934f8062832a0b90b5cc21b25ab4e --- /dev/null +++ b/src/structures/services/structure.service.spec.ts @@ -0,0 +1,109 @@ +import { HttpModule, HttpStatus } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ElasticsearchService } from '@nestjs/elasticsearch'; +import { getModelToken } from '@nestjs/mongoose'; +import { Test, TestingModule } from '@nestjs/testing'; +import { UsersServiceMock } from '../../../test/mock/services/user.mock.service'; +import { ConfigurationService } from '../../configuration/configuration.service'; +import { MailerModule } from '../../mailer/mailer.module'; +import { MailerService } from '../../mailer/mailer.service'; +import { SearchModule } from '../../search/search.module'; +import { UsersService } from '../../users/users.service'; +import { structureDto } from '../dto/structure.dto'; +import { Structure } from '../schemas/structure.schema'; +import { StructuresSearchService } from './structures-search.service'; +import { StructuresService } from './structures.service'; +describe('StructuresService', () => { + let service: StructuresService; + + const mockStructureModel = { + create: jest.fn(), + deleteOne: jest.fn(), + countDocuments: jest.fn(), + findOne: jest.fn(), + exec: jest.fn(), + find: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [HttpModule, MailerModule, SearchModule, ConfigModule], + providers: [ + StructuresService, + ConfigurationService, + StructuresSearchService, + { + provide: getModelToken(Structure.name), + useValue: mockStructureModel, + }, + { + provide: UsersService, + useClass: UsersServiceMock, + }, + ], + }).compile(); + + service = module.get<StructuresService>(StructuresService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should Initiate structure', () => { + const res = service.initiateStructureIndex(); + expect(res).toBeTruthy(); + }); + + it('should searchForStructures', () => { + let res = service.searchForStructures('a', [{ nbPrinters: '1' }]); + expect(res).toBeTruthy(); + res = service.searchForStructures('a'); + expect(res).toBeTruthy(); + }); + + it('should create structure', () => { + const structure = new structureDto(); + let res = service.create(null, structure); + expect(res).toBeTruthy(); + res = service.create('tsfsf6296', structure); + expect(res).toBeTruthy(); + }); + + it('should search structure', () => { + const filters = [{ nbPrinters: '1' }]; + let res = service.search('', filters); + expect(res).toBeTruthy(); + res = service.search(null, filters); + expect(res).toBeTruthy(); + res = service.search(null); + expect(res).toBeTruthy(); + }); + + it('should find all structures', () => { + const res = service.findAll(); + expect(res).toBeTruthy(); + }); + + it('should find all unclaimed structures', () => { + const res = service.findAllUnclaimed(); + expect(res).toBeTruthy(); + }); + + it('should find all formated structures', () => { + const res = service.findAllFormated(null, null, null); + expect(res).toBeTruthy(); + }); + + it('should populate ES', () => { + const res = service.populateES(); + expect(res).toBeTruthy(); + }); + + it('should report structure Error', () => { + let res = service.reportStructureError('6093ba0e2ab5775cfc01ed3e', ''); + expect(res).toBeTruthy(); + res = service.reportStructureError(null, ''); + expect(res).toBeTruthy(); + }); +}); diff --git a/src/structures/services/structures.service.ts b/src/structures/services/structures.service.ts index d5eb2c1eb593a6d49db7b508164aa3168d9d8994..b58185d9d840c3c2e5ce30963d07fb5647ac879f 100644 --- a/src/structures/services/structures.service.ts +++ b/src/structures/services/structures.service.ts @@ -430,7 +430,8 @@ export class StructuresService { public async sendAdminStructureNotification( structure: StructureDocument, templateLocation: any, - jsonConfigLocation: any + jsonConfigLocation: any, + user: any = null ) { const uniqueAdminEmails = [...new Set((await this.userService.getAdmins()).map((admin) => admin.email))].map( (item) => { @@ -438,14 +439,21 @@ export class StructuresService { } ); + //ici récupérer le user Actuel avec le service afin de remplir le mail. const config = this.mailerService.config; const ejsPath = this.mailerService.getTemplateLocation(templateLocation); const jsonConfig = this.mailerService.loadJsonConfig(jsonConfigLocation); - const html = await ejs.renderFile(ejsPath, { config, id: structure ? structure._id : 0, structureName: structure ? structure.structureName : '', + structureAdress: structure + ? structure.address.numero + ? `${structure.address.numero} ${structure.address.street} ${structure.address.commune}` + : `${structure.address.street} ${structure.address.commune}` + : '', + structureDescription: structure ? structure.otherDescription : '', + user: user, }); this.mailerService.send(uniqueAdminEmails, jsonConfig.subject, html); } diff --git a/src/structures/structures.controller.spec.ts b/src/structures/structures.controller.spec.ts index 1eee0eaaa2d68dcb9b33924f6c2c58ad684808e2..6762f14c34fca751913f7ded03c94dff3fcf3b96 100644 --- a/src/structures/structures.controller.spec.ts +++ b/src/structures/structures.controller.spec.ts @@ -12,6 +12,7 @@ import { CategoriesFormationsService } from '../categories/services/categories-f import { CategoriesOthersService } from '../categories/services/categories-others.service'; import { TempUserService } from '../temp-user/temp-user.service'; import { UsersService } from '../users/users.service'; +import { CreateStructureDto } from './dto/create-structure.dto'; import { StructuresService } from './services/structures.service'; import { StructuresController } from './structures.controller'; describe('AuthController', () => { @@ -60,8 +61,178 @@ describe('AuthController', () => { expect(controller).toBeDefined(); }); + it('should get structure coordinates', async () => { + const coords = controller.getCoordinates('Lyon'); + expect(coords).toBeTruthy(); + }); + + it('should create structure', async () => { + const structure: CreateStructureDto = { + idUser: '1', + structure: null, + }; + const res = controller.create(structure); + expect(res).toBeTruthy(); + }); + + it('should update structure after ownerVerify', async () => { + const structureId = '1'; + const res = controller.updateAfterOwnerVerify(structureId); + expect(res).toBeTruthy(); + }); + + it('should update structure', async () => { + const structureService = new StructuresServiceMock(); + const structure = structureService.findOne('6093ba0e2ab5775cfc01ed3e'); + + const structureId = '1'; + const res = await controller.update(structureId, { + numero: null, + deletedAt: null, + remoteAccompaniment: null, + ...structure, + }); + expect(res.structureName).toBe('a'); + }); + + it('should get all structure', async () => { + const res = await controller.findAll(); + expect(res.length).toBe(2); + }); + + it('should get all Formated structure', async () => { + const res = await controller.findAllFormated(); + expect(res.length).toBe(2); + }); + + it('should search structure', async () => { + const res = controller.search(null, null); + expect(res).toBeTruthy(); + }); + + it('should reset Search Index', async () => { + const res = controller.resetES(); + expect(res).toBeTruthy(); + }); + + it('should see if structure is claimed', async () => { + const res = controller.isClaimed('1'); + expect(res).toBeTruthy(); + }); + + it('should claim structure', async () => { + const userMock = new UsersServiceMock(); + const user = userMock.findOne('pauline.dupont@mii.com'); + const res = controller.claim('1', { + phone: null, + resetPasswordToken: null, + changeEmailToken: null, + newEmail: null, + pendingStructuresLink: null, + structuresLink: null, + structureOutdatedMailSent: null, + email: user.email, + name: user.name, + surname: user.surname, + emailVerified: true, + password: user.password, + validationToken: null, + role: null, + }); + expect(res).toBeTruthy(); + }); + + it('should count', async () => { + const res = controller.countCategories([{ id: 'equipmentsAndServices', text: 'wifiEnAccesLibre' }]); + expect(res).toBeTruthy(); + }); + + it('should search an address', async () => { + const res = controller.searchAddress({ searchQuery: 'Rue Alphonse Daudet' }); + expect(res).toBeTruthy(); + }); + + it('should find struct', async () => { + let res = controller.find('6093ba0e2ab5775cfc01ed3e'); + expect(res).toBeTruthy(); + res = controller.find(''); + expect(res).toBeTruthy(); + }); + + it('should find struct with owners', async () => { + const res = controller.findWithOwners('6093ba0e2ab5775cfc01ed3e', { emailUser: 'pauline.dupont@mii.com' }); + expect(res).toBeTruthy(); + }); + + it('should delete struct', async () => { + const res = controller.delete('6093ba0e2ab5775cfc01ed3e'); + expect(res).toBeTruthy(); + }); + + it('should add Owner', async () => { + let res = controller.addOwner('6093ba0e2ab5775cfc01ed3e', { email: 'pauline.dupont@mii.com' }); + expect(res).toBeTruthy(); + res = controller.addOwner('6093ba0e2ab5775cfc01ed3e', { email: 'pauline.dupont@mii.fr' }); + expect(res).toBeTruthy(); + res = controller.addOwner('', { email: 'pauline.dupont@mii.fr' }); + expect(res).toBeTruthy(); + }); + + it('should remove Owner', async () => { + let res = controller.removeOwner('6093ba0e2ab5775cfc01ed3e', 'tsfsf6296'); + expect(res).toBeTruthy(); + res = controller.removeOwner('6093ba0e2ab5775cfc01ed3e', '1'); + expect(res).toBeTruthy(); + res = controller.removeOwner('', '1'); + expect(res).toBeTruthy(); + }); + + it('should join user', async () => { + const userMock = new UsersServiceMock(); + const user = userMock.findOne('pauline.dupont@mii.com'); + let res = controller.join('6093ba0e2ab5775cfc01ed3e', { + phone: null, + resetPasswordToken: null, + changeEmailToken: null, + newEmail: null, + pendingStructuresLink: null, + structuresLink: null, + structureOutdatedMailSent: null, + email: user.email, + name: user.name, + surname: user.surname, + emailVerified: true, + password: user.password, + validationToken: null, + role: null, + }); + expect(res).toBeTruthy(); + res = controller.join('', null); + expect(res).toBeTruthy(); + res = controller.join('6093ba0e2ab5775cfc01ed3e', null); + expect(res).toBeTruthy(); + }); + + it('should join in struct', async () => { + let res = controller.joinValidation('6093ba0e2ab5775cfc01ed3e', 'true', 'tsfsf6296'); + expect(res).toBeTruthy(); + res = controller.joinValidation('6093ba0e2ab5775cfc01ed3e', 'true', ''); + expect(res).toBeTruthy(); + res = controller.joinValidation('', 'true', ''); + expect(res).toBeTruthy(); + }); + + it('should remove user from struct', async () => { + const res = controller.joinValidation('6093ba0e2ab5775cfc01ed3e', 'false', 'tsfsf6296'); + expect(res).toBeTruthy(); + }); + + it('should report any structure error', async () => { + const res = controller.reportStructureError({ structureId: '6093ba0e2ab5775cfc01ed3e', content: null }); + expect(res).toBeTruthy(); + }); + //TODO: test structure controler endpoint - //create, search, updateAccountVerified, update, findAll, findAllFormated, isClaimed - //updateStructureLinkedClaim, countCategories, searchAddress, find, findWithOwners - //delete, addOwner, join, joinValidation, removeOwner, reportStructureError + //updateAccountVerified, + //updateStructureLinkedClaim }); diff --git a/src/structures/structures.controller.ts b/src/structures/structures.controller.ts index 65dcfd00b0b4d5495ce84306e940f99a82982981..8d313a822ff90e89b1cf4cb8ddd9f68373424e99 100644 --- a/src/structures/structures.controller.ts +++ b/src/structures/structures.controller.ts @@ -108,7 +108,8 @@ export class StructuresController { @Post(':id/claim') public async claim(@Param('id') idStructure: string, @Body() user: User): Promise<Types.ObjectId[]> { - return this.userService.updateStructureLinkedClaim(user.email, idStructure); + const structure = await this.structureService.findOne(idStructure); + return this.userService.updateStructureLinkedClaim(user.email, idStructure, structure); } @Post('count') diff --git a/src/tcl/tclStopPoint.service.spec.ts b/src/tcl/tclStopPoint.service.spec.ts deleted file mode 100644 index 2c6d4220f3f2a135aabaa0b0ec423dab6713a53b..0000000000000000000000000000000000000000 --- a/src/tcl/tclStopPoint.service.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { HttpModule } from '@nestjs/common'; -import { getModelToken } from '@nestjs/mongoose'; -import { Test, TestingModule } from '@nestjs/testing'; -import { TclStopPoint } from './tclStopPoint.schema'; -import { TclStopPointService } from './tclStopPoint.service'; - -describe('TclService', () => { - let service: TclStopPointService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [HttpModule], - providers: [ - TclStopPointService, - { - provide: getModelToken('TclStopPoint'), - useValue: TclStopPoint, - }, - ], - }).compile(); - - service = module.get<TclStopPointService>(TclStopPointService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/src/temp-user/temp-user.controller.spec.ts b/src/temp-user/temp-user.controller.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9005d4c87a29559f813af9a1e7c0752fc85c1e6f --- /dev/null +++ b/src/temp-user/temp-user.controller.spec.ts @@ -0,0 +1,47 @@ +import { HttpModule, HttpStatus } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MailerModule } from '../mailer/mailer.module'; +import { TempUserController } from './temp-user.controller'; +import { TempUserService } from './temp-user.service'; + +describe('TempUserService', () => { + let controller: TempUserController; + + const mockTempUserService = { + findById: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [HttpModule, MailerModule], + providers: [{ provide: TempUserService, useValue: mockTempUserService }], + controllers: [TempUserController], + }).compile(); + + controller = module.get<TempUserController>(TempUserController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getTempUser', () => { + it('should get temporary users', async () => { + const tmpUser = { email: 'test@test.com', pendingStructuresLink: [] }; + mockTempUserService.findById.mockReturnValueOnce(tmpUser); + expect(await controller.getTempUser('addq651')).toEqual(tmpUser); + }); + it('should throw error in cas of no users', async () => { + const tmpUser = null; + mockTempUserService.findById.mockReturnValueOnce(tmpUser); + try { + await controller.getTempUser('addq651'); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toEqual('User does not exists'); + expect(e.status).toEqual(HttpStatus.BAD_REQUEST); + } + }); + }); +}); diff --git a/src/temp-user/temp-user.interface.ts b/src/temp-user/temp-user.interface.ts index 1e0a132a1bb71e3fdd88ddc3bd5262e82e0c085e..2a4e74ff88ff5ec98d24b32178e84ac01bcb494b 100644 --- a/src/temp-user/temp-user.interface.ts +++ b/src/temp-user/temp-user.interface.ts @@ -1,7 +1,6 @@ import { Document, Types } from 'mongoose'; export interface ITempUser extends Document { - readonly _id: string; email: string; - pendingStructuresLink: Types.ObjectId[]; + pendingStructuresLink?: Types.ObjectId[]; } diff --git a/src/temp-user/temp-user.schema.ts b/src/temp-user/temp-user.schema.ts index dc70ce2ac22516550162e44b51d4d2523164aa82..3112b3112f6217e5f5ab30242257aece39726a51 100644 --- a/src/temp-user/temp-user.schema.ts +++ b/src/temp-user/temp-user.schema.ts @@ -9,7 +9,7 @@ export class TempUser { email: string; @Prop({ default: null }) - pendingStructuresLink: Types.ObjectId[]; + pendingStructuresLink?: Types.ObjectId[]; } export const TempUserSchema = SchemaFactory.createForClass(TempUser); diff --git a/src/temp-user/temp-user.service.spec.ts b/src/temp-user/temp-user.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..14c2995c97c9c3497760956e0968aa0cc7497da8 --- /dev/null +++ b/src/temp-user/temp-user.service.spec.ts @@ -0,0 +1,107 @@ +import { HttpModule, HttpStatus } from '@nestjs/common'; +import { getModelToken } from '@nestjs/mongoose'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MailerModule } from '../mailer/mailer.module'; +import { TempUserService } from './temp-user.service'; + +describe('TempUserService', () => { + let service: TempUserService; + + const tempUserModelMock = { + create: jest.fn(), + findOne: jest.fn(), + findById: jest.fn(), + deleteOne: jest.fn(), + find: jest.fn(), + exec: jest.fn(), + updateOne: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [HttpModule, MailerModule], + providers: [ + TempUserService, + { + provide: getModelToken('TempUser'), + useValue: tempUserModelMock, + }, + ], + }).compile(); + + service = module.get<TempUserService>(TempUserService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + describe('create', () => { + const tmpUser = { email: 'test@test.com', pendingStructuresLink: [] }; + it('should not create temporary user: already exist', async () => { + tempUserModelMock.findOne.mockResolvedValueOnce(tmpUser); + try { + await service.create(tmpUser, 'PIMMS Framboise'); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toEqual('User already exists'); + expect(e.status).toEqual(HttpStatus.BAD_REQUEST); + } + }); + it('should create temporary user', async () => { + tempUserModelMock.findOne.mockResolvedValueOnce(null).mockResolvedValueOnce(tmpUser); + tempUserModelMock.create.mockResolvedValueOnce(tmpUser); + expect(await service.create(tmpUser, 'PIMMS Framboise')).toEqual(tmpUser); + }); + }); + it('should find one', async () => { + const tmpUser = { email: 'test2@test.com', pendingStructuresLink: [] }; + tempUserModelMock.findOne.mockResolvedValueOnce(tmpUser); + expect(await service.findOne('test2@test.com')).toEqual(tmpUser); + }); + + it('should find one by id', async () => { + const tmpUser = { email: 'test2@test.com', pendingStructuresLink: [] }; + tempUserModelMock.findById.mockResolvedValueOnce(tmpUser); + expect(await service.findById('5fbb92e480a5c257dc0161f0')).toEqual(tmpUser); + }); + + describe('delete', () => { + it('should delete a temp user', async () => { + const tmpUser = { email: 'test2@test.com', pendingStructuresLink: [] }; + tempUserModelMock.findOne.mockResolvedValueOnce(tmpUser); + tempUserModelMock.deleteOne.mockImplementationOnce(() => {}); + expect(await service.delete('toto@test.com')).toEqual(tmpUser); + }); + it('should return an error : user does not exist', async () => { + tempUserModelMock.findOne.mockResolvedValueOnce(null); + try { + await service.delete('toto@test.com'); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toEqual('User does not exists'); + expect(e.status).toEqual(HttpStatus.BAD_REQUEST); + } + }); + }); + + describe('updateStructureLinked', () => { + it('should update structure linked', async () => { + const tmpUser = { email: 'test2@test.com', pendingStructuresLink: [] }; + tempUserModelMock.find.mockReturnThis(); + tempUserModelMock.exec.mockResolvedValueOnce([]).mockResolvedValueOnce(tmpUser); + tempUserModelMock.updateOne.mockReturnThis(); + expect(await service.updateStructureLinked(tmpUser)).toEqual(tmpUser); + }); + it('should not update structure linked: User already linked', async () => { + const tmpUser = { email: 'test2@test.com', pendingStructuresLink: [] }; + tempUserModelMock.find.mockReturnThis(); + tempUserModelMock.exec.mockResolvedValueOnce([tmpUser]); + try { + await service.updateStructureLinked(tmpUser); + } catch (e) { + expect(e.message).toEqual('User already linked'); + expect(e.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + } + }); + }); +}); diff --git a/src/temp-user/temp-user.service.ts b/src/temp-user/temp-user.service.ts index f4588a4b9c199c84ea49152a2d4ad3e29630faf1..8a43832237b1555687568f84cc92b2f877858336 100644 --- a/src/temp-user/temp-user.service.ts +++ b/src/temp-user/temp-user.service.ts @@ -1,4 +1,4 @@ -import { HttpException, HttpService, HttpStatus, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model, Types } from 'mongoose'; import { MailerService } from '../mailer/mailer.service'; @@ -10,7 +10,6 @@ import { ITempUser } from './temp-user.interface'; @Injectable() export class TempUserService { constructor( - private readonly httpService: HttpService, private readonly mailerService: MailerService, @InjectModel(TempUser.name) private tempUserModel: Model<ITempUser> ) {} @@ -20,27 +19,26 @@ export class TempUserService { if (userInDb) { throw new HttpException('User already exists', HttpStatus.BAD_REQUEST); } - const createUser = new this.tempUserModel(createTempUser); + const createUser = await this.tempUserModel.create(createTempUser); // Send email this.sendUserMail(createUser, structureName); - createUser.save(); - return await this.findOne(createTempUser.email); + return this.findOne(createTempUser.email); } - public async findOne(mail: string): Promise<TempUser | undefined> { - return this.tempUserModel.findOne({ email: mail }).exec(); + public async findOne(mail: string): Promise<TempUser> { + return this.tempUserModel.findOne({ email: mail }); } - public async findById(id: string): Promise<TempUser | undefined> { - return this.tempUserModel.findById(Types.ObjectId(id)).exec(); + public async findById(id: string): Promise<TempUser> { + return this.tempUserModel.findById(Types.ObjectId(id)); } public async delete(mail: string): Promise<TempUser> { const userInDb = await this.findOne(mail); if (!userInDb) { - throw new HttpException('User already exists', HttpStatus.BAD_REQUEST); + throw new HttpException('User does not exists', HttpStatus.BAD_REQUEST); } - this.tempUserModel.deleteOne({ email: mail }).exec(); + this.tempUserModel.deleteOne({ email: mail }); return userInDb; } diff --git a/src/users/guards/isStructureOwner.guard.spec.ts b/src/users/guards/isStructureOwner.guard.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a92ddcf4f77b41434d9956902f2816d87d9f5c23 --- /dev/null +++ b/src/users/guards/isStructureOwner.guard.spec.ts @@ -0,0 +1,103 @@ +import { Reflector } from '@nestjs/core'; +import { createMock } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ExecutionContext } from '@nestjs/common'; +import { UserRole } from '../enum/user-role.enum'; +import { IsStructureOwnerGuard } from './isStructureOwner.guard'; +import { Types } from 'mongoose'; + +describe('isStrructureOwner', () => { + let guard: IsStructureOwnerGuard; + let reflector: Reflector; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + IsStructureOwnerGuard, + { + provide: Reflector, + useValue: { + constructor: jest.fn(), + get: jest.fn(), + }, + }, + ], + }).compile(); + + guard = module.get<IsStructureOwnerGuard>(IsStructureOwnerGuard); + reflector = module.get<Reflector>(Reflector); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + it('should return true if structure is in user linked structures', async () => { + const context = createMock<ExecutionContext>({ + getHandler: jest.fn(), + switchToHttp: jest.fn().mockReturnValueOnce({ + getRequest: jest.fn().mockReturnValueOnce({ + user: { + structuresLink: ['6001a38516b08100062e4161'], + structureOutdatedMailSent: [], + pendingStructuresLink: [], + newEmail: null, + changeEmailToken: null, + role: 0, + resetPasswordToken: null, + validationToken: null, + emailVerified: true, + email: 'jp@test.com', + name: 'Jean-Paul', + surname: 'DESCHAMPS', + phone: '06 11 11 11 11', + }, + params: { + id: '6001a38516b08100062e4161', + }, + }), + }), + }); + const result = await guard.canActivate(context); + + expect(result).toBeTruthy(); + }); + + it('should return false if structure is not user linked structures', async () => { + jest.spyOn(reflector, 'get').mockImplementation((a: any, b: any) => []); + const context = createMock<ExecutionContext>({ + getHandler: jest.fn(), + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user: { + structuresLink: ['unIdQuiExistePasTropTrop'], + structureOutdatedMailSent: [], + pendingStructuresLink: [], + newEmail: null, + changeEmailToken: null, + role: UserRole.user, + resetPasswordToken: null, + validationToken: null, + emailVerified: true, + email: 'jp@test.com', + name: 'Jean-Paul', + surname: 'DESCHAMPS', + phone: '06 11 11 11 11', + }, + params: { + id: '6001a38516b08100062e4161', + }, + }), + }), + }); + const result = await guard.canActivate(context); + + expect(result).toBeFalsy(); + expect(reflector.get).toBeCalled(); + }); +}); diff --git a/src/users/guards/roles.guard.spec.ts b/src/users/guards/roles.guard.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ef376e7c64a77fd3c15ab741758bf8f64a5c549 --- /dev/null +++ b/src/users/guards/roles.guard.spec.ts @@ -0,0 +1,95 @@ +import { Reflector } from '@nestjs/core'; +import { createMock } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { RolesGuard } from './roles.guard'; +import { ExecutionContext } from '@nestjs/common'; +import { UserRole } from '../enum/user-role.enum'; + +describe('RolesGuard', () => { + let guard: RolesGuard; + let reflector: Reflector; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + RolesGuard, + { + provide: Reflector, + useValue: { + constructor: jest.fn(), + get: jest.fn(), + }, + }, + ], + }).compile(); + + guard = module.get<RolesGuard>(RolesGuard); + reflector = module.get<Reflector>(Reflector); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + it('should skip(return true) if the `HasRoles` decorator is not set', async () => { + jest.spyOn(reflector, 'get').mockImplementation((a: any, b: any) => null); + const context = createMock<ExecutionContext>(); + const result = await guard.canActivate(context); + + expect(result).toBeTruthy(); + expect(reflector.get).toBeCalled(); + }); + + it('should return true if the `HasRoles` decorator and role is admin', async () => { + jest.spyOn(reflector, 'get').mockImplementation((a: any, b: any) => ['admin']); + const context = createMock<ExecutionContext>({ + getHandler: jest.fn(), + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user: { role: UserRole.admin }, + }), + }), + }); + + const result = await guard.canActivate(context); + expect(result).toBeTruthy(); + expect(reflector.get).toBeCalled(); + }); + + it('should return false if the `HasRoles` decorator is set but role is not allowed', async () => { + jest.spyOn(reflector, 'get').mockImplementation((a: any, b: any) => ['admin']); + const context = createMock<ExecutionContext>({ + getHandler: jest.fn(), + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user: { role: null }, + }), + }), + }); + + const result = await guard.canActivate(context); + expect(result).toBeFalsy(); + expect(reflector.get).toBeCalled(); + }); + + it('should return true if the `HasRoles` decorator is and role is not allowed', async () => { + jest.spyOn(reflector, 'get').mockImplementation((a: any, b: any) => ['user']); + const context = createMock<ExecutionContext>({ + getHandler: jest.fn(), + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user: { role: UserRole.user }, + }), + }), + }); + + const result = await guard.canActivate(context); + expect(result).toBeTruthy(); + expect(reflector.get).toBeCalled(); + }); +}); diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts index 68da5450f95300fde9828f6a2ad2d1905b738753..e65b3965aa24378b580c213e074b100d9e96d158 100644 --- a/src/users/users.controller.spec.ts +++ b/src/users/users.controller.spec.ts @@ -1,7 +1,6 @@ import { HttpModule } from '@nestjs/common'; import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; -import { CategoriesModule } from '../categories/categories.module'; import { ConfigurationModule } from '../configuration/configuration.module'; import { MailerService } from '../mailer/mailer.service'; import { SearchModule } from '../search/search.module'; diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 6f513374851bfd2b405d920bf55e78b56cc5bd5e..6830094ce1b7bc71510a2a2fc597cdfb9e824eaf 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -40,11 +40,13 @@ export class UsersController { } const user = await this.usersService.create(createUserDto); if (structureId) { - this.usersService.updateStructureLinkedClaim(createUserDto.email, structureId); + const structure = await this.structureService.findOne(structureId); + this.usersService.updateStructureLinkedClaim(createUserDto.email, structureId, structure); this.structureService.sendAdminStructureNotification( null, this.configurationService.config.templates.adminStructureClaim.ejs, - this.configurationService.config.templates.adminStructureClaim.json + this.configurationService.config.templates.adminStructureClaim.json, + user ); } // Remove temp user if exist diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 6a481a9e123a681074b40daac020f3b9244c2d65..87795896f53bdd466a281e6e15d6441a787e60e6 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -6,6 +6,7 @@ import { User, UserSchema } from './schemas/user.schema'; import { MailerModule } from '../mailer/mailer.module'; import { StructuresModule } from '../structures/structures.module'; import { TempUserModule } from '../temp-user/temp-user.module'; + @Module({ imports: [ MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 3782bd1d87544613dd9afe5a7915d220642bf77d..325205d208ad02942929cf11fee0eddc64ee20b1 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -12,6 +12,7 @@ import { IUser } from './interfaces/user.interface'; import { EmailChangeDto } from './dto/change-email.dto'; import { PendingStructureDto } from '../admin/dto/pending-structure.dto'; import { OwnerDto } from './dto/owner.dto'; +import { StructureDocument } from '../structures/schemas/structure.schema'; @Injectable() export class UsersService { @@ -157,6 +158,26 @@ export class UsersService { }); } + /** + * Send to all admins mail for aptic duplicated data + */ + public async sendAdminApticNewStructureMail(structure: StructureDocument): Promise<any> { + const config = this.mailerService.config; + const ejsPath = this.mailerService.getTemplateLocation(config.templates.newApticStructure.ejs); + const jsonConfig = this.mailerService.loadJsonConfig(config.templates.newApticStructure.json); + + const html = await ejs.renderFile(ejsPath, { + config, + name: structure.structureName, + address: `${structure.address.numero} ${structure.address.street} ${structure.address.commune}`, + description: structure.otherDescription, + }); + const admins = await this.getAdmins(); + admins.forEach((admin) => { + this.mailerService.send(admin.email, jsonConfig.subject, html); + }); + } + /** * Send approval email for user * a new account. @@ -317,13 +338,22 @@ export class UsersService { * Send to all admins validation email for structures * new account. */ - private async sendAdminStructureValidationMail(): Promise<any> { + private async sendAdminStructureValidationMail(userEmail: string, structure: StructureDocument): Promise<any> { const config = this.mailerService.config; const ejsPath = this.mailerService.getTemplateLocation(config.templates.adminStructureClaim.ejs); const jsonConfig = this.mailerService.loadJsonConfig(config.templates.adminStructureClaim.json); - + const user = await this.findOne(userEmail); const html = await ejs.renderFile(ejsPath, { config, + id: structure ? structure._id : 0, + structureName: structure ? structure.structureName : '', + structureAdress: structure + ? structure.address.numero + ? `${structure.address.numero} ${structure.address.street} ${structure.address.commune}` + : `${structure.address.street} ${structure.address.commune}` + : '', + structureDescription: structure ? structure.otherDescription : '', + user: user, }); const admins = await this.getAdmins(); admins.forEach((admin) => { @@ -331,9 +361,13 @@ export class UsersService { }); } - public async updateStructureLinkedClaim(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { + public async updateStructureLinkedClaim( + userEmail: string, + idStructure: string, + structure: StructureDocument + ): Promise<Types.ObjectId[]> { const stucturesLinked = this.updatePendingStructureLinked(userEmail, idStructure); - this.sendAdminStructureValidationMail(); + this.sendAdminStructureValidationMail(userEmail, structure); return stucturesLinked; } diff --git a/test/mock/services/newsletter.mock.service.ts b/test/mock/services/newsletter.mock.service.ts index d7b4d640c0d9cd298961fe88a660aeebbb158b1d..6b4f0d47557ff8364078971221f44eccc0ff3405 100644 --- a/test/mock/services/newsletter.mock.service.ts +++ b/test/mock/services/newsletter.mock.service.ts @@ -44,7 +44,7 @@ export class NewsletterServiceMock { }); } - deleteOneEmail(email: string) { + newsletterUnsubscribe(email: string) { if (email === 'test@test.com') { throw new HttpException('Invalid email', HttpStatus.BAD_REQUEST); } diff --git a/test/mock/services/structures.mock.service.ts b/test/mock/services/structures.mock.service.ts index a3432c034ab8ecb859199518b728d59adf6b5c8c..f088613ba024969b276d4998118a707852e7240d 100644 --- a/test/mock/services/structures.mock.service.ts +++ b/test/mock/services/structures.mock.service.ts @@ -635,4 +635,234 @@ export class StructuresServiceMock { }, ]; } + + findAllFormated() { + return [ + { + _id: '6093ba0e2ab5775cfc01abcd', + coord: [4.8498155, 45.7514817], + equipmentsAndServices: ['wifiEnAccesLibre'], + digitalCultureSecurity: [], + parentingHelp: [], + socialAndProfessional: [], + accessRight: [], + baseSkills: [], + proceduresAccompaniment: [], + publicsAccompaniment: [], + publics: ['adultes'], + labelsQualifications: [], + accessModality: ['telephoneVisio'], + structureType: null, + structureName: 'a', + description: null, + lockdownActivity: null, + address: { + numero: null, + street: 'Rue Alphonse Daudet', + commune: 'Lyon 7ème Arrondissement', + }, + contactMail: '', + contactPhone: '', + website: '', + facebook: null, + twitter: null, + instagram: null, + linkedin: null, + hours: { + monday: { + open: false, + time: [], + }, + tuesday: { + open: false, + time: [], + }, + wednesday: { + open: false, + time: [], + }, + thursday: { + open: false, + time: [], + }, + friday: { + open: false, + time: [], + }, + saturday: { + open: false, + time: [], + }, + sunday: { + open: false, + time: [], + }, + }, + pmrAccess: false, + exceptionalClosures: null, + otherDescription: null, + nbComputers: 1, + nbPrinters: 1, + nbTablets: 1, + nbNumericTerminal: 1, + nbScanners: 1, + freeWorkShop: false, + accountVerified: true, + createdAt: '2021-05-06T09:42:38.000Z', + updatedAt: '2021-05-06T09:42:50.000Z', + __v: 0, + }, + { + _id: '6093ba0e2ab5775cfc01ffff', + coord: [4.8498155, 45.7514817], + equipmentsAndServices: ['wifiEnAccesLibre'], + digitalCultureSecurity: [], + parentingHelp: [], + socialAndProfessional: [], + accessRight: [], + baseSkills: [], + proceduresAccompaniment: [], + publicsAccompaniment: [], + publics: ['adultes'], + labelsQualifications: [], + accessModality: ['telephoneVisio'], + structureType: null, + structureName: 'a', + description: null, + lockdownActivity: null, + address: { + numero: null, + street: 'Rue Alphonse Daudet', + commune: 'Lyon 7ème Arrondissement', + }, + contactMail: '', + contactPhone: '', + website: '', + facebook: null, + twitter: null, + instagram: null, + linkedin: null, + hours: { + monday: { + open: false, + time: [], + }, + tuesday: { + open: false, + time: [], + }, + wednesday: { + open: false, + time: [], + }, + thursday: { + open: false, + time: [], + }, + friday: { + open: false, + time: [], + }, + saturday: { + open: false, + time: [], + }, + sunday: { + open: false, + time: [], + }, + }, + pmrAccess: false, + exceptionalClosures: null, + otherDescription: null, + nbComputers: 1, + nbPrinters: 1, + nbTablets: 1, + nbNumericTerminal: 1, + nbScanners: 1, + freeWorkShop: false, + accountVerified: true, + createdAt: '2021-05-06T09:42:38.000Z', + updatedAt: '2021-05-06T09:42:50.000Z', + __v: 0, + }, + ]; + } + + update() { + return { + _id: '6093ba0e2ab5775cfc01ed3e', + coord: [4.8498155, 45.7514817], + equipmentsAndServices: ['wifiEnAccesLibre'], + digitalCultureSecurity: [], + parentingHelp: [], + socialAndProfessional: [], + accessRight: [], + baseSkills: [], + proceduresAccompaniment: [], + publicsAccompaniment: [], + publics: ['adultes'], + labelsQualifications: [], + accessModality: ['telephoneVisio'], + structureType: null, + structureName: 'a', + description: null, + lockdownActivity: null, + address: { + numero: null, + street: 'Rue Alphonse Daudet', + commune: 'Lyon 7ème Arrondissement', + }, + contactMail: '', + contactPhone: '', + website: '', + facebook: null, + twitter: null, + instagram: null, + linkedin: null, + hours: { + monday: { + open: false, + time: [], + }, + tuesday: { + open: false, + time: [], + }, + wednesday: { + open: false, + time: [], + }, + thursday: { + open: false, + time: [], + }, + friday: { + open: false, + time: [], + }, + saturday: { + open: false, + time: [], + }, + sunday: { + open: false, + time: [], + }, + }, + pmrAccess: false, + exceptionalClosures: null, + otherDescription: null, + nbComputers: 1, + nbPrinters: 1, + nbTablets: 1, + nbNumericTerminal: 1, + nbScanners: 1, + freeWorkShop: false, + accountVerified: true, + createdAt: '2021-05-06T09:42:38.000Z', + updatedAt: '2021-05-06T09:42:50.000Z', + __v: 0, + }; + } } diff --git a/test/mock/services/tempUser.mock.service.ts b/test/mock/services/tempUser.mock.service.ts index 8769fa94f3b52596b1a366312475df0f08b05830..4986277f3125fb1add58f8bbd3fa065d09ca47c8 100644 --- a/test/mock/services/tempUser.mock.service.ts +++ b/test/mock/services/tempUser.mock.service.ts @@ -1 +1,39 @@ -export class TempUserServiceMock {} +export class TempUserServiceMock { + findOne(mail: string, passwordQuery?: boolean) { + if (mail === 'pauline.dupont@mii.com') { + return { + _id: 'tsfsf6296', + validationToken: + 'cf1c74c22cedb6b575945098db42d2f493fb759c9142c6aff7980f252886f36ee086574ee99a06bc99119079257116c959c8ec870949cebdef2b293666dbca42', + emailVerified: true, + email: 'pauline.dupont@mii.com', + password: '$2a$12$vLQjJ9zAWyUwiFLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu', + role: 0, + name: 'DUPONT', + surname: 'Pauline', + }; + } + if (mail === 'jacques.dupont@mii.com') { + if (passwordQuery) { + return { + _id: 'tsfsf6296', + validationToken: + 'cf1c74c22cedb6b575945098db42d2f493fb759c9142c6aff7980f252886f36ee086574ee99a06bc99119079257116c959c8ec870949cebdef2b293666dbca42', + emailVerified: false, + email: 'jacques.dupont@mii.com', + password: '$2a$12$vLQjJ9zAWyUwiFLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu', + role: 0, + }; + } + return { + _id: 'tsfsf6296', + validationToken: + 'cf1c74c22cedb6b575945098db42d2f493fb759c9142c6aff7980f252886f36ee086574ee99a06bc99119079257116c959c8ec870949cebdef2b293666dbca42', + emailVerified: false, + email: 'jacques.dupont@mii.com', + role: 0, + }; + } + return null; + } +}