From 4042dc1e98e2c6c8bf477f42a94f7af533f54d09 Mon Sep 17 00:00:00 2001 From: Dorian De Rosa Date: Tue, 21 Nov 2023 15:39:39 +0100 Subject: [PATCH] wip --- .env.test | 4 + .../FormulaireRechercheEmploisEurope.tsx | 1 + .../RechercherEmploisEurope.test.tsx | 2 +- .../FormulaireRechercheStages3eme.tsx | 38 ++++++++ .../ListeResultatsStage3eme.tsx | 40 ++++++++ .../Rechercher/RechercherStages3eme.test.tsx | 75 +++++++++++++++ .../Rechercher/RechercherStages3eme.tsx | 84 +++++++++++++++++ .../Stages3eme/RechercherStages3eme.test.tsx | 18 ---- .../Stages3eme/RechercherStages3eme.tsx | 20 ---- .../R\303\251sultatRechercherSolution.tsx" | 4 +- src/client/dependencies.container.ts | 6 ++ .../stage3eme/bff.stage3eme.service.test.ts | 22 +++++ .../stage3eme/bff.stage3eme.service.ts | 12 +++ .../stage3eme/stage3eme.service.fixture.ts | 11 +++ .../services/stage3eme/stage3eme.service.ts | 6 ++ src/pages/api/stages-3eme/index.controller.ts | 14 +++ src/pages/stages-3eme/index.page.test.tsx | 11 ++- src/pages/stages-3eme/index.page.tsx | 2 +- .../configuration/dependencies.container.ts | 19 ++++ .../services/configuration.service.fixture.ts | 2 + .../services/serverConfiguration.service.ts | 4 + .../configuration/dependencies.container.ts | 12 +++ .../stage-3eme/stage3emeHttpClient.config.ts | 10 ++ .../stage-3eme/domain/stage3eme.fixture.ts | 44 +++++++++ .../stage-3eme/domain/stage3eme.repository.ts | 7 ++ src/server/stage-3eme/domain/stage3eme.ts | 19 ++++ .../apiImmersionFacileStage3eme.fixture.ts | 15 +++ ...apiImmersionFacileStage3eme.mapper.test.ts | 63 +++++++++++++ .../apiImmersionFacileStage3eme.mapper.ts | 18 ++++ ...mmersionFacileStage3eme.repository.test.ts | 93 +++++++++++++++++++ .../apiImmersionFacileStage3eme.repository.ts | 35 +++++++ .../apiImmersionFacileStage3eme.ts | 25 +++++ .../useCase/rechercherStage3eme.useCase.ts | 9 ++ 33 files changed, 702 insertions(+), 43 deletions(-) create mode 100644 src/client/components/features/Stages3eme/FormulaireRecherche/FormulaireRechercheStages3eme.tsx create mode 100644 src/client/components/features/Stages3eme/FormulaireRecherche/ListeResultatsStage3eme.tsx create mode 100644 src/client/components/features/Stages3eme/Rechercher/RechercherStages3eme.test.tsx create mode 100644 src/client/components/features/Stages3eme/Rechercher/RechercherStages3eme.tsx delete mode 100644 src/client/components/features/Stages3eme/RechercherStages3eme.test.tsx delete mode 100644 src/client/components/features/Stages3eme/RechercherStages3eme.tsx create mode 100644 src/client/services/stage3eme/bff.stage3eme.service.test.ts create mode 100644 src/client/services/stage3eme/bff.stage3eme.service.ts create mode 100644 src/client/services/stage3eme/stage3eme.service.fixture.ts create mode 100644 src/client/services/stage3eme/stage3eme.service.ts create mode 100644 src/pages/api/stages-3eme/index.controller.ts create mode 100644 src/server/stage-3eme/configuration/dependencies.container.ts create mode 100644 src/server/stage-3eme/configuration/stage-3eme/stage3emeHttpClient.config.ts create mode 100644 src/server/stage-3eme/domain/stage3eme.fixture.ts create mode 100644 src/server/stage-3eme/domain/stage3eme.repository.ts create mode 100644 src/server/stage-3eme/domain/stage3eme.ts create mode 100644 src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.fixture.ts create mode 100644 src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.mapper.test.ts create mode 100644 src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.mapper.ts create mode 100644 src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.repository.test.ts create mode 100644 src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.repository.ts create mode 100644 src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.ts create mode 100644 src/server/stage-3eme/useCase/rechercherStage3eme.useCase.ts diff --git a/.env.test b/.env.test index 202db3be58..5e2c026ef3 100644 --- a/.env.test +++ b/.env.test @@ -16,6 +16,10 @@ API_ETABLISSEMENTS_PUBLICS=https://etablissements-publics.api.gouv.fr/v3/ API_EURES_BASE_URL=https://webgate.acceptance.ec.europa.eu/eures-api/output/api/v1/jv/ API_EURES_IS_MOCK_ACTIVE=0 +# API IMMERSION FACILE +API_IMMERSION_FACILE_STAGE_3EME_API_KEY=API_IMMERSION_FACILE_STAGE_3EME_API_KEY +API_IMMERSION_FACILE_STAGE_3EME_URL=https://staging.immersion-facile.beta.gouv.fr/api/v2 + # API ONISEP API_ONISEP_BASE_URL=https://api.opendata.onisep.fr/api/1.0 API_ONISEP_ACCOUNT_EMAIL=fake@example.com diff --git a/src/client/components/features/EmploisEurope/FormulaireRecherche/FormulaireRechercheEmploisEurope.tsx b/src/client/components/features/EmploisEurope/FormulaireRecherche/FormulaireRechercheEmploisEurope.tsx index 0065bbbcea..bf57caf0f1 100644 --- a/src/client/components/features/EmploisEurope/FormulaireRecherche/FormulaireRechercheEmploisEurope.tsx +++ b/src/client/components/features/EmploisEurope/FormulaireRecherche/FormulaireRechercheEmploisEurope.tsx @@ -116,6 +116,7 @@ export function FormulaireRechercheEmploisEurope() { return (
{ , ); - const formulaireRechercheEmploisEurope = screen.getByRole('form'); + const formulaireRechercheEmploisEurope = screen.getByRole('search'); // THEN expect(formulaireRechercheEmploisEurope).toBeVisible(); diff --git a/src/client/components/features/Stages3eme/FormulaireRecherche/FormulaireRechercheStages3eme.tsx b/src/client/components/features/Stages3eme/FormulaireRecherche/FormulaireRechercheStages3eme.tsx new file mode 100644 index 0000000000..bb297281ad --- /dev/null +++ b/src/client/components/features/Stages3eme/FormulaireRecherche/FormulaireRechercheStages3eme.tsx @@ -0,0 +1,38 @@ +import { useRouter } from 'next/router'; +import { FormEvent, useRef } from 'react'; + +import styles + from '~/client/components/features/OffreEmploi/FormulaireRecherche/FormulaireRechercheOffreEmploi.module.scss'; +import { ButtonComponent } from '~/client/components/ui/Button/ButtonComponent'; +import { Icon } from '~/client/components/ui/Icon/Icon'; + +export function FormulaireRechercheStages3eme() { + const rechercheStage3emeForm = useRef(null); + + const router = useRouter(); + + function updateRechercherEmploiEuropeQueryParams(event: FormEvent) { + event.preventDefault(); + // NOTE (DORO 22-11-2023): Query params temporaire pour afficher les résultats de recherche (à remplacer par les vrais query params quand la recherche par localisation sera implémentée) + return router.push({ query: 'location=here' }, undefined, { shallow: true }); + } + + return ( + +
+ } + iconPosition='right' + type='submit' + /> +
+ + ); +} diff --git a/src/client/components/features/Stages3eme/FormulaireRecherche/ListeResultatsStage3eme.tsx b/src/client/components/features/Stages3eme/FormulaireRecherche/ListeResultatsStage3eme.tsx new file mode 100644 index 0000000000..71665e12ad --- /dev/null +++ b/src/client/components/features/Stages3eme/FormulaireRecherche/ListeResultatsStage3eme.tsx @@ -0,0 +1,40 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { + ListeRésultatsRechercherSolution, +} from '~/client/components/layouts/RechercherSolution/ListeRésultats/ListeRésultatsRechercherSolution'; +import { RésultatRechercherSolution } from '~/client/components/layouts/RechercherSolution/Résultat/RésultatRechercherSolution'; +import { ResultatRechercheStage3eme, Stage3eme } from '~/server/stage-3eme/domain/stage3eme'; + +interface ListeResultatsStage3emeProps { + resultatList: ResultatRechercheStage3eme | undefined; +} + +export function ListeResultatsStage3eme({ resultatList }: ListeResultatsStage3emeProps) { + if (!resultatList || resultatList.resultats.length === 0) { + return null; + } + + return ( + + {resultatList.resultats.map((stage3eme) => ResultatStage3eme(stage3eme))} + + ); +} + +function ResultatStage3eme(stage3eme: Stage3eme) { + return ( +
  • + +

    {stage3eme.domaine}

    +

    {stage3eme.adresse.ligne}, {stage3eme.adresse.codePostal} {stage3eme.adresse.ville}

    + } + étiquetteOffreList={[]} + /> +
  • + ); +} diff --git a/src/client/components/features/Stages3eme/Rechercher/RechercherStages3eme.test.tsx b/src/client/components/features/Stages3eme/Rechercher/RechercherStages3eme.test.tsx new file mode 100644 index 0000000000..8e1ca65882 --- /dev/null +++ b/src/client/components/features/Stages3eme/Rechercher/RechercherStages3eme.test.tsx @@ -0,0 +1,75 @@ +/** + * @jest-environment jsdom + */ + +import { render, screen, within } from '@testing-library/react'; + +import { mockUseRouter } from '~/client/components/useRouter.mock'; +import { mockSmallScreen } from '~/client/components/window.mock'; +import { DependenciesProvider } from '~/client/context/dependenciesContainer.context'; +import { aStage3emeService } from '~/client/services/stage3eme/stage3eme.service.fixture'; +import { createSuccess } from '~/server/errors/either'; +import { aResultatRechercheStage3eme } from '~/server/stage-3eme/domain/stage3eme.fixture'; + +import RechercherStages3eme from './RechercherStages3eme'; + +describe('La recherche des stages de 3ème', () => { + describe('quand le composant est affiché sans paramètres de recherche dans l’URL', () => { + it('affiche un formulaire de recherche', async () => { + // GIVEN + mockUseRouter({}); + const stage3emeServiceMock = aStage3emeService(); + // WHEN + render( + + ); + + // THEN + const formulaireRecherche = await screen.findByRole('search', { name: 'Rechercher un stage de 3ème' }); + expect(formulaireRecherche).toBeVisible(); + const titre = await screen.findByRole('heading', { + level: 1, + name: 'Des milliers d’entreprises prêtes à vous accueillir pour votre stage de 3ème', + }); + expect(titre).toBeVisible(); + }); + }); + describe('quand le composant est affiché pour une recherche avec résultats', () => { + it('affiche les résultats de la recherche', async () => { + // GIVEN + mockSmallScreen(); + mockUseRouter({ query: { location: 'here' } }); + const stage3emeServiceMock = aStage3emeService(); + const resultatRecherche = aResultatRechercheStage3eme({ + nombreDeResultats: 1, + resultats: [ + { + adresse: { + codeDepartement: '75', + codePostal: '75000', + ligne: '1 rue de la Paix', + ville: 'Paris', + }, + domaine: 'Informatique', + nomEntreprise: 'Entreprise 1', + }, + ], + }); + jest.spyOn(stage3emeServiceMock, 'rechercherStage3eme').mockResolvedValue(createSuccess(resultatRecherche)); + + // WHEN + render( + + ); + const resultatsUl = await screen.findAllByRole('list', { name: 'Stages de 3ème' }); + const resultats = await within(resultatsUl[0]).findAllByTestId('RésultatRechercherSolution'); + + // THEN + expect(resultats).toHaveLength(resultatRecherche.nombreDeResultats); + expect(resultats[0]).toHaveTextContent('Entreprise 1'); + expect(resultats[0]).toHaveTextContent('Informatique'); + expect(resultats[0]).toHaveTextContent('1 rue de la Paix'); + expect(resultats[0]).toHaveTextContent('75000 Paris'); + }); + }); +}); diff --git a/src/client/components/features/Stages3eme/Rechercher/RechercherStages3eme.tsx b/src/client/components/features/Stages3eme/Rechercher/RechercherStages3eme.tsx new file mode 100644 index 0000000000..8aba6e9fd1 --- /dev/null +++ b/src/client/components/features/Stages3eme/Rechercher/RechercherStages3eme.tsx @@ -0,0 +1,84 @@ +import { useEffect, useMemo, useState } from 'react'; + +import { + FormulaireRechercheStages3eme, +} from '~/client/components/features/Stages3eme/FormulaireRecherche/FormulaireRechercheStages3eme'; +import { + ListeResultatsStage3eme, +} from '~/client/components/features/Stages3eme/FormulaireRecherche/ListeResultatsStage3eme'; +import { Head } from '~/client/components/head/Head'; +import { RechercherSolutionLayout } from '~/client/components/layouts/RechercherSolution/RechercherSolutionLayout'; +import { LightHero, LightHeroPrimaryText, LightHeroSecondaryText } from '~/client/components/ui/Hero/LightHero'; +import { useDependency } from '~/client/context/dependenciesContainer.context'; +import { Stage3emeService } from '~/client/services/stage3eme/stage3eme.service'; +import { formatRechercherSolutionDocumentTitle } from '~/client/utils/formatRechercherSolutionDocumentTitle.util'; +import { Erreur } from '~/server/errors/erreur.types'; +import { ResultatRechercheStage3eme } from '~/server/stage-3eme/domain/stage3eme'; + +const PREFIX_TITRE_PAGE = 'Rechercher un stage de 3ème'; + +export default function RechercherStages3eme() { + const stage3emeService = useDependency('stage3emeService'); + + const [title, setTitle] = useState(`${PREFIX_TITRE_PAGE} | 1jeune1solution`); + const [isLoading, setIsLoading] = useState(false); + const [erreurRecherche, setErreurRecherche] = useState(undefined); + const [stage3emeList, setStage3emeList] = useState(undefined); + + useEffect(() => { + setIsLoading(true); + setErreurRecherche(undefined); + + stage3emeService.rechercherStage3eme() + .then((response) => { + if (response.instance === 'success') { + setTitle(formatRechercherSolutionDocumentTitle(`${PREFIX_TITRE_PAGE}${response.result.nombreDeResultats === 0 ? ' - Aucun résultat' : ''}`)); + setStage3emeList(response.result); + } else { + setTitle(formatRechercherSolutionDocumentTitle(PREFIX_TITRE_PAGE, response.errorType)); + setErreurRecherche(response.errorType); + } + setIsLoading(false); + }); + }, [stage3emeService]); + + const messageResultatsRecherche: string = useMemo(() => { + const messageResultatRechercheSplit: string[] = [`${stage3emeList?.nombreDeResultats}`]; + if (stage3emeList && stage3emeList.nombreDeResultats > 1) { + messageResultatRechercheSplit.push('stages de 3ème'); + } else { + messageResultatRechercheSplit.push('stage de 3ème'); + } + return messageResultatRechercheSplit.join(' '); + }, [stage3emeList]); + + return <> + +
    + } + erreurRecherche={erreurRecherche} + formulaireRecherche={} + isLoading={isLoading} + listeSolutionElement={} + messageRésultatRecherche={messageResultatsRecherche} + nombreSolutions={stage3emeList?.nombreDeResultats ?? 0} + /> +
    + ; +} + +function BaniereStages3eme() { + return ( + +

    + Des milliers d’entreprises prêtes à vous accueillir + pour votre stage de 3ème +

    +
    + ); +} diff --git a/src/client/components/features/Stages3eme/RechercherStages3eme.test.tsx b/src/client/components/features/Stages3eme/RechercherStages3eme.test.tsx deleted file mode 100644 index 2c7ef7c20c..0000000000 --- a/src/client/components/features/Stages3eme/RechercherStages3eme.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import { render, screen } from '@testing-library/react'; - -import RechercherStages3eme from './RechercherStages3eme'; - -describe('La recherche des stages de 3ème', () => { - it('affiche un titre', () => { - // WHEN - render(); - - // THEN - const titre = screen.getByRole('heading', { level: 1, name: 'Des milliers d’entreprises prêtes à vous accueillir pour votre stage de 3ème' }); - expect(titre).toBeVisible(); - }); -}); diff --git a/src/client/components/features/Stages3eme/RechercherStages3eme.tsx b/src/client/components/features/Stages3eme/RechercherStages3eme.tsx deleted file mode 100644 index 15873b76ee..0000000000 --- a/src/client/components/features/Stages3eme/RechercherStages3eme.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Head } from '~/client/components/head/Head'; -import { LightHero, LightHeroPrimaryText, LightHeroSecondaryText } from '~/client/components/ui/Hero/LightHero'; - -export default function RechercherStages3eme() { - return <> - -
    - -

    - Des milliers d’entreprises prêtes à vous accueillir - pour votre stage de 3ème -

    -
    -
    - ; -} diff --git "a/src/client/components/layouts/RechercherSolution/R\303\251sultat/R\303\251sultatRechercherSolution.tsx" "b/src/client/components/layouts/RechercherSolution/R\303\251sultat/R\303\251sultatRechercherSolution.tsx" index ec1c4e14bc..1b99462028 100644 --- "a/src/client/components/layouts/RechercherSolution/R\303\251sultat/R\303\251sultatRechercherSolution.tsx" +++ "b/src/client/components/layouts/RechercherSolution/R\303\251sultat/R\303\251sultatRechercherSolution.tsx" @@ -1,6 +1,6 @@ import classNames from 'classnames'; import Image from 'next/image'; -import React, { PropsWithChildren, useId } from 'react'; +import React, { PropsWithChildren, ReactNode, useId } from 'react'; import styles from '~/client/components/layouts/RechercherSolution/Résultat/RésultatRechercherSolution.module.scss'; import { Icon } from '~/client/components/ui/Icon/Icon'; @@ -13,7 +13,7 @@ type RésultatRechercherSolutionProps = { lienOffre?: string; intituléOffre: string; intituléLienOffre?: string; - sousTitreOffre?: string; + sousTitreOffre?: string | ReactNode; étiquetteOffreList: string[]; } & LogoProps diff --git a/src/client/dependencies.container.ts b/src/client/dependencies.container.ts index 058335bb47..a0a0d45bcf 100644 --- a/src/client/dependencies.container.ts +++ b/src/client/dependencies.container.ts @@ -36,6 +36,8 @@ import { MetierService } from '~/client/services/metiers/metier.service'; import { MissionEngagementService } from '~/client/services/missionEngagement/missionEngagement.service'; import { OffreService } from '~/client/services/offre/offre.service'; import { StageService } from '~/client/services/stage/stage.service'; +import { BffStage3emeService } from '~/client/services/stage3eme/bff.stage3eme.service'; +import { Stage3emeService } from '~/client/services/stage3eme/stage3eme.service'; import { VideoService } from '~/client/services/video/video.service'; import { YoutubeVideoService } from '~/client/services/video/youtube/youtube.video.service'; @@ -59,6 +61,7 @@ export type Dependencies = { marketingService: MarketingService dateService: DateService emploiEuropeService: EmploiEuropeService + stage3emeService: Stage3emeService } class DependencyInitException extends Error { @@ -115,6 +118,8 @@ export default function dependenciesContainer(sessionId: string): Dependencies { primaryKey: 'slug', }, ); + + const stage3emeService = new BffStage3emeService(httpClientService); return { alternanceService, @@ -132,6 +137,7 @@ export default function dependenciesContainer(sessionId: string): Dependencies { missionEngagementService, offreService, rechercheClientService, + stage3emeService, stageService, youtubeService, établissementAccompagnementService, diff --git a/src/client/services/stage3eme/bff.stage3eme.service.test.ts b/src/client/services/stage3eme/bff.stage3eme.service.test.ts new file mode 100644 index 0000000000..a83d8212c3 --- /dev/null +++ b/src/client/services/stage3eme/bff.stage3eme.service.test.ts @@ -0,0 +1,22 @@ +/** + * @jest-environment jsdom + */ + +import { anHttpClientService } from '~/client/services/httpClientService.fixture'; +import { BffStage3emeService } from '~/client/services/stage3eme/bff.stage3eme.service'; + +describe('BffStage3emeService', () => { + describe('rechercherStage3eme', () => { + it('appelle le endpoint', async () => { + // Given + const httpClientService = anHttpClientService(); + const bffStage3emeService = new BffStage3emeService(httpClientService); + + // When + await bffStage3emeService.rechercherStage3eme(); + + // Then + expect(httpClientService.get).toHaveBeenCalledWith('stages-3eme'); + }); + }); +}); diff --git a/src/client/services/stage3eme/bff.stage3eme.service.ts b/src/client/services/stage3eme/bff.stage3eme.service.ts new file mode 100644 index 0000000000..f53c3a34e2 --- /dev/null +++ b/src/client/services/stage3eme/bff.stage3eme.service.ts @@ -0,0 +1,12 @@ +import { HttpClientService } from '~/client/services/httpClient.service'; +import { ResultatRechercheStage3eme } from '~/server/stage-3eme/domain/stage3eme'; + +import { Stage3emeService } from './stage3eme.service'; + +export class BffStage3emeService implements Stage3emeService { + constructor(private httpClientService: HttpClientService) {} + + async rechercherStage3eme() { + return await this.httpClientService.get('stages-3eme'); + } +} diff --git a/src/client/services/stage3eme/stage3eme.service.fixture.ts b/src/client/services/stage3eme/stage3eme.service.fixture.ts new file mode 100644 index 0000000000..bd4f81f7c6 --- /dev/null +++ b/src/client/services/stage3eme/stage3eme.service.fixture.ts @@ -0,0 +1,11 @@ +import { createSuccess } from '~/server/errors/either'; +import { aResultatRechercheStage3eme } from '~/server/stage-3eme/domain/stage3eme.fixture'; + +import { Stage3emeService } from './stage3eme.service'; + +export function aStage3emeService(override?: Partial): Stage3emeService { + return { + rechercherStage3eme: jest.fn().mockResolvedValue(createSuccess(aResultatRechercheStage3eme())), + ...override, + }; +} diff --git a/src/client/services/stage3eme/stage3eme.service.ts b/src/client/services/stage3eme/stage3eme.service.ts new file mode 100644 index 0000000000..30b1133ea6 --- /dev/null +++ b/src/client/services/stage3eme/stage3eme.service.ts @@ -0,0 +1,6 @@ +import { Either } from '~/server/errors/either'; +import { ResultatRechercheStage3eme } from '~/server/stage-3eme/domain/stage3eme'; + +export interface Stage3emeService { + rechercherStage3eme(): Promise>; +} diff --git a/src/pages/api/stages-3eme/index.controller.ts b/src/pages/api/stages-3eme/index.controller.ts new file mode 100644 index 0000000000..b59507d432 --- /dev/null +++ b/src/pages/api/stages-3eme/index.controller.ts @@ -0,0 +1,14 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +import { withMonitoring } from '~/pages/api/middlewares/monitoring/monitoring.middleware'; +import { ErrorHttpResponse } from '~/pages/api/utils/response/response.type'; +import { handleResponse } from '~/pages/api/utils/response/response.util'; +import { ResultatRechercheStage3eme } from '~/server/stage-3eme/domain/stage3eme'; +import { dependencies } from '~/server/start'; + +export async function rechercherStage3emeHandler(req: NextApiRequest, res: NextApiResponse) { + const resultatsRechercheStage3eme = await dependencies.stage3emeDependencies.rechercherStage3eme.handle(); + return handleResponse(resultatsRechercheStage3eme, res); +} + +export default withMonitoring(rechercherStage3emeHandler); diff --git a/src/pages/stages-3eme/index.page.test.tsx b/src/pages/stages-3eme/index.page.test.tsx index 9f8ccdea5f..50391dad27 100644 --- a/src/pages/stages-3eme/index.page.test.tsx +++ b/src/pages/stages-3eme/index.page.test.tsx @@ -10,6 +10,7 @@ import { mockUseRouter } from '~/client/components/useRouter.mock'; import { mockSmallScreen } from '~/client/components/window.mock'; import { DependenciesProvider } from '~/client/context/dependenciesContainer.context'; import { aManualAnalyticsService } from '~/client/services/analytics/analytics.service.fixture'; +import { aStage3emeService } from '~/client/services/stage3eme/stage3eme.service.fixture'; import Stages3emePage, { getServerSideProps } from './index.page'; @@ -38,9 +39,11 @@ describe('Page stages de 3ème', () => { const { container } = render( ); + await screen.findByRole('heading', { name: 'Des milliers d’entreprises prêtes à vous accueillir pour votre stage de 3ème' }); await expect(container).toBeAccessible(); }); @@ -48,10 +51,13 @@ describe('Page stages de 3ème', () => { render( ); + await screen.findByRole('heading', { name: 'Des milliers d’entreprises prêtes à vous accueillir pour votre stage de 3ème' }); + const pageHeading = screen.getByRole('heading', { level: 1, name: 'Des milliers d’entreprises prêtes à vous accueillir pour votre stage de 3ème', @@ -59,16 +65,19 @@ describe('Page stages de 3ème', () => { expect(pageHeading).toBeVisible(); }); - it('envoie les analytics', () => { + it('envoie les analytics', async () => { const analyticsService = aManualAnalyticsService(); render( , ); + await screen.findByRole('heading', { name: 'Des milliers d’entreprises prêtes à vous accueillir pour votre stage de 3ème' }); + expect(analyticsService.envoyerAnalyticsPageVue).toHaveBeenCalledWith({ page_template: 'contenu_liste_niv_1', pagegroup: 'stages_3e_liste', diff --git a/src/pages/stages-3eme/index.page.tsx b/src/pages/stages-3eme/index.page.tsx index d089563328..7f4f19f806 100644 --- a/src/pages/stages-3eme/index.page.tsx +++ b/src/pages/stages-3eme/index.page.tsx @@ -1,6 +1,6 @@ import { GetServerSidePropsResult } from 'next'; -import RechercherStages3eme from '~/client/components/features/Stages3eme/RechercherStages3eme'; +import RechercherStages3eme from '~/client/components/features/Stages3eme/Rechercher/RechercherStages3eme'; import useAnalytics from '~/client/hooks/useAnalytics'; import analytics from './index.analytics'; diff --git a/src/server/configuration/dependencies.container.ts b/src/server/configuration/dependencies.container.ts index 486b44a51f..fd7335810b 100644 --- a/src/server/configuration/dependencies.container.ts +++ b/src/server/configuration/dependencies.container.ts @@ -174,6 +174,16 @@ import { SitemapDependencies, sitemapDependenciesContainer, } from '~/server/sitemap/configuration/dependencies.container'; +import { + Stage3emeDependencies, + stage3emeDependenciesContainer, +} from '~/server/stage-3eme/configuration/dependencies.container'; +import { + getApiImmersionFacileStage3emeConfig, +} from '~/server/stage-3eme/configuration/stage-3eme/stage3emeHttpClient.config'; +import { + ApiImmersionFacileStage3emeRepository, +} from '~/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.repository'; export type Dependencies = { ficheMetierDependencies: FicheMetierDependencies; @@ -196,6 +206,7 @@ export type Dependencies = { établissementAccompagnementDependencies: ÉtablissementAccompagnementDependencies; loggerService: LoggerService emploiEuropeDependencies: EmploiEuropeDependencies; + stage3emeDependencies: Stage3emeDependencies; } export function dependenciesContainer(): Dependencies { @@ -328,6 +339,13 @@ export function dependenciesContainer(): Dependencies { ? emploiEuropeDependenciesContainer(new MockEmploiEuropeRepository(apiEuresEmploiEuropeMapper)) : emploiEuropeDependenciesContainer(apiEuresEmploiEuropeRepository); + const stage3emeHttpClientService = new PublicHttpClientService(getApiImmersionFacileStage3emeConfig(serverConfigurationService)); + const apiImmersionFacileStage3emeRepository = new ApiImmersionFacileStage3emeRepository( + stage3emeHttpClientService, + defaultErrorManagementService, + ); + const stage3emeDependencies = stage3emeDependenciesContainer(apiImmersionFacileStage3emeRepository); + return { alternanceDependencies, cmsDependencies, @@ -348,6 +366,7 @@ export function dependenciesContainer(): Dependencies { offreJobÉtudiantDependencies, robotsDependencies, sitemapDependencies, + stage3emeDependencies, établissementAccompagnementDependencies, }; } diff --git a/src/server/services/configuration.service.fixture.ts b/src/server/services/configuration.service.fixture.ts index 7e288dedd7..54bd6dcdb0 100644 --- a/src/server/services/configuration.service.fixture.ts +++ b/src/server/services/configuration.service.fixture.ts @@ -17,6 +17,8 @@ export class ConfigurationServiceFixture implements ConfigurationService { API_EURES_BASE_URL: 'https://webgate.acceptance.ec.europa.eu/eures-api/output/api/v1/jv/', API_EURES_IS_MOCK_ACTIVE: false, API_GEO_BASE_URL: 'https://geo.api.gouv.fr/', + API_IMMERSION_FACILE_STAGE_3EME_API_KEY: 'API_IMMERSION_FACILE_STAGE_3EME_API_KEY', + API_IMMERSION_FACILE_STAGE_3EME_URL: 'https://api.immersion-facile.beta.gouv.fr/v1/', API_LA_BONNE_ALTERNANCE_CALLER: '1jeune-1solution-test', API_LA_BONNE_ALTERNANCE_URL: 'https://labonnealternance-recette.beta.gouv.fr/api/', API_LES_ENTREPRISES_SENGAGENT_URL: 'https://staging.lesentreprises-sengagent.local', diff --git a/src/server/services/serverConfiguration.service.ts b/src/server/services/serverConfiguration.service.ts index af425e58cb..f9fa6777c5 100644 --- a/src/server/services/serverConfiguration.service.ts +++ b/src/server/services/serverConfiguration.service.ts @@ -10,6 +10,8 @@ export default class ServerConfigurationService implements ConfigurationService API_EURES_BASE_URL: ServerConfigurationService.getOrThrowError('API_EURES_BASE_URL'), API_EURES_IS_MOCK_ACTIVE: Boolean(Number(ServerConfigurationService.getOrDefault('API_EURES_IS_MOCK_ACTIVE', '0'))), API_GEO_BASE_URL: ServerConfigurationService.getOrThrowError('API_GEO_BASE_URL'), + API_IMMERSION_FACILE_STAGE_3EME_API_KEY: ServerConfigurationService.getOrThrowError('API_IMMERSION_FACILE_STAGE_3EME_API_KEY'), + API_IMMERSION_FACILE_STAGE_3EME_URL: ServerConfigurationService.getOrThrowError('API_IMMERSION_FACILE_STAGE_3EME_URL'), API_LA_BONNE_ALTERNANCE_CALLER: ServerConfigurationService.getOrThrowError('API_LA_BONNE_ALTERNANCE_CALLER'), API_LA_BONNE_ALTERNANCE_URL: ServerConfigurationService.getOrThrowError('API_LA_BONNE_ALTERNANCE_URL'), API_LES_ENTREPRISES_SENGAGENT_URL: ServerConfigurationService.getOrThrowError('API_LES_ENTREPRISES_SENGAGENT_URL'), @@ -95,6 +97,8 @@ export interface EnvironmentVariables { readonly API_EURES_BASE_URL: string readonly API_EURES_IS_MOCK_ACTIVE: boolean readonly API_GEO_BASE_URL: string + readonly API_IMMERSION_FACILE_STAGE_3EME_API_KEY: string + readonly API_IMMERSION_FACILE_STAGE_3EME_URL: string readonly API_LA_BONNE_ALTERNANCE_CALLER: string readonly API_LA_BONNE_ALTERNANCE_URL: string readonly API_LES_ENTREPRISES_SENGAGENT_URL: string diff --git a/src/server/stage-3eme/configuration/dependencies.container.ts b/src/server/stage-3eme/configuration/dependencies.container.ts new file mode 100644 index 0000000000..97af4b25b2 --- /dev/null +++ b/src/server/stage-3eme/configuration/dependencies.container.ts @@ -0,0 +1,12 @@ +import { Stage3emeRepository } from '../domain/stage3eme.repository'; +import { RechercherStage3emeUseCase } from '../useCase/rechercherStage3eme.useCase'; + +export interface Stage3emeDependencies { + rechercherStage3eme: RechercherStage3emeUseCase +} + +export function stage3emeDependenciesContainer(repository: Stage3emeRepository): Stage3emeDependencies { + return { + rechercherStage3eme: new RechercherStage3emeUseCase(repository), + }; +} diff --git a/src/server/stage-3eme/configuration/stage-3eme/stage3emeHttpClient.config.ts b/src/server/stage-3eme/configuration/stage-3eme/stage3emeHttpClient.config.ts new file mode 100644 index 0000000000..c8b9c6ca01 --- /dev/null +++ b/src/server/stage-3eme/configuration/stage-3eme/stage3emeHttpClient.config.ts @@ -0,0 +1,10 @@ +import { ConfigurationService } from '~/server/services/configuration.service'; +import { PublicHttpClientConfig } from '~/server/services/http/publicHttpClient.service'; + +export function getApiImmersionFacileStage3emeConfig(configurationService: ConfigurationService): PublicHttpClientConfig { + return ({ + apiHeaders: { authorization: configurationService.getConfiguration().API_IMMERSION_FACILE_STAGE_3EME_API_KEY }, + apiName: 'API_IMMERSION_FACILE_STAGE_3EME', + apiUrl: configurationService.getConfiguration().API_IMMERSION_FACILE_STAGE_3EME_URL, + }); +} diff --git a/src/server/stage-3eme/domain/stage3eme.fixture.ts b/src/server/stage-3eme/domain/stage3eme.fixture.ts new file mode 100644 index 0000000000..3bb35ea999 --- /dev/null +++ b/src/server/stage-3eme/domain/stage3eme.fixture.ts @@ -0,0 +1,44 @@ +import { ResultatRechercheStage3eme, Stage3eme } from './stage3eme'; + +export function aResultatRechercheStage3eme(override?: Partial): ResultatRechercheStage3eme { + return { + nombreDeResultats: 2, + resultats: [ + aStage3eme({ + adresse: { + codeDepartement: '75', + codePostal: '75000', + ligne: '1 rue de la Boulangerie', + ville: 'Paris', + }, + domaine: 'Boulangerie', + nomEntreprise: 'La Boulangerie', + }), + aStage3eme({ + adresse: { + codeDepartement: '75', + codePostal: '75000', + ligne: '1 rue de la Pâtisserie', + ville: 'Paris', + }, + domaine: 'Pâtisserie', + nomEntreprise: 'La Pâtisserie', + }), + ], + ...override, + }; +} + +export function aStage3eme(override?: Partial): Stage3eme { + return { + adresse: { + codeDepartement: '75', + codePostal: '75000', + ligne: '1 rue de la Boulangerie', + ville: 'Paris', + }, + domaine: 'Boulangerie', + nomEntreprise: 'La Boulangerie', + ...override, + }; +} diff --git a/src/server/stage-3eme/domain/stage3eme.repository.ts b/src/server/stage-3eme/domain/stage3eme.repository.ts new file mode 100644 index 0000000000..1d4f1369a6 --- /dev/null +++ b/src/server/stage-3eme/domain/stage3eme.repository.ts @@ -0,0 +1,7 @@ +import { Either } from '~/server/errors/either'; + +import { ResultatRechercheStage3eme } from './stage3eme'; + +export interface Stage3emeRepository { + search(): Promise> +} diff --git a/src/server/stage-3eme/domain/stage3eme.ts b/src/server/stage-3eme/domain/stage3eme.ts new file mode 100644 index 0000000000..82b1bbf372 --- /dev/null +++ b/src/server/stage-3eme/domain/stage3eme.ts @@ -0,0 +1,19 @@ +export interface Stage3eme { + nomEntreprise: string + adresse: Stage3eme.Adresse + domaine: string +} + +export namespace Stage3eme { + export interface Adresse { + codeDepartement: string + codePostal: string + ligne: string + ville: string + } +} + +export interface ResultatRechercheStage3eme { + nombreDeResultats: number + resultats: Array +} diff --git a/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.fixture.ts b/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.fixture.ts new file mode 100644 index 0000000000..6925bc0800 --- /dev/null +++ b/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.fixture.ts @@ -0,0 +1,15 @@ +import { ApiImmersionFacileStage3emeRechercheResponse } from './apiImmersionFacileStage3eme'; + +export function anApiImmersionFacileStage3eme(override?: Partial): ApiImmersionFacileStage3emeRechercheResponse { + return { + address: { + city: 'Paris', + departmentCode: '75', + postcode: '75001', + streetNumberAndAddress: '1 Rue de la Lune', + }, + name: 'La Boulangerie', + romeLabel: 'Boulangerie', + ...override, + }; +} diff --git a/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.mapper.test.ts b/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.mapper.test.ts new file mode 100644 index 0000000000..607bd3e7a2 --- /dev/null +++ b/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.mapper.test.ts @@ -0,0 +1,63 @@ +import { aResultatRechercheStage3eme } from '~/server/stage-3eme/domain/stage3eme.fixture'; +import { + anApiImmersionFacileStage3eme, +} from '~/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.fixture'; +import { mapRechercheStage3eme } from '~/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.mapper'; + +describe('mapRechercheStage3eme.mapper', () => { + it('retourne un ResultatRechercheStage3eme avec les données de l’api Immersion Facile', () => { + // Given + const apiImmersionFacileStage3eme = [ + anApiImmersionFacileStage3eme({ + address: { + city: 'Paris', + departmentCode: '75', + postcode: '75001', + streetNumberAndAddress: '1 Rue de la Lune', + }, + name: 'La Boulangerie', + romeLabel: 'Boulangerie', + }), + anApiImmersionFacileStage3eme({ + address: { + city: 'Paris', + departmentCode: '75', + postcode: '75002', + streetNumberAndAddress: '2 Rue de la Lune', + }, + name: 'La Boulangerie 2', + romeLabel: 'Boulangerie', + }), + ]; + + // When + const result = mapRechercheStage3eme(apiImmersionFacileStage3eme); + + // Then + expect(result).toEqual(aResultatRechercheStage3eme({ + nombreDeResultats: 2, + resultats: [ + { + adresse: { + codeDepartement: '75', + codePostal: '75001', + ligne: '1 Rue de la Lune', + ville: 'Paris', + }, + domaine: 'Boulangerie', + nomEntreprise: 'La Boulangerie', + }, + { + adresse: { + codeDepartement: '75', + codePostal: '75002', + ligne: '2 Rue de la Lune', + ville: 'Paris', + }, + domaine: 'Boulangerie', + nomEntreprise: 'La Boulangerie 2', + }, + ], + })); + }); +}); diff --git a/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.mapper.ts b/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.mapper.ts new file mode 100644 index 0000000000..37f39f2f6b --- /dev/null +++ b/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.mapper.ts @@ -0,0 +1,18 @@ +import { ResultatRechercheStage3eme } from '../../domain/stage3eme'; +import { ApiImmersionFacileStage3emeRechercheResponse } from './apiImmersionFacileStage3eme'; + +export function mapRechercheStage3eme(apiResponse: Array): ResultatRechercheStage3eme { + return { + nombreDeResultats: apiResponse.length, + resultats: apiResponse.map((stage3eme) => ({ + adresse: { + codeDepartement: stage3eme.address.departmentCode, + codePostal: stage3eme.address.postcode, + ligne: stage3eme.address.streetNumberAndAddress, + ville: stage3eme.address.city, + }, + domaine: stage3eme.romeLabel, + nomEntreprise: stage3eme.name, + })), + }; +} diff --git a/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.repository.test.ts b/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.repository.test.ts new file mode 100644 index 0000000000..e40a22b9f6 --- /dev/null +++ b/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.repository.test.ts @@ -0,0 +1,93 @@ +import { createFailure, Success } from '~/server/errors/either'; +import { ErreurMetier } from '~/server/errors/erreurMetier.types'; +import { anErrorManagementService } from '~/server/services/error/errorManagement.fixture'; +import { anHttpError } from '~/server/services/http/httpError.fixture'; +import { anAxiosResponse, aPublicHttpClientService } from '~/server/services/http/publicHttpClient.service.fixture'; +import { ResultatRechercheStage3eme } from '~/server/stage-3eme/domain/stage3eme'; +import { aResultatRechercheStage3eme } from '~/server/stage-3eme/domain/stage3eme.fixture'; +import { + anApiImmersionFacileStage3eme, +} from '~/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.fixture'; +import { + ApiImmersionFacileStage3emeRepository, +} from '~/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.repository'; + +describe('ApiImmersionFacileStage3emeRepository', () => { + describe('search', () => { + it('appelle l’api Immersion Facile avec les bon paramètres', () => { + // Given + const httpClientService = aPublicHttpClientService(); + const repository = new ApiImmersionFacileStage3emeRepository(httpClientService, anErrorManagementService()); + + // When + repository.search(); + + // Then + expect(httpClientService.get).toHaveBeenCalledWith('/search?latitude=48.8535&longitude=2.34839&distanceKm=10'); + }); + + describe('quand l’api Immersion Facile répond avec des données valide', () => { + it('retourne les données de l’api Immersion Facile', async () => { + // Given + const httpClientService = aPublicHttpClientService(); + jest.spyOn(httpClientService, 'get').mockResolvedValue(anAxiosResponse([anApiImmersionFacileStage3eme( + { + address: { + city: 'Paris', + departmentCode: '75', + postcode: '75001', + streetNumberAndAddress: '1 Rue de la Lune', + }, + name: 'La Boulangerie', + romeLabel: 'Boulangerie', + }, + )])); + const repository = new ApiImmersionFacileStage3emeRepository(httpClientService, anErrorManagementService()); + + // When + const result = await repository.search() as Success; + + // Then + expect(result.result).toEqual(aResultatRechercheStage3eme({ + nombreDeResultats: 1, + resultats: [ + { + adresse: { + codeDepartement: '75', + codePostal: '75001', + ligne: '1 Rue de la Lune', + ville: 'Paris', + }, + domaine: 'Boulangerie', + nomEntreprise: 'La Boulangerie', + }, + ], + })); + }); + }); + + describe('quand l’api répond avec une erreur', () => { + it('log les informations de l’erreur et retourne une erreur métier associée', async () => { + // GIVEN + const httpError = anHttpError(500); + const httpClientService = aPublicHttpClientService(); + const errorManagementService = anErrorManagementService(); + jest.spyOn(httpClientService, 'get').mockRejectedValue(httpError); + const repository = new ApiImmersionFacileStage3emeRepository(httpClientService, errorManagementService); + const errorReturnedByErrorManagementService = ErreurMetier.SERVICE_INDISPONIBLE; + jest.spyOn(errorManagementService, 'handleFailureError').mockReturnValue(createFailure(errorReturnedByErrorManagementService)); + + // WHEN + const result = await repository.search(); + + // THEN + expect(result).toEqual(createFailure(errorReturnedByErrorManagementService)); + expect(errorManagementService.handleFailureError).toHaveBeenCalledWith(httpError, { + apiSource: 'API Immersion Facile Stage 3eme', + contexte: 'search stage 3eme', + message: 'impossible d’effectuer une recherche de stage 3eme', + }); + }); + }); + }); +}); diff --git a/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.repository.ts b/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.repository.ts new file mode 100644 index 0000000000..50d76025ad --- /dev/null +++ b/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.repository.ts @@ -0,0 +1,35 @@ +import { createSuccess } from '~/server/errors/either'; +import { validateApiResponse } from '~/server/services/error/apiResponseValidator'; +import { ErrorManagementService } from '~/server/services/error/errorManagement.service'; +import { PublicHttpClientService } from '~/server/services/http/publicHttpClient.service'; +import { Stage3emeRepository } from '~/server/stage-3eme/domain/stage3eme.repository'; +import { ApiImmersionFacileStage3emeRechercheResponse, apiImmersionFacileStage3emeSchemas } from '~/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme'; +import { mapRechercheStage3eme } from '~/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.mapper'; + +export class ApiImmersionFacileStage3emeRepository implements Stage3emeRepository { + constructor(private readonly httpClientService: PublicHttpClientService, private readonly errorManagementService: ErrorManagementService) { + } + + async search() { + try { + const endpoint = '/search?latitude=48.8535&longitude=2.34839&distanceKm=10'; + const response = await this.httpClientService.get>(endpoint); + const apiValidationError = validateApiResponse>(response.data, apiImmersionFacileStage3emeSchemas.search); + if (apiValidationError) { + this.errorManagementService.logValidationError(apiValidationError, { + apiSource: 'API Immersion Facile Stage 3eme', + contexte: 'search stage 3eme', + message: 'erreur de validation du schéma de l’api', + }); + } + const mappedResponse = mapRechercheStage3eme(response.data); + return createSuccess(mappedResponse); + } catch (error) { + return this.errorManagementService.handleFailureError(error, { + apiSource: 'API Immersion Facile Stage 3eme', + contexte: 'search stage 3eme', + message: 'impossible d’effectuer une recherche de stage 3eme', + }); + } + } +} diff --git a/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.ts b/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.ts new file mode 100644 index 0000000000..4658474668 --- /dev/null +++ b/src/server/stage-3eme/infra/repositories/apiImmersionFacileStage3eme.ts @@ -0,0 +1,25 @@ +import Joi from 'joi'; + +export interface ApiImmersionFacileStage3emeRechercheResponse { + name: string + address: { + city: string + postcode: string + departmentCode: string + streetNumberAndAddress: string + } + romeLabel: string +} + +export const apiImmersionFacileStage3emeSchemas = { + search: Joi.array().items(Joi.object({ + address: Joi.object({ + city: Joi.string(), + departmentCode: Joi.string(), + postcode: Joi.string(), + streetNumberAndAddress: Joi.string(), + }), + name: Joi.string(), + romeLabel: Joi.string(), + })).options({ allowUnknown: true }), +}; diff --git a/src/server/stage-3eme/useCase/rechercherStage3eme.useCase.ts b/src/server/stage-3eme/useCase/rechercherStage3eme.useCase.ts new file mode 100644 index 0000000000..adb52a903b --- /dev/null +++ b/src/server/stage-3eme/useCase/rechercherStage3eme.useCase.ts @@ -0,0 +1,9 @@ +import { Stage3emeRepository } from '../domain/stage3eme.repository'; + +export class RechercherStage3emeUseCase { + constructor(private repository: Stage3emeRepository) {} + + async handle() { + return this.repository.search(); + } +}