Skip to content

Commit bf0acd7

Browse files
committed
split webhook creation into two to avoid dangling webhooks
1 parent 6175d34 commit bf0acd7

6 files changed

Lines changed: 282 additions & 113 deletions

File tree

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
} from './transformers';
3232

3333
import { getConfig } from '../../config';
34+
import { FigmaFileWebhookEventType } from '../../domain/entities';
3435
import {
3536
generateConnectUserInfo,
3637
generateFigmaDesignIdentifier,
@@ -888,14 +889,14 @@ describe('FigmaService', () => {
888889
});
889890
});
890891

891-
describe('createFileContextWebhooks', () => {
892+
describe('createWebhookForFile', () => {
892893
beforeEach(() => {
893894
jest
894895
.spyOn(figmaAuthService, 'getCredentials')
895896
.mockResolvedValue(MOCK_CREDENTIALS);
896897
});
897898

898-
it('should create a FILE_UPDATE and DEV_MODE_STATUS_UPDATE webhook for a file', async () => {
899+
it('should create a webhook for a file', async () => {
899900
const fileUpdateWebhookId = uuidv4();
900901
const devModeStatusUpdateWebhookId = uuidv4();
901902

@@ -933,8 +934,16 @@ describe('FigmaService', () => {
933934
description,
934935
} as PostWebhookResponse);
935936

936-
await figmaService.createFileContextWebhooks(
937+
await figmaService.createWebhookForFile(
937938
fileKey,
939+
FigmaFileWebhookEventType.FILE_UPDATE,
940+
passcode,
941+
MOCK_CONNECT_USER_INFO,
942+
);
943+
944+
await figmaService.createWebhookForFile(
945+
fileKey,
946+
FigmaFileWebhookEventType.DEV_MODE_STATUS_UPDATE,
938947
passcode,
939948
MOCK_CONNECT_USER_INFO,
940949
);
@@ -977,8 +986,9 @@ describe('FigmaService', () => {
977986
);
978987

979988
await expect(() =>
980-
figmaService.createFileContextWebhooks(
989+
figmaService.createWebhookForFile(
981990
teamId,
991+
FigmaFileWebhookEventType.FILE_UPDATE,
982992
connectInstallationSecret,
983993
MOCK_CONNECT_USER_INFO,
984994
),
@@ -995,8 +1005,9 @@ describe('FigmaService', () => {
9951005
jest.spyOn(figmaClient, 'createWebhook').mockRejectedValue(error);
9961006

9971007
await expect(() =>
998-
figmaService.createFileContextWebhooks(
1008+
figmaService.createWebhookForFile(
9991009
teamId,
1010+
FigmaFileWebhookEventType.FILE_UPDATE,
10001011
connectInstallationSecret,
10011012
MOCK_CONNECT_USER_INFO,
10021013
),
@@ -1010,8 +1021,9 @@ describe('FigmaService', () => {
10101021
jest.spyOn(figmaClient, 'createWebhook').mockRejectedValue(expectedError);
10111022

10121023
await expect(() =>
1013-
figmaService.createFileContextWebhooks(
1024+
figmaService.createWebhookForFile(
10141025
teamId,
1026+
FigmaFileWebhookEventType.FILE_UPDATE,
10151027
connectInstallationSecret,
10161028
MOCK_CONNECT_USER_INFO,
10171029
),

src/infrastructure/figma/figma-service.ts

Lines changed: 32 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {
2626
AtlassianDesign,
2727
ConnectUserInfo,
2828
FigmaDesignIdentifier,
29+
FigmaFileWebhookEventType,
2930
FigmaOAuth2UserCredentials,
3031
FigmaUser,
3132
} from '../../domain/entities';
@@ -323,63 +324,45 @@ export class FigmaService {
323324
});
324325

325326
/**
326-
* Creates a FILE_UPDATE webhook and a DEV_MODE_STATUS_UPDATE webhook for the provided file.
327+
* Creates a webhook of the specified event type for the provided file.
327328
* @throws {UnauthorizedFigmaServiceError} Not authorized to access Figma.
328329
* @throws {PaidPlanRequiredFigmaServiceError} A Figma paid plan is required to perform this operation.
329330
*/
330-
createFileContextWebhooks = async (
331+
createWebhookForFile = async (
331332
fileKey: string,
333+
eventType: FigmaFileWebhookEventType,
332334
passcode: string,
333335
user: ConnectUserInfo,
334-
): Promise<{
335-
fileWebhook: PostWebhookResponse;
336-
devModeStatusUpdateWebhook: PostWebhookResponse;
337-
}> =>
338-
this.withErrorTranslation(async () => {
339-
const { accessToken } = await figmaAuthService.getCredentials(user);
340-
341-
const fileUpdateRequest: PostWebhookRequestBody = {
342-
event_type: 'FILE_UPDATE',
343-
context: 'file',
344-
context_id: fileKey,
345-
endpoint: buildAppUrl('figma/webhook').toString(),
346-
passcode,
347-
description: 'Figma for Jira Cloud',
348-
};
349-
350-
const devModeStatusUpdateRequest: PostWebhookRequestBody = {
351-
...fileUpdateRequest,
352-
event_type: 'DEV_MODE_STATUS_UPDATE',
353-
};
354-
355-
try {
356-
const [fileWebhook, devModeStatusUpdateWebhook] = await Promise.all([
357-
figmaClient.createWebhook(fileUpdateRequest, accessToken),
358-
figmaClient.createWebhook(devModeStatusUpdateRequest, accessToken),
359-
]);
360-
361-
return {
362-
fileWebhook,
363-
devModeStatusUpdateWebhook,
364-
};
365-
} catch (e: unknown) {
366-
if (
367-
e instanceof BadRequestHttpClientError &&
368-
isOfSchema(e.response, ERROR_RESPONSE_SCHEMA)
369-
) {
370-
// Figma allows to create webhooks only on paid plans.
371-
// See https://www.figma.com/pricing/.
372-
if (e.response.message == 'Access Denied') {
373-
throw new PaidPlanRequiredFigmaServiceError(
374-
'A Figma paid plan is required to perform this operation.',
375-
e,
376-
);
377-
}
336+
): Promise<PostWebhookResponse> => {
337+
const { accessToken } = await figmaAuthService.getCredentials(user);
338+
const postWebhookRequest: PostWebhookRequestBody = {
339+
event_type: eventType,
340+
context: 'file',
341+
context_id: fileKey,
342+
endpoint: buildAppUrl('figma/webhook').toString(),
343+
passcode,
344+
description: 'Figma for Jira Cloud',
345+
};
346+
try {
347+
return await figmaClient.createWebhook(postWebhookRequest, accessToken);
348+
} catch (e: unknown) {
349+
if (
350+
e instanceof BadRequestHttpClientError &&
351+
isOfSchema(e.response, ERROR_RESPONSE_SCHEMA)
352+
) {
353+
// Figma allows to create webhooks only on paid plans.
354+
// See https://www.figma.com/pricing/.
355+
if (e.response.message == 'Access Denied') {
356+
throw new PaidPlanRequiredFigmaServiceError(
357+
'A Figma paid plan is required to perform this operation.',
358+
e,
359+
);
378360
}
379-
380-
throw e;
381361
}
382-
});
362+
363+
throw e;
364+
}
365+
};
383366

384367
/**
385368
* Tries to delete the given webhook.

src/infrastructure/repositories/figma-file-webhook-repository.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,32 @@ export class FigmaFileWebhookRepository {
3333
return this.mapToFigmaFileWebhook(dbModel);
3434
};
3535

36+
/**
37+
* @internal
38+
* Required for tests only.
39+
*/
40+
getAll = async (): Promise<FigmaFileWebhook[]> => {
41+
const dbModels = await prismaClient.get().figmaFileWebhook.findMany();
42+
43+
return dbModels.map((dbModel) => this.mapToFigmaFileWebhook(dbModel));
44+
};
45+
46+
findByFileKeyAndEventTypeAndConnectInstallationId = async (
47+
fileKey: string,
48+
eventType: FigmaFileWebhookEventType,
49+
connectInstallationId: string,
50+
): Promise<FigmaFileWebhook | null> => {
51+
const dbModel = await prismaClient.get().figmaFileWebhook.findFirst({
52+
where: {
53+
fileKey,
54+
eventType,
55+
connectInstallationId: BigInt(connectInstallationId),
56+
},
57+
});
58+
59+
return dbModel ? this.mapToFigmaFileWebhook(dbModel) : null;
60+
};
61+
3662
findManyByFileKeyAndConnectInstallationId = async (
3763
fileKey: string,
3864
connectInstallationId: string,

src/usecases/on-design-associated-with-issue-use-case.test.ts

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -83,20 +83,18 @@ describe('onDesignAssociatedWithIssueUseCase', () => {
8383
jest
8484
.spyOn(
8585
figmaFileWebhookRepository,
86-
'findManyByFileKeyAndConnectInstallationId',
86+
'findByFileKeyAndEventTypeAndConnectInstallationId',
8787
)
88-
.mockResolvedValue([]);
88+
.mockResolvedValue(null);
8989
const fileWebhook = generateWebhookV2(figmaDesignId.fileKey, 'FILE_UPDATE');
9090
const devModeStatusUpdateWebhook = generateWebhookV2(
9191
figmaDesignId.fileKey,
9292
'DEV_MODE_STATUS_UPDATE',
9393
);
94-
const createFileContextWebhooksMock = jest
95-
.spyOn(figmaService, 'createFileContextWebhooks')
96-
.mockResolvedValue({
97-
fileWebhook,
98-
devModeStatusUpdateWebhook,
99-
});
94+
const createWebhookForFileMock = jest
95+
.spyOn(figmaService, 'createWebhookForFile')
96+
.mockResolvedValueOnce(fileWebhook)
97+
.mockResolvedValueOnce(devModeStatusUpdateWebhook);
10098
jest
10199
.spyOn(figmaFileWebhookRepository, 'upsert')
102100
.mockResolvedValue({} as FigmaFileWebhook);
@@ -118,39 +116,64 @@ describe('onDesignAssociatedWithIssueUseCase', () => {
118116
connectInstallation: params.connectInstallation,
119117
});
120118
expect(
121-
figmaFileWebhookRepository.findManyByFileKeyAndConnectInstallationId,
119+
figmaFileWebhookRepository.findByFileKeyAndEventTypeAndConnectInstallationId,
122120
).toHaveBeenCalledWith(
123121
figmaDesignId.fileKey,
122+
'FILE_UPDATE',
124123
params.connectInstallation.id,
125124
);
126-
expect(figmaService.createFileContextWebhooks).toHaveBeenCalledWith(
125+
expect(
126+
figmaFileWebhookRepository.findByFileKeyAndEventTypeAndConnectInstallationId,
127+
).toHaveBeenCalledWith(
128+
figmaDesignId.fileKey,
129+
'DEV_MODE_STATUS_UPDATE',
130+
params.connectInstallation.id,
131+
);
132+
expect(figmaService.createWebhookForFile).toHaveBeenCalledWith(
133+
figmaDesignId.fileKey,
134+
'FILE_UPDATE',
135+
expect.any(String),
136+
{
137+
connectInstallationId: params.connectInstallation.id,
138+
atlassianUserId: params.atlassianUserId,
139+
},
140+
);
141+
expect(figmaService.createWebhookForFile).toHaveBeenCalledWith(
127142
figmaDesignId.fileKey,
143+
'DEV_MODE_STATUS_UPDATE',
128144
expect.any(String),
129145
{
130146
connectInstallationId: params.connectInstallation.id,
131147
atlassianUserId: params.atlassianUserId,
132148
},
133149
);
134150

135-
const passcode = createFileContextWebhooksMock.mock.calls[0][1];
151+
const passcode = createWebhookForFileMock.mock.calls[0][2];
136152
expect(figmaFileWebhookRepository.upsert).toHaveBeenNthCalledWith(
137153
1,
138154
expect.objectContaining({
139155
webhookId: fileWebhook.id,
140-
webhookPasscode: passcode,
141-
connectInstallationId: params.connectInstallation.id,
142-
creatorAtlassianUserId: params.atlassianUserId,
156+
fileKey: figmaDesignId.fileKey,
143157
eventType: 'FILE_UPDATE',
158+
webhookPasscode: passcode,
159+
createdBy: {
160+
connectInstallationId: params.connectInstallation.id,
161+
atlassianUserId: params.atlassianUserId,
162+
},
144163
}),
145164
);
165+
const secondPasscode = createWebhookForFileMock.mock.calls[1][2];
146166
expect(figmaFileWebhookRepository.upsert).toHaveBeenNthCalledWith(
147167
2,
148168
expect.objectContaining({
149169
webhookId: devModeStatusUpdateWebhook.id,
150-
webhookPasscode: passcode,
151-
connectInstallationId: params.connectInstallation.id,
152-
creatorAtlassianUserId: params.atlassianUserId,
170+
fileKey: figmaDesignId.fileKey,
153171
eventType: 'DEV_MODE_STATUS_UPDATE',
172+
webhookPasscode: secondPasscode,
173+
createdBy: {
174+
connectInstallationId: params.connectInstallation.id,
175+
atlassianUserId: params.atlassianUserId,
176+
},
154177
}),
155178
);
156179
});
@@ -188,22 +211,30 @@ describe('onDesignAssociatedWithIssueUseCase', () => {
188211
jest
189212
.spyOn(
190213
figmaFileWebhookRepository,
191-
'findManyByFileKeyAndConnectInstallationId',
214+
'findByFileKeyAndEventTypeAndConnectInstallationId',
192215
)
193-
.mockResolvedValue([fileWebhook, devModeStatusUpdateWebhook]);
194-
195-
jest.spyOn(figmaService, 'createFileContextWebhooks');
216+
.mockResolvedValueOnce(fileWebhook)
217+
.mockResolvedValueOnce(devModeStatusUpdateWebhook);
218+
jest.spyOn(figmaService, 'createWebhookForFile');
196219
jest.spyOn(figmaFileWebhookRepository, 'upsert');
197220

198221
await onDesignAssociatedWithIssueUseCaseParams.execute(params);
199222

200223
expect(
201-
figmaFileWebhookRepository.findManyByFileKeyAndConnectInstallationId,
224+
figmaFileWebhookRepository.findByFileKeyAndEventTypeAndConnectInstallationId,
202225
).toHaveBeenCalledWith(
203226
figmaDesignId.fileKey,
227+
'FILE_UPDATE',
228+
params.connectInstallation.id,
229+
);
230+
expect(
231+
figmaFileWebhookRepository.findByFileKeyAndEventTypeAndConnectInstallationId,
232+
).toHaveBeenCalledWith(
233+
figmaDesignId.fileKey,
234+
'DEV_MODE_STATUS_UPDATE',
204235
params.connectInstallation.id,
205236
);
206-
expect(figmaService.createFileContextWebhooks).toHaveBeenCalledTimes(0);
237+
expect(figmaService.createWebhookForFile).toHaveBeenCalledTimes(0);
207238
expect(figmaFileWebhookRepository.upsert).toHaveBeenCalledTimes(0);
208239
});
209240

0 commit comments

Comments
 (0)