Skip to content

Commit 805e1e7

Browse files
committed
handle ftConnect in AfterOauthSuccess use case
1 parent fa25adc commit 805e1e7

39 files changed

Lines changed: 1295 additions & 960 deletions

File tree

back/src/adapters/primary/routers/auth/auth.e2e.test.ts

Lines changed: 208 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import {
1616
expectEmailOfType,
1717
expectHttpResponseToEqual,
1818
expectToEqual,
19+
FTConnectError,
1920
frontRoutes,
21+
ManagedFTConnectError,
2022
noAgencyDashboards,
2123
noEstablishmentDashboard,
2224
queryParamsAsString,
@@ -62,6 +64,7 @@ import { processEventsForEmailToBeSent } from "../../../../utils/processEventsFo
6264

6365
describe("auth router", () => {
6466
const immersionDomain = "immersion.fr";
67+
const now = new Date();
6568

6669
let authRoutesClient: HttpClient<AuthRoutes>;
6770
let technicalRoutesClient: HttpClient<TechnicalRoutes>;
@@ -99,7 +102,7 @@ describe("auth router", () => {
99102
technicalRoutes,
100103
request,
101104
);
102-
gateways.timeGateway.setNextDate(new Date());
105+
gateways.timeGateway.setNextDate(now);
103106
});
104107

105108
describe("user connexion flow", () => {
@@ -148,6 +151,7 @@ describe("auth router", () => {
148151
);
149152

150153
gateways.proConnectOAuthGateway.setAccessTokenResponse({
154+
type: "proConnect",
151155
accessToken: proConnectToken,
152156
idToken,
153157
expire: 1,
@@ -247,6 +251,7 @@ describe("auth router", () => {
247251
inMemoryUow.agencyRepository.agencies = [toAgencyWithRights(agency)];
248252

249253
gateways.proConnectOAuthGateway.setAccessTokenResponse({
254+
type: "proConnect",
250255
accessToken: proConnectToken,
251256
expire: 1,
252257
idToken,
@@ -404,6 +409,208 @@ describe("auth router", () => {
404409
});
405410
});
406411

412+
describe("Right path with FT Connect", () => {
413+
const ftConnectAuthCode = "ft-connect-auth-code";
414+
const ftConnectAccessToken = "ft-connect-token";
415+
const ftConnectIdToken = "ft-connect-id-token";
416+
const ftConnectExternalId = "ft-connect-external-id";
417+
const conventionDraftId = "ft-connect-convention-draft-id";
418+
419+
describe(displayRouteName(authRoutes.initiateLoginByOAuth), () => {
420+
it("redirects to FT Connect login and stores ongoing OAuth", async () => {
421+
const uuids = [nonce, state];
422+
uuidGenerator.new = () => uuids.shift() ?? "no-uuid-provided";
423+
424+
expectHttpResponseToEqual(
425+
await authRoutesClient.initiateLoginByOAuth({
426+
queryParams: {
427+
provider: "peConnect",
428+
redirectUri: `/${frontRoutes.conventionImmersionRoute}`,
429+
},
430+
}),
431+
{
432+
body: {},
433+
status: 302,
434+
headers: {
435+
location: encodeURI(
436+
`https://fake-ft-connect-login-url?${queryParamsAsString({
437+
nonce,
438+
state,
439+
})}`,
440+
),
441+
},
442+
},
443+
);
444+
445+
expectToEqual(inMemoryUow.ongoingOAuthRepository.ongoingOAuths, [
446+
{
447+
provider: "peConnect",
448+
nonce,
449+
state,
450+
usedAt: null,
451+
fromUri: `/${frontRoutes.conventionImmersionRoute}`,
452+
},
453+
]);
454+
});
455+
});
456+
457+
describe(displayRouteName(authRoutes.afterFTConnectOAuthLogin), () => {
458+
it("redirects to convention immersion page with convention draft id", async () => {
459+
uuidGenerator.new = () => conventionDraftId;
460+
inMemoryUow.ongoingOAuthRepository.ongoingOAuths = [
461+
{
462+
provider: "peConnect",
463+
nonce,
464+
state,
465+
usedAt: null,
466+
fromUri: `/${frontRoutes.conventionImmersionRoute}`,
467+
},
468+
];
469+
gateways.ftConnectGateway.setAccessTokenResult({
470+
type: "ftConnect",
471+
accessToken: ftConnectAccessToken,
472+
expire: 1,
473+
idToken: ftConnectIdToken,
474+
payload: { nonce },
475+
});
476+
gateways.ftConnectGateway.setUser({
477+
isJobseeker: true,
478+
firstName: "Jean",
479+
lastName: "Dupont",
480+
birthdate: "1990-01-01",
481+
peExternalId: ftConnectExternalId,
482+
});
483+
gateways.ftConnectGateway.setAdvisors([
484+
{
485+
type: "PLACEMENT",
486+
firstName: "Alice",
487+
lastName: "Martin",
488+
email: "conseiller@francetravail.fr",
489+
},
490+
]);
491+
492+
const response = await authRoutesClient.afterFTConnectOAuthLogin({
493+
queryParams: {
494+
code: ftConnectAuthCode,
495+
state,
496+
},
497+
});
498+
499+
expectHttpResponseToEqual(response, {
500+
body: {},
501+
status: 302,
502+
headers: {
503+
location: `${appConfig.immersionFacileBaseUrl}/${frontRoutes.conventionImmersionRoute}?conventionDraftId=${conventionDraftId}`,
504+
},
505+
});
506+
507+
expectToEqual(inMemoryUow.ongoingOAuthRepository.ongoingOAuths, [
508+
{
509+
provider: "peConnect",
510+
nonce,
511+
state,
512+
usedAt: now,
513+
accessToken: ftConnectAccessToken,
514+
fromUri: `/${frontRoutes.conventionImmersionRoute}`,
515+
},
516+
]);
517+
expectToEqual(
518+
inMemoryUow.conventionDraftRepository.conventionDrafts,
519+
[
520+
{
521+
id: conventionDraftId,
522+
internshipKind: "immersion",
523+
fromPeConnectedUser: true,
524+
updatedAt: gateways.timeGateway.now().toISOString(),
525+
signatories: {
526+
beneficiary: {
527+
firstName: "Jean",
528+
lastName: "Dupont",
529+
birthdate: "1990-01-01",
530+
federatedIdentity: {
531+
provider: "peConnect",
532+
token: ftConnectExternalId,
533+
},
534+
},
535+
},
536+
validators: {
537+
agencyCounsellor: {
538+
firstname: "Alice",
539+
lastname: "Martin",
540+
},
541+
},
542+
},
543+
],
544+
);
545+
});
546+
547+
it("redirects to managed FT Connect error page when FT Connect throws a managed error", async () => {
548+
inMemoryUow.ongoingOAuthRepository.ongoingOAuths = [
549+
{
550+
provider: "peConnect",
551+
nonce,
552+
state,
553+
usedAt: null,
554+
fromUri: `/${frontRoutes.conventionImmersionRoute}`,
555+
},
556+
];
557+
gateways.ftConnectGateway.getAccessToken = async () => {
558+
throw new ManagedFTConnectError("peConnectNoAuthorisation");
559+
};
560+
561+
const response = await authRoutesClient.afterFTConnectOAuthLogin({
562+
queryParams: {
563+
code: ftConnectAuthCode,
564+
state,
565+
},
566+
});
567+
568+
expectHttpResponseToEqual(response, {
569+
body: {},
570+
status: 302,
571+
headers: {
572+
location: `${appConfig.immersionFacileBaseUrl}/${frontRoutes.error}?kind=peConnectNoAuthorisation`,
573+
},
574+
});
575+
});
576+
577+
it("redirects to raw FT Connect error page when FT Connect throws a raw error", async () => {
578+
const rawErrorTitle = "Erreur France Travail";
579+
const rawErrorMessage = "Le service France Travail est indisponible";
580+
inMemoryUow.ongoingOAuthRepository.ongoingOAuths = [
581+
{
582+
provider: "peConnect",
583+
nonce,
584+
state,
585+
usedAt: null,
586+
fromUri: `/${frontRoutes.conventionImmersionRoute}`,
587+
},
588+
];
589+
gateways.ftConnectGateway.getAccessToken = async () => {
590+
throw new FTConnectError(rawErrorTitle, rawErrorMessage);
591+
};
592+
593+
const response = await authRoutesClient.afterFTConnectOAuthLogin({
594+
queryParams: {
595+
code: ftConnectAuthCode,
596+
state,
597+
},
598+
});
599+
600+
const { params } = decodeURIWithParams(
601+
response.headers.location as string,
602+
);
603+
expect(response.headers.location).toContain(
604+
`${appConfig.immersionFacileBaseUrl}/${frontRoutes.error}`,
605+
);
606+
expectToEqual(params, {
607+
title: encodeURIComponent(rawErrorTitle),
608+
message: encodeURIComponent(rawErrorMessage),
609+
});
610+
});
611+
});
612+
});
613+
407614
describe(`${displayRouteName(
408615
authRoutes.getOAuthLogoutUrl,
409616
)} returns the logout url`, () => {

back/src/adapters/primary/routers/auth/createAuthRouter.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { NextFunction, Request, Response } from "express";
22
import { Router } from "express";
3-
import { authRoutes } from "shared";
3+
import { authRoutes, FTConnectError, ManagedFTConnectError } from "shared";
44
import { createExpressSharedRouter } from "shared-routes/express";
55
import type { AppDependencies } from "../../../../config/bootstrap/createAppDependencies";
6+
import { handleHttpJsonResponseError } from "../../../../config/helpers/handleHttpJsonResponseError";
67
import { sendHttpResponse } from "../../../../config/helpers/sendHttpResponse";
78
import { sendRedirectResponse } from "../../../../config/helpers/sendRedirectResponse";
89
import { getGenericAuthOrThrow } from "../../../../domains/core/authentication/connected-user/entities/user.helper";
@@ -25,10 +26,33 @@ export const createAuthRouter = (deps: AppDependencies) => {
2526
if (useCaseResult.provider === "proConnect") {
2627
return res.status(302).redirect(useCaseResult.redirectUri);
2728
}
29+
if (useCaseResult.provider === "peConnect") {
30+
throw new Error("Incorrect provider for this route"); //TODO
31+
}
2832
return useCaseResult;
2933
});
3034
});
3135

36+
authSharedRouter.afterFTConnectOAuthLogin(async (req, res) => {
37+
try {
38+
const useCaseResult =
39+
await deps.useCases.afterOAuthSuccessRedirection.execute(req.query);
40+
if (useCaseResult.provider !== "peConnect") {
41+
throw new Error("Incorrect provider for this route"); //TODO
42+
}
43+
return res.status(302).redirect(useCaseResult.redirectUri);
44+
} catch (error) {
45+
if (error instanceof ManagedFTConnectError)
46+
return deps.errorHandlers.handleManagedRedirectResponseError(
47+
error,
48+
res,
49+
);
50+
if (error instanceof FTConnectError)
51+
return deps.errorHandlers.handleRawRedirectResponseError(error, res);
52+
return handleHttpJsonResponseError(req, res, error);
53+
}
54+
});
55+
3256
authSharedRouter.initiateLoginByEmail((req, res) =>
3357
sendHttpResponse(req, res, () =>
3458
deps.useCases.initiateLoginByEmail.execute(req.body),

back/src/adapters/primary/routers/ftConnect/createFtConnectRouter.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

back/src/config/bootstrap/createGateways.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { addressesExternalRoutes } from "../../domains/core/address/adapters/Htt
1111
import { InMemoryAddressGateway } from "../../domains/core/address/adapters/InMemoryAddressGateway";
1212
import { HttpSubscribersGateway } from "../../domains/core/api-consumer/adapters/HttpSubscribersGateway";
1313
import { InMemorySubscribersGateway } from "../../domains/core/api-consumer/adapters/InMemorySubscribersGateway";
14-
import { FtConnectOAuthGateway } from "../../domains/core/authentication/connected-user/adapters/oauth-gateway/FtConnectOAuthGateway";
1514
import { InMemoryOAuthGateway } from "../../domains/core/authentication/connected-user/adapters/oauth-gateway/InMemoryOAuthGateway";
1615
import { ProConnectOAuthGateway } from "../../domains/core/authentication/connected-user/adapters/oauth-gateway/ProConnectOAuthGateway";
1716
import { makeProConnectRoutes } from "../../domains/core/authentication/connected-user/adapters/oauth-gateway/proConnect.routes";
@@ -186,12 +185,7 @@ export const createGateways = async (
186185
}),
187186
axiosInstance: axiosWithoutValidateStatus,
188187
}),
189-
{
190-
immersionFacileBaseUrl: config.immersionFacileBaseUrl,
191-
franceTravailClientId: config.franceTravailClientId,
192-
franceTravailClientSecret: config.franceTravailClientSecret,
193-
ftAuthCandidatUrl: config.ftAuthCandidatUrl,
194-
},
188+
config.ftConnectConfig,
195189
config.ftConnectMaxRequestsPerInterval,
196190
)
197191
: new InMemoryFtConnectGateway();
@@ -210,11 +204,6 @@ export const createGateways = async (
210204
)
211205
: new InMemoryOAuthGateway(config.proConnectConfig);
212206

213-
const ftConnectOAuthGateway: OAuthGateway =
214-
config.ftConnectGateway === "HTTPS"
215-
? new FtConnectOAuthGateway(config.ftConnectConfig)
216-
: new InMemoryOAuthGateway(config.ftConnectConfig);
217-
218207
const createEmailValidationGateway = (config: AppConfig) =>
219208
({
220209
IN_MEMORY: () => new InMemoryEmailValidationGateway(),
@@ -398,7 +387,6 @@ export const createGateways = async (
398387
emailValidationGateway: createEmailValidationGateway(config),
399388

400389
proConnectOAuthGateway,
401-
ftConnectOAuthGateway,
402390
laBonneBoiteGateway:
403391
config.laBonneBoiteGateway === "HTTPS"
404392
? new HttpLaBonneBoiteGateway(

0 commit comments

Comments
 (0)