@getjones/test-infra 5.2.11 → 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 (31) hide show
  1. package/package.json +2 -2
  2. package/src/common/commonFlows/records/records.js +55 -3
  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 +46 -1
  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/requirements/consts/consts.js +3 -1
  24. package/src/saas/internal-api/schedulerJobRun/api/scheduler.job.run.api.js +31 -0
  25. package/src/saas/internal-api/schedulerJobRun/api/scheduler.job.run.service.js +56 -0
  26. package/src/saas/internal-api/schedulerJobRun/index.js +3 -0
  27. package/src/saas/internal-api/thread-scheduler/runner.js +17 -0
  28. package/src/saas/internal-api/thread-scheduler/schedulerJobs.js +2 -0
  29. package/src/saas/internal-api/uploadFiles/api/upload.file.service.js +19 -5
  30. package/src/saas/internal-api/users/models/notification.settings.model.js +2 -1
  31. package/src/saas/internal-api/users/utils/util.js +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getjones/test-infra",
3
- "version": "5.2.11",
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",
@@ -20,7 +20,10 @@ const recordTypes = require('../../../common/consts/recordTypes');
20
20
  const policyNames = require('../../../defaults/index').polices;
21
21
  const { ExtractionRequestModel } = require('../../../saas/internal-api/requirements/models/extraction.request.model');
22
22
  const { TabExtractionRequestModel } = require('../../../saas/internal-api/requirements/models/tab.extraction.request.model');
23
+ const { DEFAULT_LEASE_PATH } = require('../../../saas/internal-api/requirements/consts/consts');
24
+ const { RELATED_DOC_TYPES } = require('../../../saas/internal-api/uploadFiles/consts');
23
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');
24
27
 
