Skip to content

Commit e6635c9

Browse files
src: handle incoming file webhook events and dev mode status payloads (#261)
* handle incoming file webhooks and dev mode status events * update tests * move private func to bottom of file * create new route for file webhooks * fix endpoint for newly created file webhooks * Add tests for new webhook endpoint * fix tests and simplify router * add DEV_MODE_STATUS_UPDATE payload to webhook endpoint schema * use test data generator helpers * use ENV as a fallback for CLUSTER_NAME
1 parent a75228b commit e6635c9

14 files changed

Lines changed: 1194 additions & 164 deletions

src/config/config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ export const getConfig = (): Config => {
5858
domain: readEnvVarString('FIGMA_DOMAIN', 'figma.com'),
5959
webBaseUrl: new URL(readEnvVarString('FIGMA_WEB_BASE_URL')),
6060
apiBaseUrl: new URL(readEnvVarString('FIGMA_API_BASE_URL')),
61-
clusterName: readEnvVarString('CLUSTER_NAME', 'unknown'),
61+
clusterName: readEnvVarString(
62+
'CLUSTER_NAME',
63+
readEnvVarString('ENV', 'unknown'),
64+
),
6265
oauth2: {
6366
authorizationServerBaseUrl: new URL(
6467
readEnvVarString('FIGMA_OAUTH2_AUTHORIZATION_SERVER_BASE_URL'),

src/infrastructure/figma/figma-service.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ describe('FigmaService', () => {
903903
const fileKey = generateFigmaFileKey();
904904

905905
const endpoint = new URL(
906-
'figma/webhook',
906+
'figma/webhook/file',
907907
getConfig().app.baseUrl,
908908
).toString();
909909
const passcode = uuidv4();

src/infrastructure/figma/figma-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ export class FigmaService {
339339
event_type: eventType,
340340
context: 'file',
341341
context_id: fileKey,
342-
endpoint: buildAppUrl('figma/webhook').toString(),
342+
endpoint: buildAppUrl('figma/webhook/file').toString(),
343343
passcode,
344344
description: 'Figma for Jira Cloud',
345345
};

src/jobs/handle-figma-file-update-event.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1-
import type { FigmaTeam } from '../domain/entities';
1+
import type {
2+
WebhookDevModeStatusUpdatePayload,
3+
WebhookFileUpdatePayload,
4+
} from '@figma/rest-api-spec';
5+
26
import { eventBus, getLogger } from '../infrastructure';
37
import { handleFigmaFileUpdateEventUseCase } from '../usecases';
4-
import type { FigmaFileUpdateWebhookEventRequestBody } from '../web/routes/figma';
8+
import type { FigmaWebhookInfo } from '../web/routes/figma';
59

610
export const handleFigmaFileUpdateEvent = async (
7-
requestBody: FigmaFileUpdateWebhookEventRequestBody,
8-
figmaTeam: FigmaTeam,
11+
requestBody: WebhookFileUpdatePayload | WebhookDevModeStatusUpdatePayload,
12+
webhookInfo: FigmaWebhookInfo,
913
): Promise<void> => {
1014
const { file_key: fileKey, webhook_id: webhookId } = requestBody;
1115
const logger = getLogger().child({
1216
job: 'handleFigmaFileUpdateEvent',
1317
webhookId,
1418
});
1519
try {
16-
await handleFigmaFileUpdateEventUseCase.execute(figmaTeam, fileKey);
17-
20+
await handleFigmaFileUpdateEventUseCase.execute(webhookInfo, fileKey);
1821
logger.info('Figma webhook callback succeeded.');
1922
eventBus.emit('job.handle-figma-file-update-event.succeeded', {
2023
webhookId,

src/usecases/handle-figma-file-update-event-use-case.test.ts

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { v4 as uuidv4 } from 'uuid';
22

33
import { handleFigmaFileUpdateEventUseCase } from './handle-figma-file-update-event-use-case';
44

5+
import * as launchDarkly from '../config/launch_darkly';
56
import { FigmaTeamAuthStatus } from '../domain/entities';
67
import {
78
generateAssociatedFigmaDesign,
89
generateAtlassianDesign,
910
generateConnectInstallation,
1011
generateFigmaDesignIdentifier,
12+
generateFigmaFileWebhook,
1113
generateFigmaTeam,
1214
} from '../domain/entities/testing';
1315
import {
@@ -20,8 +22,65 @@ import {
2022
connectInstallationRepository,
2123
figmaTeamRepository,
2224
} from '../infrastructure/repositories';
25+
import type { FigmaWebhookInfo } from '../web/routes/figma';
2326

2427
describe('handleFigmaFileUpdateEventUseCase', () => {
28+
describe('file webhook', () => {
29+
beforeEach(() => {
30+
jest.spyOn(launchDarkly, 'getLDClient').mockResolvedValue(null);
31+
jest.spyOn(launchDarkly, 'getFeatureFlag').mockResolvedValue(true);
32+
});
33+
it('should handle file webhook events', async () => {
34+
const connectInstallation = generateConnectInstallation();
35+
jest
36+
.spyOn(connectInstallationRepository, 'get')
37+
.mockResolvedValue(connectInstallation);
38+
39+
const figmaFileWebhook = generateFigmaFileWebhook({
40+
createdBy: {
41+
connectInstallationId: connectInstallation.id,
42+
atlassianUserId: uuidv4(),
43+
},
44+
});
45+
const fileKey = uuidv4();
46+
const associatedFigmaDesigns = [1, 2, 3].map((i) =>
47+
generateAssociatedFigmaDesign({
48+
designId: generateFigmaDesignIdentifier({
49+
fileKey,
50+
nodeId: `${i}:${i}`,
51+
}),
52+
connectInstallationId: connectInstallation.id,
53+
}),
54+
);
55+
jest
56+
.spyOn(
57+
associatedFigmaDesignRepository,
58+
'findManyByFileKeyAndConnectInstallationId',
59+
)
60+
.mockResolvedValue(associatedFigmaDesigns);
61+
const associatedAtlassianDesigns = associatedFigmaDesigns.map(
62+
(figmaDesign) =>
63+
generateAtlassianDesign({
64+
id: figmaDesign.designId.toAtlassianDesignId(),
65+
}),
66+
);
67+
jest
68+
.spyOn(figmaService, 'getAvailableDesignsFromSameFile')
69+
.mockResolvedValue(associatedAtlassianDesigns);
70+
71+
jest.spyOn(jiraService, 'submitDesigns').mockResolvedValue();
72+
73+
const webhookInfo: FigmaWebhookInfo = {
74+
figmaFileWebhook,
75+
webhookType: 'file',
76+
};
77+
await handleFigmaFileUpdateEventUseCase.execute(webhookInfo, fileKey);
78+
expect(jiraService.submitDesigns).toHaveBeenCalledWith(
79+
associatedAtlassianDesigns,
80+
connectInstallation,
81+
);
82+
});
83+
});
2584
describe('error handling', () => {
2685
const connectInstallation = generateConnectInstallation();
2786
const figmaTeam = generateFigmaTeam({
@@ -44,7 +103,11 @@ describe('handleFigmaFileUpdateEventUseCase', () => {
44103
.mockRejectedValue(new UnauthorizedFigmaServiceError());
45104
jest.spyOn(figmaTeamRepository, 'updateAuthStatus').mockResolvedValue();
46105

47-
await handleFigmaFileUpdateEventUseCase.execute(figmaTeam, fileKey);
106+
const webhookInfo: FigmaWebhookInfo = {
107+
figmaTeam,
108+
webhookType: 'team',
109+
};
110+
await handleFigmaFileUpdateEventUseCase.execute(webhookInfo, fileKey);
48111

49112
expect(figmaTeamRepository.updateAuthStatus).toHaveBeenCalledWith(
50113
figmaTeam.id,
@@ -75,7 +138,11 @@ describe('handleFigmaFileUpdateEventUseCase', () => {
75138
.mockResolvedValue(associatedAtlassianDesigns);
76139
jest.spyOn(jiraService, 'submitDesigns').mockResolvedValue();
77140

78-
await handleFigmaFileUpdateEventUseCase.execute(figmaTeam, fileKey);
141+
const webhookInfo: FigmaWebhookInfo = {
142+
figmaTeam,
143+
webhookType: 'team',
144+
};
145+
await handleFigmaFileUpdateEventUseCase.execute(webhookInfo, fileKey);
79146

80147
expect(figmaTeamRepository.updateAuthStatus).not.toHaveBeenCalled();
81148
expect(jiraService.submitDesigns).toHaveBeenCalledWith(
@@ -111,7 +178,11 @@ describe('handleFigmaFileUpdateEventUseCase', () => {
111178
.mockResolvedValue(associatedAtlassianDesigns);
112179
jest.spyOn(jiraService, 'submitDesigns').mockResolvedValue();
113180

114-
await handleFigmaFileUpdateEventUseCase.execute(figmaTeam, fileKey);
181+
const webhookInfo: FigmaWebhookInfo = {
182+
figmaTeam,
183+
webhookType: 'team',
184+
};
185+
await handleFigmaFileUpdateEventUseCase.execute(webhookInfo, fileKey);
115186

116187
expect(jiraService.submitDesigns).toHaveBeenCalledWith(
117188
associatedAtlassianDesigns,
@@ -138,7 +209,11 @@ describe('handleFigmaFileUpdateEventUseCase', () => {
138209
.mockRejectedValue(new UnauthorizedFigmaServiceError());
139210
jest.spyOn(figmaTeamRepository, 'updateAuthStatus').mockResolvedValue();
140211

141-
await handleFigmaFileUpdateEventUseCase.execute(figmaTeam, fileKey);
212+
const webhookInfo: FigmaWebhookInfo = {
213+
figmaTeam,
214+
webhookType: 'team',
215+
};
216+
await handleFigmaFileUpdateEventUseCase.execute(webhookInfo, fileKey);
142217

143218
expect(figmaTeamRepository.updateAuthStatus).toHaveBeenCalledWith(
144219
figmaTeam.id,
@@ -166,8 +241,12 @@ describe('handleFigmaFileUpdateEventUseCase', () => {
166241
.mockRejectedValue(error);
167242
jest.spyOn(jiraService, 'submitDesigns');
168243

244+
const webhookInfo: FigmaWebhookInfo = {
245+
figmaTeam,
246+
webhookType: 'team',
247+
};
169248
await expect(
170-
handleFigmaFileUpdateEventUseCase.execute(figmaTeam, fileKey),
249+
handleFigmaFileUpdateEventUseCase.execute(webhookInfo, fileKey),
171250
).rejects.toStrictEqual(error);
172251
expect(jiraService.submitDesigns).not.toHaveBeenCalled();
173252
});
Lines changed: 82 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { AtlassianDesign, FigmaTeam } from '../domain/entities';
1+
import { getFeatureFlag, getLDClient } from '../config/launch_darkly';
2+
import type { ConnectUserInfo } from '../domain/entities';
23
import { FigmaTeamAuthStatus } from '../domain/entities';
34
import { getLogger } from '../infrastructure';
45
import {
@@ -11,55 +12,95 @@ import {
1112
connectInstallationRepository,
1213
figmaTeamRepository,
1314
} from '../infrastructure/repositories';
15+
import type { FigmaWebhookInfo } from '../web/routes/figma';
1416

1517
export const handleFigmaFileUpdateEventUseCase = {
16-
execute: async (figmaTeam: FigmaTeam, fileKey: string): Promise<void> => {
17-
try {
18-
const teamName = await figmaService.getTeamName(
19-
figmaTeam.teamId,
20-
figmaTeam.adminInfo,
21-
);
22-
await figmaTeamRepository.updateTeamName(figmaTeam.id, teamName);
23-
} catch (e: unknown) {
24-
if (e instanceof UnauthorizedFigmaServiceError) {
25-
return figmaTeamRepository.updateAuthStatus(
26-
figmaTeam.id,
27-
FigmaTeamAuthStatus.ERROR,
28-
);
29-
}
18+
execute: async (
19+
webhookInfo: FigmaWebhookInfo,
20+
fileKey: string,
21+
): Promise<void> => {
22+
switch (webhookInfo.webhookType) {
23+
case 'team': {
24+
const figmaTeam = webhookInfo.figmaTeam;
25+
try {
26+
const teamName = await figmaService.getTeamName(
27+
figmaTeam.teamId,
28+
figmaTeam.adminInfo,
29+
);
30+
await figmaTeamRepository.updateTeamName(figmaTeam.id, teamName);
31+
} catch (e: unknown) {
32+
if (e instanceof UnauthorizedFigmaServiceError) {
33+
return figmaTeamRepository.updateAuthStatus(
34+
figmaTeam.id,
35+
FigmaTeamAuthStatus.ERROR,
36+
);
37+
}
3038

31-
getLogger().warn(e, `Failed to sync team name for ${figmaTeam.id}`);
32-
}
39+
getLogger().warn(e, `Failed to sync team name for ${figmaTeam.id}`);
40+
}
3341

34-
const [connectInstallation, associatedFigmaDesigns] = await Promise.all([
35-
connectInstallationRepository.get(figmaTeam.connectInstallationId),
36-
associatedFigmaDesignRepository.findManyByFileKeyAndConnectInstallationId(
37-
fileKey,
38-
figmaTeam.connectInstallationId,
39-
),
40-
]);
42+
try {
43+
await syncDesignsToJira(
44+
fileKey,
45+
figmaTeam.connectInstallationId,
46+
figmaTeam.adminInfo,
47+
);
48+
} catch (e: unknown) {
49+
if (e instanceof UnauthorizedFigmaServiceError) {
50+
return figmaTeamRepository.updateAuthStatus(
51+
figmaTeam.id,
52+
FigmaTeamAuthStatus.ERROR,
53+
);
54+
}
55+
throw e;
56+
}
4157

42-
if (!associatedFigmaDesigns.length) return;
58+
return;
59+
}
4360

44-
let designs: AtlassianDesign[];
61+
case 'file': {
62+
const ldClient = await getLDClient();
63+
const useFileWebhooks = await getFeatureFlag(
64+
ldClient,
65+
'ext_figma_for_jira_use_file_webhooks',
66+
false,
67+
);
68+
if (!useFileWebhooks) {
69+
return;
70+
}
4571

46-
try {
47-
designs = await figmaService.getAvailableDesignsFromSameFile(
48-
associatedFigmaDesigns.map((design) => design.designId),
49-
figmaTeam.adminInfo,
50-
);
51-
} catch (e: unknown) {
52-
if (e instanceof UnauthorizedFigmaServiceError) {
53-
return figmaTeamRepository.updateAuthStatus(
54-
figmaTeam.id,
55-
FigmaTeamAuthStatus.ERROR,
72+
const figmaFileWebhook = webhookInfo.figmaFileWebhook;
73+
return await syncDesignsToJira(
74+
fileKey,
75+
figmaFileWebhook.createdBy.connectInstallationId,
76+
figmaFileWebhook.createdBy,
5677
);
5778
}
58-
throw e;
5979
}
60-
61-
if (!designs.length) return;
62-
63-
await jiraService.submitDesigns(designs, connectInstallation);
6480
},
6581
};
82+
83+
async function syncDesignsToJira(
84+
fileKey: string,
85+
connectInstallationId: string,
86+
adminInfo: ConnectUserInfo,
87+
): Promise<void> {
88+
const [connectInstallation, associatedFigmaDesigns] = await Promise.all([
89+
connectInstallationRepository.get(connectInstallationId),
90+
associatedFigmaDesignRepository.findManyByFileKeyAndConnectInstallationId(
91+
fileKey,
92+
connectInstallationId,
93+
),
94+
]);
95+
96+
if (!associatedFigmaDesigns.length) return;
97+
98+
const designs = await figmaService.getAvailableDesignsFromSameFile(
99+
associatedFigmaDesigns.map((design) => design.designId),
100+
adminInfo,
101+
);
102+
103+
if (!designs.length) return;
104+
105+
await jiraService.submitDesigns(designs, connectInstallation);
106+
}

0 commit comments

Comments
 (0)