@stamhoofd/backend 2.80.1 → 2.82.0

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.
Files changed (32) hide show
  1. package/index.ts +1 -4
  2. package/package.json +10 -10
  3. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +139 -0
  4. package/src/endpoints/global/email/PatchEmailEndpoint.ts +28 -5
  5. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +16 -35
  6. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +8 -8
  7. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +5 -1
  8. package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +68 -0
  9. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +5 -7
  10. package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +2 -2
  11. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +1 -1
  12. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +1 -1
  13. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +1 -1
  14. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +106 -0
  15. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +16 -3
  16. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +247 -0
  17. package/src/endpoints/{auth → organization/dashboard/users}/PatchApiUserEndpoint.ts +24 -5
  18. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +5 -0
  19. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +6 -1
  20. package/src/excel-loaders/event-notifications.ts +133 -0
  21. package/src/excel-loaders/index.ts +5 -0
  22. package/src/excel-loaders/members.ts +15 -38
  23. package/src/excel-loaders/organizations.ts +2 -2
  24. package/src/excel-loaders/payments.ts +2 -2
  25. package/src/helpers/AuthenticatedStructures.ts +8 -0
  26. package/src/helpers/CheckSettlements.ts +1 -1
  27. package/src/helpers/Context.ts +28 -12
  28. package/src/helpers/StripeHelper.ts +12 -1
  29. package/src/helpers/{xlsxAddressTransformerColumnFactory.ts → XlsxTransformerColumnHelper.ts} +68 -2
  30. package/tests/e2e/api-rate-limits.test.ts +188 -0
  31. package/tests/helpers/StripeMocker.ts +7 -1
  32. /package/src/endpoints/global/platform/{GetPlatformEnpoint.ts → GetPlatformEndpoint.ts} +0 -0
@@ -6,7 +6,7 @@ import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEn
6
6
  import { GetMembersEndpoint } from '../endpoints/global/members/GetMembersEndpoint';
7
7
  import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures';
8
8
  import { Context } from '../helpers/Context';
9
- import { XlsxTransformerColumnHelper } from '../helpers/xlsxAddressTransformerColumnFactory';
9
+ import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper';
10
10
 
11
11
  // Assign to a typed variable to assure we have correct type checking in place
12
12
  const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
