@getjones/test-infra 5.2.12 → 5.2.13

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 (28) hide show
  1. package/package.json +2 -2
  2. package/src/common/commonFlows/records/records.js +43 -0
  3. package/src/common/consts/projectSettings.js +7 -0
  4. package/src/common/errorCodes.js +2 -1
  5. package/src/index.js +4 -0
  6. package/src/saas/internal-api/accounts/api/core/accounts.core.api.js +8 -2
  7. package/src/saas/internal-api/accounts/api/core/accounts.core.service.js +10 -0
  8. package/src/saas/internal-api/auth/api/auth.service.js +13 -2
  9. package/src/saas/internal-api/certificial/api/certificial.api.js +15 -0
  10. package/src/saas/internal-api/certificial/api/certificial.service.js +24 -0
  11. package/src/saas/internal-api/certificial/consts.js +8 -0
  12. package/src/saas/internal-api/certificial/index.js +4 -0
  13. package/src/saas/internal-api/compliance/api/overrideGap/override.gap.api.js +25 -2
  14. package/src/saas/internal-api/compliance/api/overrideGap/override.gap.service.js +93 -2
  15. package/src/saas/internal-api/compliance/consts/overrideGapRequest/override.gap.request.js +8 -1
  16. package/src/saas/internal-api/compliance/models/overrideGapRequest/override.gap.request.model.js +16 -1
  17. package/src/saas/internal-api/dbEmails/api/db.emails.service.js +31 -0
  18. package/src/saas/internal-api/dbEmails/consts/consts.js +3 -0
  19. package/src/saas/internal-api/notificationCenter/consts.js +4 -1
  20. package/src/saas/internal-api/outlook/api/outlook.api.js +2 -2
  21. package/src/saas/internal-api/outlook/api/outlook.service.js +2 -2
  22. package/src/saas/internal-api/projects/models/settings/update.project.settings.model.js +17 -1
  23. package/src/saas/internal-api/schedulerJobRun/api/scheduler.job.run.api.js +31 -0
  24. package/src/saas/internal-api/schedulerJobRun/api/scheduler.job.run.service.js +56 -0
  25. package/src/saas/internal-api/schedulerJobRun/index.js +3 -0
  26. package/src/saas/internal-api/thread-scheduler/runner.js +17 -0
  27. package/src/saas/internal-api/thread-scheduler/schedulerJobs.js +2 -0
  28. package/src/saas/internal-api/uploadFiles/api/upload.file.service.js +19 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getjones/test-infra",
3
- "version": "5.2.12",
3
+ "version": "5.2.13",
4
4
  "description": "Providing internal and openapi interface",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -19,7 +19,7 @@
19
19
  "license": "ISC",
