Skip to content

Commit

Permalink
add slackMessage service.
Browse files Browse the repository at this point in the history
- update cdd.processor.ts to use new service
- add new env's to worker config file and .env.sample.worker
- update and add tests
  • Loading branch information
F-OBrien committed Aug 23, 2024
1 parent a07c346 commit f184c88
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 112 deletions.
4 changes: 4 additions & 0 deletions .env.sample.worker
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ REDIS_PASSWORD=someSecret
OTLP_EXPORT_URL='http://localhost:4318' # Telemetry is disabled without this set
OTLP_HOSTNAME # defaults to `os.hostname`
OTLP_SERVICE # defaults to `PolymeshCdd`

SLACK_SIGNING_SECRET # Slack signs the requests we send you using this secret
SLACK_BOT_TOKEN # Bot User OAuth Token
SLACK_CHANNEL # Name of the slack channel to post messages to
2 changes: 2 additions & 0 deletions apps/cdd-backend/src/cdd-worker/cdd-worker.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { AppRedisModule } from '../app-redis/app-redis.module';
import { AppBullModule } from '../app-bull/app-bull.module';
import { PolymeshModule } from '../polymesh/polymesh.module';
import { CddProcessor } from './cdd.processor';
import { SlackMessageModule } from '../slack/slackMessage.module';

