From 87d6c56b20b1c52cdfc0094324ae13d96c5b8e1f Mon Sep 17 00:00:00 2001 From: Pooya Raki Date: Mon, 16 Dec 2024 11:05:28 +0100 Subject: [PATCH] Enable hooks to be triggered via HTTP when the feature flag is activated. --- .../entities/__tests__/configuration.ts | 1 + src/config/entities/configuration.ts | 2 + src/routes/hooks/hooks.controller.ts | 14 ++- .../hooks/hooks.http.controller.spec.ts | 106 ++++++++++++++++++ 4 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 src/routes/hooks/hooks.http.controller.spec.ts diff --git a/src/config/entities/__tests__/configuration.ts b/src/config/entities/__tests__/configuration.ts index b5b564f542..790dd16440 100644 --- a/src/config/entities/__tests__/configuration.ts +++ b/src/config/entities/__tests__/configuration.ts @@ -155,6 +155,7 @@ export default (): ReturnType => ({ counterfactualBalances: false, accounts: false, pushNotifications: false, + hookHttpPostEvent: false, improvedAddressPoisoning: false, }, httpClient: { requestTimeout: faker.number.int() }, diff --git a/src/config/entities/configuration.ts b/src/config/entities/configuration.ts index 57b562c8d2..851292778c 100644 --- a/src/config/entities/configuration.ts +++ b/src/config/entities/configuration.ts @@ -239,6 +239,8 @@ export default () => ({ accounts: process.env.FF_ACCOUNTS?.toLowerCase() === 'true', pushNotifications: process.env.FF_PUSH_NOTIFICATIONS?.toLowerCase() === 'true', + hookHttpPostEvent: + process.env.FF_HOOK_HTTP_POST_EVENT?.toLowerCase() === 'true', improvedAddressPoisoning: process.env.FF_IMPROVED_ADDRESS_POISONING?.toLowerCase() === 'true', }, diff --git a/src/routes/hooks/hooks.controller.ts b/src/routes/hooks/hooks.controller.ts index 09e95a7777..c60bb2a906 100644 --- a/src/routes/hooks/hooks.controller.ts +++ b/src/routes/hooks/hooks.controller.ts @@ -17,6 +17,7 @@ import { ILoggingService, LoggingService } from '@/logging/logging.interface'; import { EventProtocolChangedError } from '@/routes/hooks/errors/event-protocol-changed.error'; import { EventProtocolChangedFilter } from '@/routes/hooks/filters/event-protocol-changed.filter'; import { ConfigEventType } from '@/routes/hooks/entities/event-type.entity'; +import { IConfigurationService } from '@/config/configuration.service.interface'; @Controller({ path: '', @@ -24,17 +25,26 @@ import { ConfigEventType } from '@/routes/hooks/entities/event-type.entity'; }) @ApiExcludeController() export class HooksController { + private isHookHttpPostEventEnabled: boolean; constructor( private readonly hooksService: HooksService, @Inject(LoggingService) private readonly loggingService: ILoggingService, - ) {} + + @Inject(IConfigurationService) + private readonly configurationService: IConfigurationService, + ) { + this.isHookHttpPostEventEnabled = + this.configurationService.getOrThrow( + 'features.hookHttpPostEvent', + ); + } @UseGuards(BasicAuthGuard) @Post('/hooks/events') @UseFilters(EventProtocolChangedFilter) @HttpCode(202) postEvent(@Body(new ValidationPipe(EventSchema)) event: Event): void { - if (this.isConfigEvent(event)) { + if (this.isConfigEvent(event) || this.isHookHttpPostEventEnabled) { this.hooksService.onEvent(event).catch((error) => { this.loggingService.error(error); }); diff --git a/src/routes/hooks/hooks.http.controller.spec.ts b/src/routes/hooks/hooks.http.controller.spec.ts new file mode 100644 index 0000000000..9efbcd4b47 --- /dev/null +++ b/src/routes/hooks/hooks.http.controller.spec.ts @@ -0,0 +1,106 @@ +import { faker } from '@faker-js/faker'; +import type { INestApplication } from '@nestjs/common'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { TestCacheModule } from '@/datasources/cache/__tests__/test.cache.module'; +import { TestNetworkModule } from '@/datasources/network/__tests__/test.network.module'; +import { TestLoggingModule } from '@/logging/__tests__/test.logging.module'; +import configuration from '@/config/entities/__tests__/configuration'; +import { IConfigurationService } from '@/config/configuration.service.interface'; +import { AppModule } from '@/app.module'; +import { CacheModule } from '@/datasources/cache/cache.module'; +import { RequestScopedLoggingModule } from '@/logging/logging.module'; +import { NetworkModule } from '@/datasources/network/network.module'; +import { TestQueuesApiModule } from '@/datasources/queues/__tests__/test.queues-api.module'; +import { QueuesApiModule } from '@/datasources/queues/queues-api.module'; +import type { Server } from 'net'; +import { TestPostgresDatabaseModule } from '@/datasources/db/__tests__/test.postgres-database.module'; +import { PostgresDatabaseModule } from '@/datasources/db/v1/postgres-database.module'; +import { PostgresDatabaseModuleV2 } from '@/datasources/db/v2/postgres-database.module'; +import { TestPostgresDatabaseModuleV2 } from '@/datasources/db/v2/test.postgres-database.module'; +import { TestTargetedMessagingDatasourceModule } from '@/datasources/targeted-messaging/__tests__/test.targeted-messaging.datasource.module'; +import { TargetedMessagingDatasourceModule } from '@/datasources/targeted-messaging/targeted-messaging.datasource.module'; + +describe('Post Hook Events Http (Unit)', () => { + let app: INestApplication; + let authToken: string; + let configurationService: IConfigurationService; + + async function initApp(): Promise { + const defaultConfiguration = configuration(); + + const testConfiguration = (): typeof defaultConfiguration => ({ + ...defaultConfiguration, + auth: { + ...defaultConfiguration.auth, + token: faker.string.hexadecimal({ length: 32 }), + }, + features: { + ...defaultConfiguration.features, + hookHttpPostEvent: true, + }, + }); + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule.register(testConfiguration)], + }) + .overrideModule(PostgresDatabaseModule) + .useModule(TestPostgresDatabaseModule) + .overrideModule(TargetedMessagingDatasourceModule) + .useModule(TestTargetedMessagingDatasourceModule) + .overrideModule(CacheModule) + .useModule(TestCacheModule) + .overrideModule(RequestScopedLoggingModule) + .useModule(TestLoggingModule) + .overrideModule(NetworkModule) + .useModule(TestNetworkModule) + .overrideModule(QueuesApiModule) + .useModule(TestQueuesApiModule) + .overrideModule(PostgresDatabaseModuleV2) + .useModule(TestPostgresDatabaseModuleV2) + .compile(); + app = moduleFixture.createNestApplication(); + + configurationService = moduleFixture.get(IConfigurationService); + authToken = configurationService.getOrThrow('auth.token'); + + await app.init(); + } + + beforeEach(async () => { + jest.resetAllMocks(); + await initApp(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('should not return 410 if the feature flag is enabled', async () => { + const payload = { + type: 'INCOMING_TOKEN', + tokenAddress: faker.finance.ethereumAddress(), + txHash: faker.string.hexadecimal({ length: 32 }), + }; + const safeAddress = faker.finance.ethereumAddress(); + const chainId = faker.string.numeric(); + const data = { + address: safeAddress, + chainId: chainId, + ...payload, + }; + + await request(app.getHttpServer()) + .post(`/hooks/events`) + .set('Authorization', `Basic ${authToken}`) + .send(data) + .expect(202); + }); + + it('should throw an error if authorization is not sent in the request headers', async () => { + await request(app.getHttpServer()) + .post(`/hooks/events`) + .send({}) + .expect(403); + }); +});