@shipfox/api-integration-jira 12.1.0 → 12.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-integration-jira",
3
3
  "license": "MIT",
4
- "version": "12.1.0",
4
+ "version": "12.1.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -2,7 +2,18 @@ import {HTTPError} from 'ky';
2
2
  import type {JiraIntegrationProviderError} from '#core/errors.js';
3
3
  import {mapJiraError} from './client.js';
4
4
 
5
- const mocks = vi.hoisted(() => ({delete: vi.fn(), post: vi.fn(), request: vi.fn()}));
5
+ const mocks = vi.hoisted(() => ({
6
+ delete: vi.fn(),
7
+ post: vi.fn(),
8
+ request: vi.fn(),
9
+ warn: vi.fn(),
10
+ }));
11
+
12
+ vi.mock('@shipfox/node-opentelemetry', () => ({
13
+ logger: () => ({
14
+ warn: mocks.warn,
15
+ }),
16
+ }));
6
17
 
7
18
  vi.mock('ky', () => {
8
19
  class MockHTTPError extends Error {
@@ -77,6 +88,7 @@ describe('Jira dynamic webhook API', () => {
77
88
  mocks.delete.mockReset();
78
89
  mocks.post.mockReset();
79
90
  mocks.request.mockReset();
91
+ mocks.warn.mockReset();
80
92
  });
81
93
 
82
94
  it('registers the six curated events with the access token', async () => {
@@ -133,6 +145,165 @@ describe('Jira dynamic webhook API', () => {
133
145
  ).rejects.toMatchObject({reason: 'malformed-provider-response'});
134
146
  });
135
147
 
148
+ it('logs provider validation errors before rejecting registration', async () => {
149
+ mocks.post.mockReturnValue(
150
+ resolves({
151
+ webhookRegistrationResult: [
152
+ {createdWebhookId: 123, errors: ['Webhook URL is not approved', 'Invalid JQL filter']},
153
+ ],
154
+ }),
155
+ );
156
+ const {createJiraApiClient} = await import('./client.js');
157
+
158
+ await expect(
159
+ createJiraApiClient().registerDynamicWebhook({
160
+ accessToken: 'access-token',
161
+ cloudId: 'cloud-1',
162
+ url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
163
+ }),
164
+ ).rejects.toMatchObject({reason: 'malformed-provider-response'});
165
+
166
+ expect(mocks.warn).toHaveBeenCalledTimes(1);
167
+ expect(mocks.warn).toHaveBeenCalledWith(
168
+ {
169
+ operation: 'register-dynamic-webhook',
170
+ providerErrors: ['Webhook URL is not approved', 'Invalid JQL filter'],
171
+ providerErrorCount: 2,
172
+ },
173
+ 'Jira dynamic webhook registration rejected',
174
+ );
175
+ });
176
+
177
+ it('limits logged provider errors to five values', async () => {
178
+ mocks.post.mockReturnValue(
179
+ resolves({
180
+ webhookRegistrationResult: [
181
+ {
182
+ errors: ['one', 'two', 'three', 'four', 'five', 'six'],
183
+ },
184
+ ],
185
+ }),
186
+ );
187
+ const {createJiraApiClient} = await import('./client.js');
188
+
189
+ await expect(
190
+ createJiraApiClient().registerDynamicWebhook({
191
+ accessToken: 'access-token',
192
+ cloudId: 'cloud-1',
193
+ url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
194
+ }),
195
+ ).rejects.toMatchObject({reason: 'malformed-provider-response'});
196
+
197
+ expect(mocks.warn).toHaveBeenCalledWith(
198
+ {
199
+ operation: 'register-dynamic-webhook',
200
+ providerErrors: ['one', 'two', 'three', 'four', 'five'],
201
+ providerErrorCount: 6,
202
+ },
203
+ 'Jira dynamic webhook registration rejected',
204
+ );
205
+ });
206
+
207
+ it('limits each logged provider error to 500 characters', async () => {
208
+ const longError = 'x'.repeat(501);
209
+ mocks.post.mockReturnValue(resolves({webhookRegistrationResult: [{errors: [longError]}]}));
210
+ const {createJiraApiClient} = await import('./client.js');
211
+
212
+ await expect(
213
+ createJiraApiClient().registerDynamicWebhook({
214
+ accessToken: 'access-token',
215
+ cloudId: 'cloud-1',
216
+ url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
217
+ }),
218
+ ).rejects.toMatchObject({reason: 'malformed-provider-response'});
219
+
220
+ expect(mocks.warn).toHaveBeenCalledWith(
221
+ {
222
+ operation: 'register-dynamic-webhook',
223
+ providerErrors: ['x'.repeat(500)],
224
+ providerErrorCount: 1,
225
+ },
226
+ 'Jira dynamic webhook registration rejected',
227
+ );
228
+ });
229
+
230
+ it('omits non-string provider errors from the warning', async () => {
231
+ mocks.post.mockReturnValue(
232
+ resolves({
233
+ webhookRegistrationResult: [
234
+ {errors: ['valid message', 42, null, {message: 'raw response'}, true]},
235
+ ],
236
+ }),
237
+ );
238
+ const {createJiraApiClient} = await import('./client.js');
239
+
240
+ await expect(
241
+ createJiraApiClient().registerDynamicWebhook({
242
+ accessToken: 'access-token',
243
+ cloudId: 'cloud-1',
244
+ url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
245
+ }),
246
+ ).rejects.toMatchObject({reason: 'malformed-provider-response'});
247
+
248
+ expect(mocks.warn).toHaveBeenCalledWith(
249
+ {
250
+ operation: 'register-dynamic-webhook',
251
+ providerErrors: ['valid message'],
252
+ providerErrorCount: 5,
253
+ },
254
+ 'Jira dynamic webhook registration rejected',
255
+ );
256
+ });
257
+
258
+ it('accepts an empty provider errors array without logging a rejection', async () => {
259
+ mocks.post.mockReturnValue(
260
+ resolves({webhookRegistrationResult: [{createdWebhookId: 123, errors: []}]}),
261
+ );
262
+ const {createJiraApiClient} = await import('./client.js');
263
+
264
+ await expect(
265
+ createJiraApiClient().registerDynamicWebhook({
266
+ accessToken: 'access-token',
267
+ cloudId: 'cloud-1',
268
+ url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
269
+ }),
270
+ ).resolves.toEqual({webhookId: 123});
271
+ expect(mocks.warn).not.toHaveBeenCalled();
272
+ });
273
+
274
+ it('rejects a non-array provider errors value without logging it', async () => {
275
+ mocks.post.mockReturnValue(
276
+ resolves({
277
+ webhookRegistrationResult: [{createdWebhookId: 123, errors: {message: 'do not log me'}}],
278
+ }),
279
+ );
280
+ const {createJiraApiClient} = await import('./client.js');
281
+
282
+ await expect(
283
+ createJiraApiClient().registerDynamicWebhook({
284
+ accessToken: 'access-token',
285
+ cloudId: 'cloud-1',
286
+ url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
287
+ }),
288
+ ).rejects.toMatchObject({reason: 'malformed-provider-response'});
289
+ expect(mocks.warn).not.toHaveBeenCalled();
290
+ });
291
+
292
+ it('keeps the malformed provider response reason for a Jira rejection', async () => {
293
+ mocks.post.mockReturnValue(
294
+ resolves({webhookRegistrationResult: [{errors: ['Provider rejected the webhook']}]}),
295
+ );
296
+ const {createJiraApiClient} = await import('./client.js');
297
+
298
+ await expect(
299
+ createJiraApiClient().registerDynamicWebhook({
300
+ accessToken: 'access-token',
301
+ cloudId: 'cloud-1',
302
+ url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
303
+ }),
304
+ ).rejects.toMatchObject({reason: 'malformed-provider-response'});
305
+ });
306
+
136
307
  it('deletes a dynamic webhook by id', async () => {
137
308
  mocks.delete.mockResolvedValue(undefined);
138
309
  const {createJiraApiClient} = await import('./client.js');
package/src/api/client.ts CHANGED
@@ -282,7 +282,21 @@ function parseDynamicWebhookRegistration(
282
282
  createdWebhookId?: unknown;
283
283
  errors?: unknown;
284
284
  };
285
- if (errors !== undefined && (!Array.isArray(errors) || errors.length > 0)) {
285
+ if (errors !== undefined && !Array.isArray(errors)) {
286
+ throw malformed('Jira webhook registration returned errors');
287
+ }
288
+ if (Array.isArray(errors) && errors.length > 0) {
289
+ logger().warn(
290
+ {
291
+ operation: 'register-dynamic-webhook',
292
+ providerErrors: errors
293
+ .filter((error): error is string => typeof error === 'string')
294
+ .slice(0, 5)
295
+ .map((error) => error.slice(0, 500)),
296
+ providerErrorCount: errors.length,
297
+ },
298
+ 'Jira dynamic webhook registration rejected',
299
+ );
286
300
  throw malformed('Jira webhook registration returned errors');
287
301
  }
288
302
  if (