Skip to content

Commit

Permalink
Refactor PPSApi class and separate different modules (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
abarghoud authored Nov 12, 2024
1 parent a8d2f96 commit eaaae94
Show file tree
Hide file tree
Showing 28 changed files with 983 additions and 440 deletions.
94 changes: 6 additions & 88 deletions apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,98 +1,16 @@
import { Module, OnModuleInit, Scope } from '@nestjs/common';
import { HttpModule, HttpService } from '@nestjs/axios';

import { Agent } from 'https';

import { IHtmlParserSymbol } from '@easy-pps/html-parser/contracts';
import { HtmlParser } from '@easy-pps';
import { Module } from '@nestjs/common';

import { GenerateController } from './generate.controller';
import { PPSGeneratorUseCase } from './pps/usecase/pps-generator-usecase.service';
import { IPPSGenerateUseCaseSymbol } from './pps/usecase/pps-generate-usecase.interface';
import { IPPSApiSymbol } from './pps/third-party/pps-api.interface';
import { PPSApi } from './pps/third-party/pps-api';
import { IPPSApiResponseAuthenticationMetaDataExtractorSymbol } from './pps/domain/authentication-metadata-extractor/pps-api-response-authentication-metadata-extractor.interface';
import { PPSApiResponseAuthenticationMetaDataExtractor } from './pps/domain/authentication-metadata-extractor/pps-api-response-authentication-metadata-extractor';
import { KeepAliveController } from './keep-alive/keep-alive.controller';
import { IRecaptchaCheckerSymbol } from '@pps-easy/recaptcha/contracts';
import { GoogleRecaptchaChecker, LocalRecaptchaChecker } from '@pps-easy/recaptcha/google';
import * as process from 'node:process';
import { RecaptchaController } from './recpatcha/recaptcha.controller';
import { RecaptchaGuard } from './guards/recaptcha.guard';

const isLocalEnvironment = process.env.ENVIRONMENT === 'local';
import { PPSModule } from './pps/pps.module';
import { SharedModule } from './shared/shared.module';

@Module({
imports: [
HttpModule.register({
httpsAgent: new Agent({
rejectUnauthorized: false
}),
}),
SharedModule,
PPSModule,
],
controllers: [GenerateController, KeepAliveController, RecaptchaController],
providers: [
{ provide: IPPSGenerateUseCaseSymbol, useClass: PPSGeneratorUseCase },
{ provide: IPPSApiSymbol, useClass: PPSApi, scope: Scope.REQUEST },
{
provide: IPPSApiResponseAuthenticationMetaDataExtractorSymbol,
useClass: PPSApiResponseAuthenticationMetaDataExtractor,
scope: Scope.REQUEST,
},
{
provide: IHtmlParserSymbol,
useClass: HtmlParser,
},
{
provide: IRecaptchaCheckerSymbol,
useValue: isLocalEnvironment ?
new LocalRecaptchaChecker() :
new GoogleRecaptchaChecker(process.env.RECAPTCHA_SITE_KEY || '', process.env.FIREBASE_PROJECT_ID || '')
},
RecaptchaGuard
],
})
export class AppModule implements OnModuleInit {
constructor(private httpService: HttpService) {}

onModuleInit() {
// Add request and response interceptors
this.httpService.axiosRef.interceptors.request.use(
(config) => {
// Log request details
console.log('Request:', {
url: config.url,
method: config.method,
headers: config.headers,
params: config.params,
data: config.data,
});
return config;
},
error => {
// Log error if any during request setup
console.error('Request error:', error);
return Promise.reject(error);
}
);

this.httpService.axiosRef.interceptors.response.use(
response => {
// Log response details
console.log('Response:', {
status: response.status,
data: response.data,
});
return response;
},
error => {
// Log response error details
console.error('Response error:', {
status: error.response?.status,
data: error.response?.data,
});
return Promise.reject(error);
}
);
}
}
export class AppModule {}
2 changes: 1 addition & 1 deletion apps/api/src/app/generate.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('The GenerateController class', () => {
const ppsProfileDto = new PPSProfileDto();
const promise = generateController.generate(ppsProfileDto);

expect(ppsGeneratorUseCase.generate).toHaveBeenCalledWith(ppsProfileDto);
expect(ppsGeneratorUseCase.generate).toHaveBeenCalled();
expect(promise).resolves.toEqual(generateResolvedValue);
});
});
Expand Down
7 changes: 2 additions & 5 deletions apps/api/src/app/generate.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,8 @@ export class GenerateController {

@Post('/generate')
@UseGuards(RecaptchaGuard)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public generate(@Body() ppsProfileDto: PPSProfileDto): Promise<string> {
try {
return this.ppsGeneratorUseCase.generate(ppsProfileDto);
} catch (error) {
console.log(error);
}
return this.ppsGeneratorUseCase.generate();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AxiosResponse } from 'axios';
import { AxiosRequestConfig, AxiosResponse } from 'axios';

export const IPPSApiResponseAuthenticationMetaDataExtractorSymbol = Symbol.for('IPPSApiResponseAuthenticationMetaDataExtractor');

Expand All @@ -8,5 +8,7 @@ export interface IAuthenticationMetadata {
}

export interface IPPSApiResponseAuthenticationMetaDataExtractor {
extract(axiosResponse: AxiosResponse): IAuthenticationMetadata;
track(axiosResponse: AxiosResponse): void;
generateAxiosHeaderWithSessionData(): AxiosRequestConfig;
getAuthenticityToken(): string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ describe('The PPSApiResponseAuthenticationMetaDataExtractor class', () => {
expect(app.get(PPSApiResponseAuthenticationMetaDataExtractor)).toBeDefined();
});

describe('The extract method', () => {
describe('The track method', () => {
describe('When axios response is errored', () => {
it('should throw axios response as json', () => {
const axiosResponse = { status: 400, data: 'race date should be within three months', statusText: 'bad request' } as AxiosResponse;
const callback = () => app.get(PPSApiResponseAuthenticationMetaDataExtractor).extract(axiosResponse);
const callback = () => app.get(PPSApiResponseAuthenticationMetaDataExtractor).track(axiosResponse);

expect(callback).toThrow(JSON.stringify(axiosResponse));
});
Expand All @@ -36,7 +36,7 @@ describe('The PPSApiResponseAuthenticationMetaDataExtractor class', () => {
describe('When axios response does not contain the cookie', () => {
it('should throw exception', () => {
const axiosResponse = { status: 200, headers: { Cookie: '' } } as unknown as AxiosResponse;
const callback = () => app.get(PPSApiResponseAuthenticationMetaDataExtractor).extract(axiosResponse);
const callback = () => app.get(PPSApiResponseAuthenticationMetaDataExtractor).track(axiosResponse);

expect(callback).toThrow(expect.objectContaining({ message: expect.stringContaining('Failed extracting authentication metadata for response :') }));
});
Expand All @@ -47,7 +47,7 @@ describe('The PPSApiResponseAuthenticationMetaDataExtractor class', () => {
mockHtmlParser.getInputValueByName.mockReturnValueOnce('QSQS1212');
});

it('should use extract session id from the cookie and authenticity token from the html using html-parser service', () => {
it('should extract session id from the cookie and authenticity token from the html using html-parser service', () => {
const sessionId = 'ijeBk462CBSVUfYozH9j2%2FW3oTOy%2FKESUNKg7VhWoqXdfVQoFNqhra0fcl43qqPZDfpvfa3CljlZLgZ%2FieVVjxDJFMN19Vto2uKq4Zck95debtv4lAbRe0Fwl52crKzmjLKUu7zg';
const html = '<div><input name="authenticity_token" value="QSQS1212"/> </div>';
const axiosResponse = {
Expand All @@ -58,10 +58,17 @@ describe('The PPSApiResponseAuthenticationMetaDataExtractor class', () => {
status: 200,
statusText: 'OK',
} as unknown as AxiosResponse;
const metadata = app.get(PPSApiResponseAuthenticationMetaDataExtractor).extract(axiosResponse);
const metadataExtractor = app.get(PPSApiResponseAuthenticationMetaDataExtractor);
metadataExtractor.track(axiosResponse);

expect(metadata.sessionId).toBe(sessionId);
expect(metadata.authenticityToken).toBe('QSQS1212');
expect(metadataExtractor.generateAxiosHeaderWithSessionData()).toEqual(
expect.objectContaining({
headers: expect.objectContaining({
Cookie: expect.stringContaining(sessionId),
}),
}),
);
expect(metadataExtractor.getAuthenticityToken()).toBe('QSQS1212');
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Inject, Injectable } from '@nestjs/common';

import { AxiosResponse, RawAxiosResponseHeaders, AxiosResponseHeaders } from 'axios';
import {
AxiosResponse,
RawAxiosResponseHeaders,
AxiosResponseHeaders,
AxiosRequestConfig,
RawAxiosRequestHeaders
} from 'axios';

import { IHtmlParser, IHtmlParserSymbol } from '@easy-pps/html-parser/contracts';

Expand All @@ -13,45 +19,86 @@ import {
export class PPSApiResponseAuthenticationMetaDataExtractor
implements IPPSApiResponseAuthenticationMetaDataExtractor
{
public constructor(@Inject(IHtmlParserSymbol) private readonly htmlParser: IHtmlParser) {
private readonly defaultHeaders: RawAxiosRequestHeaders = {
'Content-Type': 'application/x-www-form-urlencoded',
Connection: 'keep-alive',
'Accept-Encoding': 'gzip, deflate, br',
Accept: '*/*',
};
private lastRequestAuthenticationMetadata: IAuthenticationMetadata = {
authenticityToken: '',
sessionId: '',
};

public constructor(
@Inject(IHtmlParserSymbol) private readonly htmlParser: IHtmlParser
) {}

public getAuthenticityToken(): string {
return this.lastRequestAuthenticationMetadata.authenticityToken;
}

public extract(axiosResponse: AxiosResponse): IAuthenticationMetadata {
public track(axiosResponse: AxiosResponse): void {
if (axiosResponse.status != 200) {
throw JSON.stringify(axiosResponse);
}

try {
const sessionId = this.extractSessionIdFromHeaders(axiosResponse.headers);
const authenticityITokennputValue = this.extractAuthenticityTokenInputValue(axiosResponse.data);
const authenticityTokenInputValue =
this.extractAuthenticityTokenInputValue(axiosResponse.data);

return {
authenticityToken: authenticityITokennputValue,
this.lastRequestAuthenticationMetadata = {
authenticityToken: authenticityTokenInputValue,
sessionId,
}
};
} catch (error) {
throw new Error(`Failed extracting authentication metadata for response : \n ${JSON.stringify(axiosResponse)}\n\n Error occurred : ${error.message}`);
throw new Error(
`Failed extracting authentication metadata for response : \n ${JSON.stringify(
axiosResponse
)}\n\n Error occurred : ${error.message}`
);
}
}

private extractSessionIdFromHeaders(headers: RawAxiosResponseHeaders | AxiosResponseHeaders): string {
public generateAxiosHeaderWithSessionData(): AxiosRequestConfig {
return {
headers: {
...this.defaultHeaders,
Cookie: `_pps_app_session=${this.lastRequestAuthenticationMetadata.sessionId}`,
},
};
}

private extractSessionIdFromHeaders(
headers: RawAxiosResponseHeaders | AxiosResponseHeaders
): string {
const sessionCookieName = '_pps_app_session=';
const cookies = headers['set-cookie'];
const ppsSessionCookie = cookies?.find((cookie: string) => cookie.includes(sessionCookieName));
const cookieValue = ppsSessionCookie?.split(';')[0].split('=')[1]
const ppsSessionCookie = cookies?.find((cookie: string) =>
cookie.includes(sessionCookieName)
);
const cookieValue = ppsSessionCookie?.split(';')[0].split('=')[1];

if (!cookieValue) {
throw new Error(`PPS Session Id Cookie not found in ${JSON.stringify(headers)}`);
throw new Error(
`PPS Session Id Cookie not found in ${JSON.stringify(headers)}`
);
}

return cookieValue;
}

private extractAuthenticityTokenInputValue(data: string): string {
const authenticity_token = this.htmlParser.getInputValueByName(data, 'authenticity_token');
const authenticity_token = this.htmlParser.getInputValueByName(
data,
'authenticity_token'
);

if (!authenticity_token) {
throw new Error(`Unable to find authenticity token it this html : ${data}`);
throw new Error(
`Unable to find authenticity token it this html : ${data}`
);
}

if (Array.isArray(authenticity_token)) {
Expand Down
86 changes: 86 additions & 0 deletions apps/api/src/app/pps/pps.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Module, Scope } from '@nestjs/common';
import { IPPSGenerateUseCaseSymbol } from './usecase/pps-generate-usecase.interface';
import { PPSGeneratorUseCase } from './usecase/pps-generator-usecase.service';
import { IPPSApiSymbol } from './third-party/pps-api.interface';
import { PPSApi } from './third-party/pps-api';
import { PPSApiFormDataGenerator } from './third-party/pps-api-form-data-generator';
import { Request } from 'express';
import { REQUEST } from '@nestjs/core';
import { StartStep } from './third-party/journey-steps/start-step';
import { RaceInformationStep } from './third-party/journey-steps/race-information-step';
import { PersonalInformationStep } from './third-party/journey-steps/personal-information-step';
import { CardiovascularStep } from './third-party/journey-steps/cardiovascular-step';
import { RiskFactorsStep } from './third-party/journey-steps/risk-factors-step';
import { PrecautionsStep } from './third-party/journey-steps/precautions-step';
import { FinalizationStep } from './third-party/journey-steps/finalization-step';
import { IPPSStepListSymbol } from './third-party/journey-steps/pps-step.interface';
import {
IPPSApiResponseAuthenticationMetaDataExtractorSymbol
} from './domain/authentication-metadata-extractor/pps-api-response-authentication-metadata-extractor.interface';
import {
PPSApiResponseAuthenticationMetaDataExtractor
} from './domain/authentication-metadata-extractor/pps-api-response-authentication-metadata-extractor';
import { SharedModule } from '../shared/shared.module';
import { PPSProfileDTOToRunnerPersonalInfos } from './domain/pps-profile-dto-to-runner-personal-infos';

@Module({
exports: [IPPSGenerateUseCaseSymbol],
imports: [SharedModule],
providers: [
{
provide: IPPSGenerateUseCaseSymbol,
useClass: PPSGeneratorUseCase,
scope: Scope.REQUEST,
},
{ provide: IPPSApiSymbol, useClass: PPSApi, scope: Scope.REQUEST },
{
provide: PPSApiFormDataGenerator,
useFactory: (request: Request) =>
new PPSApiFormDataGenerator(new PPSProfileDTOToRunnerPersonalInfos(request.body)),
inject: [REQUEST],
scope: Scope.REQUEST,
},
StartStep,
RaceInformationStep,
PersonalInformationStep,
CardiovascularStep,
RiskFactorsStep,
PrecautionsStep,
FinalizationStep,
{
provide: IPPSStepListSymbol,
useFactory: (
startStep: StartStep,
raceInformationStep: RaceInformationStep,
personalInformationStep: PersonalInformationStep,
cardiovascularStep: CardiovascularStep,
riskFactorsStep: RiskFactorsStep,
precautionsStep: PrecautionsStep,
finalizationStep: FinalizationStep
) => [
startStep,
raceInformationStep,
personalInformationStep,
cardiovascularStep,
riskFactorsStep,
precautionsStep,
finalizationStep,
],
inject: [
StartStep,
RaceInformationStep,
PersonalInformationStep,
CardiovascularStep,
RiskFactorsStep,
PrecautionsStep,
FinalizationStep,
],
},
{
provide: IPPSApiResponseAuthenticationMetaDataExtractorSymbol,
useClass: PPSApiResponseAuthenticationMetaDataExtractor,
scope: Scope.REQUEST,
},
],
})
export class PPSModule {}
Loading

0 comments on commit eaaae94

Please sign in to comment.