diff --git a/.env.example b/.env.example index 3d8fe1f3..a8e47468 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,7 @@ SLACK_BOT_USER_ACCESS_TOKEN='' GOCD_WEBHOOK_SECRET='' KAFKA_CONTROL_PLANE_WEBHOOK_SECRET='' SENTRY_OPTIONS_WEBHOOK_SECRET='' +EXAMPLE_SERVICE_SECRET='' # Silence some GCP noise DRY_RUN=true diff --git a/.env.test b/.env.test index f0a9afd1..69e7c647 100644 --- a/.env.test +++ b/.env.test @@ -19,6 +19,7 @@ SLACK_BOT_APP_ID="5678" GOCD_WEBHOOK_SECRET="webhooksecret" KAFKA_CONTROL_PLANE_WEBHOOK_SECRET="kcpwebhooksecret" SENTRY_OPTIONS_WEBHOOK_SECRET="sentryoptionswebhooksecret" +EXAMPLE_SERVICE_SECRET="examplewebhooksecret" # Other GOCD_SENTRYIO_FE_PIPELINE_NAME="getsentry-frontend" diff --git a/bin/deploy.sh b/bin/deploy.sh index c0b62162..92edb3c7 100755 --- a/bin/deploy.sh +++ b/bin/deploy.sh @@ -27,6 +27,7 @@ GOCD_WEBHOOK_SECRET KAFKA_CONTROL_PLANE_WEBHOOK_SECRET SENTRY_OPTIONS_WEBHOOK_SECRET " +# TODO: Revamp this and make it easier to add secrets & deploy to GCP secrets="" for secret_name in $secret_names; do diff --git a/src/README.md b/src/README.md index 835ffd3d..aae002e7 100644 --- a/src/README.md +++ b/src/README.md @@ -20,6 +20,10 @@ Below are descriptions for how this application is organized. Each directory con ## Common Use Cases +## Generic Event Notifier + +You can use this service to send a message to Sentry Slack or Datadog. All you have to do is create a small PR to create a HMAC secret for your use case, and your service can send messages to Sentry Slack and Datadog via infra-hub. See [this README](webhooks/README.md) for more details. + ### Adding a New Webhook To add a new webhook, nagivate to `webhooks` and follow the directions there. Most of the logic should be self-contained within the `webhooks` directory, with handlers in `brain` being appropriate if the webhook is for receiving event streams. To send a message to external sources, use the APIs in `api`. diff --git a/src/buildServer.ts b/src/buildServer.ts index 21dbd43b..09909212 100644 --- a/src/buildServer.ts +++ b/src/buildServer.ts @@ -15,7 +15,6 @@ import { loadBrain } from '@utils/loadBrain'; import { SENTRY_DSN } from './config'; import { routeJobs } from './jobs'; -import { SlackRouter } from './slack'; export async function buildServer( logger: boolean | { prettyPrint: boolean } = { @@ -96,11 +95,5 @@ export async function buildServer( // Endpoints for Cloud Scheduler webhooks (Cron Jobs) server.register(routeJobs, { prefix: '/jobs' }); - server.post<{ Params: { service: string } }>( - '/slack/:service/webhook', - {}, - SlackRouter(server) - ); - return server; } diff --git a/src/config/secrets.ts b/src/config/secrets.ts new file mode 100644 index 00000000..c2ea6d5b --- /dev/null +++ b/src/config/secrets.ts @@ -0,0 +1,15 @@ +/* + +This file contains secrets used for verifying incoming events from different HTTP sources. + +*/ + +export const EVENT_NOTIFIER_SECRETS = { + // Follow the pattern below to add a new secret + // The secret will also need to be added in the deploy.sh script and in + // Google Secret manager + // 'example-service': process.env.EXAMPLE_SERVICE_SECRET, +}; +if (process.env.ENV !== 'production') + EVENT_NOTIFIER_SECRETS['example-service'] = + process.env.EXAMPLE_SERVICE_SECRET; diff --git a/src/service-registry/service_registry.json b/src/service-registry/service_registry.json new file mode 100644 index 00000000..279a6560 --- /dev/null +++ b/src/service-registry/service_registry.json @@ -0,0 +1,36 @@ +{ + "example_service": { + "alert_slack_channels": [ + "slack-channel-id-or-name" + ], + "aspiring_domain_experts": [], + "component": "tool", + "dashboard": null, + "docs": { + "notion page": "https://www.notion.so/" + }, + "domain_experts": [ + { + "email": "example@sentry.io", + "name": "Example Person" + } + ], + "escalation": "https://sentry.pagerduty.com/", + "id": "example_service", + "name": "Example service", + "notes": null, + "production_readiness_docs": [], + "slack_channels": [ + "discuss-stuff" + ], + "slos": [], + "teams": [ + { + "display_name": "Team 1", + "id": "team1", + "tags": [] + } + ], + "tier": 1 + } +} \ No newline at end of file diff --git a/src/service-registry/types/index.ts b/src/service-registry/types/index.ts new file mode 100644 index 00000000..fff2bc55 --- /dev/null +++ b/src/service-registry/types/index.ts @@ -0,0 +1,32 @@ +export interface Team { + id: string; + display_name: string; + tags: string[]; +} + +export interface Expert { + email: string; + name: string; +} + +export interface Service { + id: string; + name: string; + tier: number | null; + component: string | null; + teams: Team[]; + slack_channels: string[]; + alert_slack_channels: string[]; + domain_experts: Expert[]; + escalation: string; + slos: string[]; + dashboard: string | null; + production_readiness_docs: string[]; + notes: string | null; + docs: Record; + aspiring_domain_experts: Expert[]; +} + +export type ServiceRegistry = { + [serviceName: string]: Service; +}; diff --git a/src/slack/README.md b/src/slack/README.md deleted file mode 100644 index 50096cff..00000000 --- a/src/slack/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Middleman Webhooks for Slack - -Handlers in this folder can be sent messages to be sent to Sentry Slack channels. - -Webhooks can be added to this directory with the name `custom-name` and will be served at `/slack/custom-name/webhook`. - -To do so, create a directory at this level with the name `custom-name` containing an index.ts file that exports the webhook handler. diff --git a/src/slack/index.ts b/src/slack/index.ts deleted file mode 100644 index 0c630876..00000000 --- a/src/slack/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import path from 'path'; - -import * as Sentry from '@sentry/node'; - -// Code taken from src/webhooks/index.ts -export function SlackRouter(_server) { - return async function (request, reply) { - const rootDir = __dirname; - let handler; - - try { - const handlerPath = path.resolve(__dirname, request.params.service); - - // Prevent directory traversals - if (!handlerPath.startsWith(rootDir)) { - throw new Error('Invalid service'); - } - - ({ handler } = require(handlerPath)); - if (!handler) { - throw new Error('Invalid service'); - } - } catch (err) { - console.error(err); - Sentry.captureException(err); - reply.callNotFound(); - return; - } - - try { - return await handler(request, reply); - } catch (err) { - console.error(err); - Sentry.captureException(err); - return reply.code(400).send('Bad Request'); - } - }; -} diff --git a/src/types/index.ts b/src/types/index.ts index 584d9f8b..9f81d896 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,7 @@ import { IncomingMessage, Server, ServerResponse } from 'http'; +import { EventAlertType } from '@datadog/datadog-api-client/dist/packages/datadog-api-client-v1'; +import { Block, KnownBlock } from '@slack/types'; import { FastifyInstance } from 'fastify'; // e.g. the return type of `buildServer` @@ -26,3 +28,40 @@ export interface KafkaControlPlaneResponse { title: string; body: string; } + +interface BaseSlackMessage { + type: 'slack'; + text: string; + blocks?: KnownBlock[] | Block[]; +} + +export interface SlackChannel extends BaseSlackMessage { + channels: string[]; +} + +// Currently service registry is only used for Slack notifications since +// it only contains Slack alert channels (and not DD or Jira or others) +export interface ServiceSlackChannel extends BaseSlackMessage { + service_name: string; +} +export type SlackMessage = SlackChannel | ServiceSlackChannel; + +export interface DatadogEvent { + type: 'datadog'; + title: string; + text: string; + tags: string[]; + alertType: EventAlertType; +} + +export interface JiraEvent { + type: 'jira'; + projectId: string; + title: string; +} + +export type GenericEvent = { + source: string; + timestamp: number; + data: (DatadogEvent | JiraEvent | SlackMessage)[]; +}; diff --git a/src/utils/misc/serviceRegistry.ts b/src/utils/misc/serviceRegistry.ts new file mode 100644 index 00000000..ca9f4c2f --- /dev/null +++ b/src/utils/misc/serviceRegistry.ts @@ -0,0 +1,8 @@ +import servicesData from '@/service-registry/service_registry.json'; +import type { Service, ServiceRegistry } from '@/service-registry/types/index'; + +const services: ServiceRegistry = servicesData; + +export function getService(serviceId: string): Service { + return services[serviceId]; +} diff --git a/src/webhooks/README.md b/src/webhooks/README.md index a0c1fb47..4aa7bec0 100644 --- a/src/webhooks/README.md +++ b/src/webhooks/README.md @@ -3,6 +3,46 @@ * Webhooks in "production" are deployed to a Google Cloud Run instance, in the project `super-big-data`. Why? (TODO insert why) * The webhook points to `https://product-eng-webhooks-vmrqv3f7nq-uw.a.run.app` +## Generic Event Notifier + +The folder `generic-notifier` provides a generic webhook which can be used to send messages to Sentry Slack channels and Sentry Datadog. + +Simply, go to `@/config/secrets.ts` and add an entry to the `EVENT_NOTIFIER_SECRETS` object. This entry should contain a mapping from the source of the message (for example, `example-service`) to an environment variable. As of now, you will also need to edit `bin/deploy.sh` to add the new secret to the deployment and also add the secret to Google Secret Manager. Make a PR with this change and get it approved & merged. + +Once this has been deployed, all you have to do is send a POST request to `https://product-eng-webhooks-vmrqv3f7nq-uw.a.run.app/event-notifier/v1` with a JSON payload in the format of the type `GenericEvent` defined in `@/types/index.ts`. Currently, only Datadog and Slack messages are supported. Example: + +```json +{ + "source": "example-service", // This must match the mapping string you define in the EVENT_NOTIFIER_SECRETS obj + "timestamp": 0, + "data": [ + { + "type": "slack", // Basic Slack message + "text": "Random text here", + "channels": ["#aaaaaa"], + // Optionally, include Slack Blocks + "blocks": [] + }, { + "type": "service_notification", // Slack message using service registry information + "service_name": "eng_pipes_gh_notifications", + "text": "Random text here", + // Optionally, include Slack Blocks + "blocks": [] + }, { + "type": "datadog", // Datadog message + "title": "This is an Example Notification", + "text": "Random text here", + "tags": ["source:example-service", "sentry-region:all", "sentry-user:bob"], + "alertType": "info" + } + ] +} +``` + +Additionally, you must compute the HMAC SHA256 hash of the raw payload string computed with the secret key, and attach it to the `Authorization` header. EX: `Authorization: ` + +TODO: Add the service registry configs to the deployed instance & replace the current dummy json at `@/service-registry/service_registry.json` with the actual service registry json. + ## Adding a webhook to GoCD event emitter * goto [gocd](deploy.getsentry.net) diff --git a/src/webhooks/generic-notifier/generic-notifier.test.ts b/src/webhooks/generic-notifier/generic-notifier.test.ts new file mode 100644 index 00000000..e2912996 --- /dev/null +++ b/src/webhooks/generic-notifier/generic-notifier.test.ts @@ -0,0 +1,105 @@ +import testInvalidPayload from '@test/payloads/generic-notifier/testInvalidPayload.json'; +import testPayload from '@test/payloads/generic-notifier/testPayload.json'; +import { createNotifierRequest } from '@test/utils/createGenericMessageRequest'; + +import { buildServer } from '@/buildServer'; +import { DATADOG_API_INSTANCE } from '@/config'; +import { GenericEvent, SlackMessage } from '@/types'; +import { bolt } from '@api/slack'; + +import { messageSlack } from './generic-notifier'; + +describe('generic messages webhook', function () { + let fastify; + beforeEach(async function () { + fastify = await buildServer(false); + }); + + afterEach(function () { + fastify.close(); + jest.clearAllMocks(); + }); + + it('correctly inserts generic notifier when stage starts', async function () { + jest.spyOn(bolt.client.chat, 'postMessage').mockImplementation(jest.fn()); + jest + .spyOn(DATADOG_API_INSTANCE, 'createEvent') + .mockImplementation(jest.fn()); + const response = await createNotifierRequest( + fastify, + testPayload as GenericEvent + ); + + expect(response.statusCode).toBe(200); + }); + + it('returns 400 for an invalid source', async function () { + const response = await fastify.inject({ + method: 'POST', + url: '/event-notifier/v1', + payload: testInvalidPayload, + }); + expect(response.statusCode).toBe(400); + }); + it('returns 400 for invalid signature', async function () { + const response = await fastify.inject({ + method: 'POST', + url: '/event-notifier/v1', + headers: { + 'x-infra-hub-signature': 'invalid', + }, + payload: testPayload, + }); + expect(response.statusCode).toBe(400); + }); + + it('returns 400 for no signature', async function () { + const response = await fastify.inject({ + method: 'POST', + url: '/event-notifier/v1', + payload: testPayload, + }); + expect(response.statusCode).toBe(400); + }); + + describe('messageSlack tests', function () { + afterEach(function () { + jest.clearAllMocks(); + }); + + it('writes to slack', async function () { + const postMessageSpy = jest.spyOn(bolt.client.chat, 'postMessage'); + await messageSlack(testPayload.data[0] as SlackMessage); + expect(postMessageSpy).toHaveBeenCalledTimes(1); + const message = postMessageSpy.mock.calls[0][0]; + expect(message).toEqual({ + channel: '#aaaaaa', + text: 'Random text here', + unfurl_links: false, + }); + }); + }); + + it('checks that slack msg is sent', async function () { + const postMessageSpy = jest.spyOn(bolt.client.chat, 'postMessage'); + const response = await createNotifierRequest( + fastify, + testPayload as GenericEvent + ); + + expect(postMessageSpy).toHaveBeenCalledTimes(2); + + expect(response.statusCode).toBe(200); + }); + it('checks that dd msg is sent', async function () { + const ddMessageSpy = jest.spyOn(DATADOG_API_INSTANCE, 'createEvent'); + const response = await createNotifierRequest( + fastify, + testPayload as GenericEvent + ); + + expect(ddMessageSpy).toHaveBeenCalledTimes(1); + + expect(response.statusCode).toBe(200); + }); +}); diff --git a/src/webhooks/generic-notifier/generic-notifier.ts b/src/webhooks/generic-notifier/generic-notifier.ts new file mode 100644 index 00000000..7642c708 --- /dev/null +++ b/src/webhooks/generic-notifier/generic-notifier.ts @@ -0,0 +1,99 @@ +import { v1 } from '@datadog/datadog-api-client'; +import * as Sentry from '@sentry/node'; +import { FastifyReply, FastifyRequest } from 'fastify'; + +import { DatadogEvent, GenericEvent, SlackMessage } from '@types'; + +import { bolt } from '@/api/slack'; +import { DATADOG_API_INSTANCE } from '@/config'; +import { EVENT_NOTIFIER_SECRETS } from '@/config/secrets'; +import { extractAndVerifySignature } from '@/utils/auth/extractAndVerifySignature'; + +import { getService } from '../../utils/misc/serviceRegistry'; + +export async function genericEventNotifier( + request: FastifyRequest<{ Body: GenericEvent }>, + reply: FastifyReply +): Promise { + try { + // If the webhook secret is not defined, throw an error + const { body }: { body: GenericEvent } = request; + if ( + body.source === undefined || + EVENT_NOTIFIER_SECRETS[body.source] === undefined + ) { + reply.code(400).send('Invalid source or missing secret'); + throw new Error('Invalid source or missing secret'); + } + const isVerified = await extractAndVerifySignature( + request, + reply, + 'x-infra-hub-signature', + EVENT_NOTIFIER_SECRETS[body.source] + ); + if (!isVerified) { + // If the signature is not verified, return (since extractAndVerifySignature sends the response) + return; + } + for (const message of body.data) { + if (message.type === 'slack') { + await messageSlack(message); + } else if (message.type === 'datadog') { + await sendEventToDatadog(message, body.timestamp); + } + } + reply.code(200).send('OK'); + return; + } catch (err) { + console.error(err); + Sentry.captureException(err); + reply.code(500).send(); + return; + } +} + +export async function sendEventToDatadog( + message: DatadogEvent, + timestamp: number +) { + try { + const params: v1.EventCreateRequest = { + title: message.title, + text: message.text, + alertType: message.alertType, + dateHappened: timestamp, + tags: message.tags, + }; + await DATADOG_API_INSTANCE.createEvent({ body: params }); + } catch (err) { + Sentry.setContext('dd msg:', { text: message.text }); + Sentry.captureException(err); + } +} + +export async function messageSlack(message: SlackMessage) { + let channels: string[] = []; + if ('channels' in message) { + channels = message.channels ?? []; + } else if ('service_name' in message) { + const service = getService(message.service_name); + channels = service.alert_slack_channels ?? []; + } + for (const channel of channels) { + try { + const args = { + channel: channel, + blocks: message.blocks, + text: message.text, + unfurl_links: false, + }; + if (message.blocks) { + args.blocks = message.blocks; + } + await bolt.client.chat.postMessage(args); + } catch (err) { + Sentry.setContext('slack msg:', { text: message.text }); + Sentry.captureException(err); + } + } +} diff --git a/src/webhooks/index.ts b/src/webhooks/index.ts index 941f3918..586b5fc1 100644 --- a/src/webhooks/index.ts +++ b/src/webhooks/index.ts @@ -6,6 +6,7 @@ import { FastifyReply, FastifyRequest } from 'fastify'; import { Fastify } from '@/types'; import { bootstrapWebhook } from './bootstrap-dev-env/bootstrap-dev-env'; +import { genericEventNotifier } from './generic-notifier/generic-notifier'; import { gocdWebhook } from './gocd/gocd'; import { kafkactlWebhook } from './kafka-control-plane/kafka-control-plane'; import { sentryOptionsWebhook } from './sentry-options/sentry-options'; @@ -54,6 +55,9 @@ export async function routeHandlers(server: Fastify, _options): Promise { server.post('/metrics/webpack/webhook', (request, reply) => handleRoute(webpackWebhook, request, reply, 'webpack') ); + server.post('/event-notifier/v1', (request, reply) => + handleRoute(genericEventNotifier, request, reply, 'generic-notifier') + ); // Default handler for invalid routes server.all('/metrics/*/webhook', async (request, reply) => { diff --git a/test/payloads/generic-notifier/testAdminPayload.json b/test/payloads/generic-notifier/testAdminPayload.json new file mode 100644 index 00000000..296049a6 --- /dev/null +++ b/test/payloads/generic-notifier/testAdminPayload.json @@ -0,0 +1,5 @@ +{ + "source": "admin", + "title": "this is a title", + "body": "this is a text body" +} diff --git a/test/payloads/generic-notifier/testBadPayload.json b/test/payloads/generic-notifier/testBadPayload.json new file mode 100644 index 00000000..dfda7d2a --- /dev/null +++ b/test/payloads/generic-notifier/testBadPayload.json @@ -0,0 +1,15 @@ +{ + "service_name": "official_service_name", + "data": { + "message": "Random text here", + "tags": [ + "source:example-service", "sentry-region:all", "sentry-user:bob" + ], + "misc": {}, + "channels": { + "slack": ["#C07GZR8LA82"], + "datadog": ["example-proj-id"], + "jira": ["INC"] + } + } +} \ No newline at end of file diff --git a/test/payloads/generic-notifier/testInvalidPayload.json b/test/payloads/generic-notifier/testInvalidPayload.json new file mode 100644 index 00000000..28f9defa --- /dev/null +++ b/test/payloads/generic-notifier/testInvalidPayload.json @@ -0,0 +1,18 @@ +{ + "source": "bad-source", + "timestamp": 0, + "service_name": "official_service_name", + "data": { + "title": "This is an Example Notification", + "message": "Random text here", + "tags": [ + "source:example-service", "sentry-region:all", "sentry-user:bob" + ], + "misc": {}, + "channels": { + "slack": ["#aaaaaa"], + "datadog": ["example-proj-id"], + "jira": ["TEST"] + } + } +} \ No newline at end of file diff --git a/test/payloads/generic-notifier/testMegaPayload.json b/test/payloads/generic-notifier/testMegaPayload.json new file mode 100644 index 00000000..4122309a --- /dev/null +++ b/test/payloads/generic-notifier/testMegaPayload.json @@ -0,0 +1,21 @@ +{ + "source": "example-service", + "timestamp": 0, + "data": [ + { + "type": "slack", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nSed fringilla venenatis ipsum eu vestibulum.\nNam ultricies, elit sed commodo eleifend, ipsum est tempus lorem, porttitor molestie urna sem in eros.\nSuspendisse non interdum sapien, vel commodo dui.\nNunc eu scelerisque augue.\nAliquam eget rhoncus leo.\nDonec nulla elit, aliquet ut porttitor at, sodales quis ex.\nMaecenas sit amet pretium neque.\nIn eu nisi vel purus mattis vehicula in sed mauris.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.\nProin varius eget nisl sed vulputate.\nFusce quis nibh eu enim blandit bibendum.\nCras volutpat est erat, sit amet molestie ante commodo hendrerit.\nPellentesque in luctus augue.Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nIn ac bibendum odio, condimentum finibus nisi.\nIn at enim vel elit aliquet dictum.\nNullam efficitur gravida gravida.\nNullam scelerisque euismod mi, non dictum elit posuere sed.\nSed dictum quam in ornare lacinia.\nDonec vulputate dictum tortor quis volutpat.\nCurabitur at nulla hendrerit, placerat nibh sed, gravida metus.\nMaecenas nec eros sollicitudin, consectetur nibh sit amet, fringilla nibh.\nUt efficitur convallis luctus.\nIn ultricies lectus non urna faucibus, a venenatis ipsum mollis.\nVivamus tincidunt interdum lorem vitae fermentum.\nVivamus ante nunc, facilisis sed velit egestas, maximus euismod enim.\nSed pretium et ligula ac suscipit.\nSed et metus ut orci faucibus lacinia.\nInteger vestibulum commodo blandit.\nIn dapibus libero nec mauris sagittis, vel accumsan erat feugiat.\nCurabitur tempus, arcu non accumsan faucibus, sapien nisl pharetra justo, id fringilla mauris sem eu ante.\nProin ac feugiat dolor.\nNulla auctor vestibulum tortor at placerat.\nCras tempus non tortor ut dictum.\nSed eleifend velit nisi, sit amet lacinia velit convallis non.\nCurabitur imperdiet tortor sit amet massa condimentum, nec cursus lectus placerat.\nUt egestas suscipit est, at ultricies tellus viverra ut.\nSed elementum dignissim nulla, a feugiat massa mollis quis.\nPellentesque a lobortis dolor.\nCras eleifend condimentum orci, a venenatis arcu feugiat at.\nSuspendisse condimentum neque metus, non faucibus libero ultricies nec.\nInteger quis orci at enim aliquet dapibus id quis nisl.\nMaecenas et convallis massa.\nSed ornare sagittis erat, et hendrerit neque auctor quis.\nSed dignissim erat nisl, sed pharetra mauris sollicitudin sed.\nNullam eu purus quis augue scelerisque aliquam id sed enim.\nMaecenas in posuere ex.\nVivamus quis sem faucibus, suscipit eros nec, bibendum purus.\nUt in urna orci.\nAenean id bibendum urna.\nDuis gravida ac massa vel egestas.\nFusce in erat hendrerit nunc tempus dictum.\nCras sapien diam, eleifend vitae commodo in, bibendum ut eros.\nNam sit amet massa tincidunt neque rutrum sodales.\nNunc vitae felis ut diam lobortis pellentesque.\nNunc nulla ante, sodales non tempor pretium, vulputate vel nisi.\nNulla facilisis dolor sit amet aliquam imperdiet.\nPellentesque lacinia augue eget nulla finibus tincidunt.\nNam urna tellus, aliquam sit amet velit vestibulum, dictum eleifend dolor.\nDonec eu commodo est, non tincidunt orci.\nCras vel nisl libero.\nPraesent cursus neque massa.\nNunc convallis vitae sem nec tincidunt.\nEtiam eget dui in ligula dictum sodales nec quis ante.\nCras sagittis, dui at tempus porttitor, lectus ipsum malesuada augue, vitae suscipit felis lacus facilisis nisl.\nDonec tristique tellus at aliquet accumsan.\nInteger elementum venenatis mollis.\nSuspendisse rutrum eros eget justo scelerisque facilisis.\nDonec vehicula neque at lectus interdum egestas.\nPellentesque dolor dui, feugiat ac placerat sit amet, lacinia sed massa.\nMaecenas aliquam, massa sodales ultrices pellentesque, est tortor pharetra metus, at congue libero ligula ut purus.\nVestibulum mattis scelerisque tellus, vitae volutpat velit ultrices nec.\nUt mollis hendrerit magna vitae mollis.\nNulla pharetra magna eros, quis facilisis arcu accumsan sodales.\nIn pharetra quam maximus turpis volutpat blandit.\nIn at dui nec velit sagittis tincidunt.\nIn felis orci, vulputate non luctus ac, molestie vitae urna.\nIn iaculis velit id convallis condimentum.", + "channels": ["#aaaaaa"] + }, { + "type": "datadog", + "title": "This is an Example Notification", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nSed fringilla venenatis ipsum eu vestibulum.\nNam ultricies, elit sed commodo eleifend, ipsum est tempus lorem, porttitor molestie urna sem in eros.\nSuspendisse non interdum sapien, vel commodo dui.\nNunc eu scelerisque augue.\nAliquam eget rhoncus leo.\nDonec nulla elit, aliquet ut porttitor at, sodales quis ex.\nMaecenas sit amet pretium neque.\nIn eu nisi vel purus mattis vehicula in sed mauris.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.\nProin varius eget nisl sed vulputate.\nFusce quis nibh eu enim blandit bibendum.\nCras volutpat est erat, sit amet molestie ante commodo hendrerit.\nPellentesque in luctus augue.Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nIn ac bibendum odio, condimentum finibus nisi.\nIn at enim vel elit aliquet dictum.\nNullam efficitur gravida gravida.\nNullam scelerisque euismod mi, non dictum elit posuere sed.\nSed dictum quam in ornare lacinia.\nDonec vulputate dictum tortor quis volutpat.\nCurabitur at nulla hendrerit, placerat nibh sed, gravida metus.\nMaecenas nec eros sollicitudin, consectetur nibh sit amet, fringilla nibh.\nUt efficitur convallis luctus.\nIn ultricies lectus non urna faucibus, a venenatis ipsum mollis.\nVivamus tincidunt interdum lorem vitae fermentum.\nVivamus ante nunc, facilisis sed velit egestas, maximus euismod enim.\nSed pretium et ligula ac suscipit.\nSed et metus ut orci faucibus lacinia.\nInteger vestibulum commodo blandit.\nIn dapibus libero nec mauris sagittis, vel accumsan erat feugiat.\nCurabitur tempus, arcu non accumsan faucibus, sapien nisl pharetra justo, id fringilla mauris sem eu ante.\nProin ac feugiat dolor.\nNulla auctor vestibulum tortor at placerat.\nCras tempus non tortor ut dictum.\nSed eleifend velit nisi, sit amet lacinia velit convallis non.\nCurabitur imperdiet tortor sit amet massa condimentum, nec cursus lectus placerat.\nUt egestas suscipit est, at ultricies tellus viverra ut.\nSed elementum dignissim nulla, a feugiat massa mollis quis.\nPellentesque a lobortis dolor.\nCras eleifend condimentum orci, a venenatis arcu feugiat at.\nSuspendisse condimentum neque metus, non faucibus libero ultricies nec.\nInteger quis orci at enim aliquet dapibus id quis nisl.\nMaecenas et convallis massa.\nSed ornare sagittis erat, et hendrerit neque auctor quis.\nSed dignissim erat nisl, sed pharetra mauris sollicitudin sed.\nNullam eu purus quis augue scelerisque aliquam id sed enim.\nMaecenas in posuere ex.\nVivamus quis sem faucibus, suscipit eros nec, bibendum purus.\nUt in urna orci.\nAenean id bibendum urna.\nDuis gravida ac massa vel egestas.\nFusce in erat hendrerit nunc tempus dictum.\nCras sapien diam, eleifend vitae commodo in, bibendum ut eros.\nNam sit amet massa tincidunt neque rutrum sodales.\nNunc vitae felis ut diam lobortis pellentesque.\nNunc nulla ante, sodales non tempor pretium, vulputate vel nisi.\nNulla facilisis dolor sit amet aliquam imperdiet.\nPellentesque lacinia augue eget nulla finibus tincidunt.\nNam urna tellus, aliquam sit amet velit vestibulum, dictum eleifend dolor.\nDonec eu commodo est, non tincidunt orci.\nCras vel nisl libero.\nPraesent cursus neque massa.\nNunc convallis vitae sem nec tincidunt.\nEtiam eget dui in ligula dictum sodales nec quis ante.\nCras sagittis, dui at tempus porttitor, lectus ipsum malesuada augue, vitae suscipit felis lacus facilisis nisl.\nDonec tristique tellus at aliquet accumsan.\nInteger elementum venenatis mollis.\nSuspendisse rutrum eros eget justo scelerisque facilisis.\nDonec vehicula neque at lectus interdum egestas.\nPellentesque dolor dui, feugiat ac placerat sit amet, lacinia sed massa.\nMaecenas aliquam, massa sodales ultrices pellentesque, est tortor pharetra metus, at congue libero ligula ut purus.\nVestibulum mattis scelerisque tellus, vitae volutpat velit ultrices nec.\nUt mollis hendrerit magna vitae mollis.\nNulla pharetra magna eros, quis facilisis arcu accumsan sodales.\nIn pharetra quam maximus turpis volutpat blandit.\nIn at dui nec velit sagittis tincidunt.\nIn felis orci, vulputate non luctus ac, molestie vitae urna.\nIn iaculis velit id convallis condimentum.", + "tags": ["source:example-service", "sentry-region:all", "sentry-user:bob"], + "alertType": "info" + }, { + "type": "jira", + "title": "This is an Example Notification", + "projectId": "TEST" + } + ] +} \ No newline at end of file diff --git a/test/payloads/generic-notifier/testPayload.json b/test/payloads/generic-notifier/testPayload.json new file mode 100644 index 00000000..72cbf248 --- /dev/null +++ b/test/payloads/generic-notifier/testPayload.json @@ -0,0 +1,25 @@ +{ + "source": "example-service", + "timestamp": 0, + "data": [ + { + "type": "slack", + "text": "Random text here", + "channels": ["#aaaaaa"] + }, { + "type": "slack", + "service_name": "example_service", + "text": "Random text here" + }, { + "type": "datadog", + "title": "This is an Example Notification", + "text": "Random text here", + "tags": ["source:example-service", "sentry-region:all", "sentry-user:bob"], + "alertType": "info" + }, { + "type": "jira", + "title": "This is an Example Notification", + "projectId": "TEST" + } + ] +} \ No newline at end of file diff --git a/test/payloads/generic-notifier/testServicePayload.json b/test/payloads/generic-notifier/testServicePayload.json new file mode 100644 index 00000000..1f903553 --- /dev/null +++ b/test/payloads/generic-notifier/testServicePayload.json @@ -0,0 +1,11 @@ +{ + "source": "example-service", + "timestamp": 0, + "data": [ + { + "type": "slack", + "service_name": "example_service", + "text": "Random text here" + } + ] +} \ No newline at end of file diff --git a/test/utils/createGenericMessageRequest.ts b/test/utils/createGenericMessageRequest.ts new file mode 100644 index 00000000..b3709eb1 --- /dev/null +++ b/test/utils/createGenericMessageRequest.ts @@ -0,0 +1,29 @@ +import { Fastify } from '@/types'; +import { createSignature } from '@/utils/auth/createSignature'; + +import { GenericEvent } from '../../src/types/index'; + +function createNotifierSignature(payload) { + return createSignature( + JSON.stringify(payload), + process.env.EXAMPLE_SERVICE_SECRET || '', + (i) => i, + 'sha256' + ); +} + +export async function createNotifierRequest( + fastify: Fastify, + payload: GenericEvent +) { + const signature = createNotifierSignature(payload); + + return await fastify.inject({ + method: 'POST', + url: '/event-notifier/v1', + headers: { + 'x-infra-hub-signature': signature.toString(), + }, + payload: payload, + }); +}