@@ -16,7 +16,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
16
16
  {
17
17
  id: 'id',
18
18
  name: 'ID',
19
- width: 20,
19
+ width: 40,
20
20
  getValue: ({ patchedMember: object }: PlatformMember) => ({
21
21
  value: object.id,
22
22
  }),
@@ -89,7 +89,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
89
89
  {
90
90
  id: 'email',
91
91
  name: 'E-mailadres',
92
- width: 20,
92
+ width: 40,
93
93
  getValue: ({ patchedMember: object }: PlatformMember) => ({
94
94
  value: object.details.email,
95
95
  }),
@@ -247,42 +247,19 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
247
247
  },
248
248
 
249
249
  // Dynamic records
250
- {
251
- match(id) {
252
- if (id.startsWith('recordAnswers.')) {
253
- const platform = PlatformStruct.shared;
254
- const organization = Context.organization;
255
-
256
- const recordSettings = [
257
- ...(organization?.meta.recordsConfiguration.recordCategories.flatMap(category => category.getAllRecords()) ?? []),
258
- ...platform.config.recordsConfiguration.recordCategories.flatMap(category => category.getAllRecords()),
259
- ];
260
-
261
- const recordSettingId = id.split('.')[1];
262
- console.log('recordSettingId', recordSettingId);
263
- const recordSetting = recordSettings.find(r => r.id === recordSettingId);
264
-
265
- if (!recordSetting) {
266
- // Will throw a proper error itself
267
- console.log('recordSetting not found', recordSettings);
268
- return;
269
- }
270
-
271
- const columns = recordSetting.excelColumns;
272
-
273
- return columns.map((columnName, index) => {
274
- return {
275
- id: `recordAnswers.${recordSettingId}.${index}`,
276
- name: columnName,
277
- width: 20,
278
- getValue: ({ patchedMember: object }: PlatformMember) => ({
279
- value: object.details.recordAnswers.get(recordSettingId)?.excelValues[index]?.value ?? '',
280
- }),
281
- };
282
- });
283
- }
250
+ XlsxTransformerColumnHelper.createRecordAnswersColumns({
251
+ matchId: 'recordAnswers',
252
+ getRecordAnswers: ({ patchedMember: object }: PlatformMember) => object.details.recordAnswers,
253
+ getRecordCategories: () => {
254
+ const platform = PlatformStruct.shared;
255
+ const organization = Context.organization;
256
+
257
+ return [
258
+ ...(organization?.meta.recordsConfiguration.recordCategories ?? []),
259
+ ...platform.config.recordsConfiguration.recordCategories,
260
+ ];
284
261
  },
285
- },
262
+ }),
286
263
 
287
264
  // Registration records
288
265
  {
@@ -2,7 +2,7 @@ import { XlsxTransformerSheet } from '@stamhoofd/excel-writer';
2
2
  import { Platform as PlatformStruct, ExcelExportType, LimitedFilteredRequest, Organization as OrganizationStruct, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, PaginatedResponse, MemberWithRegistrationsBlob, Premise } from '@stamhoofd/structures';
3
3
  import { GetOrganizationsEndpoint } from '../endpoints/admin/organizations/GetOrganizationsEndpoint';
4
4
  import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint';
5
- import { XlsxTransformerColumnHelper } from '../helpers/xlsxAddressTransformerColumnFactory';
5
+ import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper';
6
6
  import { Group, Member, MemberResponsibilityRecord } from '@stamhoofd/models';
7
7
  import { Formatter, Sorter } from '@stamhoofd/utility';
8
8
  import { ArrayDecoder, field } from '@simonbackx/simple-encoding';
@@ -33,7 +33,7 @@ const sheet: XlsxTransformerSheet<Object, Object> = {
33
33
  {
34
34
  id: 'id',
35
35
  name: 'ID',
36
- width: 35,
36
+ width: 40,
37
37
  getValue: (object: Object) => ({
38
38
  value: object.id,
39
39
  }),
@@ -5,7 +5,7 @@ import { BalanceItemPaymentDetailed, BalanceItemRelationType, ExcelExportType, g
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint';
7
7
  import { GetPaymentsEndpoint } from '../endpoints/organization/dashboard/payments/GetPaymentsEndpoint';
8
- import { XlsxTransformerColumnHelper } from '../helpers/xlsxAddressTransformerColumnFactory';
8
+ import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper';
9
9
 
10
10
  type PaymentWithItem = {
11
11
  payment: PaymentGeneral;
@@ -475,7 +475,7 @@ function getPayingOrganizationColumns(): XlsxTransformerColumn<PaymentGeneral>[]
475
475
  {
476
476
  id: 'payingOrganization.id',
477
477
  name: 'ID betalende groep',
478
- width: 30,
478
+ width: 40,
479
479
  getValue: (object: PaymentGeneralWithStripeAccount) => {
480
480
  return {
481
481
  value: object.payingOrganization?.id || '',
@@ -423,6 +423,14 @@ export class AuthenticatedStructures {
423
423
  }
424
424
  }
425
425
 
426
+ if (includeContextOrganization && STAMHOOFD.singleOrganization && !Context.auth.organization) {
427
+ const found = organizations.get(STAMHOOFD.singleOrganization);
428
+ if (!found) {
429
+ const organization = await Context.auth.getOrganization(STAMHOOFD.singleOrganization);
430
+ organizations.set(organization.id, organization);
431
+ }
432
+ }
433
+
426
434
  const memberBlobs: MemberWithRegistrationsBlob[] = [];
427
435
  for (const member of members) {
428
436
  for (const registration of member.registrations) {
@@ -23,7 +23,7 @@ type MolliePaymentJSON = {
23
23
  let lastSettlementCheck: Date | null = null;
24
24
 
25
25
  export async function checkAllStripePayouts(checkAll = false) {
26
- if (STAMHOOFD.environment !== 'production') {
26
+ if (STAMHOOFD.environment !== 'production' || !STAMHOOFD.STRIPE_SECRET_KEY) {
27
27
  console.log('Skip settlement check');
28
28
  return;
29
29
  }
@@ -4,29 +4,42 @@ import { I18n } from '@stamhoofd/backend-i18n';
4
4
  import { Organization, Platform, RateLimiter, Token, User } from '@stamhoofd/models';
5
5
  import { AsyncLocalStorage } from 'async_hooks';
6
6
 
7
+ import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
8
+ import { ApiUserRateLimits } from '@stamhoofd/structures';
7
9
  import { AdminPermissionChecker } from './AdminPermissionChecker';
8
- import { AutoEncoder, field, Decoder, StringDecoder } from '@simonbackx/simple-encoding';
9
10
 
10
11
  export const apiUserRateLimiter = new RateLimiter({
11
12
  limits: [
12
13
  {
13
- // Block heavy bursts (5req/s for 5s)
14
- limit: 25,
14
+ limit: {
15
+ '': 25, // (5req/s for 5s)
16
+ [ApiUserRateLimits.Medium]: 10 * 5, // (10req/s for 5s)
17
+ [ApiUserRateLimits.High]: 25 * 5, // (100req/s for 5s)
18
+ },
15
19
  duration: 5 * 1000,
16
20
  },
17
21
  {
18
- // max 1req/s during 150s
19
- limit: 150,
20
- duration: 150 * 1000,
22
+ limit: {
23
+ '': 120, // max 1req/s during 150s
24
+ [ApiUserRateLimits.Medium]: 240, // (2req/s for 150s)
25
+ [ApiUserRateLimits.High]: 480, // (4req/s for 150s)
26
+ },
27
+ duration: 120 * 1000,
21
28
  },
22
29
  {
23
- // 1000 requests per hour
24
- limit: 1000,
30
+ limit: {
31
+ '': 1000, // ± 0.27 request/s sustained for an hour = 3.6s between each request
32
+ [ApiUserRateLimits.Medium]: 2000, // ± 0.56 request/s sustained for an hour
33
+ [ApiUserRateLimits.High]: 4000, // ± 1.11 request/s sustained for an hour
34
+ },
25
35
  duration: 60 * 1000 * 60,
26
36
  },
27
37
  {
28
- // 2000 requests per day
29
- limit: 2000,
38
+ limit: {
39
+ '': 2_000, // max 2000 requests per day
40
+ [ApiUserRateLimits.Medium]: 14_400, // max 4000 requests per day
41
+ [ApiUserRateLimits.High]: 18_000, // max 10 requests per minute, sustained for a full day
42
+ },
30
43
  duration: 24 * 60 * 1000 * 60,
31
44
  },
32
45
  ],
@@ -169,7 +182,10 @@ export class ContextInstance {
169
182
  return await this.authenticate({ allowWithoutAccount });
170
183
  }
171
184
  catch (e) {
172
- return {};
185
+ if (e.code === 'not_authenticated') {
186
+ return {};
187
+ }
188
+ throw e;
173
189
  }
174
190
  }
175
191
 
@@ -235,7 +251,7 @@ export class ContextInstance {
235
251
 
236
252
  // Rate limits for api users
237
253
  if (token.user.isApiUser) {
238
- apiUserRateLimiter.track(this.organization?.id ?? token.user.id);
254
+ apiUserRateLimiter.track(this.organization?.id ?? token.user.id, 1, token.user.meta?.rateLimits ?? undefined);
239
255
  }
240
256
 
241
257
  const user = token.user;
@@ -6,7 +6,18 @@ import { Formatter } from '@stamhoofd/utility';
6
6
  import Stripe from 'stripe';
7
7
 
8
8
  export class StripeHelper {
9
+ static get notConfiguredError() {
10
+ return new SimpleError({
11
+ code: 'not_configured',
12
+ message: 'Stripe is not yet configured for this platform',
13
+ human: $t('Stripe is nog niet geconfigureerd voor dit platform'),
14
+ });
15
+ }
16
+
9
17
  static getInstance(accountId: string | null = null) {
18
+ if (!STAMHOOFD.STRIPE_SECRET_KEY) {
19
+ throw this.notConfiguredError;
20
+ }
10
21
  return new Stripe(STAMHOOFD.STRIPE_SECRET_KEY, { apiVersion: '2024-06-20', typescript: true, maxNetworkRetries: 0, timeout: 10000, stripeAccount: accountId ?? undefined });
11
22
  }
12
23
 
@@ -81,7 +92,7 @@ export class StripeHelper {
81
92
  }
82
93
 
83
94
  static async getStatus(payment: Payment, cancel = false, testMode = false): Promise<PaymentStatus> {
84
- if (testMode && !STAMHOOFD.STRIPE_SECRET_KEY.startsWith('sk_test_')) {
95
+ if (testMode && !STAMHOOFD.STRIPE_SECRET_KEY?.startsWith('sk_test_')) {
85
96
  // Do not query anything
86
97
  return payment.status;
87
98
  }
@@ -1,5 +1,5 @@
1
1
  import { XlsxTransformerColumn } from '@stamhoofd/excel-writer';
2
- import { Address, CountryHelper, Parent, ParentTypeHelper, PlatformMember } from '@stamhoofd/structures';
2
+ import { Address, CountryHelper, Parent, ParentTypeHelper, PlatformMember, RecordAnswer, RecordCategory, RecordSettings, RecordType } from '@stamhoofd/structures';
3
3
 
4
4
  export class XlsxTransformerColumnHelper {
5
5
  static formatBoolean(value: boolean | undefined | null): string {
@@ -115,7 +115,8 @@ export class XlsxTransformerColumnHelper {
115
115
  {
116
116
  id: getId('street'),
117
117
  name: `Straat`,
118
- width: 30,
118
+ defaultCategory: 'Adres', // Ignore this name
119
+ width: 40,
119
120
  getValue: (object: T) => {
120
121
  const address = getAddress(object);
121
122
  return {
@@ -126,6 +127,7 @@ export class XlsxTransformerColumnHelper {
126
127
  {
127
128
  id: getId('number'),
128
129
  name: 'Nummer',
130
+ defaultCategory: 'Adres', // Ignore this name
129
131
  width: 20,
130
132
  getValue: (object: T) => {
131
133
  const address = getAddress(object);
@@ -137,6 +139,7 @@ export class XlsxTransformerColumnHelper {
137
139
  {
138
140
  id: getId('postalCode'),
139
141
  name: 'Postcode',
142
+ defaultCategory: 'Adres', // Ignore this name
140
143
  width: 20,
141
144
  getValue: (object: T) => {
142
145
  const address = getAddress(object);
@@ -148,6 +151,7 @@ export class XlsxTransformerColumnHelper {
148
151
  {
149
152
  id: getId('city'),
150
153
  name: 'Stad',
154
+ defaultCategory: 'Adres', // Ignore this name
151
155
  width: 20,
152
156
  getValue: (object: T) => {
153
157
  const address = getAddress(object);
@@ -159,6 +163,7 @@ export class XlsxTransformerColumnHelper {
159
163
  {
160
164
  id: getId('country'),
161
165
  name: 'Land',
166
+ defaultCategory: 'Adres', // Ignore this name
162
167
  width: 20,
163
168
  getValue: (object: T) => {
164
169
  const address = getAddress(object);
@@ -173,4 +178,65 @@ export class XlsxTransformerColumnHelper {
173
178
  },
174
179
  };
175
180
  }
181
+
182
+ static createRecordAnswersColumns<T>({ matchId, getRecordCategories, getRecordAnswers }: { matchId: string; getRecordCategories: () => RecordCategory[]; getRecordAnswers: (object: T) => Map<string, RecordAnswer> }): XlsxTransformerColumn<T> {
183
+ return {
184
+ match(id) {
185
+ if (id.startsWith(matchId + '.')) {
186
+ const recordCategories = getRecordCategories();
187
+ const flattenedCategories = RecordCategory.flattenCategoriesWith(recordCategories, r => r.excelColumns.length > 0);
188
+
189
+ let recordCategory: RecordCategory | undefined;
190
+ let recordSetting: RecordSettings | undefined;
191
+ const recordSettingId = id.split('.')[1];
192
+
193
+ for (const category of flattenedCategories) {
194
+ const recordSettings = category.getAllRecords();
195
+ const rr = recordSettings.find(r => r.id === recordSettingId);
196
+
197
+ if (rr) {
198
+ recordSetting = rr;
199
+ recordCategory = category;
200
+ break;
201
+ }
202
+ }
203
+
204
+ if (!recordSetting || !recordCategory) {
205
+ // Will throw a proper error itself
206
+ console.log('recordSetting not found');
207
+ return;
208
+ }
209
+
210
+ const columns = recordSetting.excelColumns;
211
+
212
+ return columns.map(({ name, width, defaultCategory }, index) => {
213
+ return {
214
+ id: `${matchId}.${recordSettingId}.${index}`,
215
+ name,
216
+ width: width ?? 20,
217
+ defaultCategory,
218
+ category: recordCategory.name,
219
+ getValue: (object: T) => {
220
+ const answers = getRecordAnswers(object);
221
+ const b = (answers.get(recordSettingId)?.excelValues[index] ?? {
222
+ value: '',
223
+ });
224
+
225
+ return {
226
+ ...b,
227
+ style: {
228
+ ...b.style,
229
+ alignment: {
230
+ ...b.style?.alignment,
231
+ wrapText: recordSetting.type === RecordType.Textarea,
232
+ },
233
+ },
234
+ };
235
+ },
236
+ };
237
+ });
238
+ }
239
+ },
240
+ };
241
+ }
176
242
  }
@@ -0,0 +1,188 @@
1
+ /* eslint-disable jest/no-conditional-expect */
2
+ import { Request } from '@simonbackx/simple-endpoints';
3
+ import { Organization, OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
4
+
5
+ import { PatchMap } from '@simonbackx/simple-encoding';
6
+ import { ApiUser, ApiUserRateLimits, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions, UserMeta, UserPermissions } from '@stamhoofd/structures';
7
+ import { SHExpect, TestUtils } from '@stamhoofd/test-utils';
8
+ import { CreateApiUserEndpoint } from '../../src/endpoints/organization/dashboard/users/CreateApiUserEndpoint';
9
+ import { testServer } from '../helpers/TestServer';
10
+ import { GetUserEndpoint } from '../../src/endpoints/auth/GetUserEndpoint';
11
+
12
+ describe('E2E.APIRateLimits', () => {
13
+ // Test endpoint
14
+ const createEndpoint = new CreateApiUserEndpoint();
15
+ const getUserEndpoint = new GetUserEndpoint();
16
+ let organization: Organization;
17
+
18
+ beforeEach(async () => {
19
+ TestUtils.setEnvironment('userMode', 'platform');
20
+ organization = await new OrganizationFactory({}).create();
21
+ });
22
+
23
+ /**
24
+ * Note: we don't use a factory because this is an E2E test and
25
+ * we also want to check if the created tokens with the API are actually marked as API-keys and not normal users.
26
+ */
27
+ async function createAPIToken(rateLimits: ApiUserRateLimits | null) {
28
+ const user = await new UserFactory({
29
+ globalPermissions: Permissions.create({
30
+ level: PermissionLevel.Full,
31
+ }),
32
+ }).create();
33
+ const token = await Token.createToken(user);
34
+
35
+ const createRequest = Request.buildJson('POST', '/api-keys', organization.getApiHost(), ApiUser.create({
36
+ permissions: UserPermissions.create({
37
+ organizationPermissions: new Map([
38
+ [organization.id, Permissions.create({ level: PermissionLevel.Read })],
39
+ ]),
40
+ }),
41
+ meta: UserMeta.create({
42
+ rateLimits,
43
+ }),
44
+ }));
45
+ createRequest.headers.authorization = 'Bearer ' + token.accessToken;
46
+ const createResponse = await testServer.test(createEndpoint, createRequest);
47
+
48
+ expect(createResponse.body.meta?.rateLimits ?? undefined).toEqual(rateLimits ?? undefined);
49
+
50
+ return createResponse.body.token;
51
+ }
52
+
53
+ test('By default throws after 25 requests in less than 5s', async () => {
54
+ const token = await createAPIToken(null);
55
+
56
+ // Start firing
57
+ for (let i = 0; i < 30; i++) {
58
+ const request = Request.buildJson('GET', '/user', organization.getApiHost());
59
+ request.headers.authorization = 'Bearer ' + token;
60
+ const promise = testServer.test(getUserEndpoint, request);
61
+
62
+ if (i < 25) {
63
+ try {
64
+ await expect(promise).toResolve();
65
+ }
66
+ catch (e) {
67
+ let error: any = null;
68
+ try {
69
+ await promise;
70
+ }
71
+ catch (e) {
72
+ error = e;
73
+ }
74
+ throw new Error('The endpoint rejected at call ' + i + ' with error message ' + error?.message);
75
+ }
76
+ }
77
+ else {
78
+ await expect(promise).rejects.toThrow(
79
+ SHExpect.simpleError({
80
+ code: 'rate_limit',
81
+ }),
82
+ );
83
+ }
84
+ }
85
+ });
86
+
87
+ test('Normal rate limit throws after 25 requests in less than 5s', async () => {
88
+ const token = await createAPIToken(ApiUserRateLimits.Normal);
89
+
90
+ // Start firing
91
+ for (let i = 0; i < 30; i++) {
92
+ const request = Request.buildJson('GET', '/user', organization.getApiHost());
93
+ request.headers.authorization = 'Bearer ' + token;
94
+ const promise = testServer.test(getUserEndpoint, request);
95
+
96
+ if (i < 25) {
97
+ try {
98
+ await expect(promise).toResolve();
99
+ }
100
+ catch (e) {
101
+ let error: any = null;
102
+ try {
103
+ await promise;
104
+ }
105
+ catch (e) {
106
+ error = e;
107
+ }
108
+ throw new Error('The endpoint rejected at call ' + i + ' with error message ' + error?.message);
109
+ }
110
+ }
111
+ else {
112
+ await expect(promise).rejects.toThrow(
113
+ SHExpect.simpleError({
114
+ code: 'rate_limit',
115
+ }),
116
+ );
117
+ }
118
+ }
119
+ });
120
+
121
+ test('Medium rate limits throw after 50 requests in less than 5s', async () => {
122
+ const token = await createAPIToken(ApiUserRateLimits.Medium);
123
+
124
+ // Start firing
125
+ for (let i = 0; i < 60; i++) {
126
+ const request = Request.buildJson('GET', '/user', organization.getApiHost());
127
+ request.headers.authorization = 'Bearer ' + token;
128
+ const promise = testServer.test(getUserEndpoint, request);
129
+
130
+ if (i < 50) {
131
+ try {
132
+ await expect(promise).toResolve();
133
+ }
134
+ catch (e) {
135
+ let error: any = null;
136
+ try {
137
+ await promise;
138
+ }
139
+ catch (e) {
140
+ error = e;
141
+ }
142
+ throw new Error('The endpoint rejected at call ' + i + ' with error message ' + error?.message);
143
+ }
144
+ }
145
+ else {
146
+ await expect(promise).rejects.toThrow(
147
+ SHExpect.simpleError({
148
+ code: 'rate_limit',
149
+ }),
150
+ );
151
+ }
152
+ }
153
+ });
154
+
155
+ test('High rate limits throw after 125 requests in less than 5s', async () => {
156
+ const token = await createAPIToken(ApiUserRateLimits.High);
157
+
158
+ // Start firing
159
+ for (let i = 0; i < 140; i++) {
160
+ const request = Request.buildJson('GET', '/user', organization.getApiHost());
161
+ request.headers.authorization = 'Bearer ' + token;
162
+ const promise = testServer.test(getUserEndpoint, request);
163
+
164
+ if (i < 125) {
165
+ try {
166
+ await expect(promise).toResolve();
167
+ }
168
+ catch (e) {
169
+ let error: any = null;
170
+ try {
171
+ await promise;
172
+ }
173
+ catch (e) {
174
+ error = e;
175
+ }
176
+ throw new Error('The endpoint rejected at call ' + i + ' with error message ' + error?.message);
177
+ }
178
+ }
179
+ else {
180
+ await expect(promise).rejects.toThrow(
181
+ SHExpect.simpleError({
182
+ code: 'rate_limit',
183
+ }),
184
+ );
185
+ }
186
+ }
187
+ });
188
+ });
@@ -165,9 +165,15 @@ export class StripeMocker {
165
165
  const endpoint = new StripeWebookEndpoint();
166
166
 
167
167
  const r = Request.buildJson('POST', `/stripe/webhooks`, undefined, payload);
168
+ const secret = payload.account ? STAMHOOFD.STRIPE_CONNECT_ENDPOINT_SECRET : STAMHOOFD.STRIPE_ENDPOINT_SECRET;
169
+
170
+ if (!secret) {
171
+ throw new Error('No stripe secret set in env');
172
+ }
173
+
168
174
  r.headers['stripe-signature'] = stripe.webhooks.generateTestHeaderString({
169
175
  payload: await r.body,
170
- secret: payload.account ? STAMHOOFD.STRIPE_CONNECT_ENDPOINT_SECRET : STAMHOOFD.STRIPE_ENDPOINT_SECRET,
176
+ secret: secret,
171
177
  });
172
178
  await testServer.test(endpoint, r);
173
179
  }