From 0622fcb338a6f022dc61bd6834a1b6ea0e737bb6 Mon Sep 17 00:00:00 2001 From: meriamBenSassi Date: Tue, 3 Sep 2024 14:54:36 +0200 Subject: [PATCH] feat: use v2 to fetch service-accounts BREAKING CHANGE: Doesn't work with v1 api anymore --- config/default.json | 7 +- config/env/test.json | 2 +- json-server/server.js | 2 +- src/algoan/algoan.module.ts | 11 ++- src/algoan/interfaces/index.ts | 1 + src/algoan/interfaces/paginated-data.ts | 7 ++ src/algoan/services/algoan-http.service.ts | 2 +- .../algoan-service-account.service.ts | 63 +++++++++++++ src/algoan/services/algoan.service.spec.ts | 6 +- src/algoan/services/algoan.service.ts | 92 +++++++++++++++++-- src/app.controller.spec.ts | 3 +- .../controllers/hooks.controller.spec.ts | 2 +- src/hooks/dto/event.dto.ts | 8 +- src/hooks/dto/service-account-updated.dto.ts | 13 +++ src/hooks/dto/subscription.dto.ts | 3 +- src/hooks/enums/event-name.enum.ts | 11 +++ src/hooks/services/hooks.service.spec.ts | 58 +++++++++++- src/hooks/services/hooks.service.ts | 68 +++++++++----- test/utils/app.ts | 20 ++-- 19 files changed, 323 insertions(+), 56 deletions(-) create mode 100644 src/algoan/interfaces/index.ts create mode 100644 src/algoan/interfaces/paginated-data.ts create mode 100644 src/algoan/services/algoan-service-account.service.ts create mode 100644 src/hooks/dto/service-account-updated.dto.ts create mode 100644 src/hooks/enums/event-name.enum.ts diff --git a/config/default.json b/config/default.json index 04f6efcf..872b4f59 100644 --- a/config/default.json +++ b/config/default.json @@ -3,7 +3,8 @@ "baseUrl": "http://localhost:4000", "clientId": "a", "clientSecret": "b", - "debug": false + "debug": false, + "version": 2 }, "bridge":{ "bankinVersion":"2021-06-01", @@ -16,7 +17,9 @@ "targetUrl": "http://localhost:8080/hooks", "eventList": [ "bank_details_required", - "aggregator_link_required" + "aggregator_link_required", + "service_account_updated", + "service_account_created" ], "restHooksSecret": "a", "port": 8080, diff --git a/config/env/test.json b/config/env/test.json index e4c029d1..e50794fb 100644 --- a/config/env/test.json +++ b/config/env/test.json @@ -4,7 +4,7 @@ ], "bridge":{ "synchronizationWaitingTime": 0, - "synchronizationTimeout": 2 + "synchronizationTimeout": 1000 }, "customerIdPassword": "random_pass" } \ No newline at end of file diff --git a/json-server/server.js b/json-server/server.js index f759b4ca..2cf9903a 100644 --- a/json-server/server.js +++ b/json-server/server.js @@ -21,7 +21,7 @@ const middlewares = jsonServer.defaults(); server.use(middlewares); // Add custom routes before JSON Server router -server.post('/v1/oauth/token', (req, res) => { +server.post('/v2/oauth/token', (req, res) => { res.json({ access_token: 'access_token_jwt', expires_in: 60, diff --git a/src/algoan/algoan.module.ts b/src/algoan/algoan.module.ts index 167ea861..af2414be 100644 --- a/src/algoan/algoan.module.ts +++ b/src/algoan/algoan.module.ts @@ -4,13 +4,20 @@ import { AlgoanAnalysisService } from './services/algoan-analysis.service'; import { AlgoanCustomerService } from './services/algoan-customer.service'; import { AlgoanHttpService } from './services/algoan-http.service'; import { AlgoanService } from './services/algoan.service'; +import { AlgoanServiceAcountService } from './services/algoan-service-account.service'; /** * Algoan module */ @Module({ imports: [ConfigModule], - providers: [AlgoanAnalysisService, AlgoanCustomerService, AlgoanHttpService, AlgoanService], - exports: [AlgoanAnalysisService, AlgoanCustomerService, AlgoanHttpService, AlgoanService], + providers: [ + AlgoanAnalysisService, + AlgoanCustomerService, + AlgoanHttpService, + AlgoanService, + AlgoanServiceAcountService, + ], + exports: [AlgoanAnalysisService, AlgoanCustomerService, AlgoanHttpService, AlgoanService, AlgoanServiceAcountService], }) export class AlgoanModule {} diff --git a/src/algoan/interfaces/index.ts b/src/algoan/interfaces/index.ts new file mode 100644 index 00000000..fa930847 --- /dev/null +++ b/src/algoan/interfaces/index.ts @@ -0,0 +1 @@ +export * from './paginated-data'; diff --git a/src/algoan/interfaces/paginated-data.ts b/src/algoan/interfaces/paginated-data.ts new file mode 100644 index 00000000..c5f19999 --- /dev/null +++ b/src/algoan/interfaces/paginated-data.ts @@ -0,0 +1,7 @@ +/** + * Paginated Algoan data + */ +export interface PaginatedData { + resources: T[]; + totalResources: number; +} diff --git a/src/algoan/services/algoan-http.service.ts b/src/algoan/services/algoan-http.service.ts index 1ac29521..faed324c 100644 --- a/src/algoan/services/algoan-http.service.ts +++ b/src/algoan/services/algoan-http.service.ts @@ -25,7 +25,7 @@ export class AlgoanHttpService { clientSecret, }, { - version: 1, + version: this.config.algoan.version, }, ); } diff --git a/src/algoan/services/algoan-service-account.service.ts b/src/algoan/services/algoan-service-account.service.ts new file mode 100644 index 00000000..3d872e37 --- /dev/null +++ b/src/algoan/services/algoan-service-account.service.ts @@ -0,0 +1,63 @@ +import { IServiceAccount, RequestBuilder, ServiceAccount } from '@algoan/rest'; +import { Inject, Injectable } from '@nestjs/common'; +import { Config } from 'node-config-ts'; + +import { CONFIG } from '../../config/config.module'; +import { PaginatedData } from '../interfaces'; +/** + * Service to manage analysis + */ +@Injectable() +export class AlgoanServiceAcountService { + private readonly apiVersion: string = 'v2'; + // Note: here we have to instantiate a request builder instead of using the algoanhttp module + // because algoanhttp's scope is REQUEST and we can't use a REQUEST scopped module in a onModuleInit + // and we have to get the service account on initing the module + private readonly requestBuilder = new RequestBuilder( + this.config.algoan.baseUrl, + { + clientId: this.config.algoan.clientId, + clientSecret: this.config.algoan.clientSecret, + }, + { + version: this.config.algoan.version, + }, + ); + + constructor(@Inject(CONFIG) private readonly config: Config) {} + + /** + * Find all service accounts linekd to the connector + */ + public async findAll(): Promise { + const path: string = `/${this.apiVersion}/service-accounts?limit=1000`; + const paginatedServiceAccounts: PaginatedData = await this.requestBuilder.request({ + url: path, + method: 'GET', + }); + + return paginatedServiceAccounts.resources.map( + (sa: IServiceAccount) => + new ServiceAccount(this.config.algoan.baseUrl, sa, { + apiVersion: this.config.algoan.version, + }), + ); + } + + /** + * Find a service account by id + */ + public async findById(id: string): Promise { + /* eslint-disable-next-line @typescript-eslint/naming-convention,camelcase */ + const path: string = `/${this.apiVersion}/service-accounts?filter=${JSON.stringify({ _id: id })}`; + const paginatedServiceAccounts: PaginatedData = await this.requestBuilder.request({ + url: path, + method: 'GET', + }); + if (paginatedServiceAccounts.resources.length > 0) { + return new ServiceAccount(this.config.algoan.baseUrl, paginatedServiceAccounts.resources?.[0], { + apiVersion: this.config.algoan.version, + }); + } + } +} diff --git a/src/algoan/services/algoan.service.spec.ts b/src/algoan/services/algoan.service.spec.ts index 8353621e..18c071d1 100644 --- a/src/algoan/services/algoan.service.spec.ts +++ b/src/algoan/services/algoan.service.spec.ts @@ -1,10 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { Algoan } from '@algoan/rest'; import { config } from 'node-config-ts'; import { CONFIG } from '../../config/config.module'; import { AlgoanService } from './algoan.service'; +import { AlgoanServiceAcountService } from './algoan-service-account.service'; describe('AlgoanService', () => { let algoanService: AlgoanService; @@ -17,6 +17,7 @@ describe('AlgoanService', () => { provide: CONFIG, useValue: config, }, + AlgoanServiceAcountService, ], }).compile(); @@ -28,7 +29,7 @@ describe('AlgoanService', () => { }); it('should start properly', async () => { - jest.spyOn(Algoan.prototype, 'initRestHooks').mockReturnValue(Promise.resolve()); + jest.spyOn(AlgoanService.prototype, 'initRestHooks').mockReturnValue(Promise.resolve()); await expect(algoanService.onModuleInit()).resolves.toEqual(undefined); }); @@ -43,6 +44,7 @@ describe('AlgoanService', () => { eventList: [], }, }, + AlgoanServiceAcountService, ], }).compile(); diff --git a/src/algoan/services/algoan.service.ts b/src/algoan/services/algoan.service.ts index 8fa8000c..d8a8aab4 100644 --- a/src/algoan/services/algoan.service.ts +++ b/src/algoan/services/algoan.service.ts @@ -1,10 +1,12 @@ -import { Algoan, EventName } from '@algoan/rest'; +import { Algoan, EventName, PostSubscriptionDTO } from '@algoan/rest'; import { Injectable, OnModuleInit, InternalServerErrorException, Inject } from '@nestjs/common'; import { utilities } from 'nest-winston'; import { Config } from 'node-config-ts'; import { format, transports } from 'winston'; - +import { ServiceAccountUpdatedDTO } from '../../hooks/dto/service-account-updated.dto'; +import { ServiceAccountCreatedDTO } from '../../hooks/dto/service-account-created.dto'; import { CONFIG } from '../../config/config.module'; +import { AlgoanServiceAcountService } from './algoan-service-account.service'; /** * Algoan service @@ -17,7 +19,10 @@ export class AlgoanService implements OnModuleInit { */ public algoanClient!: Algoan; - constructor(@Inject(CONFIG) private readonly config: Config) {} + constructor( + @Inject(CONFIG) private readonly config: Config, + private readonly serviceAccountService: AlgoanServiceAcountService, + ) {} /** * Fetch services and creates subscription @@ -33,7 +38,7 @@ export class AlgoanService implements OnModuleInit { baseUrl: this.config.algoan.baseUrl, clientId: this.config.algoan.clientId, clientSecret: this.config.algoan.clientSecret, - version: 1, + version: this.config.algoan.version, loggerOptions: { format: nodeEnv === 'production' ? format.json() : format.combine(format.timestamp(), utilities.format.nestLike()), @@ -53,10 +58,81 @@ export class AlgoanService implements OnModuleInit { throw new InternalServerErrorException('No event list given'); } - await this.algoanClient.initRestHooks( - this.config.targetUrl, - this.config.eventList as EventName[], - this.config.restHooksSecret, + await this.initRestHooks(this.config.targetUrl, this.config.eventList as EventName[], this.config.restHooksSecret); + } + + /** + * Init resthooks in V2 + */ + public async initRestHooks(target: string, events: EventName[] = [], secret?: string): Promise { + this.algoanClient.serviceAccounts = await this.serviceAccountService.findAll(); + + if (this.algoanClient.serviceAccounts.length === 0) { + return; + } + const subscriptionDTO: PostSubscriptionDTO[] = this.fromEventToSubscriptionDTO(target, events, secret); + + for (const serviceAccount of this.algoanClient.serviceAccounts) { + await serviceAccount.getOrCreateSubscriptions(subscriptionDTO, events); + } + } + + /** + * Transform a list of events to a Subscription request body + * @param target Base URL + * @param eventName List of events + * @param secret Secret + */ + // eslint-disable-next-line + private readonly fromEventToSubscriptionDTO = ( + target: string, + events: EventName[], + secret?: string, + ): PostSubscriptionDTO[] => + events.map( + (event: EventName): PostSubscriptionDTO => ({ + target, + secret, + eventName: event, + }), ); + + /** + * Store the new service account in-memory and create subscriptions + * @param serviceAccount + * @param subscriptionDto + */ + public async saveServiceAccount(payload: ServiceAccountCreatedDTO): Promise { + const addedServiceAccount = await this.serviceAccountService.findById(payload.serviceAccountId); + + const eventNames: EventName[] = this.config.eventList as EventName[]; + + if (addedServiceAccount !== undefined) { + const subscriptionDTO: PostSubscriptionDTO[] = this.fromEventToSubscriptionDTO( + this.config.targetUrl, + eventNames, + this.config.restHooksSecret, + ); + this.algoanClient.serviceAccounts.push(addedServiceAccount); + await addedServiceAccount.getOrCreateSubscriptions(subscriptionDTO, eventNames); + } + } + + /** + * Update the service account config + * @param payload + */ + public async updateServiceAccount(payload: ServiceAccountUpdatedDTO): Promise { + const updatedServiceAccount = await this.serviceAccountService.findById(payload.serviceAccountId); + + if (updatedServiceAccount) { + const oldServiceAccountIdx = this.algoanClient.serviceAccounts.findIndex( + (serviceAccount) => serviceAccount.clientId === updatedServiceAccount?.clientId, + ); + // eslint-disable-next-line + if (oldServiceAccountIdx > -1) { + this.algoanClient.serviceAccounts[oldServiceAccountIdx].config = updatedServiceAccount.config; + } + } } } diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts index 449beb9b..15b33a4f 100644 --- a/src/app.controller.spec.ts +++ b/src/app.controller.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AlgoanService } from './algoan/services/algoan.service'; +import { AlgoanServiceAcountService } from './algoan/services/algoan-service-account.service'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from './config/config.module'; @@ -12,7 +13,7 @@ describe('AppController', () => { const app: TestingModule = await Test.createTestingModule({ imports: [ConfigModule], controllers: [AppController], - providers: [AppService, AlgoanService], + providers: [AppService, AlgoanService, AlgoanServiceAcountService], }).compile(); appController = app.get(AppController); diff --git a/src/hooks/controllers/hooks.controller.spec.ts b/src/hooks/controllers/hooks.controller.spec.ts index 3918c7d0..8460e33d 100644 --- a/src/hooks/controllers/hooks.controller.spec.ts +++ b/src/hooks/controllers/hooks.controller.spec.ts @@ -47,7 +47,7 @@ describe('Hooks Controller', () => { time: 1586177798388, index: 32, id: 'eventId', - }; + } as unknown as EventDTO; const spy = jest.spyOn(hooksService, 'handleWebhook').mockReturnValue(Promise.resolve()); await controller.controlHook(event, { diff --git a/src/hooks/dto/event.dto.ts b/src/hooks/dto/event.dto.ts index 3713872d..967cd074 100644 --- a/src/hooks/dto/event.dto.ts +++ b/src/hooks/dto/event.dto.ts @@ -5,12 +5,18 @@ import { AggregatorLinkRequiredDTO } from './aggregator-link-required.dto'; import { BanksDetailsRequiredDTO } from './bank-details-required.dto'; import { ServiceAccountCreatedDTO } from './service-account-created.dto'; import { ServiceAccountDeletedDTO } from './service-account-deleted.dto'; +import { ServiceAccountUpdatedDTO } from './service-account-updated.dto'; import { SubscriptionDTO } from './subscription.dto'; /** * Events payload types */ -type Events = ServiceAccountCreatedDTO | ServiceAccountDeletedDTO | AggregatorLinkRequiredDTO | BanksDetailsRequiredDTO; +type Events = + | ServiceAccountCreatedDTO + | ServiceAccountDeletedDTO + | AggregatorLinkRequiredDTO + | BanksDetailsRequiredDTO + | ServiceAccountUpdatedDTO; /** * Event diff --git a/src/hooks/dto/service-account-updated.dto.ts b/src/hooks/dto/service-account-updated.dto.ts new file mode 100644 index 00000000..ec54fc0c --- /dev/null +++ b/src/hooks/dto/service-account-updated.dto.ts @@ -0,0 +1,13 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +/** + * ServiceAccountUpdated DTO + */ +export class ServiceAccountUpdatedDTO { + /** + * Unique service account DTO + */ + @IsNotEmpty() + @IsString() + public readonly serviceAccountId: string; +} diff --git a/src/hooks/dto/subscription.dto.ts b/src/hooks/dto/subscription.dto.ts index d19d1c17..cb672cbd 100644 --- a/src/hooks/dto/subscription.dto.ts +++ b/src/hooks/dto/subscription.dto.ts @@ -1,5 +1,6 @@ import { IsNotEmpty } from 'class-validator'; -import { EventName, SubscriptionStatus } from '@algoan/rest'; +import { SubscriptionStatus } from '@algoan/rest'; +import { EventName } from '../enums/event-name.enum'; /** * Subscription diff --git a/src/hooks/enums/event-name.enum.ts b/src/hooks/enums/event-name.enum.ts new file mode 100644 index 00000000..8aca38e1 --- /dev/null +++ b/src/hooks/enums/event-name.enum.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention,camelcase */ +/** + * Received events names + */ +export enum EventName { + SERVICE_ACCOUNT_CREATED = 'service_account_created', + SERVICE_ACCOUNT_UPDATED = 'service_account_updated', + SERVICE_ACCOUNT_DELETED = 'service_account_deleted', + AGGREGATOR_LINK_REQUIRED = 'aggregator_link_required', + BANK_DETAILS_REQUIRED = 'bank_details_required', +} diff --git a/src/hooks/services/hooks.service.spec.ts b/src/hooks/services/hooks.service.spec.ts index 7243340a..adc46c21 100644 --- a/src/hooks/services/hooks.service.spec.ts +++ b/src/hooks/services/hooks.service.spec.ts @@ -1,6 +1,5 @@ /* eslint-disable max-lines */ import { - Algoan, EventName, IServiceAccount, ISubscriptionEvent, @@ -24,6 +23,7 @@ import { AggregatorService } from '../../aggregator/services/aggregator.service' import { AlgoanModule } from '../../algoan/algoan.module'; import { analysisMock } from '../../algoan/dto/analysis.objects.mock'; import { customerMock } from '../../algoan/dto/customer.objects.mock'; +import { AlgoanServiceAcountService } from '../../algoan/services/algoan-service-account.service'; import { AlgoanAnalysisService } from '../../algoan/services/algoan-analysis.service'; import { AlgoanCustomerService } from '../../algoan/services/algoan-customer.service'; import { AlgoanHttpService } from '../../algoan/services/algoan-http.service'; @@ -42,6 +42,7 @@ describe('HooksService', () => { let algoanHttpService: AlgoanHttpService; let algoanCustomerService: AlgoanCustomerService; let algoanAnalysisService: AlgoanAnalysisService; + let algoanServiceAcountService: AlgoanServiceAcountService; const mockEvent = { subscription: { @@ -99,7 +100,7 @@ describe('HooksService', () => { ], }).compile(); - jest.spyOn(Algoan.prototype, 'initRestHooks').mockResolvedValue(); + jest.spyOn(AlgoanService.prototype, 'initRestHooks').mockResolvedValue(); hooksService = await moduleRef.resolve(HooksService, contextId); aggregatorService = await moduleRef.resolve(AggregatorService, contextId); @@ -107,6 +108,10 @@ describe('HooksService', () => { algoanHttpService = await moduleRef.resolve(AlgoanHttpService, contextId); algoanCustomerService = await moduleRef.resolve(AlgoanCustomerService, contextId); algoanAnalysisService = await moduleRef.resolve(AlgoanAnalysisService, contextId); + algoanServiceAcountService = await moduleRef.resolve( + AlgoanServiceAcountService, + contextId, + ); await algoanService.onModuleInit(); }); @@ -130,7 +135,7 @@ describe('HooksService', () => { it('handles aggregator link required', async () => { mockEvent.subscription.eventName = EventName.AGGREGATOR_LINK_REQUIRED; const spy = jest.spyOn(hooksService, 'handleAggregatorLinkRequired').mockResolvedValue(); - await hooksService.handleWebhook(mockEvent as EventDTO, 'mockSignature'); + await hooksService.handleWebhook(mockEvent as unknown as EventDTO, 'mockSignature'); expect(spy).toBeCalledWith(mockServiceAccount, mockEvent.payload); }); @@ -138,7 +143,7 @@ describe('HooksService', () => { it('handles bank details required', async () => { mockEvent.subscription.eventName = EventName.BANK_DETAILS_REQUIRED; const spy = jest.spyOn(hooksService, 'handleBankDetailsRequiredEvent').mockResolvedValue(); - await hooksService.handleWebhook(mockEvent as EventDTO, 'mockSignature'); + await hooksService.handleWebhook(mockEvent as unknown as EventDTO, 'mockSignature'); expect(spy).toBeCalledWith(mockServiceAccount, mockEvent.payload, expect.any(Date)); }); @@ -713,7 +718,6 @@ describe('HooksService', () => { }; await hooksService.handleBankDetailsRequiredEvent(mockServiceAccount, mockEventPayload, new Date()); - expect(algoanAuthenticateSpy).toBeCalledWith(mockServiceAccount.clientId, mockServiceAccount.clientSecret); expect(getCustomerSpy).toBeCalledWith(mockEventPayload.customerId); expect(refreshSpy).toBeCalledWith('mockItemId', 'mockPermToken', mockServiceAccountConfig); @@ -828,4 +832,48 @@ describe('HooksService', () => { expect(transactionSpy).toBeCalledWith('mockPermToken', undefined, mockServiceAccountConfig); expect(deleteUserSpy).toHaveBeenCalledTimes(0); }); + + describe('Func handleServiceAccountUpdatedEvent()', () => { + it('should update service account config', async () => { + const findServiceAccountSpy = jest.spyOn(algoanServiceAcountService, 'findById').mockResolvedValue({ + clientId: 'clientId', + config: { + test: true, + }, + } as ServiceAccount); + + await hooksService.handleServiceAccountUpdatedEvent({ + serviceAccountId: 'id', + }); + + expect(findServiceAccountSpy).toBeCalled(); + }); + }); + + describe('Func handleServiceAccountCreatedEvent()', () => { + it('should update service account list and create subscriptions', async () => { + const findServiceAccountSpy = jest.spyOn(algoanServiceAcountService, 'findById').mockResolvedValue( + new ServiceAccount('url', { + id: 'id', + clientId: 'clientId', + clientSecret: 'secret', + createdAt: new Date().toISOString(), + }), + ); + + const getOrCreateSubscriptionsSpy = jest + .spyOn(ServiceAccount.prototype, 'getOrCreateSubscriptions') + .mockResolvedValue([]); + + await hooksService.handleServiceAccountCreatedEvent({ + serviceAccountId: 'id', + }); + + expect(findServiceAccountSpy).toBeCalled(); + expect(getOrCreateSubscriptionsSpy).toBeCalledWith( + [{ eventName: 'service_account_created', secret: 'a', target: 'http://localhost:8080/hooks' }], + ['service_account_created'], + ); + }); + }); }); diff --git a/src/hooks/services/hooks.service.ts b/src/hooks/services/hooks.service.ts index 417ad14b..b2bec021 100644 --- a/src/hooks/services/hooks.service.ts +++ b/src/hooks/services/hooks.service.ts @@ -1,4 +1,4 @@ -import { EventName, EventStatus, ServiceAccount, Subscription, SubscriptionEvent } from '@algoan/rest'; +import { EventStatus, ServiceAccount, Subscription, SubscriptionEvent } from '@algoan/rest'; import { Inject, Injectable, Logger, UnauthorizedException } from '@nestjs/common'; import * as delay from 'delay'; import { isEmpty } from 'lodash'; @@ -32,6 +32,9 @@ import { CONFIG } from '../../config/config.module'; import { AggregatorLinkRequiredDTO } from '../dto/aggregator-link-required.dto'; import { BanksDetailsRequiredDTO } from '../dto/bank-details-required.dto'; import { EventDTO } from '../dto/event.dto'; +import { EventName } from '../enums/event-name.enum'; +import { ServiceAccountCreatedDTO } from '../dto/service-account-created.dto'; +import { ServiceAccountUpdatedDTO } from '../dto/service-account-updated.dto'; /** * Hook service @@ -58,32 +61,36 @@ export class HooksService { * @param signature Signature headers, to check if the call is from Algoan */ public async handleWebhook(event: EventDTO, signature: string): Promise { - const aggregationStartDate: Date = new Date(); - const serviceAccount = this.algoanService.algoanClient.getServiceAccountBySubscriptionId(event.subscription.id); + if (event.subscription.eventName === EventName.SERVICE_ACCOUNT_CREATED) { + await this.handleServiceAccountCreatedEvent(event.payload as ServiceAccountCreatedDTO); + } else { + const aggregationStartDate: Date = new Date(); + const serviceAccount = this.algoanService.algoanClient.getServiceAccountBySubscriptionId(event.subscription.id); - this.logger.debug(`Found a service account for subscription "${event.subscription.id}"`); + this.logger.debug(`Found a service account for subscription "${event.subscription.id}"`); - if (serviceAccount === undefined) { - throw new UnauthorizedException(`No service account found for subscription ${event.subscription.id}`); - } + if (serviceAccount === undefined) { + throw new UnauthorizedException(`No service account found for subscription ${event.subscription.id}`); + } - // From the Bridge connector, need to find the bridge equivalent - const subscription: Subscription | undefined = serviceAccount.subscriptions.find( - (sub: Subscription) => sub.id === event.subscription.id, - ); + // From the Bridge connector, need to find the bridge equivalent + const subscription: Subscription | undefined = serviceAccount.subscriptions.find( + (sub: Subscription) => sub.id === event.subscription.id, + ); - if (subscription === undefined) { - return; - } + if (subscription === undefined) { + return; + } - if (!subscription.validateSignature(signature, event.payload as unknown as { [key: string]: string })) { - throw new UnauthorizedException('Invalid X-Hub-Signature: you cannot call this API'); - } + if (!subscription.validateSignature(signature, event.payload as unknown as { [key: string]: string })) { + throw new UnauthorizedException('Invalid X-Hub-Signature: you cannot call this API'); + } - // Handle the event asynchronously - void this.dispatchAndHandleWebhook(event, subscription, serviceAccount, aggregationStartDate); + // Handle the event asynchronously + void this.dispatchAndHandleWebhook(event, subscription, serviceAccount, aggregationStartDate); - return; + return; + } } /** @@ -113,7 +120,9 @@ export class HooksService { aggregationStartDate, ); break; - + case EventName.SERVICE_ACCOUNT_UPDATED: + await this.handleServiceAccountUpdatedEvent(event.payload as ServiceAccountUpdatedDTO); + break; // The default case should never be reached, as the eventName is already checked in the DTO default: void se.update({ status: EventStatus.FAILED }); @@ -386,4 +395,21 @@ export class HooksService { moment(tr1.date).isBefore(moment(tr2.date)) ? -1 : 1, ); } + + /** + * Handles the service_account_created event + * @param payload the new service account id + * @param subscription + */ + public async handleServiceAccountCreatedEvent(payload: ServiceAccountCreatedDTO) { + await this.algoanService.saveServiceAccount(payload); + } + + /** + * Handles the service_account_updated event + * @param payload service account update dto + */ + public async handleServiceAccountUpdatedEvent(payload: ServiceAccountUpdatedDTO) { + await this.algoanService.updateServiceAccount(payload); + } } diff --git a/test/utils/app.ts b/test/utils/app.ts index 6a2e3f2f..ef1ab786 100644 --- a/test/utils/app.ts +++ b/test/utils/app.ts @@ -31,20 +31,22 @@ export const buildFakeApp = async (): Promise => { expires_in: 3000, refresh_expires_in: 10000, }, - path: '/v1/oauth/token', + path: '/v2/oauth/token', nbOfCalls: 2, }); const fakeServiceAccounts: nock.Scope = fakeAPI({ baseUrl: fakeAlgoanBaseUrl, method: 'get', - result: [ - { - clientId: 'client1', - clientSecret: 'secret', - id: 'id1', - }, - ], - path: '/v1/service-accounts', + result: { + resources: [ + { + clientId: 'client1', + clientSecret: 'secret', + id: 'id1', + }, + ], + }, + path: '/v2/service-accounts?limit=1000', }); const fakeGetSubscriptions: nock.Scope = fakeAPI({ baseUrl: fakeAlgoanBaseUrl,