@Module({
imports: [
PolymeshModule,
AppRedisModule,
AppBullModule,
SlackMessageModule,
BullModule.registerQueue({}),
],
providers: [CddProcessor],
Expand Down
77 changes: 21 additions & 56 deletions apps/cdd-backend/src/cdd-worker/cdd.processor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { Polymesh } from '@polymeshassociation/polymesh-sdk';
import { Job } from 'bull';
import { MockPolymesh, slackAppMock } from '../test-utils/mocks';
import { MockPolymesh } from '../test-utils/mocks';
import { CddProcessor } from './cdd.processor';
import {
JumioCddJob,
Expand All @@ -20,6 +20,7 @@ import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { AppRedisService } from '../app-redis/app-redis.service';
import { AddressBookService } from '../polymesh/address-book.service';
import { Account, Identity } from '@polymeshassociation/polymesh-sdk/types';
import { SlackMessageService } from '../slack/slackMessage.service';

describe('cddProcessor', () => {
let mockRedis: DeepMocked<AppRedisService>;
Expand All @@ -28,6 +29,7 @@ describe('cddProcessor', () => {
let mockPolymesh: MockPolymesh;
let mockAddressBook: DeepMocked<AddressBookService>;
let processor: CddProcessor;
let mockSlackMessage: DeepMocked<SlackMessageService>;
let mockRun: jest.Mock;

beforeEach(async () => {
Expand All @@ -36,6 +38,10 @@ describe('cddProcessor', () => {
CddProcessor,
{ provide: Polymesh, useValue: new MockPolymesh() },
{ provide: AppRedisService, useValue: createMock<AppRedisService>() },
{
provide: SlackMessageService,
useValue: createMock<SlackMessageService>(),
},
{ provide: WINSTON_MODULE_PROVIDER, useValue: createMock<Logger>() },
{
provide: AddressBookService,
Expand All @@ -48,10 +54,7 @@ describe('cddProcessor', () => {
mockRedis = module.get<typeof mockRedis>(AppRedisService);
mockPolymesh = module.get<typeof mockPolymesh>(Polymesh);
mockAddressBook = module.get<typeof mockAddressBook>(AddressBookService);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
(processor as any).slackApp = slackAppMock;

mockSlackMessage = module.get<typeof mockSlackMessage>(SlackMessageService);
mockRun = jest.fn().mockResolvedValue('test-tx-result');
mockPolymesh.identities.registerIdentity.mockResolvedValue({
run: mockRun,
Expand Down Expand Up @@ -174,23 +177,10 @@ describe('cddProcessor', () => {
mockNetkiCompletedJob.data.value.identity.state = 'hold';
await processor.generateCdd(mockNetkiCompletedJob);

expect(slackAppMock.client.chat.postMessage).toHaveBeenNthCalledWith(
1,
{
token: expect.any(String),
channel: expect.any(String),
text: 'A Netki Onboarding application requires review',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: ':warning: Netki CDD application with access code *xyz* has been placed on *HOLD*.\n:mag: Please review and process in the Netki dashboard.',
},
},
],
}
);
expect(mockSlackMessage.sendMessage).toHaveBeenCalledWith({
header: 'A Netki Onboarding application requires review',
body: ':warning: Netki CDD application with access code *xyz* has been placed on *HOLD*.\n:mag: Please review and process in the Netki dashboard.',
});
expect(
mockPolymesh.identities.registerIdentity
).not.toHaveBeenCalled();
Expand Down Expand Up @@ -303,23 +293,10 @@ describe('cddProcessor', () => {

await processor.generateCdd(mockNetkiCompletedJob);

expect(slackAppMock.client.chat.postMessage).toHaveBeenNthCalledWith(
2,
{
token: expect.any(String),
channel: expect.any(String),
text: 'New Netki business application requires review',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: ':bell: Netki Business CDD application received from *Some Business Name*.\n:mag: Please review and process in the Netki dashboard.',
},
},
],
}
);
expect(mockSlackMessage.sendMessage).toHaveBeenCalledWith({
header: 'New Netki business application requires review',
body: ':bell: Netki Business CDD application received from *Some Business Name*.\n:mag: Please review and process in the Netki dashboard.',
});
expect(
mockPolymesh.identities.registerIdentity
).not.toHaveBeenCalled();
Expand All @@ -330,23 +307,11 @@ describe('cddProcessor', () => {

await processor.generateCdd(mockNetkiCompletedJob);

expect(slackAppMock.client.chat.postMessage).toHaveBeenNthCalledWith(
3,
{
token: expect.any(String),
channel: expect.any(String),
text: 'Netki business application on requires review',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: ':warning: Netki Business CDD application from *Some Business Name* was placed on *HOLD*.\n:mag: Please review and process in the Netki dashboard.',
},
},
],
}
);
expect(mockSlackMessage.sendMessage).toHaveBeenCalledWith({
header: 'Netki business application on requires review',
body: ':warning: Netki Business CDD application from *Some Business Name* was placed on *HOLD*.\n:mag: Please review and process in the Netki dashboard.',
});

expect(
mockPolymesh.identities.registerIdentity
).not.toHaveBeenCalled();
Expand Down
60 changes: 12 additions & 48 deletions apps/cdd-backend/src/cdd-worker/cdd.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,17 @@ import {
} from './types';
import { Identity } from '@polymeshassociation/polymesh-sdk/types';
import { NetkiBusinessApplicationModel } from '../app-redis/models/netki-business-application.model';
import { App as SlackApp } from '@slack/bolt';
import { SlackMessageService } from '../slack/slackMessage.service';

@Processor()
export class CddProcessor {
private slackApp: SlackApp;

constructor(
private readonly polymesh: Polymesh,
private readonly signerLookup: AddressBookService,
private readonly redis: AppRedisService,
private readonly slackMessageService: SlackMessageService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
) {
this.slackApp = new SlackApp({
signingSecret: process.env.SLACK_SIGNING_SECRET,
token: process.env.SLACK_BOT_TOKEN,
});
}
) {}

@Process()
async generateCdd(job: Job<CddJob>) {
Expand Down Expand Up @@ -61,36 +55,16 @@ export class CddProcessor {
}: NetkiBusinessJob): Promise<void> {
switch (status) {
case 'open':
await this.slackApp.client.chat.postMessage({
token: process.env.SLACK_BOT_TOKEN,
channel: process.env.SLACK_CHANNEL || '',
text: 'New Netki business application requires review',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `:bell: Netki Business CDD application received from *${name}*.\n:mag: Please review and process in the Netki dashboard.`,
},
},
],
await this.slackMessageService.sendMessage({
header: 'New Netki business application requires review',
body: `:bell: Netki Business CDD application received from *${name}*.\n:mag: Please review and process in the Netki dashboard.`,
});
break;

case 'hold':
await this.slackApp.client.chat.postMessage({
token: process.env.SLACK_BOT_TOKEN,
channel: process.env.SLACK_CHANNEL || '',
text: 'Netki business application on requires review',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `:warning: Netki Business CDD application from *${name}* was placed on *HOLD*.\n:mag: Please review and process in the Netki dashboard.`,
},
},
],
await this.slackMessageService.sendMessage({
header: 'Netki business application on requires review',
body: `:warning: Netki Business CDD application from *${name}* was placed on *HOLD*.\n:mag: Please review and process in the Netki dashboard.`,
});
break;

Expand Down Expand Up @@ -196,19 +170,9 @@ export class CddProcessor {
break;

case 'hold':
await this.slackApp.client.chat.postMessage({
token: process.env.SLACK_BOT_TOKEN,
channel: process.env.SLACK_CHANNEL || '',
text: 'A Netki Onboarding application requires review',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `:warning: Netki CDD application with access code *${code}* has been placed on *HOLD*.\n:mag: Please review and process in the Netki dashboard.`,
},
},
],
await this.slackMessageService.sendMessage({
header: 'A Netki Onboarding application requires review',
body: `:warning: Netki CDD application with access code *${code}* has been placed on *HOLD*.\n:mag: Please review and process in the Netki dashboard.`,
});
break;

Expand Down
19 changes: 19 additions & 0 deletions apps/cdd-backend/src/config/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ const configZ = z
.describe('Polymesh chain node ws url'),
})
.describe('Polymesh chain related config'),

slackApp: z
.object({
signingSecret: z
.string()
.describe('Slack signing secret for verifying requests'),
botToken: z
.string()
.describe('Slack bot OAuth token for authentication'),
channel: z
.string()
.describe('Slack channel ID where messages will be posted'),
})
.describe('Slack app related config'),
})
.describe('config values needed for "worker" mode');

Expand Down Expand Up @@ -64,6 +78,11 @@ export const workerEnvConfig = (): WorkerConfig => {
polymesh: {
nodeUrl: process.env.MESH_NODE_URL,
},
slackApp: {
signingSecret: process.env.SLACK_SIGNING_SECRET || '',
botToken: process.env.SLACK_BOT_TOKEN || '',
channel: process.env.SLACK_CHANNEL || '',
},
};

return configZ.parse(rawConfig);
Expand Down
24 changes: 24 additions & 0 deletions apps/cdd-backend/src/slack/slackMessage.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { workerEnvConfig } from '../config/worker';
import { SlackMessageService } from './slackMessage.service';
import { App as SlackApp } from '@slack/bolt';

@Module({
imports: [ConfigModule.forFeature(() => workerEnvConfig())],
providers: [
{
provide: SlackApp,
inject: [ConfigService],
useFactory: (config: ConfigService) => {
return new SlackApp({
signingSecret: config.getOrThrow('slackApp.signingSecret'),
token: config.getOrThrow('slackApp.botToken'),
});
},
},
SlackMessageService,
],
exports: [SlackMessageService],
})
export class SlackMessageModule {}
65 changes: 65 additions & 0 deletions apps/cdd-backend/src/slack/slackMessage.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { Logger } from 'winston';
import { ConfigService } from '@nestjs/config';
import { SlackMessageService } from './slackMessage.service';
import { App as SlackApp } from '@slack/bolt';

describe('SlackMessageService', () => {
let service: SlackMessageService;
let slackApp: DeepMocked<SlackApp>;
let configService: DeepMocked<ConfigService>;
let logger: DeepMocked<Logger>;

const mockChannel = 'mock-channel';
const mockHeader = 'Test Header';
const mockBody = 'Test Body';

beforeEach(() => {
slackApp = createMock<SlackApp>({
client: {
chat: {
postMessage: jest.fn(),
},
},
});
configService = createMock<ConfigService>();
logger = createMock<Logger>();

configService.getOrThrow.mockReturnValue(mockChannel);

service = new SlackMessageService(slackApp, configService, logger);
});

describe('sendMessage', () => {
it('should send a message to Slack', async () => {
await service.sendMessage({ header: mockHeader, body: mockBody });

expect(slackApp.client.chat.postMessage).toHaveBeenCalledWith({
channel: mockChannel,
text: mockHeader,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: mockBody,
},
},
],
});
});

it('should log an error if Slack message fails', async () => {
(slackApp.client.chat.postMessage as jest.Mock).mockRejectedValue(
new Error('Slack API Error')
);

await service.sendMessage({ header: mockHeader, body: mockBody });

expect(logger.error).toHaveBeenCalledWith(
'Failed to send message to Slack',
expect.any(Error)
);
});
});
});
Loading

0 comments on commit f184c88

Please sign in to comment.