Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NONE: Update our webhook route to send a status code back immediately #207

Merged
merged 4 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/infrastructure/event-bus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { EventEmitter } from 'events';

export const eventBus = new EventEmitter();
1 change: 1 addition & 0 deletions src/infrastructure/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './logger';
export * from './event-bus';
7 changes: 7 additions & 0 deletions src/infrastructure/testing/event-bus-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { eventBus } from '../event-bus';

export function waitForEvent(eventName: string): Promise<void> {
return new Promise((resolve) => {
eventBus.once(eventName, resolve);
});
}
1 change: 1 addition & 0 deletions src/infrastructure/testing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './event-bus-utils';
22 changes: 22 additions & 0 deletions src/jobs/handle-figma-file-update-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { FigmaTeam } from '../domain/entities';
import { eventBus, getLogger } from '../infrastructure';
import { handleFigmaFileUpdateEventUseCase } from '../usecases';
import type { FigmaFileUpdateWebhookEventRequestBody } from '../web/routes/figma';

export const handleFigmaFileUpdateEvent = async (
requestBody: FigmaFileUpdateWebhookEventRequestBody,
figmaTeam: FigmaTeam,
): Promise<void> => {
const { file_key: fileKey, webhook_id: webhookId } = requestBody;
try {
await handleFigmaFileUpdateEventUseCase.execute(figmaTeam, fileKey);

getLogger().info({ webhookId }, 'Figma webhook callback succeeded.');
eventBus.emit('figma.webhook.succeeded', { webhookId });
} catch (e) {
getLogger().error(e, 'Figma webhook callback failed.', {
webhookId,
});
eventBus.emit('figma.webhook.failed', { webhookId });
}
};
1 change: 1 addition & 0 deletions src/jobs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './handle-figma-file-update-event';
23 changes: 12 additions & 11 deletions src/web/routes/figma/figma-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ import type {
} from './types';

import { getLogger } from '../../../infrastructure';
import {
handleFigmaAuthorizationResponseUseCase,
handleFigmaFileUpdateEventUseCase,
} from '../../../usecases';
import { handleFigmaFileUpdateEvent } from '../../../jobs';
import { handleFigmaAuthorizationResponseUseCase } from '../../../usecases';
import { requestSchemaValidationMiddleware } from '../../middleware';
import { figmaWebhookAuthMiddleware } from '../../middleware/figma/figma-webhook-auth-middleware';