25
28
  const makeRecordNotResponsive = async function(recordId, context) {
26
29
  const { data: record } = await contractors.services.getContractorById(recordId, context);
@@ -326,6 +329,45 @@ const rejectOverrideGapRequest = async (overrideGapRequestId, rejectReason, reco
326
329
  return compliance.services.overrideGap.rejectOverrideGapRequest(overrideGapRequestId, rejectReason, record.dateModified, context);
327
330
  };
328
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
+
329
371
  const uploadMultipleCoiFiles = async (recordId, coiFilePaths, uploadedBy, uploaderEmail, context, { uploaderContext, duplicateHandling } = {}) => {
330
372
  const recordResponse = await contractors.services.getContractorById(recordId, context);
331
373
  const recordDateModified = recordResponse.data.dateModified;
@@ -392,18 +434,25 @@ const requestRequirementsExtraction = async (recordId, context, { leaseFilePath,
392
434
  };
393
435
 
394
436
  const requestTabRequirementsExtraction = async (tabId, context,
395
- { leaseFilePath, requirementName, notes, docType } = {}, { expectError, readContext = context } = {}) => {
437
+ { leaseFilePath = DEFAULT_LEASE_PATH, requirementName, notes, docType = RELATED_DOC_TYPES.SERVICE_AGREEMENT } = {}, { expectError, readContext = context } = {}) => {
396
438
  const stagedDocument = await uploadFile.services.stageDocumentToTempBucket(tabId, context, { filePath: leaseFilePath });
397
439
  const tabExtractionRequestModel = new TabExtractionRequestModel({ ... stagedDocument, requirementName, docType, notes });
440
+ const existingLeases = expectError ? [] : await requirements.lease.services.getLeasesByTabId(tabId, readContext, { openOnly: false });
441
+ const existingLeaseIds = new Set(existingLeases.map(({ leaseId }) => leaseId));
398
442
  const requestResponse = await requirements.lease.services.requestTabLeaseExtraction(tabId, tabExtractionRequestModel, context, { expectError });
399
443
 
400
444
  if (expectError) {
401
445
  return { requestResponse, stagedDocument };
402
446
  }
403
447
 
404
- const [lease] = await requirements.lease.services.getLeasesByTabId(tabId, readContext);
448
+ const openLeases = await requirements.lease.services.getLeasesByTabId(tabId, readContext);
449
+ const createdLease = openLeases.find(({ leaseId }) => !existingLeaseIds.has(leaseId));
450
+
451
+ if (!createdLease) {
452
+ throw Error(`No new tab-level lease found for tab: ${tabId}`);
453
+ }
405
454
 
406
- return lease.leaseId;
455
+ return createdLease.leaseId;
407
456
  };
408
457
 
409
458
  const cancelOpenTabRequests = async (tabId, icsContext) => {
@@ -438,6 +487,9 @@ module.exports = {
438
487
  getActiveOverrides,
439
488
  createOverrideGapRequest,
440
489
  rejectOverrideGapRequest,
490
+ createPmOverrideRequest,
491
+ editPmOverrideRequest,
492
+ deletePmOverrideRequest,
441
493
  uploadMultipleCoiFiles,
442
494
  createTenantUser,
443
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,
@@ -4,6 +4,19 @@ const { credentials } = getEnv();
4
4
  const httpResponseStatusCodes = require('../../../../common/httpResponseStatusCodes.json');
5
5
 
6
6
  const authApi = require('./auth.api');
7
+ const timeUtils = require('../../../../common/utils/timeUtils');
8
+
9
+ const RETRYABLE_LOGIN_STATUSES = [
10
+ httpResponseStatusCodes.BAD_GATEWAY,
11
+ httpResponseStatusCodes.SERVICE_UNAVAILABLE,
12
+ httpResponseStatusCodes.GATEWAY_TIMEOUT
13
+ ];
14
+ const MAX_LOGIN_ATTEMPTS = 3;
15
+ const LOGIN_RETRY_STEP_TIMEOUT = 2000;
16
+ const LOGIN_RETRY_RANGE_TIMEOUT = 5000;
17
+
18
+ // Parallel callers for the same user share one login request
19
+ const pendingLogins = new Map();
7
20
 
8
21
  const getAuthCookie = async (email, password) => {
9
22
  const systemUser = getSystemUserIdentity(email);
@@ -12,9 +25,32 @@ const getAuthCookie = async (email, password) => {
12
25
  return systemUser.cookie;
13
26
  }
14
27
 
15
- try {
28
+ if (!pendingLogins.has(email)) {
29
+ const login = loginAndCacheCookie(email, password).finally(() => pendingLogins.delete(email));
30
+ pendingLogins.set(email, login);
31
+ }
32
+
33
+ return pendingLogins.get(email);
34
+ };
35
+
36
+ const loginWithRetry = async (email, password) => {
37
+ for (let attempt = 1; ; attempt++) {
16
38
  const response = await authApi.login(email, password);
17
39
 
40
+ if (!RETRYABLE_LOGIN_STATUSES.includes(response.status) || attempt === MAX_LOGIN_ATTEMPTS) {
41
+ return response;
42
+ }
43
+
44
+ console.warn(`Login for ${email} failed with status ${response.status}, retrying`);
45
+ const retryRandomTimeout = attempt * LOGIN_RETRY_STEP_TIMEOUT + Math.random() * LOGIN_RETRY_RANGE_TIMEOUT;
46
+ await timeUtils.waitFor(retryRandomTimeout).mSeconds;
47
+ }
48
+ };
49
+
50
+ const loginAndCacheCookie = async (email, password) => {
51
+ try {
52
+ const response = await loginWithRetry(email, password);
53
+
18
54
  if (response.status !== httpResponseStatusCodes.OK) {
19
55
  throw Error(`Error: ${JSON.stringify(response.data)}`);
20
56
  }
@@ -97,6 +133,14 @@ const getAuthenticatedUser = context => {
97
133
  return authApi.getAuthenticatedUser(context);
98
134
  };
99
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
+
100
144
  const isHubUser = async (email, { expectError } = {}) => {
101
145
  const response = await authApi.isHubUser(email);
102
146
 
@@ -164,6 +208,7 @@ module.exports = {
164
208
  twoFAResendCode,
165
209
  twoFAVerify,
166
210
  getAuthenticatedUser,
211
+ getUserId,
167
212
  isHubUser,
168
213
  resetPassword,
169
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
+ };
@@ -12,9 +12,11 @@ const leaseExtractionStatus = {
12
12
  closedReqSet: 'CLOSED_REQ_SET'
13
13
  };
14
14
 
15
+ const DEFAULT_LEASE_PATH = 'packages/test-infra/src/resources/lease/defaultLease.pdf';
16
+
15
17
  const LEASE_TRIGGER_TYPES = {
16
18
  record: 'RECORD',
17
19
  tab: 'TAB'
18
20
  };
19
21
 
20
- module.exports = { REQUIREMENT_SHARING_LEVELS, leaseExtractionStatus, LEASE_TRIGGER_TYPES };
22
+ module.exports = { REQUIREMENT_SHARING_LEVELS, leaseExtractionStatus, LEASE_TRIGGER_TYPES, DEFAULT_LEASE_PATH };
@@ -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);
@@ -1,11 +1,12 @@
1
1
  class NotificationSettingsModel {
2
- constructor({ aboutToExpire = true, auditCompliant = true, auditNotCompliant = true, missingCoi = true, recordOverrideRequest = true, timedOverrideExpired = true } = {}) {
2
+ constructor({ aboutToExpire = true, auditCompliant = true, auditNotCompliant = true, missingCoi = true, recordOverrideRequest = true, timedOverrideExpired = true, weeklySummary = true } = {}) {
3
3
  this.aboutToExpire = aboutToExpire;
4
4
  this.auditCompliant = auditCompliant;
5
5
  this.auditNotCompliant = auditNotCompliant;
6
6
  this.missingCoi = missingCoi;
7
7
  this.recordOverrideRequest = recordOverrideRequest;
8
8
  this.timedOverrideExpired = timedOverrideExpired;
9
+ this.weeklySummary = weeklySummary;
9
10
  }
10
11
  }
11
12
 
@@ -13,7 +13,8 @@ const attributesConvertor = {
13
13
  'COI is compliant.': 'auditCompliant',
14
14
  'Unresponsive contact.': 'missingCoi',
15
15
  'Record Override Request': 'recordOverrideRequest',
16
- 'Timed override expired — record is not compliant.': 'timedOverrideExpired'
16
+ 'Timed override expired — record is not compliant.': 'timedOverrideExpired',
17
+ 'Weekly summary': 'weeklySummary'
17
18
  };
18
19
 
19
20
  const getUserEncryptedData = async (email, role, context) => {