20
20
  "dependencies": {
21
21
  "axios": "1.8.1",
22
- "chai": "4.3.4",
22
+ "chai": "6.3.0",
23
23
  "dotenv": "16.5.0",
24
24
  "enum": "3.0.4",
25
25
  "form-data": "4.0.6",
@@ -23,6 +23,7 @@ const { TabExtractionRequestModel } = require('../../../saas/internal-api/requir
23
23
  const { DEFAULT_LEASE_PATH } = require('../../../saas/internal-api/requirements/consts/consts');
24
24
  const { RELATED_DOC_TYPES } = require('../../../saas/internal-api/uploadFiles/consts');
25
25
  const { WAIVER_REASONS, EXPIRY_TYPES } = compliance.models.waive;
26
+ const { DEFAULT_PM_OVERRIDE_REASON } = require('../../../saas/internal-api/compliance/consts/overrideGapRequest/override.gap.request');
26
27
 
27
28
  const makeRecordNotResponsive = async function(recordId, context) {
28
29
  const { data: record } = await contractors.services.getContractorById(recordId, context);
@@ -328,6 +329,45 @@ const rejectOverrideGapRequest = async (overrideGapRequestId, rejectReason, reco
328
329
  return compliance.services.overrideGap.rejectOverrideGapRequest(overrideGapRequestId, rejectReason, record.dateModified, context);
329
330
  };
330
331
 
332
+ const createPmOverrideRequest = async (recordId, policyName, attributeName, assigneeUserIds, context, {
333
+ requestReason = DEFAULT_PM_OVERRIDE_REASON,
334
+ expiryType = EXPIRY_TYPES.NEVER,
335
+ expiryDate = null,
336
+ expectError
337
+ } = {}) => {
338
+ const { data: record } = await contractors.services.getContractorById(recordId, context);
339
+ const auditorContractorId = compliance.services.overrideGap.getAuditorContractorIdFromGap(record, policyName, attributeName);
340
+ return compliance.services.overrideGap.createPmOverrideRequest(auditorContractorId, context, {
341
+ requestReason,
342
+ expiryType,
343
+ expiryDate,
344
+ assigneeUserIds,
345
+ recordDateModified: record.dateModified,
346
+ expectError
347
+ });
348
+ };
349
+
350
+ const editPmOverrideRequest = async (overrideGapRequestId, recordId, context, {
351
+ requestReason,
352
+ expiryType,
353
+ expiryDate = null,
354
+ assigneeUserIds,
355
+ expectError
356
+ } = {}) => {
357
+ const { data: record } = await contractors.services.getContractorById(recordId, context);
358
+ return compliance.services.overrideGap.editPmOverrideRequest(overrideGapRequestId, context, {
359
+ requestReason,
360
+ expiryType,
361
+ expiryDate,
362
+ assigneeUserIds,
363
+ recordDateModified: record.dateModified,
364
+ expectError
365
+ });
366
+ };
367
+
368
+ const deletePmOverrideRequest = (overrideGapRequestId, context, { expectError } = {}) =>
369
+ compliance.services.overrideGap.deletePmOverrideRequest(overrideGapRequestId, context, { expectError });
370
+
331
371
  const uploadMultipleCoiFiles = async (recordId, coiFilePaths, uploadedBy, uploaderEmail, context, { uploaderContext, duplicateHandling } = {}) => {
332
372
  const recordResponse = await contractors.services.getContractorById(recordId, context);
333
373
  const recordDateModified = recordResponse.data.dateModified;
@@ -447,6 +487,9 @@ module.exports = {
447
487
  getActiveOverrides,
448
488
  createOverrideGapRequest,
449
489
  rejectOverrideGapRequest,
490
+ createPmOverrideRequest,
491
+ editPmOverrideRequest,
492
+ deletePmOverrideRequest,
450
493
  uploadMultipleCoiFiles,
451
494
  createTenantUser,
452
495
  deleteRecordByName,
@@ -21,6 +21,13 @@ module.exports = {
21
21
  onForTenants: { overrideGapRequestSettings: { isOverrideGapRequestEnabledForVendors: false, isOverrideGapRequestEnabledForTenants: true, isOverrideGapRequestEnabledForGuests: false } },
22
22
  onForGuests: { overrideGapRequestSettings: { isOverrideGapRequestEnabledForVendors: false, isOverrideGapRequestEnabledForTenants: false, isOverrideGapRequestEnabledForGuests: true } }
23
23
  },
24
+ userOverrideRequest: {
25
+ onForAll: { userOverrideRequestSettings: { isUserOverrideRequestEnabledForVendors: true, isUserOverrideRequestEnabledForTenants: true, isUserOverrideRequestEnabledForGuests: true } },
26
+ offForAll: { userOverrideRequestSettings: { isUserOverrideRequestEnabledForVendors: false, isUserOverrideRequestEnabledForTenants: false, isUserOverrideRequestEnabledForGuests: false } },
27
+ onForVendors: { userOverrideRequestSettings: { isUserOverrideRequestEnabledForVendors: true, isUserOverrideRequestEnabledForTenants: false, isUserOverrideRequestEnabledForGuests: false } },
28
+ onForTenants: { userOverrideRequestSettings: { isUserOverrideRequestEnabledForVendors: false, isUserOverrideRequestEnabledForTenants: true, isUserOverrideRequestEnabledForGuests: false } },
29
+ onForGuests: { userOverrideRequestSettings: { isUserOverrideRequestEnabledForVendors: false, isUserOverrideRequestEnabledForTenants: false, isUserOverrideRequestEnabledForGuests: true } }
30
+ },
24
31
  onboardingPhase: {
25
32
  on: { onboardingPhase: true },
26
33
  off: { onboardingPhase: false }
@@ -8,5 +8,6 @@ module.exports = {
8
8
  NOT_FOUND: 'NOT_FOUND',
9
9
  NOT_ELIGIBLE: 'NOT_ELIGIBLE',
10
10
  RECORD_NOT_IN_ACCOUNT: 'RECORD_NOT_IN_ACCOUNT',
11
- GLOBAL_RECORD_NOT_IN_ACCOUNT: 'GLOBAL_RECORD_NOT_IN_ACCOUNT'
11
+ GLOBAL_RECORD_NOT_IN_ACCOUNT: 'GLOBAL_RECORD_NOT_IN_ACCOUNT',
12
+ IDENTITY_CHANGED: 'IDENTITY_CHANGED'
12
13
  };
package/src/index.js CHANGED
@@ -47,6 +47,8 @@ const getInternalAPI = () => {
47
47
  const sampleCoi = require('./saas/internal-api/sampleCoi');
48
48
 
49
49
  const threadScheduler = require('./saas/internal-api/thread-scheduler/runner');
50
+ const schedulerJobRun = require('./saas/internal-api/schedulerJobRun');
51
+ const certificial = require('./saas/internal-api/certificial');
50
52
  const dbEmails = require('./saas/internal-api/dbEmails/index');
51
53
  const agreement = require('./saas/internal-api/agreement/index');
52
54
  const uploadFile = require('./saas/internal-api/uploadFiles/index');
@@ -86,6 +88,8 @@ const getInternalAPI = () => {
86
88
  requestsTable,
87
89
  sampleCoi,
88
90
  threadScheduler,
91
+ schedulerJobRun,
92
+ certificial,
89
93
  dbEmails,
90
94
  emailCustomization,
91
95
  factory,
@@ -5,6 +5,7 @@ const { backendUrl } = require('../../../../../env').getEnv();
5
5
 
6
6
  const Endpoints = {
7
7
  getAccount: `${backendUrl}/saas/accounts`,
8
+ getAccountList: `${backendUrl}/saas/accountList`,
8
9
  create: `${backendUrl}/saas/createaccount`,
9
10
  delete: `${backendUrl}/saas/account`,
10
11
  edit: accountId => `${backendUrl}/saas/account/${accountId}/settings`,
@@ -24,9 +25,13 @@ const updateAccountSettings = (accountId, settings, context) => {
24
25
  return requests.putRequestHandler(Endpoints.edit(accountId), { accountId, ... settings }, context);
25
26
  };
26
27
 
27
- const getByName = (accountName, context) => {
28
+ const getByName = (accountName, context, headers = {}) => {
28
29
  const params = { name: accountName };
29
- return requests.getRequestHandler(Endpoints.getAccount, params, context);
30
+ return requests.getRequestHandler(Endpoints.getAccount, params, context, headers);
31
+ };
32
+
33
+ const getAccountList = (context, headers = {}) => {
34
+ return requests.getRequestHandler(Endpoints.getAccountList, {}, context, headers);
30
35
  };
31
36
 
32
37
  const getFromCertificateHolder = certificateHolder => {
@@ -41,6 +46,7 @@ const getSupportAutomationAllowed = accountIds => {
41
46
  module.exports = {
42
47
  create,
43
48
  getByName,
49
+ getAccountList,
44
50
  deleteAccount,
45
51
  updateAccountSettings,
46
52
  getFromCertificateHolder,
@@ -18,6 +18,10 @@ const createAccount = async (createAccountModel, context) => {
18
18
  return accountsAPI.create(createAccountModel, context);
19
19
  };
20
20
 
21
+ const getAccountsResponse = (accountName, context, headers = {}) => {
22
+ return accountsAPI.getByName(accountName, context, headers);
23
+ };
24
+
21
25
  const getAccount = async (accountName, context) => {
22
26
  const response = await accountsAPI.getByName(accountName, context);
23
27
  expect(response.status, 'Failed to get accounts by name').to.equal(httpResponseStatusCodes.OK);
@@ -35,6 +39,10 @@ const getAccount = async (accountName, context) => {
35
39
  return existingAccounts[0];
36
40
  };
37
41
 
42
+ const getAccountList = context => {
43
+ return accountsAPI.getAccountList(context);
44
+ };
45
+
38
46
  const getOrCreateAccount = async (createAccountModel, context) => {
39
47
  validateType(createAccountModel, CreateAccountModel);
40
48
  const existingAccount = await getAccount(createAccountModel.companyName, context);
@@ -94,6 +102,8 @@ const getSupportAutomationAllowed = async (accountIds, { expectError } = {}) =>
94
102
 
95
103
  module.exports = {
96
104
  getAccount,
105
+ getAccountsResponse,
106
+ getAccountList,
97
107
  getOrCreateAccount,
98
108
  getAccountId,
99
109
  createAccount,
@@ -12,7 +12,8 @@ const RETRYABLE_LOGIN_STATUSES = [
12
12
  httpResponseStatusCodes.GATEWAY_TIMEOUT
13
13
  ];
14
14
  const MAX_LOGIN_ATTEMPTS = 3;
15
- const LOGIN_RETRY_DELAY_SECONDS = 1;
15
+ const LOGIN_RETRY_STEP_TIMEOUT = 2000;
16
+ const LOGIN_RETRY_RANGE_TIMEOUT = 5000;
16
17
 
17
18
  // Parallel callers for the same user share one login request
18
19
  const pendingLogins = new Map();
@@ -41,7 +42,8 @@ const loginWithRetry = async (email, password) => {
41
42
  }
42
43
 
43
44
  console.warn(`Login for ${email} failed with status ${response.status}, retrying`);
44
- await timeUtils.waitFor(LOGIN_RETRY_DELAY_SECONDS).seconds;
45
+ const retryRandomTimeout = attempt * LOGIN_RETRY_STEP_TIMEOUT + Math.random() * LOGIN_RETRY_RANGE_TIMEOUT;
46
+ await timeUtils.waitFor(retryRandomTimeout).mSeconds;
45
47
  }
46
48
  };
47
49
 
@@ -131,6 +133,14 @@ const getAuthenticatedUser = context => {
131
133
  return authApi.getAuthenticatedUser(context);
132
134
  };
133
135
 
136
+ // The id the backend stamps into the session on login — the same value
137
+ // getLoggedInUserId() resolves from the JWT. Available only after the email
138
+ // has logged in (getAuthCookie caches it); returns null otherwise.
139
+ const getUserId = email => {
140
+ const identity = getSystemUserIdentity(email);
141
+ return identity ? identity.id : null;
142
+ };
143
+
134
144
  const isHubUser = async (email, { expectError } = {}) => {
135
145
  const response = await authApi.isHubUser(email);
136
146
 
@@ -198,6 +208,7 @@ module.exports = {
198
208
  twoFAResendCode,
199
209
  twoFAVerify,
200
210
  getAuthenticatedUser,
211
+ getUserId,
201
212
  isHubUser,
202
213
  resetPassword,
203
214
  getLoginAttempts,
@@ -0,0 +1,15 @@
1
+ const requests = require('../../../../common/requests/cookieAuth/requests');
2
+ const { backendUrl, threadSchedulerSecret } = require('../../../../env').getEnv();
3
+
4
+ const Endpoints = {
5
+ enrollments: `${backendUrl}/schedulerJob/certificial/enrollments`
6
+ };
7
+
8
+ // Authenticated by x-scheduler-secret, not a cookie, so no ics context is passed.
9
+ const getEnrollments = recordIds => {
10
+ const headers = { 'x-scheduler-secret': threadSchedulerSecret };
11
+
12
+ return requests.getRequestHandler(Endpoints.enrollments, { recordIds: recordIds.toString() }, null, headers);
13
+ };
14
+
15
+ module.exports = { getEnrollments };
@@ -0,0 +1,24 @@
1
+ const certificialApi = require('./certificial.api');
2
+ const httpResponseStatusCodes = require('../../../../common/httpResponseStatusCodes.json');
3
+
4
+ // One row per requested record id. A record with no enrollment comes back with null fields, not a 404.
5
+ const getEnrollmentStates = async recordIds => {
6
+ const response = await certificialApi.getEnrollments(recordIds);
7
+
8
+ if (response.status !== httpResponseStatusCodes.OK) {
9
+ throw new Error(`Failed to read Certificial enrollment state for records ${recordIds}, response status - ${response.status}`);
10
+ }
11
+
12
+ return response.data;
13
+ };
14
+
15
+ const getEnrollmentState = async recordId => {
16
+ const [enrollmentState] = await getEnrollmentStates([recordId]);
17
+
18
+ return enrollmentState;
19
+ };
20
+
21
+ module.exports = {
22
+ getEnrollmentStates,
23
+ getEnrollmentState
24
+ };
@@ -0,0 +1,8 @@
1
+ // PENDING means Certificial never answered, so the row is never auto-retried.
2
+ const ENROLLMENT_STATUSES = {
3
+ PENDING: 'PENDING',
4
+ ENROLLED: 'ENROLLED',
5
+ REJECTED: 'REJECTED'
6
+ };
7
+
8
+ module.exports = { ENROLLMENT_STATUSES };
@@ -0,0 +1,4 @@
1
+ const services = require('./api/certificial.service');
2
+ const consts = require('./consts');
3
+
4
+ module.exports = { services, consts };
@@ -13,7 +13,10 @@ const Endpoints = {
13
13
  updateOverrideExpiryDate: auditorContractorId => `${backendUrl}/saas/integration-tests/override/${auditorContractorId}/expiry-date`,
14
14
  getOverrideGapRequestById: overrideGapRequestId => `${backendUrl}/saas/gaps/override-gap-request/${overrideGapRequestId}`,
15
15
  createOverrideGapRequest: `${backendUrl}/secured/vendor-app/record/override-gap-request`,
16
- rejectOverrideGapRequest: `${backendUrl}/saas/gaps/override-gap-request/reject`
16
+ rejectOverrideGapRequest: `${backendUrl}/saas/gaps/override-gap-request/reject`,
17
+ createPmOverrideRequest: `${backendUrl}/saas/gaps/override-gap-request`,
18
+ pmOverrideRequestById: overrideGapRequestId => `${backendUrl}/saas/gaps/override-gap-request/${overrideGapRequestId}`,
19
+ eligibleOverrideAssignees: requestId => `${backendUrl}/saas/gaps/override-gap-request/assignees/${requestId}`
17
20
  };
18
21
 
19
22
  const waive = async (contractorId, waiveRequestModel, context) => {
@@ -70,6 +73,22 @@ const rejectOverrideGapRequest = (rejectModel, context) => {
70
73
  return requests.patchRequestHandler(Endpoints.rejectOverrideGapRequest, context, rejectModel);
71
74
  };
72
75
 
76
+ const createPmOverrideRequest = (pmOverrideRequestModel, context) => {
77
+ return requests.postRequestHandler(Endpoints.createPmOverrideRequest, pmOverrideRequestModel, context);
78
+ };
79
+
80
+ const editPmOverrideRequest = (overrideGapRequestId, pmOverrideRequestModel, context) => {
81
+ return requests.patchRequestHandler(Endpoints.pmOverrideRequestById(overrideGapRequestId), context, pmOverrideRequestModel);
82
+ };
83
+
84
+ const deletePmOverrideRequest = (overrideGapRequestId, context) => {
85
+ return requests.deleteRequestHandler(Endpoints.pmOverrideRequestById(overrideGapRequestId), context);
86
+ };
87
+
88
+ const getEligibleOverrideAssignees = (requestId, context) => {
89
+ return requests.getRequestHandler(Endpoints.eligibleOverrideAssignees(requestId), {}, context);
90
+ };
91
+
73
92
  module.exports = {
74
93
  waive,
75
94
  retract,
@@ -82,5 +101,9 @@ module.exports = {
82
101
  updateOverrideExpiryDate,
83
102
  getOverrideGapRequestById,
84
103
  createOverrideGapRequest,
85
- rejectOverrideGapRequest
104
+ rejectOverrideGapRequest,
105
+ createPmOverrideRequest,
106
+ editPmOverrideRequest,
107
+ deletePmOverrideRequest,
108
+ getEligibleOverrideAssignees
86
109
  };
@@ -10,7 +10,7 @@ const {
10
10
  WAIVER_REASONS,
11
11
  EXPIRY_TYPES
12
12
  } = require('../../models/waive/waive.model');
13
- const { OverrideGapRequestCreateModel, OverrideGapRequestRejectModel } = require('../../models/overrideGapRequest/override.gap.request.model');
13
+ const { OverrideGapRequestCreateModel, OverrideGapRequestRejectModel, PmOverrideRequestModel } = require('../../models/overrideGapRequest/override.gap.request.model');
14
14
  const { validateType } = require('../../../../../common/validations/typeValidations');
15
15
  const auditUtil = require('../../utils/auditUtil');
16
16
  const httpResponseStatusCodes = require('../../../../../common/httpResponseStatusCodes.json');
@@ -277,6 +277,93 @@ const updateOverrideExpiryDate = async (auditorContractorId, expiryDate, context
277
277
  return response;
278
278
  };
279
279
 
280
+ const createPmOverrideRequest = async (auditorContractorId, context, {
281
+ requestReason,
282
+ expiryType,
283
+ expiryDate = null,
284
+ assigneeUserIds,
285
+ recordDateModified,
286
+ expectError
287
+ } = {}) => {
288
+ const model = new PmOverrideRequestModel({
289
+ auditorContractorId,
290
+ requestReason,
291
+ expiryType,
292
+ expiryDate,
293
+ assigneeUserIds,
294
+ recordDateModified
295
+ });
296
+ validateType(model, PmOverrideRequestModel);
297
+ const response = await overrideGapApi.createPmOverrideRequest(model, context);
298
+
299
+ if (expectError) {
300
+ return response;
301
+ }
302
+
303
+ if (response.status !== httpResponseStatusCodes.CREATED) {
304
+ throw Error(`Failed to create PM override request. Status code: ${response.status}. Body: ${JSON.stringify(response.data)}`);
305
+ }
306
+
307
+ return response.data;
308
+ };
309
+
310
+ const editPmOverrideRequest = async (overrideGapRequestId, context, {
311
+ requestReason,
312
+ expiryType,
313
+ expiryDate = null,
314
+ assigneeUserIds,
315
+ recordDateModified,
316
+ expectError
317
+ } = {}) => {
318
+ const model = new PmOverrideRequestModel({
319
+ requestReason,
320
+ expiryType,
321
+ expiryDate,
322
+ assigneeUserIds,
323
+ recordDateModified
324
+ });
325
+ validateType(model, PmOverrideRequestModel);
326
+ const response = await overrideGapApi.editPmOverrideRequest(overrideGapRequestId, model, context);
327
+
328
+ if (expectError) {
329
+ return response;
330
+ }
331
+
332
+ if (response.status !== httpResponseStatusCodes.NO_CONTENT) {
333
+ throw Error(`Failed to edit PM override request. Status code: ${response.status}`);
334
+ }
335
+
336
+ return response;
337
+ };
338
+
339
+ const deletePmOverrideRequest = async (overrideGapRequestId, context, { expectError } = {}) => {
340
+ const response = await overrideGapApi.deletePmOverrideRequest(overrideGapRequestId, context);
341
+
342
+ if (expectError) {
343
+ return response;
344
+ }
345
+
346
+ if (response.status !== httpResponseStatusCodes.NO_CONTENT) {
347
+ throw Error(`Failed to delete PM override request. Status code: ${response.status}`);
348
+ }
349
+
350
+ return response;
351
+ };
352
+
353
+ const getEligibleOverrideAssignees = async (requestId, context, { expectError } = {}) => {
354
+ const response = await overrideGapApi.getEligibleOverrideAssignees(requestId, context);
355
+
356
+ if (expectError) {
357
+ return response;
358
+ }
359
+
360
+ if (response.status !== httpResponseStatusCodes.OK) {
361
+ throw Error(`Failed to get eligible override assignees. Status code: ${response.status}`);
362
+ }
363
+
364
+ return response.data;
365
+ };
366
+
280
367
  module.exports = {
281
368
  waive,
282
369
  waiveAll,
@@ -296,5 +383,9 @@ module.exports = {
296
383
  getOverrideGapRequestById,
297
384
  rejectOverrideGapRequest,
298
385
  getAuditorContractorIdFromGap,
299
- updateOverrideExpiryDate
386
+ updateOverrideExpiryDate,
387
+ createPmOverrideRequest,
388
+ editPmOverrideRequest,
389
+ deletePmOverrideRequest,
390
+ getEligibleOverrideAssignees
300
391
  };
@@ -4,4 +4,11 @@ const OVERRIDE_GAP_REQUEST_STATUS = {
4
4
  REJECTED: 'REJECTED'
5
5
  };
6
6
 
7
- module.exports = { OVERRIDE_GAP_REQUEST_STATUS };
7
+ const OVERRIDE_REQUESTER_TYPE = {
8
+ HUB_USER: 'HUB_USER',
9
+ INVITER: 'INVITER'
10
+ };
11
+
12
+ const DEFAULT_PM_OVERRIDE_REASON = 'PM override request reason for integration tests exceeding minimum length of 35 characters.';
13
+
14
+ module.exports = { OVERRIDE_GAP_REQUEST_STATUS, OVERRIDE_REQUESTER_TYPE, DEFAULT_PM_OVERRIDE_REASON };
@@ -13,4 +13,19 @@ class OverrideGapRequestRejectModel {
13
13
  this.recordDateModified = recordDateModified;
14
14
  }
15
15
  }
16
- module.exports = { OverrideGapRequestCreateModel, OverrideGapRequestRejectModel };
16
+
17
+ class PmOverrideRequestModel {
18
+ constructor({ auditorContractorId, requestReason, expiryType, expiryDate = null, assigneeUserIds, recordDateModified }) {
19
+ if (auditorContractorId !== undefined) {
20
+ this.auditorContractorId = auditorContractorId;
21
+ }
22
+
23
+ this.requestReason = requestReason;
24
+ this.expiryType = expiryType;
25
+ this.expiryDate = expiryDate;
26
+ this.assigneeUserIds = assigneeUserIds;
27
+ this.recordDateModified = recordDateModified;
28
+ }
29
+ }
30
+
31
+ module.exports = { OverrideGapRequestCreateModel, OverrideGapRequestRejectModel, PmOverrideRequestModel };
@@ -167,9 +167,40 @@ const waitForMail = async (emailModel, { maxSecondToWait = 30 } = {}) => {
167
167
  return foundMails;
168
168
  };
169
169
 
170
+ const waitForMails = async (emailModels, { maxSecondToWait = 60 } = {}) => {
171
+ const oneSecondInMilliseconds = 1000;
172
+ const started = new Date();
173
+ let timeFromTheStart = 0;
174
+ const foundMailsByModel = emailModels.map(() => []);
175
+
176
+ while (foundMailsByModel.some(found => found.length === 0) && timeFromTheStart < maxSecondToWait) {
177
+ for (let index = 0; index < emailModels.length; index++) {
178
+ if (foundMailsByModel[index].length > 0) {
179
+ continue;
180
+ }
181
+
182
+ const { data } = await getAllMessages(emailModels[index]);
183
+
184
+ if (data.length > 0) {
185
+ foundMailsByModel[index] = data;
186
+ }
187
+ }
188
+
189
+ if (foundMailsByModel.every(found => found.length > 0)) {
190
+ break;
191
+ }
192
+
193
+ timeFromTheStart = (new Date() - started) / oneSecondInMilliseconds;
194
+ await timeUtils.waitFor(3).seconds;
195
+ }
196
+
197
+ return foundMailsByModel;
198
+ };
199
+
170
200
  module.exports = {
171
201
  util,
172
202
  waitForMail,
203
+ waitForMails,
173
204
  getAllMessages,
174
205
  getLastMessage,
175
206
  deleteMultipleMessages,
@@ -54,6 +54,9 @@ const EMAIL_SUBJECT = {
54
54
  overrideGapRequestPending: ({ vendorCompanyName }) => `${vendorCompanyName} asked for an exception`,
55
55
  overrideGapRequestRejected: ({ accountName }) => `${accountName} denied your exception request`,
56
56
  overrideGapRequestApproved: ({ accountName }) => `${accountName} approved your exception request`,
57
+ pmOverrideRequestAssigned: ({ requesterFullName, companyName }) => `${requesterFullName} asked you to override a gap for ${companyName}`,
58
+ pmOverrideRequestDenied: ({ rejecterFullName, companyName }) => `${rejecterFullName} Rejected your override request for ${companyName}`,
59
+ pmOverrideRequestApproved: ({ approverFullName, companyName }) => `${approverFullName} Approved your override request for ${companyName}`,
57
60
  timedOverrideExpired: ({ recordCompanyName, requesterName }) => `Override ended - ${recordCompanyName} in ${requesterName}`,
58
61
  allRecordsExport: 'Jones - All Records Export'
59
62
  };
@@ -3,7 +3,10 @@ const NOTIFICATION_TYPES = {
3
3
  OVERRIDE_GAP_REQUEST_PENDING: 'NewOverrideGapRequest',
4
4
  RELATED_DOC_UPDATED: 'RelatedDocUpdated',
5
5
  GLOBAL_RECORD_RELATED_DOC_UPDATED: 'GlobalRecordRelatedDocUpdated',
6
- TIMED_OVERRIDE_EXPIRED: 'TimedOverrideExpired'
6
+ TIMED_OVERRIDE_EXPIRED: 'TimedOverrideExpired',
7
+ PM_OVERRIDE_REQUEST: 'NewPmOverrideRequest',
8
+ PM_OVERRIDE_REQUEST_APPROVED: 'PmOverrideRequestApproved',
9
+ PM_OVERRIDE_REQUEST_REJECTED: 'PmOverrideRequestRejected'
7
10
  };
8
11
 
9
12
  module.exports = { NOTIFICATION_TYPES };
@@ -5,8 +5,8 @@ const Endpoints = {
5
5
  getRequestIdByRecordIdBase64: base64RecordId => `${backendUrl}/secured/outlook/record/${base64RecordId}/compliance-report`
6
6
  };
7
7
 
8
- const getRequestIdByRecordIdBase64 = async (base64RecordId, context) => {
9
- return requests.getRequestHandler(Endpoints.getRequestIdByRecordIdBase64(base64RecordId), {}, context);
8
+ const getRequestIdByRecordIdBase64 = async (base64RecordId, context, headers = {}) => {
9
+ return requests.getRequestHandler(Endpoints.getRequestIdByRecordIdBase64(base64RecordId), {}, context, headers);
10
10
  };
11
11
 
12
12
  module.exports = {
@@ -1,7 +1,7 @@
1
1
  const vendorProfileApi = require('./outlook.api');
2
2
 
3
- const getRequestIdByRecordIdBase64 = async (base64RecordId, context) => {
4
- return vendorProfileApi.getRequestIdByRecordIdBase64(base64RecordId, context);
3
+ const getRequestIdByRecordIdBase64 = async (base64RecordId, context, headers = {}) => {
4
+ return vendorProfileApi.getRequestIdByRecordIdBase64(base64RecordId, context, headers);
5
5
  };
6
6
 
7
7
  const generateRecordIdBase64 = recordId => {
@@ -8,7 +8,9 @@ class UpdateProjectSettingsModel {
8
8
  projectId = null,
9
9
  tenantSolution = false,
10
10
  autocompleteSettings = {},
11
- overrideGapRequestSettings = {}
11
+ overrideGapRequestSettings = {},
12
+ userOverrideRequestSettings = {},
13
+ relevantComplianceDocumentsSettings = {}
12
14
  } = {}
13
15
  ) {
14
16
  this.autoGaps = { ... DEFAULT_AUTOGAPS, ... autoGaps };
@@ -19,6 +21,8 @@ class UpdateProjectSettingsModel {
19
21
  this.tenantSolution = tenantSolution;
20
22
  this.autocompleteSettings = { ... DEFAULT_AUTOCOMPLETE_ADD_VENDORS, ... autocompleteSettings };
21
23
  this.overrideGapRequestSettings = { ... DEFAULT_OVERRIDE_GAP_REQUEST, ... overrideGapRequestSettings };
24
+ this.userOverrideRequestSettings = { ... DEFAULT_USER_OVERRIDE_REQUEST, ... userOverrideRequestSettings };
25
+ this.relevantComplianceDocumentsSettings = { ... DEFAULT_RELEVANT_COMPLIANCE_DOCUMENTS, ... relevantComplianceDocumentsSettings };
22
26
  }
23
27
  }
24
28
 
@@ -46,3 +50,15 @@ const DEFAULT_OVERRIDE_GAP_REQUEST = {
46
50
  isOverrideGapRequestEnabledForTenants: false,
47
51
  isOverrideGapRequestEnabledForGuests: false
48
52
  };
53
+
54
+ const DEFAULT_USER_OVERRIDE_REQUEST = {
55
+ isUserOverrideRequestEnabledForVendors: false,
56
+ isUserOverrideRequestEnabledForTenants: false,
57
+ isUserOverrideRequestEnabledForGuests: false
58
+ };
59
+
60
+ const DEFAULT_RELEVANT_COMPLIANCE_DOCUMENTS = {
61
+ isRelevantComplianceDocumentsEnabledForVendors: false,
62
+ isRelevantComplianceDocumentsEnabledForTenants: false,
63
+ isRelevantComplianceDocumentsEnabledForGuests: false
64
+ };
@@ -0,0 +1,31 @@
1
+ const requests = require('../../../../common/requests/cookieAuth/requests');
2
+ const { backendUrl } = require('../../../../env').getEnv();
3
+
4
+ const Endpoints = {
5
+ base: `${backendUrl}/saas/integration-tests/scheduler-job-run`,
6
+ byId: id => `${backendUrl}/saas/integration-tests/scheduler-job-run/${id}`,
7
+ reclaim: `${backendUrl}/saas/integration-tests/scheduler-job-run/reclaim`
8
+ };
9
+
10
+ const seedJobRun = (body, context) => {
11
+ return requests.postRequestHandler(Endpoints.base, body, context);
12
+ };
13
+
14
+ const reclaimStaleJobRuns = (jobType, context) => {
15
+ return requests.postRequestHandler(Endpoints.reclaim, null, context, {}, { jobType });
16
+ };
17
+
18
+ const getJobRun = (id, context) => {
19
+ return requests.getRequestHandler(Endpoints.byId(id), {}, context);
20
+ };
21
+
22
+ const deleteJobRunsByType = (jobType, context) => {
23
+ return requests.deleteRequestHandler(Endpoints.base, context, {}, { jobType });
24
+ };
25
+
26
+ module.exports = {
27
+ seedJobRun,
28
+ reclaimStaleJobRuns,
29
+ getJobRun,
30
+ deleteJobRunsByType
31
+ };
@@ -0,0 +1,56 @@
1
+ const httpResponseStatusCodes = require('../../../../common/httpResponseStatusCodes.json');
2
+
3
+ const authService = require('../../auth/api/auth.service');
4
+ const schedulerJobRunApi = require('./scheduler.job.run.api');
5
+
6
+ const SUCCESS_STATUSES = [httpResponseStatusCodes.OK, httpResponseStatusCodes.NO_CONTENT];
7
+
8
+ // Seeds a job_run with backdated timestamps (applied with the DB clock) to simulate an orphaned run.
9
+ // heartbeatMinutesAgo: null seeds a NULL heartbeat. Returns the id.
10
+ const seedJobRun = async ({ jobType, status, startedMinutesAgo, heartbeatMinutesAgo = null }) => {
11
+ const response = await schedulerJobRunApi.seedJobRun(
12
+ { jobType, status, startedMinutesAgo, heartbeatMinutesAgo }, await authService.getIcsCookie());
13
+
14
+ if (response.status !== httpResponseStatusCodes.OK) {
15
+ throw new Error(`seedJobRun failed for ${jobType}: ${JSON.stringify(response.data)}`);
16
+ }
17
+
18
+ return response.data;
19
+ };
20
+
21
+ // Runs reclaim in isolation (no overlap check, no job submitted) so a test can assert it without starting a real job.
22
+ const reclaimStaleJobRuns = async jobType => {
23
+ const response = await schedulerJobRunApi.reclaimStaleJobRuns(jobType, await authService.getIcsCookie());
24
+
25
+ if (response.status !== httpResponseStatusCodes.OK) {
26
+ throw new Error(`reclaimStaleJobRuns failed for ${jobType}: ${JSON.stringify(response.data)}`);
27
+ }
28
+
29
+ return response.data;
30
+ };
31
+
32
+ const getJobRun = async id => {
33
+ const response = await schedulerJobRunApi.getJobRun(id, await authService.getIcsCookie());
34
+
35
+ if (response.status !== httpResponseStatusCodes.OK) {
36
+ throw new Error(`getJobRun failed for id ${id}: ${JSON.stringify(response.data)}`);
37
+ }
38
+
39
+ return response.data;
40
+ };
41
+
42
+ // Clears all job_run rows of a type between runs. Safe outside production, where only seeded rows exist.
43
+ const deleteJobRunsByType = async jobType => {
44
+ const response = await schedulerJobRunApi.deleteJobRunsByType(jobType, await authService.getIcsCookie());
45
+
46
+ if (!SUCCESS_STATUSES.includes(response.status)) {
47
+ throw new Error(`deleteJobRunsByType failed for ${jobType}: ${JSON.stringify(response.data)}`);
48
+ }
49
+ };
50
+
51
+ module.exports = {
52
+ seedJobRun,
53
+ reclaimStaleJobRuns,
54
+ getJobRun,
55
+ deleteJobRunsByType
56
+ };
@@ -0,0 +1,3 @@
1
+ const services = require('./api/scheduler.job.run.service');
2
+
3
+ module.exports = { services };
@@ -58,11 +58,28 @@ module.exports = {
58
58
  runJobForExpireTimedOverrides: function(entityIds) {
59
59
  return runSchedulerJob({ jobType: schedulerJobs.EXPIRE_TIMED_OVERRIDES, entityIds });
60
60
  },
61
+ runJobForCertificialEnrollment: function(entityIds) {
62
+ requireScope(entityIds, schedulerJobs.CERTIFICIAL_ENROLLMENT);
63
+ return runSchedulerJob({ jobType: schedulerJobs.CERTIFICIAL_ENROLLMENT, entityIds });
64
+ },
65
+ runJobForCertificialMonitoring: function(entityIds) {
66
+ requireScope(entityIds, schedulerJobs.CERTIFICIAL_MONITORING);
67
+ return runSchedulerJob({ jobType: schedulerJobs.CERTIFICIAL_MONITORING, entityIds });
68
+ },
61
69
  runAllJobsNew: function(entityIds) {
62
70
  return runSchedulerJob({ jobType: schedulerJobs.ALL, entityIds });
63
71
  }
64
72
  };
65
73
 
74
+ /* An unscoped enrollment run emails a real broker for every eligible record in the environment and
75
+ cannot be undone; an unscoped monitoring run polls and notifies on every active enrollment. A
76
+ since: token still counts as a scope. */
77
+ function requireScope(entityIds, jobType) {
78
+ if (!entityIds || !entityIds.length) {
79
+ throw new Error(`${jobType} must be scoped with entityIds`);
80
+ }
81
+ }
82
+
66
83
  function isOverloadError(errorCode) {
67
84
  const overloadResponseErrors = [httpResponseStatusCodes.TOO_MANY_REQUESTS, httpResponseStatusCodes.CONFLICT];
68
85
  return overloadResponseErrors.includes(errorCode);
@@ -4,5 +4,7 @@ module.exports = {
4
4
  SCHEDULED_REPORTS: 'SCHEDULED_REPORTS',
5
5
  RELATED_DOCS: 'RELATED_DOCS',
6
6
  EXPIRE_TIMED_OVERRIDES: 'EXPIRE_TIMED_OVERRIDES',
7
+ CERTIFICIAL_ENROLLMENT: 'CERTIFICIAL_ENROLLMENT',
8
+ CERTIFICIAL_MONITORING: 'CERTIFICIAL_MONITORING',
7
9
  ALL: 'ALL'
8
10
  };
@@ -128,14 +128,28 @@ const createPolicyFiles = async (requestId, policyFileList, context, { shouldExp
128
128
  return createPolicyFilesResponse;
129
129
  };
130
130
 
131
- const uploadAndCreatePolicyFile = async (policyTypeList, policyFilePath, requestId, context, { markAsScanned } = {}) => {
131
+ const uploadPolicyModels = (policyTypeList, policyFilePath, requestId, context) => {
132
+ return Promise.all(policyTypeList.map(policyType => uploadPolicyFileAndGetPolicyModel(requestId, policyFilePath, policyType, context)));
133
+ };
134
+
135
+ const uploadAndCreatePolicyFile = async (policyTypeList, policyFilePath, requestId, context, { markAsScanned, maxRetries = 3 } = {}) => {
132
136
  validateArray(policyTypeList);
133
137
 
134
- const uploadedPolicyModelsList = await Promise.all(policyTypeList.map(async fileType => {
135
- return uploadPolicyFileAndGetPolicyModel(requestId, policyFilePath, fileType, context);
136
- }));
138
+ const policyModels = await uploadPolicyModels(policyTypeList, policyFilePath, requestId, context);
139
+ const policyFilesResponse = await createPolicyFiles(requestId, policyModels, context);
140
+
141
+ for (let retry = 1; retry <= maxRetries; retry++) {
142
+ const missingPolicyTypes = policyTypeList.filter(policyType => !policyFilesResponse.data[policyType]);
137
143
 
138
- const policyFilesResponse = await createPolicyFiles(requestId, uploadedPolicyModelsList, context);
144
+ if (!missingPolicyTypes.length) {
145
+ break;
146
+ }
147
+
148
+ console.warn(`Policy types ${missingPolicyTypes} were not created for request ${requestId}, retry`);
149
+ const missingPolicyModels = await uploadPolicyModels(missingPolicyTypes, policyFilePath, requestId, context);
150
+ const retryResponse = await createPolicyFiles(requestId, missingPolicyModels, context);
151
+ Object.assign(policyFilesResponse.data, retryResponse.data);
152
+ }
139
153
 
140
154
  if (markAsScanned) {
141
155
  const { data: request } = await requestsApi.getRequestById(requestId, context);