Expand All @@ -33,18 +31,21 @@ figmaRouter.post(
'/webhook',
requestSchemaValidationMiddleware(FIGMA_WEBHOOK_EVENT_REQUEST_SCHEMA),
figmaWebhookAuthMiddleware,
(req: FigmaWebhookEventRequest, res: FigmaWebhookEventResponse, next) => {
(req: FigmaWebhookEventRequest, res: FigmaWebhookEventResponse) => {
const { figmaTeam } = res.locals;

switch (req.body.event_type) {
case 'FILE_UPDATE': {
const { file_key } = req.body;
// Making body its own variable so typescript is happy with the refinement
// we did on the discriminated union
const body = req.body;
void setImmediate(
() => void handleFigmaFileUpdateEvent(body, figmaTeam),
);

handleFigmaFileUpdateEventUseCase
.execute(figmaTeam, file_key)
.then(() => res.sendStatus(HttpStatusCode.Ok))
.catch(next);
return;
// Immediately send a 200 back to figma, before doing any of our own
// async processing
return res.sendStatus(HttpStatusCode.Ok);
}
default:
return res.sendStatus(HttpStatusCode.Ok);
Expand Down
94 changes: 38 additions & 56 deletions src/web/routes/figma/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
figmaOAuth2UserCredentialsRepository,
figmaTeamRepository,
} from '../../../infrastructure/repositories';
import { waitForEvent } from '../../../infrastructure/testing';
import {
mockFigmaGetFileEndpoint,
mockFigmaGetTeamProjectsEndpoint,
Expand Down Expand Up @@ -200,6 +201,8 @@ describe('/figma', () => {
.post(FIGMA_WEBHOOK_EVENT_ENDPOINT)
.send(webhookEventRequestBody)
.expect(HttpStatusCode.Ok);

await waitForEvent('figma.webhook.succeeded');
});

it('should ignore if no associated designs are found for the file key', async () => {
Expand All @@ -224,6 +227,8 @@ describe('/figma', () => {
.post(FIGMA_WEBHOOK_EVENT_ENDPOINT)
.send(otherFilewebhookEventRequestBody)
.expect(HttpStatusCode.Ok);

await waitForEvent('figma.webhook.succeeded');
});

it('should ignore if Figma file is not found', async () => {
Expand Down Expand Up @@ -259,6 +264,8 @@ describe('/figma', () => {
.post(FIGMA_WEBHOOK_EVENT_ENDPOINT)
.send(webhookEventRequestBody)
.expect(HttpStatusCode.Ok);

await waitForEvent('figma.webhook.succeeded');
});

it('should ingest designs for available Figma nodes and ignore deleted nodes', async () => {
Expand Down Expand Up @@ -317,6 +324,8 @@ describe('/figma', () => {
.post(FIGMA_WEBHOOK_EVENT_ENDPOINT)
.send(webhookEventRequestBody)
.expect(HttpStatusCode.Ok);

await waitForEvent('figma.webhook.succeeded');
});

it('should return a 200 if fetching Figma team name fails with non-auth error', async () => {
Expand Down Expand Up @@ -364,33 +373,15 @@ describe('/figma', () => {
),
});

await request(app)
.post(FIGMA_WEBHOOK_EVENT_ENDPOINT)
.send(webhookEventRequestBody)
.expect(HttpStatusCode.Ok);
});

it("should set the FigmaTeam status to 'ERROR' and return a 200 if fetching Figma team name fails with auth error", async () => {
mockFigmaGetTeamProjectsEndpoint({
baseUrl: getConfig().figma.apiBaseUrl,
teamId: figmaTeam.teamId,
status: HttpStatusCode.Forbidden,
});

await request(app)
.post(FIGMA_WEBHOOK_EVENT_ENDPOINT)
.send(webhookEventRequestBody)
.expect(HttpStatusCode.Ok);

const updatedFigmaTeam = await figmaTeamRepository.findByWebhookId(
figmaTeam.webhookId,
);
expect(updatedFigmaTeam?.authStatus).toStrictEqual(
FigmaTeamAuthStatus.ERROR,
);
await waitForEvent('figma.webhook.succeeded');
});

it('should return error if fetching Figma designs fails with unexpected error', async () => {
it('should send an error event if fetching Figma designs fails with unexpected error', async () => {
const associatedFigmaDesigns =
await associatedFigmaDesignRepository.findManyByFileKeyAndConnectInstallationId(
fileKey,
Expand Down Expand Up @@ -422,7 +413,31 @@ describe('/figma', () => {
await request(app)
.post(FIGMA_WEBHOOK_EVENT_ENDPOINT)
.send(webhookEventRequestBody)
.expect(HttpStatusCode.InternalServerError);
.expect(HttpStatusCode.Ok);

await waitForEvent('figma.webhook.failed');
});

it("should set the FigmaTeam status to 'ERROR' and return a 200 if fetching Figma team name fails with auth error", async () => {
mockFigmaGetTeamProjectsEndpoint({
baseUrl: getConfig().figma.apiBaseUrl,
teamId: figmaTeam.teamId,
status: HttpStatusCode.Forbidden,
});

await request(app)
.post(FIGMA_WEBHOOK_EVENT_ENDPOINT)
.send(webhookEventRequestBody)
.expect(HttpStatusCode.Ok);

await waitForEvent('figma.webhook.succeeded');

const updatedFigmaTeam = await figmaTeamRepository.findByWebhookId(
figmaTeam.webhookId,
);
expect(updatedFigmaTeam?.authStatus).toStrictEqual(
FigmaTeamAuthStatus.ERROR,
);
});

it("should set the FigmaTeam status to 'ERROR' and return a 200 if fetching Figma designs fails with auth error", async () => {
Expand Down Expand Up @@ -459,6 +474,8 @@ describe('/figma', () => {
.send(webhookEventRequestBody)
.expect(HttpStatusCode.Ok);

await waitForEvent('figma.webhook.succeeded');

const updatedFigmaTeam = await figmaTeamRepository.findByWebhookId(
figmaTeam.webhookId,
);
Expand All @@ -481,41 +498,6 @@ describe('/figma', () => {
.send(webhookEventRequestBody)
.expect(HttpStatusCode.BadRequest);
});

it('should return a 500 if fetching designs from Figma fails', async () => {
const associatedFigmaDesigns =
await associatedFigmaDesignRepository.findManyByFileKeyAndConnectInstallationId(
fileKey,
connectInstallation.id,
);
const nodeIds = associatedFigmaDesigns
.map(({ designId }) => designId.nodeId!)
.filter(isString);

mockFigmaGetTeamProjectsEndpoint({
baseUrl: getConfig().figma.apiBaseUrl,
teamId: figmaTeam.teamId,
response: generateGetTeamProjectsResponse({
name: figmaTeam.teamName,
}),
});
mockFigmaGetFileEndpoint({
baseUrl: getConfig().figma.apiBaseUrl,
accessToken: adminFigmaOAuth2UserCredentials.accessToken,
fileKey: fileKey,
query: {
ids: nodeIds.join(','),
depth: '0',
node_last_modified: 'true',
},
status: HttpStatusCode.InternalServerError,
});

await request(app)
.post(FIGMA_WEBHOOK_EVENT_ENDPOINT)
.send(webhookEventRequestBody)
.expect(HttpStatusCode.InternalServerError);
});
});

describe('PING event', () => {
Expand Down
Loading