@contrail/flexplm 1.7.5 → 1.7.6-alpha.a804d5a
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.
|
@@ -29,7 +29,8 @@ class BaseProcessPublishAssortment {
|
|
|
29
29
|
let publisher;
|
|
30
30
|
const assortmentId = event.assortmentId;
|
|
31
31
|
try {
|
|
32
|
-
console.info('process-start
|
|
32
|
+
console.info('process-start: assortmentId: ' + assortmentId
|
|
33
|
+
+ ', assortmentPublishChangeId: ' + assortmentPublishChangeId);
|
|
33
34
|
let seasonFed;
|
|
34
35
|
try {
|
|
35
36
|
seasonFed = await this.getSeasonFederation(assortmentId);
|
|
@@ -74,9 +75,9 @@ class BaseProcessPublishAssortment {
|
|
|
74
75
|
e.publishInfo = publishInfo;
|
|
75
76
|
}
|
|
76
77
|
catch (e2) {
|
|
77
|
-
console.
|
|
78
|
+
console.error('getPublishInfo failed: ' + e2.message);
|
|
78
79
|
}
|
|
79
|
-
console.
|
|
80
|
+
console.error('process failed: assortmentId: ' + assortmentId + ': ' + e.message);
|
|
80
81
|
throw e;
|
|
81
82
|
}
|
|
82
83
|
}
|
|
@@ -315,19 +316,17 @@ class BaseProcessPublishAssortment {
|
|
|
315
316
|
}
|
|
316
317
|
async downloadHydratedChangeDetail(assortmentPublishChange) {
|
|
317
318
|
try {
|
|
318
|
-
console.info('downloadHydratedChangeDetail-start');
|
|
319
319
|
const link = assortmentPublishChange?.hydratedDetailDownloadLink;
|
|
320
320
|
const response = await fetch(link);
|
|
321
321
|
const data = await response.json();
|
|
322
322
|
return data;
|
|
323
323
|
}
|
|
324
324
|
catch (e) {
|
|
325
|
-
console.
|
|
325
|
+
console.error('Error hydratedDetailDownloadLink: ' + e.message);
|
|
326
326
|
}
|
|
327
327
|
return undefined;
|
|
328
328
|
}
|
|
329
329
|
async downloadAssortmentBaseline(assortmentPublishChange) {
|
|
330
|
-
console.info('downloadAssortmentBaseline-start');
|
|
331
330
|
try {
|
|
332
331
|
const link = assortmentPublishChange?.assortmentBaselineDownloadLink;
|
|
333
332
|
const response = await fetch(link);
|
|
@@ -335,15 +334,12 @@ class BaseProcessPublishAssortment {
|
|
|
335
334
|
return data;
|
|
336
335
|
}
|
|
337
336
|
catch (e) {
|
|
338
|
-
console.
|
|
337
|
+
console.error('Error assortmentBaselineDownloadLink: ' + e.message);
|
|
339
338
|
}
|
|
340
339
|
return undefined;
|
|
341
340
|
}
|
|
342
341
|
async getDeleteChanges(assortmentPublishChange, apcHistory, assortmentBaseline, sinceDate) {
|
|
343
|
-
console.info('getDeleteChanges(): ' + assortmentPublishChange?.id);
|
|
344
|
-
console.info(sinceDate);
|
|
345
342
|
const currentAPCIndex = apcHistory.findIndex(pc => pc?.id === assortmentPublishChange?.id);
|
|
346
|
-
console.info(' currentAPCIndex: ' + currentAPCIndex);
|
|
347
343
|
if (currentAPCIndex == 0) {
|
|
348
344
|
return [];
|
|
349
345
|
}
|
|
@@ -352,16 +348,13 @@ class BaseProcessPublishAssortment {
|
|
|
352
348
|
const apcDeletes = await this.downloadDeleteChanges(assortmentPublishChange);
|
|
353
349
|
let previousApcDate = Date.parse(apcCreatedOn);
|
|
354
350
|
const sinceDateMilliseconds = sinceDate.getTime();
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
console.info('sinceDate: ' + sinceDate);
|
|
358
|
-
console.info('sinceDateMilliseconds: ' + sinceDateMilliseconds);
|
|
359
|
-
console.info('apcDateMilliseconds: ' + previousApcDate);
|
|
360
|
-
}
|
|
351
|
+
console.info('getDeleteChanges(): apc: ' + assortmentPublishChange?.id
|
|
352
|
+
+ ', index: ' + currentAPCIndex + ', previousApcCreatedOn: ' + apcCreatedOn);
|
|
361
353
|
//if only 1 apc, no processing needed
|
|
362
354
|
if (sinceDateMilliseconds !== previousApcDate) {
|
|
363
|
-
console.info('sinceDateMilliseconds !== apcDateMilliseconds');
|
|
364
355
|
const currentAssortmentItemIds = this.getBaselineItemIds(assortmentBaseline);
|
|
356
|
+
let scannedCount = 0;
|
|
357
|
+
let addedCount = 0;
|
|
365
358
|
const deleteIds = (apcDeletes.length === 0)
|
|
366
359
|
? []
|
|
367
360
|
: apcDeletes.map(item => item?.itemId);
|
|
@@ -376,19 +369,20 @@ class BaseProcessPublishAssortment {
|
|
|
376
369
|
? apcHistory[i - 1]
|
|
377
370
|
: undefined;
|
|
378
371
|
const deleteChanges = await this.buildDeleteChanges(workingAPC, previousApc);
|
|
379
|
-
console.info('checking deleteChanges');
|
|
380
372
|
for (const dItem of deleteChanges) {
|
|
381
373
|
const dItemId = dItem?.itemId;
|
|
382
|
-
|
|
374
|
+
scannedCount++;
|
|
383
375
|
if (!currentAssortmentItemIds.includes(dItemId) && !deleteIds.includes(dItemId)) {
|
|
384
|
-
|
|
376
|
+
addedCount++;
|
|
385
377
|
deleteIds.push(dItemId);
|
|
386
378
|
apcDeletes.push(dItem);
|
|
387
379
|
}
|
|
388
380
|
}
|
|
389
381
|
}
|
|
390
382
|
}
|
|
391
|
-
console.info('getDeleteChanges()
|
|
383
|
+
console.info('getDeleteChanges(): baselineItems: ' + currentAssortmentItemIds.length
|
|
384
|
+
+ ', scanned ' + scannedCount + ' historical delete changes, added ' + addedCount
|
|
385
|
+
+ ', deleteIds: ' + deleteIds.length);
|
|
392
386
|
console.info('getDeleteChanges()-deleteIds: ' + deleteIds);
|
|
393
387
|
}
|
|
394
388
|
return apcDeletes;
|
|
@@ -402,7 +396,6 @@ class BaseProcessPublishAssortment {
|
|
|
402
396
|
return itemIds;
|
|
403
397
|
}
|
|
404
398
|
async downloadDeleteChanges(apc) {
|
|
405
|
-
console.info('deleteDataDownloadLink-start');
|
|
406
399
|
try {
|
|
407
400
|
const link = apc?.deleteDataDownloadLink;
|
|
408
401
|
const response = await fetch(link);
|
|
@@ -410,19 +403,17 @@ class BaseProcessPublishAssortment {
|
|
|
410
403
|
return data;
|
|
411
404
|
}
|
|
412
405
|
catch (e) {
|
|
413
|
-
console.
|
|
406
|
+
console.error('Error deleteDataDownloadLink: ' + e.message);
|
|
414
407
|
}
|
|
415
408
|
return undefined;
|
|
416
409
|
}
|
|
417
410
|
async buildDeleteChanges(apc, previousApc) {
|
|
418
|
-
console.info('buildDeleteChanges()');
|
|
419
411
|
if (apc?.deleteDataDownloadLink) {
|
|
420
412
|
const deleteChanges = await this.downloadDeleteChanges(apc);
|
|
421
413
|
if (deleteChanges) {
|
|
422
414
|
return deleteChanges;
|
|
423
415
|
}
|
|
424
416
|
}
|
|
425
|
-
console.info('pulling down full APC');
|
|
426
417
|
apc = await this.downloadAssortmentPublishChange(apc?.assortmentId, apc?.id);
|
|
427
418
|
if (apc?.deleteDataDownloadLink) {
|
|
428
419
|
const deleteChanges = await this.downloadDeleteChanges(apc);
|
|
@@ -434,19 +425,18 @@ class BaseProcessPublishAssortment {
|
|
|
434
425
|
throw new Error(BaseProcessPublishAssortment.NOT_ABLE_TO_PROCESS_DELETE_CHANGES);
|
|
435
426
|
}
|
|
436
427
|
let previousBaseline;
|
|
437
|
-
console.info('check previousApc');
|
|
438
428
|
if (previousApc?.assortmentBaselineDownloadLink) {
|
|
439
429
|
previousBaseline = await this.downloadAssortmentBaseline(previousApc);
|
|
440
430
|
}
|
|
441
431
|
else {
|
|
442
|
-
console.info('previousApc pulling down full APC');
|
|
443
432
|
previousApc = await this.downloadAssortmentPublishChange(previousApc?.assortmentId, previousApc?.id);
|
|
444
433
|
previousBaseline = await this.downloadAssortmentBaseline(previousApc);
|
|
445
434
|
}
|
|
446
435
|
const deleteIds = apc?.detail?.deletes.map(dItem => dItem?.id);
|
|
447
436
|
//building deletes based on previous baseline; because some APCs don't have delete data
|
|
448
437
|
const deleteArray = previousBaseline?.assortmentItems.filter(aItem => deleteIds.includes(aItem?.itemId));
|
|
449
|
-
console.info('
|
|
438
|
+
console.info('buildDeleteChanges(): no deleteDataDownloadLink on apc ' + apc?.id
|
|
439
|
+
+ '; rebuilding ' + deleteArray.length + ' deletes from previous baseline');
|
|
450
440
|
return deleteArray;
|
|
451
441
|
}
|
|
452
442
|
async getItemFederatedIds( /*itemIds*/) {
|
|
@@ -484,7 +474,6 @@ class BaseProcessPublishAssortment {
|
|
|
484
474
|
return deleteItems;
|
|
485
475
|
}
|
|
486
476
|
getReleasedForDevelopmentItemAndFamilyIds(fullChange, deleteChanges) {
|
|
487
|
-
console.info('getReleasedForDevelopmentItemAndFamilyIds');
|
|
488
477
|
const releasedForDevelopmentItemIds = [];
|
|
489
478
|
const itemFamilySet = new Set();
|
|
490
479
|
const assortmentItemsArray = fullChange?.assortmentItems;
|
|
@@ -561,7 +550,6 @@ class BaseProcessPublishAssortment {
|
|
|
561
550
|
return !!lifecycleStage && !this.config.itemPreDevelopmentLifecycleStages.includes(lifecycleStage);
|
|
562
551
|
}
|
|
563
552
|
async processPublish(pcd, changeDetail, fullChange, deleteChanges) {
|
|
564
|
-
console.info('processPublish-start');
|
|
565
553
|
const event = {
|
|
566
554
|
assortmentId: pcd.assortmentId,
|
|
567
555
|
assortmentPublishChangeId: pcd.assortmentPublishChangeId
|
|
@@ -745,7 +733,6 @@ class BaseProcessPublishAssortment {
|
|
|
745
733
|
+ '-' + d.getUTCMilliseconds();
|
|
746
734
|
}
|
|
747
735
|
getItemFamilyChanges(pcd, changeDetail, assortmentItemFullChangeMap, assortmentItemDeleteMap) {
|
|
748
|
-
console.info('getItemFamilyChanges-start');
|
|
749
736
|
const itemFamilyChanges = new Map();
|
|
750
737
|
const { adds, deletes, updates, familyItemsRemoved } = changeDetail;
|
|
751
738
|
const addIds = adds.map(item => item.id);
|
|
@@ -836,8 +823,7 @@ class BaseProcessPublishAssortment {
|
|
|
836
823
|
}
|
|
837
824
|
else {
|
|
838
825
|
if (!item) {
|
|
839
|
-
console.error('Failed to find deleted item entity');
|
|
840
|
-
console.error(' itemFamilyId: ' + itemFamilyId + ' -itemId: ' + itemId);
|
|
826
|
+
console.error('Failed to find deleted item entity: itemFamilyId: ' + itemFamilyId + ', itemId: ' + itemId);
|
|
841
827
|
continue;
|
|
842
828
|
}
|
|
843
829
|
}
|
|
@@ -878,7 +864,6 @@ class BaseProcessPublishAssortment {
|
|
|
878
864
|
return itemFamilyChanges;
|
|
879
865
|
}
|
|
880
866
|
async getEventsForPublishChangeData(publishChangeData) {
|
|
881
|
-
console.info('getEventsForPublishChangeData-start');
|
|
882
867
|
const seasonalPayloads = [];
|
|
883
868
|
for (const itemFamilyChange of publishChangeData.itemFamilyChanges.values()) {
|
|
884
869
|
const events = await this.getEventsForItemFamilyChanges(itemFamilyChange, publishChangeData.assortmentId, publishChangeData.seasonFed, publishChangeData.itemToFederatedIdMapping);
|
|
@@ -886,6 +871,8 @@ class BaseProcessPublishAssortment {
|
|
|
886
871
|
seasonalPayloads.push(...events);
|
|
887
872
|
}
|
|
888
873
|
}
|
|
874
|
+
console.info('getEventsForPublishChangeData: itemFamilies: '
|
|
875
|
+
+ publishChangeData.itemFamilyChanges.size + ', payloads: ' + seasonalPayloads.length);
|
|
889
876
|
return seasonalPayloads;
|
|
890
877
|
}
|
|
891
878
|
/**Returns the events for a given ItemFamilyChanges object
|
|
@@ -905,7 +892,6 @@ class BaseProcessPublishAssortment {
|
|
|
905
892
|
* @returns
|
|
906
893
|
*/
|
|
907
894
|
async getEventsForItemFamilyChanges(itemFamilyChanges, assortmentId, seasonFed, itemToFederatedIdMapping) {
|
|
908
|
-
console.info('getEventsForItemFamilyChanges()');
|
|
909
895
|
const events = [];
|
|
910
896
|
const LCSSeason = Object.assign({}, seasonFed);
|
|
911
897
|
const assortment = await this.getAssortment(assortmentId);
|
|
@@ -175,18 +175,12 @@ class DataConverter {
|
|
|
175
175
|
}
|
|
176
176
|
async getObjectReferenceValue(prop, newData, inflateObjRef = false) {
|
|
177
177
|
const slug = prop['slug'];
|
|
178
|
-
if (app_framework_1.Logger.isDebugOn()) {
|
|
179
|
-
console.debug('getObjectReferenceValue-prop: ' + slug);
|
|
180
|
-
}
|
|
181
178
|
let value = newData[slug];
|
|
182
179
|
const entityType = prop['referencedTypeRootSlug'];
|
|
183
180
|
const entityId = newData[slug + 'Id'];
|
|
184
181
|
if ((!value || typeof value === 'string') && inflateObjRef) {
|
|
185
182
|
if (entityId) {
|
|
186
183
|
if (this.objRefCache[entityId]) {
|
|
187
|
-
if (app_framework_1.Logger.isDebugOn()) {
|
|
188
|
-
console.debug('cache hit: ' + entityId);
|
|
189
|
-
}
|
|
190
184
|
return this.objRefCache[entityId];
|
|
191
185
|
}
|
|
192
186
|
const criteria = {
|
|
@@ -432,9 +426,6 @@ class DataConverter {
|
|
|
432
426
|
return "";
|
|
433
427
|
}
|
|
434
428
|
if (this.objRefCache[ctx.cacheKey]) {
|
|
435
|
-
if (app_framework_1.Logger.isDebugOn()) {
|
|
436
|
-
console.debug(`object reference cache hit: ${ctx.cacheKey}`);
|
|
437
|
-
}
|
|
438
429
|
return this.objRefCache[ctx.cacheKey];
|
|
439
430
|
}
|
|
440
431
|
const objectReferenceId = ctx.useIdentityService
|
|
@@ -634,9 +625,6 @@ class DataConverter {
|
|
|
634
625
|
}
|
|
635
626
|
let cacheUser = DataConverter.getFromStaticCache(nd.email);
|
|
636
627
|
if (cacheUser) {
|
|
637
|
-
if (app_framework_1.Logger.isDebugOn()) {
|
|
638
|
-
console.debug('user cache hit: ' + nd.email);
|
|
639
|
-
}
|
|
640
628
|
await this.processGroupMemberCheck(prop, nd.email);
|
|
641
629
|
return cacheUser;
|
|
642
630
|
}
|
|
@@ -706,18 +694,12 @@ class DataConverter {
|
|
|
706
694
|
*/
|
|
707
695
|
async getUserListValue(prop, newData) {
|
|
708
696
|
const slug = prop['slug'];
|
|
709
|
-
if (app_framework_1.Logger.isDebugOn()) {
|
|
710
|
-
console.debug('getUserListValue-prop: ' + slug);
|
|
711
|
-
}
|
|
712
697
|
const entityId = newData[slug + 'Id'];
|
|
713
698
|
if (!entityId) {
|
|
714
699
|
return {};
|
|
715
700
|
}
|
|
716
701
|
const cacheUser = DataConverter.getFromStaticCache(entityId);
|
|
717
702
|
if (cacheUser) {
|
|
718
|
-
if (app_framework_1.Logger.isDebugOn()) {
|
|
719
|
-
console.debug('user cache hit: ' + entityId);
|
|
720
|
-
}
|
|
721
703
|
return Object.assign({}, cacheUser);
|
|
722
704
|
}
|
|
723
705
|
const user = await this.getUserById(entityId);
|
|
@@ -5,10 +5,19 @@ export declare class FlexPLMConnect {
|
|
|
5
5
|
private staticHeaders;
|
|
6
6
|
payloadSendAsArray: boolean;
|
|
7
7
|
constructor(_config: FCConfig, endPoint?: any, payloadAsArray?: any);
|
|
8
|
+
private get sessionOptions();
|
|
9
|
+
private getSessionKey;
|
|
10
|
+
private getCachedSession;
|
|
11
|
+
private storeSession;
|
|
12
|
+
private clearSession;
|
|
13
|
+
/** Returns the session cookie value Windchill issued on this response, if any (last one wins). */
|
|
14
|
+
private extractSessionCookie;
|
|
15
|
+
private cookieHeader;
|
|
8
16
|
private getRequestOptions;
|
|
9
|
-
getCSRF(): Promise<{
|
|
17
|
+
getCSRF(getNewSession?: boolean): Promise<{
|
|
10
18
|
nonce_key: any;
|
|
11
19
|
nonce: any;
|
|
20
|
+
sessionCookie: string;
|
|
12
21
|
}>;
|
|
13
22
|
sendRequest(csrf: any, payload: any): Promise<Response>;
|
|
14
23
|
protected processRequest(payload: any): Promise<FlexPLMResponseData>;
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FlexPLMConnect = void 0;
|
|
4
4
|
const app_framework_1 = require("@contrail/app-framework");
|
|
5
|
+
// Module-level on purpose: a new FlexPLMConnect is constructed for every event. Declaring for Session resuing use case.
|
|
6
|
+
const sessionCache = new Map();
|
|
7
|
+
// Added for Session reuse - ET-390 - Ends
|
|
5
8
|
class FlexPLMConnect {
|
|
6
9
|
constructor(_config, endPoint = undefined, payloadAsArray = undefined) {
|
|
7
10
|
this.vibeEventEndpoint = '';
|
|
@@ -19,9 +22,75 @@ class FlexPLMConnect {
|
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
/////////////////////////////////////////////////////////////////////////////
|
|
25
|
+
///////// ET-390 - Session reuse: start
|
|
26
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
27
|
+
get sessionOptions() {
|
|
28
|
+
const sessReuseBool = this.config?.flexplmConnect?.sessionReuse;
|
|
29
|
+
return {
|
|
30
|
+
enabled: sessReuseBool?.enabled === true || String(sessReuseBool?.enabled).toLowerCase() === 'true',
|
|
31
|
+
ttlMs: (sessReuseBool?.ttlSeconds ?? 900) * 1000,
|
|
32
|
+
cookieName: sessReuseBool?.cookieName ?? 'JSESSIONID',
|
|
33
|
+
retryStatuses: (sessReuseBool?.retryStatuses ?? [401, 403]),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
getSessionKey() {
|
|
37
|
+
return [this.config.apiHost, this.config.urlContext, this.config.userName(), this.config.plmEnviornment].join('|');
|
|
38
|
+
}
|
|
39
|
+
getCachedSession() {
|
|
40
|
+
if (!this.sessionOptions.enabled)
|
|
41
|
+
return undefined;
|
|
42
|
+
const key = this.getSessionKey();
|
|
43
|
+
const cachedSessValue = sessionCache.get(key);
|
|
44
|
+
if (!cachedSessValue)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (cachedSessValue.expiresAt <= Date.now()) {
|
|
47
|
+
sessionCache.delete(key);
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return cachedSessValue.cookie;
|
|
51
|
+
}
|
|
52
|
+
storeSession(cookie) {
|
|
53
|
+
// sliding expiry: Windchill's session timeout is also sliding
|
|
54
|
+
sessionCache.set(this.getSessionKey(), { cookie, expiresAt: Date.now() + this.sessionOptions.ttlMs });
|
|
55
|
+
}
|
|
56
|
+
clearSession() {
|
|
57
|
+
sessionCache.delete(this.getSessionKey());
|
|
58
|
+
}
|
|
59
|
+
/** Returns the session cookie value Windchill issued on this response, if any (last one wins). */
|
|
60
|
+
extractSessionCookie(response) {
|
|
61
|
+
const respHeader = response?.headers;
|
|
62
|
+
if (!respHeader)
|
|
63
|
+
return undefined;
|
|
64
|
+
let raw = [];
|
|
65
|
+
if (typeof respHeader.getSetCookie === 'function') {
|
|
66
|
+
raw = respHeader.getSetCookie();
|
|
67
|
+
}
|
|
68
|
+
else if (typeof respHeader.get === 'function' && respHeader.get('set-cookie')) {
|
|
69
|
+
raw = [respHeader.get('set-cookie')];
|
|
70
|
+
}
|
|
71
|
+
const name = this.sessionOptions.cookieName;
|
|
72
|
+
const regexFilter = new RegExp('(?:^|[,\\s])' + name + '=([^;,\\s]+)', 'g');
|
|
73
|
+
let jSession;
|
|
74
|
+
for (const line of raw) {
|
|
75
|
+
let cookieNameMatcher = regexFilter.exec(line);
|
|
76
|
+
while (cookieNameMatcher !== null) {
|
|
77
|
+
jSession = cookieNameMatcher[1];
|
|
78
|
+
cookieNameMatcher = regexFilter.exec(line);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return jSession;
|
|
82
|
+
}
|
|
83
|
+
cookieHeader(sessionCookie) {
|
|
84
|
+
return sessionCookie ? `${this.sessionOptions.cookieName}=${sessionCookie}` : undefined;
|
|
85
|
+
}
|
|
86
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
87
|
+
///////// ET-390 - Session reuse: end
|
|
88
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
89
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
22
90
|
///////// Custom getRequestOptions: start
|
|
23
91
|
/////////////////////////////////////////////////////////////////////////////
|
|
24
|
-
getRequestOptions
|
|
92
|
+
// ET-390 - adding a new parameter sessionCookie to the getRequestOptions function
|
|
93
|
+
getRequestOptions(method, sessionCookie) {
|
|
25
94
|
const csrfOptions = {
|
|
26
95
|
method,
|
|
27
96
|
headers: {
|
|
@@ -33,9 +102,19 @@ class FlexPLMConnect {
|
|
|
33
102
|
if (this.staticHeaders) {
|
|
34
103
|
Object.assign(csrfOptions.headers, this.staticHeaders);
|
|
35
104
|
}
|
|
105
|
+
// Added for Session reuse - ET-390 - Starts
|
|
106
|
+
const cookie = this.cookieHeader(sessionCookie);
|
|
107
|
+
if (cookie) {
|
|
108
|
+
csrfOptions.headers['Cookie'] = cookie;
|
|
109
|
+
}
|
|
110
|
+
// Added for Session reuse - ET-390 - Ends
|
|
36
111
|
if (app_framework_1.Logger.isInfoOn()) {
|
|
37
112
|
const logOptions = JSON.parse(JSON.stringify(csrfOptions));
|
|
38
113
|
logOptions['headers']['Authorization'] = logOptions?.headers?.Authorization.substring(0, 9);
|
|
114
|
+
// Added for Session reuse - ET-390 - Starts
|
|
115
|
+
if (logOptions.headers['Cookie'])
|
|
116
|
+
logOptions.headers['Cookie'] = '***masked***';
|
|
117
|
+
// Added for Session reuse - ET-390 - Ends
|
|
39
118
|
console.info('csrfOptions: ' + JSON.stringify(logOptions));
|
|
40
119
|
}
|
|
41
120
|
return csrfOptions;
|
|
@@ -43,13 +122,30 @@ class FlexPLMConnect {
|
|
|
43
122
|
/////////////////////////////////////////////////////////////////////////////
|
|
44
123
|
///////// Custom getRequestOptions: end
|
|
45
124
|
/////////////////////////////////////////////////////////////////////////////
|
|
46
|
-
|
|
125
|
+
// ET-390 - adding a new parameter getNewSession to the getCSRF function
|
|
126
|
+
// Whenever a getCSRF is called without getNewSession, the default value of false will be used
|
|
127
|
+
async getCSRF(getNewSession = false) {
|
|
47
128
|
const urlContext = this.config.urlContext;
|
|
48
129
|
const csrfEndpoint = this.config.csrfEndpoint;
|
|
49
130
|
const csrfURL = this.config.apiHost + urlContext + csrfEndpoint;
|
|
50
131
|
console.info('csrfURL: ' + csrfURL);
|
|
51
|
-
|
|
52
|
-
|
|
132
|
+
// Added for Session reuse - ET-390 - Starts
|
|
133
|
+
if (getNewSession)
|
|
134
|
+
this.clearSession();
|
|
135
|
+
let sentCookie = this.getCachedSession();
|
|
136
|
+
// getRequestOptions will be called with additional sentCookie parameter
|
|
137
|
+
const csrfOptions = this.getRequestOptions('GET', sentCookie);
|
|
138
|
+
// Added for Session reuse - ET-390 - Ends
|
|
139
|
+
let response = await fetch(csrfURL, csrfOptions);
|
|
140
|
+
// Added for Session reuse - ET-390 - Starts
|
|
141
|
+
// if the response status is 401 or 403, then get a new session id
|
|
142
|
+
if (sentCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
143
|
+
console.warn('Cached FlexPLM session rejected on csrf call (status ' + response.status + '). Retrying with a new session.');
|
|
144
|
+
this.clearSession();
|
|
145
|
+
sentCookie = undefined;
|
|
146
|
+
response = await fetch(csrfURL, this.getRequestOptions('GET'));
|
|
147
|
+
}
|
|
148
|
+
// Added for Session reuse - ET-390 - Ends
|
|
53
149
|
if (response.status >= 300) {
|
|
54
150
|
const message = 'Error connecting to FlexPLM:status: ' + response.status;
|
|
55
151
|
console.error(message);
|
|
@@ -58,14 +154,25 @@ class FlexPLMConnect {
|
|
|
58
154
|
e['httpResponseStatus'] = response.status;
|
|
59
155
|
throw e;
|
|
60
156
|
}
|
|
157
|
+
// Added for Session reuse - ET-390 - Starts
|
|
158
|
+
// If the cookie we sent was invalid, Windchill answers with a fresh Set-Cookie --> replace the cache.
|
|
159
|
+
let sessionCookie = sentCookie;
|
|
160
|
+
if (this.sessionOptions.enabled) {
|
|
161
|
+
sessionCookie = this.extractSessionCookie(response) ?? sentCookie;
|
|
162
|
+
if (sessionCookie)
|
|
163
|
+
this.storeSession(sessionCookie);
|
|
164
|
+
}
|
|
165
|
+
// Added for Session reuse - ET-390 - Ends
|
|
61
166
|
try {
|
|
62
167
|
const body = await response.json();
|
|
63
168
|
const nonce_key = body.items[0].attributes.nonce_key;
|
|
64
169
|
const nonce = body.items[0].attributes.nonce;
|
|
65
170
|
console.info('nonce_key: ' + nonce_key);
|
|
171
|
+
//ET-390 - adding sessionCookie to the return
|
|
66
172
|
return {
|
|
67
173
|
nonce_key,
|
|
68
|
-
nonce
|
|
174
|
+
nonce,
|
|
175
|
+
sessionCookie
|
|
69
176
|
};
|
|
70
177
|
}
|
|
71
178
|
catch (e) {
|
|
@@ -79,7 +186,7 @@ class FlexPLMConnect {
|
|
|
79
186
|
if (this.payloadSendAsArray && !Array.isArray(payload)) {
|
|
80
187
|
payload = [payload];
|
|
81
188
|
}
|
|
82
|
-
const xferOptions = this.getRequestOptions('POST');
|
|
189
|
+
const xferOptions = this.getRequestOptions('POST', csrf?.sessionCookie);
|
|
83
190
|
xferOptions['body'] = JSON.stringify(payload);
|
|
84
191
|
xferOptions.headers[csrf.nonce_key] = csrf.nonce;
|
|
85
192
|
const urlContext = this.config.urlContext;
|
|
@@ -90,6 +197,8 @@ class FlexPLMConnect {
|
|
|
90
197
|
const logOptions = JSON.parse(JSON.stringify(xferOptions));
|
|
91
198
|
if (logOptions?.headers?.Authorization)
|
|
92
199
|
logOptions.headers.Authorization = logOptions?.headers?.Authorization.substring(0, 9);
|
|
200
|
+
if (logOptions?.headers?.Cookie)
|
|
201
|
+
logOptions.headers.Cookie = '***masked***';
|
|
93
202
|
if (csrf?.nonce_key)
|
|
94
203
|
logOptions.headers[csrf.nonce_key] = csrf.nonce ? `***masked(length:${csrf.nonce.length})***` : '***masked(empty)***';
|
|
95
204
|
console.info('csrfOptions: ' + JSON.stringify(logOptions));
|
|
@@ -107,13 +216,21 @@ class FlexPLMConnect {
|
|
|
107
216
|
if (app_framework_1.Logger.isInfoOn()) {
|
|
108
217
|
console.info('payload: ' + JSON.stringify(payload));
|
|
109
218
|
}
|
|
110
|
-
|
|
219
|
+
let csrf = await this.getCSRF();
|
|
111
220
|
if (!csrf) {
|
|
112
221
|
const message = 'Failed to get CSRF nonce';
|
|
113
222
|
console.error(message);
|
|
114
223
|
throw new Error(message);
|
|
115
224
|
}
|
|
116
|
-
|
|
225
|
+
/// Added for Session reuse - ET-390 - Starts
|
|
226
|
+
// const response = await this.sendRequest(csrf, payload);
|
|
227
|
+
let response = await this.sendRequest(csrf, payload);
|
|
228
|
+
if (csrf.sessionCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
229
|
+
console.warn('FlexPLM rejected session/nonce (status ' + response.status + '). Getting a new session and retrying once.');
|
|
230
|
+
csrf = await this.getCSRF(true);
|
|
231
|
+
response = await this.sendRequest(csrf, payload);
|
|
232
|
+
}
|
|
233
|
+
// Added for Session reuse - ET-390 - Ends
|
|
117
234
|
const status = response.status;
|
|
118
235
|
if (status >= 300) {
|
|
119
236
|
const message = 'Error sending data to FlexPLM:status: ' + response.status;
|
|
@@ -166,8 +283,20 @@ class FlexPLMConnect {
|
|
|
166
283
|
const urlContext = includeUrlContext ? this.config.urlContext : '';
|
|
167
284
|
const path = urlPath || ('/servlet/rest' + this.vibeEventEndpoint);
|
|
168
285
|
const vibeEventsURL = this.config.apiHost + urlContext + path;
|
|
169
|
-
|
|
170
|
-
const
|
|
286
|
+
// Added for Session reuse - ET-390 - Starts
|
|
287
|
+
const cached = this.getCachedSession();
|
|
288
|
+
const csrfOptions = this.getRequestOptions('GET', cached);
|
|
289
|
+
let response = await fetch(vibeEventsURL, csrfOptions);
|
|
290
|
+
if (cached && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
291
|
+
this.clearSession();
|
|
292
|
+
response = await fetch(vibeEventsURL, this.getRequestOptions('GET'));
|
|
293
|
+
}
|
|
294
|
+
if (this.sessionOptions.enabled) {
|
|
295
|
+
const cachedCookie = this.extractSessionCookie(response) ?? cached;
|
|
296
|
+
if (cachedCookie)
|
|
297
|
+
this.storeSession(cachedCookie);
|
|
298
|
+
}
|
|
299
|
+
// Added for Session reuse - ET-390 - Ends
|
|
171
300
|
if (response.status >= 300) {
|
|
172
301
|
const message = 'Error connecting to FlexPLM:status: ' + response.status;
|
|
173
302
|
console.error(message);
|
|
@@ -28,6 +28,22 @@ const createConfig = (overrides) => ({
|
|
|
28
28
|
useDistinctRestEndPointForImages: false,
|
|
29
29
|
...overrides,
|
|
30
30
|
});
|
|
31
|
+
// Added for Session reuse - ET-390 - Starts
|
|
32
|
+
const csrfBody = { items: [{ attributes: { nonce_key: 'CSRF_NONCE', nonce: 'n1' } }] };
|
|
33
|
+
// Response with Set-Cookie support. setCookie = list of raw Set-Cookie header lines.
|
|
34
|
+
const resp = (status, setCookie = [], json = csrfBody) => ({
|
|
35
|
+
status,
|
|
36
|
+
json: jest.fn().mockResolvedValue(json),
|
|
37
|
+
text: jest.fn().mockResolvedValue('error text'),
|
|
38
|
+
headers: { getSetCookie: () => setCookie, get: () => null },
|
|
39
|
+
});
|
|
40
|
+
let userCounter = 0;
|
|
41
|
+
const uniqueUser = () => 'sess-user-' + (++userCounter);
|
|
42
|
+
const sessionConfig = (user, sessionReuse = { enabled: true }) => createConfig({ userName: () => user, flexplmConnect: { sessionReuse } });
|
|
43
|
+
const fetchMock = () => global.fetch;
|
|
44
|
+
const callHeaders = (i) => fetchMock().mock.calls[i][1].headers;
|
|
45
|
+
const callMethod = (i) => fetchMock().mock.calls[i][1].method;
|
|
46
|
+
// Added for Session reuse - ET-390 - Ends
|
|
31
47
|
describe('FlexPLMConnect.getRequest', () => {
|
|
32
48
|
it('should call default URL with urlContext when no params provided', async () => {
|
|
33
49
|
const connect = new flexplm_connect_1.FlexPLMConnect(createConfig());
|
|
@@ -121,3 +137,249 @@ describe('FlexPLMConnect.sendRequest', () => {
|
|
|
121
137
|
consoleInfoSpy.mockRestore();
|
|
122
138
|
});
|
|
123
139
|
});
|
|
140
|
+
// Added for Session reuse - ET-390 - Starts
|
|
141
|
+
describe('FlexPLMConnect session reuse', () => {
|
|
142
|
+
const payload = { id: '123' };
|
|
143
|
+
it('captures JSESSIONID from the csrf GET and sends it on the POST', async () => {
|
|
144
|
+
const u = uniqueUser();
|
|
145
|
+
fetchMock()
|
|
146
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1; Path=/; HttpOnly']))
|
|
147
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
148
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
149
|
+
expect(fetchMock()).toHaveBeenCalledTimes(2);
|
|
150
|
+
expect(callHeaders(0).Cookie).toBeUndefined(); // first ever call: no cached session
|
|
151
|
+
expect(callHeaders(1).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
152
|
+
expect(callHeaders(1).CSRF_NONCE).toBe('n1');
|
|
153
|
+
});
|
|
154
|
+
it('reuses the cached session on the next event (new FlexPLMConnect instance)', async () => {
|
|
155
|
+
const u = uniqueUser();
|
|
156
|
+
fetchMock()
|
|
157
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1; Path=/'])) // event 1 GET
|
|
158
|
+
.mockResolvedValueOnce(resp(200, [], {})) // event 1 POST
|
|
159
|
+
.mockResolvedValueOnce(resp(200, [])) // event 2 GET (valid session -> no Set-Cookie)
|
|
160
|
+
.mockResolvedValueOnce(resp(200, [], {})); // event 2 POST
|
|
161
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
162
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
163
|
+
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1'); // GET reuses it
|
|
164
|
+
expect(callHeaders(3).Cookie).toBe('JSESSIONID=abc.tomcat1'); // POST reuses it
|
|
165
|
+
});
|
|
166
|
+
it('replaces the cached session when Windchill answers the GET with a new Set-Cookie', async () => {
|
|
167
|
+
const u = uniqueUser();
|
|
168
|
+
fetchMock()
|
|
169
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1; Path=/']))
|
|
170
|
+
.mockResolvedValueOnce(resp(200, [], {}))
|
|
171
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1; Path=/'])) // old one was stale -> new issued
|
|
172
|
+
.mockResolvedValueOnce(resp(200, [], {}))
|
|
173
|
+
.mockResolvedValueOnce(resp(200, []))
|
|
174
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
175
|
+
const connect = () => new flexplm_connect_1.FlexPLMConnect(sessionConfig(u));
|
|
176
|
+
await connect().sendToFlexPLM(payload);
|
|
177
|
+
await connect().sendToFlexPLM(payload);
|
|
178
|
+
await connect().sendToFlexPLM(payload);
|
|
179
|
+
expect(callHeaders(2).Cookie).toBe('JSESSIONID=old.tomcat1'); // event 2 GET sent the old one
|
|
180
|
+
expect(callHeaders(3).Cookie).toBe('JSESSIONID=new.tomcat1'); // POST uses the replacement
|
|
181
|
+
expect(callHeaders(4).Cookie).toBe('JSESSIONID=new.tomcat1'); // event 3 GET sends the replacement
|
|
182
|
+
});
|
|
183
|
+
it('retries once with a new session when the POST is rejected with 403', async () => {
|
|
184
|
+
const u = uniqueUser();
|
|
185
|
+
fetchMock()
|
|
186
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1; Path=/'])) // GET
|
|
187
|
+
.mockResolvedValueOnce(resp(403, [], {})) // POST rejected
|
|
188
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1; Path=/'])) // GET again, fresh session
|
|
189
|
+
.mockResolvedValueOnce(resp(200, [], { ok: true })); // POST succeeds
|
|
190
|
+
const result = await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
191
|
+
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
192
|
+
expect(callHeaders(2).Cookie).toBeUndefined(); // retry GET must NOT send the stale cookie
|
|
193
|
+
expect(callHeaders(3).Cookie).toBe('JSESSIONID=new.tomcat1');
|
|
194
|
+
expect(result.status).toBe(200);
|
|
195
|
+
expect(result.data).toEqual({ ok: true });
|
|
196
|
+
});
|
|
197
|
+
it('also retries on 401', async () => {
|
|
198
|
+
const u = uniqueUser();
|
|
199
|
+
fetchMock()
|
|
200
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
201
|
+
.mockResolvedValueOnce(resp(401, [], {}))
|
|
202
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
|
|
203
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
204
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
205
|
+
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
206
|
+
});
|
|
207
|
+
it('does not loop: a second 403 after the retry is surfaced as an error', async () => {
|
|
208
|
+
const u = uniqueUser();
|
|
209
|
+
fetchMock()
|
|
210
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
211
|
+
.mockResolvedValueOnce(resp(403, [], {}))
|
|
212
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
|
|
213
|
+
.mockResolvedValueOnce(resp(403, [], {}));
|
|
214
|
+
await expect(new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload))
|
|
215
|
+
.rejects.toThrow('Error sending data to FlexPLM:status: 403');
|
|
216
|
+
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
217
|
+
});
|
|
218
|
+
it('does not retry on non-auth errors such as 500 (event may already be processed)', async () => {
|
|
219
|
+
const u = uniqueUser();
|
|
220
|
+
fetchMock()
|
|
221
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
222
|
+
.mockResolvedValueOnce(resp(500, [], {}));
|
|
223
|
+
await expect(new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload))
|
|
224
|
+
.rejects.toThrow('Error sending data to FlexPLM:status: 500');
|
|
225
|
+
expect(fetchMock()).toHaveBeenCalledTimes(2);
|
|
226
|
+
});
|
|
227
|
+
it('retries the csrf GET without a cookie when the cached session is rejected there', async () => {
|
|
228
|
+
const u = uniqueUser();
|
|
229
|
+
fetchMock()
|
|
230
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1'])) // event 1 GET
|
|
231
|
+
.mockResolvedValueOnce(resp(200, [], {})) // event 1 POST
|
|
232
|
+
.mockResolvedValueOnce(resp(401, [])) // event 2 GET with cached cookie rejected
|
|
233
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1'])) // event 2 GET retry, no cookie
|
|
234
|
+
.mockResolvedValueOnce(resp(200, [], {})); // event 2 POST
|
|
235
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
236
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
237
|
+
expect(callHeaders(2).Cookie).toBe('JSESSIONID=old.tomcat1');
|
|
238
|
+
expect(callHeaders(3).Cookie).toBeUndefined();
|
|
239
|
+
expect(callHeaders(4).Cookie).toBe('JSESSIONID=new.tomcat1');
|
|
240
|
+
});
|
|
241
|
+
it('expires the cached session after ttlSeconds', async () => {
|
|
242
|
+
const u = uniqueUser();
|
|
243
|
+
const realNow = Date.now();
|
|
244
|
+
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(realNow);
|
|
245
|
+
fetchMock()
|
|
246
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
247
|
+
.mockResolvedValueOnce(resp(200, [], {}))
|
|
248
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=def.tomcat1']))
|
|
249
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
250
|
+
const cfg = sessionConfig(u, { enabled: true, ttlSeconds: 1 });
|
|
251
|
+
await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
|
|
252
|
+
nowSpy.mockReturnValue(realNow + 2000); // 2s later > 1s ttl
|
|
253
|
+
await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
|
|
254
|
+
expect(callHeaders(2).Cookie).toBeUndefined(); // expired -> GET sends no cookie
|
|
255
|
+
expect(callHeaders(3).Cookie).toBe('JSESSIONID=def.tomcat1');
|
|
256
|
+
nowSpy.mockRestore();
|
|
257
|
+
});
|
|
258
|
+
it('uses the last JSESSIONID when several Set-Cookie lines are returned', async () => {
|
|
259
|
+
const u = uniqueUser();
|
|
260
|
+
fetchMock()
|
|
261
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=first.tomcat1; Path=/', 'OTHER=x; Path=/', 'JSESSIONID=second.tomcat1; Path=/']))
|
|
262
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
263
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
264
|
+
expect(callHeaders(1).Cookie).toBe('JSESSIONID=second.tomcat1');
|
|
265
|
+
});
|
|
266
|
+
it('falls back to headers.get("set-cookie") when getSetCookie is not available', async () => {
|
|
267
|
+
const u = uniqueUser();
|
|
268
|
+
const legacy = {
|
|
269
|
+
status: 200,
|
|
270
|
+
json: jest.fn().mockResolvedValue(csrfBody),
|
|
271
|
+
text: jest.fn(),
|
|
272
|
+
headers: { get: (k) => (k.toLowerCase() === 'set-cookie' ? 'JSESSIONID=legacy.tomcat1; Path=/' : null) },
|
|
273
|
+
};
|
|
274
|
+
fetchMock().mockResolvedValueOnce(legacy).mockResolvedValueOnce(resp(200, [], {}));
|
|
275
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
276
|
+
expect(callHeaders(1).Cookie).toBe('JSESSIONID=legacy.tomcat1');
|
|
277
|
+
});
|
|
278
|
+
it('honours a custom cookieName', async () => {
|
|
279
|
+
const u = uniqueUser();
|
|
280
|
+
fetchMock()
|
|
281
|
+
.mockResolvedValueOnce(resp(200, ['MYSESSION=zzz; Path=/', 'JSESSIONID=ignored; Path=/']))
|
|
282
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
283
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'MYSESSION' })).sendToFlexPLM(payload);
|
|
284
|
+
expect(callHeaders(1).Cookie).toBe('MYSESSION=zzz');
|
|
285
|
+
});
|
|
286
|
+
it('accepts enabled as the string "true"', async () => {
|
|
287
|
+
const u = uniqueUser();
|
|
288
|
+
fetchMock()
|
|
289
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
290
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
291
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: 'true' })).sendToFlexPLM(payload);
|
|
292
|
+
expect(callHeaders(1).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
293
|
+
});
|
|
294
|
+
it('keeps separate sessions per user', async () => {
|
|
295
|
+
const a = uniqueUser();
|
|
296
|
+
const b = uniqueUser();
|
|
297
|
+
fetchMock()
|
|
298
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=aaa.tomcat1']))
|
|
299
|
+
.mockResolvedValueOnce(resp(200, [], {}))
|
|
300
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=bbb.tomcat1']))
|
|
301
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
302
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(a)).sendToFlexPLM(payload);
|
|
303
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(b)).sendToFlexPLM(payload);
|
|
304
|
+
expect(callHeaders(2).Cookie).toBeUndefined(); // user b must not see user a's cookie
|
|
305
|
+
expect(callHeaders(3).Cookie).toBe('JSESSIONID=bbb.tomcat1');
|
|
306
|
+
});
|
|
307
|
+
it('never logs the cookie value', async () => {
|
|
308
|
+
const isInfoOn = jest.spyOn(app_framework_1.Logger, 'isInfoOn').mockReturnValue(true);
|
|
309
|
+
const info = jest.spyOn(console, 'info').mockImplementation();
|
|
310
|
+
const u = uniqueUser();
|
|
311
|
+
fetchMock()
|
|
312
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=supersecret.tomcat1']))
|
|
313
|
+
.mockResolvedValueOnce(resp(200, [], {}))
|
|
314
|
+
.mockResolvedValueOnce(resp(200, []))
|
|
315
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
316
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
317
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload); // 2nd event sends Cookie on GET and POST
|
|
318
|
+
const logged = info.mock.calls.map(c => String(c[0] ?? '')).join('\n');
|
|
319
|
+
expect(logged).toContain('"Cookie":"***masked***"');
|
|
320
|
+
expect(logged).not.toContain('supersecret');
|
|
321
|
+
isInfoOn.mockRestore();
|
|
322
|
+
info.mockRestore();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
describe('FlexPLMConnect session reuse - feature disabled (default)', () => {
|
|
326
|
+
const payload = { id: '123' };
|
|
327
|
+
it('sends no Cookie header and makes exactly one GET + one POST', async () => {
|
|
328
|
+
const u = uniqueUser();
|
|
329
|
+
fetchMock()
|
|
330
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1'])) // Set-Cookie present but must be ignored
|
|
331
|
+
.mockResolvedValueOnce(resp(200, [], {}))
|
|
332
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=def.tomcat1']))
|
|
333
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
334
|
+
const cfg = createConfig({ userName: () => u }); // no flexplmConnect at all
|
|
335
|
+
await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
|
|
336
|
+
await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
|
|
337
|
+
for (let i = 0; i < 4; i++) {
|
|
338
|
+
expect(callHeaders(i).Cookie).toBeUndefined();
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
it('does not retry on 403 (behaves exactly as before)', async () => {
|
|
342
|
+
const u = uniqueUser();
|
|
343
|
+
fetchMock()
|
|
344
|
+
.mockResolvedValueOnce(resp(200))
|
|
345
|
+
.mockResolvedValueOnce(resp(403, [], {}));
|
|
346
|
+
await expect(new flexplm_connect_1.FlexPLMConnect(createConfig({ userName: () => u })).sendToFlexPLM(payload))
|
|
347
|
+
.rejects.toThrow('Error sending data to FlexPLM:status: 403');
|
|
348
|
+
expect(fetchMock()).toHaveBeenCalledTimes(2);
|
|
349
|
+
});
|
|
350
|
+
it('explicit enabled:false behaves the same as not configured', async () => {
|
|
351
|
+
const u = uniqueUser();
|
|
352
|
+
fetchMock()
|
|
353
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
354
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
355
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: false })).sendToFlexPLM(payload);
|
|
356
|
+
expect(callHeaders(1).Cookie).toBeUndefined();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
describe('FlexPLMConnect.getRequest session reuse (only if you applied step 2g)', () => {
|
|
360
|
+
it('sends the cached cookie on image/GET requests', async () => {
|
|
361
|
+
const u = uniqueUser();
|
|
362
|
+
fetchMock()
|
|
363
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1'])) // csrf GET primes the cache
|
|
364
|
+
.mockResolvedValueOnce(resp(200, [], {})) // POST
|
|
365
|
+
.mockResolvedValueOnce(resp(200, [], { items: [] })); // getRequest
|
|
366
|
+
const connect = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u));
|
|
367
|
+
await connect.sendToFlexPLM({ id: '1' });
|
|
368
|
+
await connect.getRequest();
|
|
369
|
+
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
370
|
+
});
|
|
371
|
+
it('retries once without the cookie when the cached session is rejected on getRequest', async () => {
|
|
372
|
+
const u = uniqueUser();
|
|
373
|
+
fetchMock()
|
|
374
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
375
|
+
.mockResolvedValueOnce(resp(200, [], {}))
|
|
376
|
+
.mockResolvedValueOnce(resp(401, [])) // getRequest with cached cookie
|
|
377
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1'], { items: [] })); // retry, no cookie
|
|
378
|
+
const connect = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u));
|
|
379
|
+
await connect.sendToFlexPLM({ id: '1' });
|
|
380
|
+
await connect.getRequest();
|
|
381
|
+
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
382
|
+
expect(callHeaders(3).Cookie).toBeUndefined();
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
// Added for Session reuse - ET-390 - Ends
|
package/package.json
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@contrail/flexplm",
|
|
3
|
-
"version": "1.7.
|
|
4
|
-
"description": "Library used for integration with flexplm.",
|
|
5
|
-
"main": "lib/index.js",
|
|
6
|
-
"types": "lib/index.d.ts",
|
|
7
|
-
"bin": {
|
|
8
|
-
"flexplm-mapping": "lib/cli/index.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"lib/**/*",
|
|
12
|
-
"scripts/copy-template.js"
|
|
13
|
-
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"build": "tsc; node scripts/copy-template.js",
|
|
16
|
-
"build:win": "tsc && node scripts/copy-template.js",
|
|
17
|
-
"format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
|
|
18
|
-
"lint": "tslint -p tsconfig.json",
|
|
19
|
-
"test": "jest",
|
|
20
|
-
"test-watch": "jest --watch",
|
|
21
|
-
"test-debug": "jest --runInBand",
|
|
22
|
-
"test-coverage": "jest --coverage"
|
|
23
|
-
},
|
|
24
|
-
"keywords": [],
|
|
25
|
-
"author": "VibeIQ",
|
|
26
|
-
"license": "ISC",
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"@types/jest": "^29.5.2",
|
|
29
|
-
"jest": "^29.5.0",
|
|
30
|
-
"prettier": "^1.19.1",
|
|
31
|
-
"ts-jest": "^29.1.1",
|
|
32
|
-
"tslint": "^5.11.0",
|
|
33
|
-
"tslint-config-prettier": "^1.18.0",
|
|
34
|
-
"typescript": "^4.0.0"
|
|
35
|
-
},
|
|
36
|
-
"peerDependencies": {
|
|
37
|
-
"typescript": ">=4.0.0"
|
|
38
|
-
},
|
|
39
|
-
"peerDependenciesMeta": {
|
|
40
|
-
"typescript": {
|
|
41
|
-
"optional": true
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
"jest": {
|
|
45
|
-
"moduleFileExtensions": [
|
|
46
|
-
"js",
|
|
47
|
-
"json",
|
|
48
|
-
"ts"
|
|
49
|
-
],
|
|
50
|
-
"rootDir": "src",
|
|
51
|
-
"testRegex": ".spec.ts$",
|
|
52
|
-
"transform": {
|
|
53
|
-
"^.+\\.(t|j)s$": "ts-jest"
|
|
54
|
-
},
|
|
55
|
-
"coverageDirectory": "../coverage",
|
|
56
|
-
"testEnvironment": "node"
|
|
57
|
-
},
|
|
58
|
-
"dependencies": {
|
|
59
|
-
"@contrail/app-framework": "^1.4.3",
|
|
60
|
-
"@contrail/sdk": "^1.5.10",
|
|
61
|
-
"@contrail/transform-data": "^1.3.2",
|
|
62
|
-
"@contrail/util": "^1.3.1",
|
|
63
|
-
"axios": "^1.4.0",
|
|
64
|
-
"p-limit": "^3.1.0"
|
|
65
|
-
}
|
|
66
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@contrail/flexplm",
|
|
3
|
+
"version": "1.7.6-alpha.a804d5a",
|
|
4
|
+
"description": "Library used for integration with flexplm.",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"flexplm-mapping": "lib/cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"lib/**/*",
|
|
12
|
+
"scripts/copy-template.js"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc; node scripts/copy-template.js",
|
|
16
|
+
"build:win": "tsc && node scripts/copy-template.js",
|
|
17
|
+
"format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
|
|
18
|
+
"lint": "tslint -p tsconfig.json",
|
|
19
|
+
"test": "jest",
|
|
20
|
+
"test-watch": "jest --watch",
|
|
21
|
+
"test-debug": "jest --runInBand",
|
|
22
|
+
"test-coverage": "jest --coverage"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [],
|
|
25
|
+
"author": "VibeIQ",
|
|
26
|
+
"license": "ISC",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/jest": "^29.5.2",
|
|
29
|
+
"jest": "^29.5.0",
|
|
30
|
+
"prettier": "^1.19.1",
|
|
31
|
+
"ts-jest": "^29.1.1",
|
|
32
|
+
"tslint": "^5.11.0",
|
|
33
|
+
"tslint-config-prettier": "^1.18.0",
|
|
34
|
+
"typescript": "^4.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"typescript": ">=4.0.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"typescript": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"jest": {
|
|
45
|
+
"moduleFileExtensions": [
|
|
46
|
+
"js",
|
|
47
|
+
"json",
|
|
48
|
+
"ts"
|
|
49
|
+
],
|
|
50
|
+
"rootDir": "src",
|
|
51
|
+
"testRegex": ".spec.ts$",
|
|
52
|
+
"transform": {
|
|
53
|
+
"^.+\\.(t|j)s$": "ts-jest"
|
|
54
|
+
},
|
|
55
|
+
"coverageDirectory": "../coverage",
|
|
56
|
+
"testEnvironment": "node"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@contrail/app-framework": "^1.4.3",
|
|
60
|
+
"@contrail/sdk": "^1.5.10",
|
|
61
|
+
"@contrail/transform-data": "^1.3.2",
|
|
62
|
+
"@contrail/util": "^1.3.1",
|
|
63
|
+
"axios": "^1.4.0",
|
|
64
|
+
"p-limit": "^3.1.0"
|
|
65
|
+
}
|
|
66
|
+
}
|