@contrail/flexplm 1.7.7-alpha.e66ae52 → 1.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/interfaces/interfaces.d.ts +0 -4
- package/lib/publish/base-process-publish-assortment.js +4 -2
- package/lib/publish/base-process-publish-assortment.spec.js +106 -0
- package/lib/util/flexplm-connect.d.ts +4 -20
- package/lib/util/flexplm-connect.js +10 -269
- package/lib/util/flexplm-connect.spec.js +0 -500
- package/package.json +1 -1
|
@@ -6,10 +6,6 @@ export interface FCConfig {
|
|
|
6
6
|
eventObjectClassParam?: string;
|
|
7
7
|
eventFlexTypePathParam?: string;
|
|
8
8
|
flexplmConnect?: any;
|
|
9
|
-
'flexplmConnect.sessionReuse.enabled'?: boolean | string;
|
|
10
|
-
'flexplmConnect.sessionReuse.ttlSeconds'?: number | string;
|
|
11
|
-
'flexplmConnect.sessionReuse.cookieName'?: string;
|
|
12
|
-
'flexplmConnect.sessionReuse.retryStatuses'?: number | number[] | string;
|
|
13
9
|
taskId?: string;
|
|
14
10
|
apiHost: string;
|
|
15
11
|
userName(): string;
|
|
@@ -909,7 +909,8 @@ class BaseProcessPublishAssortment {
|
|
|
909
909
|
? itemFamilyChanges.assortmentItemFullChangeMap.get(entityReference)?.item
|
|
910
910
|
: itemFamilyChanges.itemFamilyObject;
|
|
911
911
|
let seasonalData = await this.getSeasonalData(projectItem);
|
|
912
|
-
|
|
912
|
+
const productSeasonMapKey = (await type_conversion_utils_1.TypeConversionUtils.getMapKey(this.transformMapFile, this.mapFileUtil, projectItem, type_conversion_utils_1.TypeConversionUtils.VIBE2FLEX_DIRECTION)) || 'LCSProductSeasonLink';
|
|
913
|
+
seasonalData = await map_utils_1.MapUtil.applyTransformMap(this.transformMapFile, this.mapFileUtil, seasonalData, productSeasonMapKey, 'vibe2flex');
|
|
913
914
|
const LCSProduct = await this.getProductFederation(entityReference, itemToFederatedIdMapping, prodEntityData);
|
|
914
915
|
const isCreatable = await type_conversion_utils_1.TypeConversionUtils.isOutboundCreatableFromEntity(this.transformMapFile, this.mapFileUtil, projectItem, { item: prodEntityData, assortment });
|
|
915
916
|
const eventType = isCreatable ? 'UPSERT_ON_SEASON' : 'UPDATE_ON_SEASON';
|
|
@@ -944,7 +945,8 @@ class BaseProcessPublishAssortment {
|
|
|
944
945
|
}
|
|
945
946
|
const projectItem = this.getProjectItem(itemFamilyChanges, entityReference);
|
|
946
947
|
let seasonalData = await this.getSeasonalData(projectItem);
|
|
947
|
-
|
|
948
|
+
const skuSeasonMapKey = (await type_conversion_utils_1.TypeConversionUtils.getMapKey(this.transformMapFile, this.mapFileUtil, projectItem, type_conversion_utils_1.TypeConversionUtils.VIBE2FLEX_DIRECTION)) || 'LCSSKUSeasonLink';
|
|
949
|
+
seasonalData = await map_utils_1.MapUtil.applyTransformMap(this.transformMapFile, this.mapFileUtil, seasonalData, skuSeasonMapKey, 'vibe2flex');
|
|
948
950
|
const LCSSKU = await this.getSKUFederation(entityReference, itemToFederatedIdMapping, item);
|
|
949
951
|
const isCreatable = await type_conversion_utils_1.TypeConversionUtils.isOutboundCreatableFromEntity(this.transformMapFile, this.mapFileUtil, projectItem, { item, assortment });
|
|
950
952
|
const eventType = isCreatable ? 'UPSERT_ON_SEASON' : 'UPDATE_ON_SEASON';
|
|
@@ -1509,6 +1509,9 @@ describe('getEventsForItemFamilyChanges - conditional eventType', () => {
|
|
|
1509
1509
|
};
|
|
1510
1510
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1511
1511
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
1512
|
+
beforeEach(() => {
|
|
1513
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getMapKey').mockResolvedValue(null);
|
|
1514
|
+
});
|
|
1512
1515
|
afterEach(() => {
|
|
1513
1516
|
jest.restoreAllMocks();
|
|
1514
1517
|
});
|
|
@@ -1705,6 +1708,108 @@ describe('getEventsForItemFamilyChanges - conditional eventType', () => {
|
|
|
1705
1708
|
// Verify SKU event called with assortment entity
|
|
1706
1709
|
expect(type_conversion_utils_1.TypeConversionUtils.isOutboundCreatableFromEntity).toHaveBeenNthCalledWith(2, undefined, mapFileUtil, colorProjectItem, { item: itemData, assortment });
|
|
1707
1710
|
});
|
|
1711
|
+
it('should use dynamic map key for LCSSKUSeasonLink when getMapKey returns a custom key', async () => {
|
|
1712
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1713
|
+
const itemFamilyId = 'family123';
|
|
1714
|
+
const colorId = 'color456';
|
|
1715
|
+
const familyItemData = { id: itemFamilyId, identifier: 'FAM-123', itemNumber: 'FAM-123' };
|
|
1716
|
+
const familyProjectItem = { id: 'proj0', updatedOn: '2023-01-15T00:00:00.000Z' };
|
|
1717
|
+
const colorProjectItem = { id: 'proj1', updatedOn: '2023-01-15T00:00:00.000Z' };
|
|
1718
|
+
const assortment = { id: 'assort123', name: 'Fall 2023 Assortment', publishToFlexPLM: true };
|
|
1719
|
+
const seasonFed = { entityReference: 'assortment:assort123', objectClass: 'LCSSeason', flexPLMSeasonName: 'Fall 2023' };
|
|
1720
|
+
const seasonalData = { someData: 'value' };
|
|
1721
|
+
const itemFamilyChanges = new item_family_changes_1.ItemFamilyChanges(itemFamilyId, new Date('2023-01-01T00:00:00.000Z'));
|
|
1722
|
+
itemFamilyChanges.colorUpdates.push(colorId);
|
|
1723
|
+
itemFamilyChanges.assortmentItemFullChangeMap.set(itemFamilyId, { item: familyItemData, projectItem: familyProjectItem });
|
|
1724
|
+
itemFamilyChanges.assortmentItemFullChangeMap.set(colorId, { item: { id: colorId, identifier: 'COLOR-456' }, projectItem: colorProjectItem });
|
|
1725
|
+
jest.spyOn(bppa, 'getAssortment').mockResolvedValue(assortment);
|
|
1726
|
+
jest.spyOn(bppa, 'getProjectItem')
|
|
1727
|
+
.mockReturnValueOnce(familyProjectItem)
|
|
1728
|
+
.mockReturnValueOnce(colorProjectItem);
|
|
1729
|
+
jest.spyOn(bppa, 'getSeasonalData').mockResolvedValue(seasonalData);
|
|
1730
|
+
jest.spyOn(map_utils_1.MapUtil, 'applyTransformMap').mockResolvedValue({ transformedData: 'value' });
|
|
1731
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierProperties').mockResolvedValue(['identifier']);
|
|
1732
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getInformationalProperties').mockResolvedValue([]);
|
|
1733
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'isOutboundCreatableFromEntity').mockResolvedValue(false);
|
|
1734
|
+
// First call: product path (return null → falls back to default)
|
|
1735
|
+
// Second call: SKU path (return custom key)
|
|
1736
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getMapKey')
|
|
1737
|
+
.mockResolvedValueOnce(null)
|
|
1738
|
+
.mockResolvedValueOnce('LCSSKUSeasonLinkFlexOwned');
|
|
1739
|
+
await bppa.getEventsForItemFamilyChanges(itemFamilyChanges, 'assort123', seasonFed, new Map());
|
|
1740
|
+
expect(map_utils_1.MapUtil.applyTransformMap).toHaveBeenCalledWith(undefined, mapFileUtil, seasonalData, 'LCSSKUSeasonLinkFlexOwned', 'vibe2flex');
|
|
1741
|
+
});
|
|
1742
|
+
it('should fall back to LCSSKUSeasonLink when getMapKey returns null', async () => {
|
|
1743
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1744
|
+
const itemFamilyId = 'family123';
|
|
1745
|
+
const colorId = 'color456';
|
|
1746
|
+
const familyItemData = { id: itemFamilyId, identifier: 'FAM-123', itemNumber: 'FAM-123' };
|
|
1747
|
+
const familyProjectItem = { id: 'proj0', updatedOn: '2023-01-15T00:00:00.000Z' };
|
|
1748
|
+
const colorProjectItem = { id: 'proj1', updatedOn: '2023-01-15T00:00:00.000Z' };
|
|
1749
|
+
const assortment = { id: 'assort123', name: 'Fall 2023 Assortment', publishToFlexPLM: true };
|
|
1750
|
+
const seasonFed = { entityReference: 'assortment:assort123', objectClass: 'LCSSeason', flexPLMSeasonName: 'Fall 2023' };
|
|
1751
|
+
const seasonalData = { someData: 'value' };
|
|
1752
|
+
const itemFamilyChanges = new item_family_changes_1.ItemFamilyChanges(itemFamilyId, new Date('2023-01-01T00:00:00.000Z'));
|
|
1753
|
+
itemFamilyChanges.colorUpdates.push(colorId);
|
|
1754
|
+
itemFamilyChanges.assortmentItemFullChangeMap.set(itemFamilyId, { item: familyItemData, projectItem: familyProjectItem });
|
|
1755
|
+
itemFamilyChanges.assortmentItemFullChangeMap.set(colorId, { item: { id: colorId, identifier: 'COLOR-456' }, projectItem: colorProjectItem });
|
|
1756
|
+
jest.spyOn(bppa, 'getAssortment').mockResolvedValue(assortment);
|
|
1757
|
+
jest.spyOn(bppa, 'getProjectItem')
|
|
1758
|
+
.mockReturnValueOnce(familyProjectItem)
|
|
1759
|
+
.mockReturnValueOnce(colorProjectItem);
|
|
1760
|
+
jest.spyOn(bppa, 'getSeasonalData').mockResolvedValue(seasonalData);
|
|
1761
|
+
jest.spyOn(map_utils_1.MapUtil, 'applyTransformMap').mockResolvedValue({ transformedData: 'value' });
|
|
1762
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierProperties').mockResolvedValue(['identifier']);
|
|
1763
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getInformationalProperties').mockResolvedValue([]);
|
|
1764
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'isOutboundCreatableFromEntity').mockResolvedValue(false);
|
|
1765
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getMapKey').mockResolvedValue(null);
|
|
1766
|
+
await bppa.getEventsForItemFamilyChanges(itemFamilyChanges, 'assort123', seasonFed, new Map());
|
|
1767
|
+
expect(map_utils_1.MapUtil.applyTransformMap).toHaveBeenCalledWith(undefined, mapFileUtil, seasonalData, 'LCSSKUSeasonLink', 'vibe2flex');
|
|
1768
|
+
});
|
|
1769
|
+
it('should use dynamic map key for LCSProductSeasonLink when getMapKey returns a custom key', async () => {
|
|
1770
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1771
|
+
const itemFamilyId = 'family123';
|
|
1772
|
+
const prodEntityData = { id: itemFamilyId, identifier: 'FAM-123', itemNumber: 'FAM-123' };
|
|
1773
|
+
const projectItem = { id: 'proj1', updatedOn: '2023-01-15T00:00:00.000Z' };
|
|
1774
|
+
const assortment = { id: 'assort123', name: 'Fall 2023 Assortment', publishToFlexPLM: true };
|
|
1775
|
+
const seasonFed = { entityReference: 'assortment:assort123', objectClass: 'LCSSeason', flexPLMSeasonName: 'Fall 2023' };
|
|
1776
|
+
const seasonalData = { someData: 'value' };
|
|
1777
|
+
const itemFamilyChanges = new item_family_changes_1.ItemFamilyChanges(itemFamilyId, new Date('2023-01-01T00:00:00.000Z'));
|
|
1778
|
+
itemFamilyChanges.familyUpdate = true;
|
|
1779
|
+
itemFamilyChanges.assortmentItemFullChangeMap.set(itemFamilyId, { item: prodEntityData, projectItem });
|
|
1780
|
+
jest.spyOn(bppa, 'getAssortment').mockResolvedValue(assortment);
|
|
1781
|
+
jest.spyOn(bppa, 'getProjectItem').mockReturnValue(projectItem);
|
|
1782
|
+
jest.spyOn(bppa, 'getSeasonalData').mockResolvedValue(seasonalData);
|
|
1783
|
+
jest.spyOn(map_utils_1.MapUtil, 'applyTransformMap').mockResolvedValue({ transformedData: 'value' });
|
|
1784
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierProperties').mockResolvedValue(['identifier']);
|
|
1785
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getInformationalProperties').mockResolvedValue([]);
|
|
1786
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'isOutboundCreatableFromEntity').mockResolvedValue(false);
|
|
1787
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getMapKey').mockResolvedValue('LCSProductSeasonLinkCustom');
|
|
1788
|
+
await bppa.getEventsForItemFamilyChanges(itemFamilyChanges, 'assort123', seasonFed, new Map());
|
|
1789
|
+
expect(map_utils_1.MapUtil.applyTransformMap).toHaveBeenCalledWith(undefined, mapFileUtil, seasonalData, 'LCSProductSeasonLinkCustom', 'vibe2flex');
|
|
1790
|
+
});
|
|
1791
|
+
it('should fall back to LCSProductSeasonLink when getMapKey returns null', async () => {
|
|
1792
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1793
|
+
const itemFamilyId = 'family123';
|
|
1794
|
+
const prodEntityData = { id: itemFamilyId, identifier: 'FAM-123', itemNumber: 'FAM-123' };
|
|
1795
|
+
const projectItem = { id: 'proj1', updatedOn: '2023-01-15T00:00:00.000Z' };
|
|
1796
|
+
const assortment = { id: 'assort123', name: 'Fall 2023 Assortment', publishToFlexPLM: true };
|
|
1797
|
+
const seasonFed = { entityReference: 'assortment:assort123', objectClass: 'LCSSeason', flexPLMSeasonName: 'Fall 2023' };
|
|
1798
|
+
const seasonalData = { someData: 'value' };
|
|
1799
|
+
const itemFamilyChanges = new item_family_changes_1.ItemFamilyChanges(itemFamilyId, new Date('2023-01-01T00:00:00.000Z'));
|
|
1800
|
+
itemFamilyChanges.familyUpdate = true;
|
|
1801
|
+
itemFamilyChanges.assortmentItemFullChangeMap.set(itemFamilyId, { item: prodEntityData, projectItem });
|
|
1802
|
+
jest.spyOn(bppa, 'getAssortment').mockResolvedValue(assortment);
|
|
1803
|
+
jest.spyOn(bppa, 'getProjectItem').mockReturnValue(projectItem);
|
|
1804
|
+
jest.spyOn(bppa, 'getSeasonalData').mockResolvedValue(seasonalData);
|
|
1805
|
+
jest.spyOn(map_utils_1.MapUtil, 'applyTransformMap').mockResolvedValue({ transformedData: 'value' });
|
|
1806
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierProperties').mockResolvedValue(['identifier']);
|
|
1807
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getInformationalProperties').mockResolvedValue([]);
|
|
1808
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'isOutboundCreatableFromEntity').mockResolvedValue(false);
|
|
1809
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getMapKey').mockResolvedValue(null);
|
|
1810
|
+
await bppa.getEventsForItemFamilyChanges(itemFamilyChanges, 'assort123', seasonFed, new Map());
|
|
1811
|
+
expect(map_utils_1.MapUtil.applyTransformMap).toHaveBeenCalledWith(undefined, mapFileUtil, seasonalData, 'LCSProductSeasonLink', 'vibe2flex');
|
|
1812
|
+
});
|
|
1708
1813
|
});
|
|
1709
1814
|
describe('sendToFlexPLM / handleVibeIQFile / sendPublishPayloadEvent', () => {
|
|
1710
1815
|
const config = {
|
|
@@ -1932,6 +2037,7 @@ describe('released family with pre-development options', () => {
|
|
|
1932
2037
|
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierProperties').mockResolvedValue(['itemNumber']);
|
|
1933
2038
|
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getInformationalProperties').mockResolvedValue([]);
|
|
1934
2039
|
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'isOutboundCreatableFromEntity').mockResolvedValue(true);
|
|
2040
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getMapKey').mockResolvedValue(null);
|
|
1935
2041
|
};
|
|
1936
2042
|
afterEach(() => { jest.restoreAllMocks(); });
|
|
1937
2043
|
/////////////////////////////////////////////////////////////////////////////
|
|
@@ -5,27 +5,11 @@ 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
|
-
/**
|
|
10
|
-
* Waits until no cold session acquisition is running for this key.
|
|
11
|
-
* Callers must `await` this only when it returns a promise: awaiting even a resolved promise yields,
|
|
12
|
-
* and the check-then-register in runColdAcquisition has to stay synchronous to be race free.
|
|
13
|
-
*/
|
|
14
|
-
private waitForInFlightAcquisition;
|
|
15
|
-
/** Runs a cold acquisition and registers it so concurrent callers wait for it instead of opening their own session. */
|
|
16
|
-
private runColdAcquisition;
|
|
17
|
-
private getSessionKey;
|
|
18
|
-
/** Releases the connection of a rejected response before it is replaced by a retry. */
|
|
19
|
-
private discardBody;
|
|
20
|
-
private getCachedSession;
|
|
21
|
-
private storeSession;
|
|
22
|
-
private clearSession;
|
|
23
|
-
/** Returns the session cookie value Windchill issued on this response, if any (last one wins). */
|
|
24
|
-
private extractSessionCookie;
|
|
25
|
-
private cookieHeader;
|
|
26
8
|
private getRequestOptions;
|
|
27
|
-
getCSRF(
|
|
28
|
-
|
|
9
|
+
getCSRF(): Promise<{
|
|
10
|
+
nonce_key: any;
|
|
11
|
+
nonce: any;
|
|
12
|
+
}>;
|
|
29
13
|
sendRequest(csrf: any, payload: any): Promise<Response>;
|
|
30
14
|
protected processRequest(payload: any): Promise<FlexPLMResponseData>;
|
|
31
15
|
sendToFlexPLM(payload: PayloadType): Promise<FlexPLMResponseData>;
|
|
@@ -2,43 +2,6 @@
|
|
|
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 reusing use case.
|
|
6
|
-
const sessionCache = new Map();
|
|
7
|
-
// In-flight cold session acquisitions per key, so concurrent events share one FlexPLM session instead of each creating their own.
|
|
8
|
-
const inFlightAcquisitions = new Map();
|
|
9
|
-
const DEFAULT_SESSION_COOKIE_NAME = 'JSESSIONID';
|
|
10
|
-
const DEFAULT_SESSION_TTL_SECONDS = 900;
|
|
11
|
-
const DEFAULT_RETRY_STATUSES = [401, 403];
|
|
12
|
-
const COOKIE_NAME_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
13
|
-
// Thrown internally when a cached cookie was rejected, so the caller can re-acquire under the lock.
|
|
14
|
-
class SessionRejectedError extends Error {
|
|
15
|
-
}
|
|
16
|
-
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17
|
-
const parseRetryStatuses = (value) => {
|
|
18
|
-
let list = [];
|
|
19
|
-
if (Array.isArray(value))
|
|
20
|
-
list = value;
|
|
21
|
-
else if (typeof value === 'string')
|
|
22
|
-
list = value.split(',');
|
|
23
|
-
else if (value !== undefined && value !== null)
|
|
24
|
-
list = [value];
|
|
25
|
-
const statuses = list
|
|
26
|
-
.map(status => (typeof status === 'string' ? status.trim() : status))
|
|
27
|
-
.filter(status => status !== '' && status !== null && status !== undefined)
|
|
28
|
-
.map(Number)
|
|
29
|
-
// Only client-error statuses can mean "session rejected". 2xx/5xx would resend events the server already processed.
|
|
30
|
-
.filter(status => Number.isInteger(status) && status >= 400 && status <= 499);
|
|
31
|
-
return statuses.length ? statuses : DEFAULT_RETRY_STATUSES;
|
|
32
|
-
};
|
|
33
|
-
const parseTtlSeconds = (value) => {
|
|
34
|
-
const ttl = Number(value);
|
|
35
|
-
return Number.isFinite(ttl) && ttl > 0 ? ttl : DEFAULT_SESSION_TTL_SECONDS;
|
|
36
|
-
};
|
|
37
|
-
const parseCookieName = (value) => {
|
|
38
|
-
const name = typeof value === 'string' ? value.trim() : '';
|
|
39
|
-
return COOKIE_NAME_TOKEN.test(name) ? name : DEFAULT_SESSION_COOKIE_NAME;
|
|
40
|
-
};
|
|
41
|
-
// Added for Session reuse - ET-390 - Ends
|
|
42
5
|
class FlexPLMConnect {
|
|
43
6
|
constructor(_config, endPoint = undefined, payloadAsArray = undefined) {
|
|
44
7
|
this.vibeEventEndpoint = '';
|
|
@@ -56,134 +19,9 @@ class FlexPLMConnect {
|
|
|
56
19
|
}
|
|
57
20
|
}
|
|
58
21
|
/////////////////////////////////////////////////////////////////////////////
|
|
59
|
-
///////// ET-390 - Session reuse: start
|
|
60
|
-
/////////////////////////////////////////////////////////////////////////////
|
|
61
|
-
/*
|
|
62
|
-
private get sessionOptions() {
|
|
63
|
-
const sessReuseBool = this.config?.flexplmConnect?.sessionReuse;
|
|
64
|
-
console.info('sessReuseBool value is --> ' + sessReuseBool);
|
|
65
|
-
console.info('enabled value is --> ' + sessReuseBool?.enabled);
|
|
66
|
-
return {
|
|
67
|
-
enabled: sessReuseBool?.enabled === true || String(sessReuseBool?.enabled).toLowerCase() === 'true',
|
|
68
|
-
ttlMs: (sessReuseBool?.ttlSeconds ?? 900) * 1000,
|
|
69
|
-
cookieName: sessReuseBool?.cookieName ?? 'JSESSIONID',
|
|
70
|
-
retryStatuses: (sessReuseBool?.retryStatuses ?? [401, 403]) as number[],
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
*/
|
|
74
|
-
get sessionOptions() {
|
|
75
|
-
const cfg = this.config ?? {};
|
|
76
|
-
const sessReuseEntries = (name) => cfg[`flexplmConnect.sessionReuse.${name}`];
|
|
77
|
-
const enabled = sessReuseEntries('enabled');
|
|
78
|
-
return {
|
|
79
|
-
enabled: enabled === true || String(enabled).toLowerCase() === 'true',
|
|
80
|
-
ttlMs: parseTtlSeconds(sessReuseEntries('ttlSeconds')) * 1000,
|
|
81
|
-
cookieName: parseCookieName(sessReuseEntries('cookieName')),
|
|
82
|
-
retryStatuses: parseRetryStatuses(sessReuseEntries('retryStatuses')),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Waits until no cold session acquisition is running for this key.
|
|
87
|
-
* Callers must `await` this only when it returns a promise: awaiting even a resolved promise yields,
|
|
88
|
-
* and the check-then-register in runColdAcquisition has to stay synchronous to be race free.
|
|
89
|
-
*/
|
|
90
|
-
waitForInFlightAcquisition() {
|
|
91
|
-
const key = this.getSessionKey();
|
|
92
|
-
if (!inFlightAcquisitions.has(key))
|
|
93
|
-
return undefined;
|
|
94
|
-
return (async () => {
|
|
95
|
-
let pending = inFlightAcquisitions.get(key);
|
|
96
|
-
while (pending) {
|
|
97
|
-
await pending.catch(() => undefined);
|
|
98
|
-
pending = inFlightAcquisitions.get(key);
|
|
99
|
-
}
|
|
100
|
-
})();
|
|
101
|
-
}
|
|
102
|
-
/** Runs a cold acquisition and registers it so concurrent callers wait for it instead of opening their own session. */
|
|
103
|
-
async runColdAcquisition(acquire) {
|
|
104
|
-
const key = this.getSessionKey();
|
|
105
|
-
const acquisition = acquire();
|
|
106
|
-
inFlightAcquisitions.set(key, acquisition);
|
|
107
|
-
try {
|
|
108
|
-
return await acquisition;
|
|
109
|
-
}
|
|
110
|
-
finally {
|
|
111
|
-
if (inFlightAcquisitions.get(key) === acquisition)
|
|
112
|
-
inFlightAcquisitions.delete(key);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
getSessionKey() {
|
|
116
|
-
// Options that change the cached value are part of the key; JSON avoids '|' collisions between fields.
|
|
117
|
-
const { cookieName, ttlMs } = this.sessionOptions;
|
|
118
|
-
return JSON.stringify([
|
|
119
|
-
this.config.apiHost, this.config.urlContext, this.config.userName(), this.config.plmEnviornment, cookieName, ttlMs,
|
|
120
|
-
]);
|
|
121
|
-
}
|
|
122
|
-
/** Releases the connection of a rejected response before it is replaced by a retry. */
|
|
123
|
-
async discardBody(response) {
|
|
124
|
-
try {
|
|
125
|
-
if (typeof response?.body?.cancel === 'function')
|
|
126
|
-
await response.body.cancel();
|
|
127
|
-
}
|
|
128
|
-
catch (e) {
|
|
129
|
-
// best effort: nothing to do if the body is already consumed or locked
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
getCachedSession() {
|
|
133
|
-
if (!this.sessionOptions.enabled)
|
|
134
|
-
return undefined;
|
|
135
|
-
const key = this.getSessionKey();
|
|
136
|
-
const cachedSessValue = sessionCache.get(key);
|
|
137
|
-
if (!cachedSessValue)
|
|
138
|
-
return undefined;
|
|
139
|
-
if (cachedSessValue.expiresAt <= Date.now()) {
|
|
140
|
-
sessionCache.delete(key);
|
|
141
|
-
return undefined;
|
|
142
|
-
}
|
|
143
|
-
return cachedSessValue.cookie;
|
|
144
|
-
}
|
|
145
|
-
storeSession(cookie) {
|
|
146
|
-
// sliding expiry: Windchill's session timeout is also sliding
|
|
147
|
-
sessionCache.set(this.getSessionKey(), { cookie, expiresAt: Date.now() + this.sessionOptions.ttlMs });
|
|
148
|
-
}
|
|
149
|
-
clearSession() {
|
|
150
|
-
sessionCache.delete(this.getSessionKey());
|
|
151
|
-
}
|
|
152
|
-
/** Returns the session cookie value Windchill issued on this response, if any (last one wins). */
|
|
153
|
-
extractSessionCookie(response) {
|
|
154
|
-
const respHeader = response?.headers;
|
|
155
|
-
if (!respHeader)
|
|
156
|
-
return undefined;
|
|
157
|
-
let raw = [];
|
|
158
|
-
if (typeof respHeader.getSetCookie === 'function') {
|
|
159
|
-
raw = respHeader.getSetCookie();
|
|
160
|
-
}
|
|
161
|
-
else if (typeof respHeader.get === 'function' && respHeader.get('set-cookie')) {
|
|
162
|
-
raw = [respHeader.get('set-cookie')];
|
|
163
|
-
}
|
|
164
|
-
const name = this.sessionOptions.cookieName;
|
|
165
|
-
const regexFilter = new RegExp('(?:^|[,\\s])' + escapeRegExp(name) + '=([^;,\\s]+)', 'g');
|
|
166
|
-
let jSession;
|
|
167
|
-
for (const line of raw) {
|
|
168
|
-
let cookieNameMatcher = regexFilter.exec(line);
|
|
169
|
-
while (cookieNameMatcher !== null) {
|
|
170
|
-
jSession = cookieNameMatcher[1];
|
|
171
|
-
cookieNameMatcher = regexFilter.exec(line);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return jSession;
|
|
175
|
-
}
|
|
176
|
-
cookieHeader(sessionCookie) {
|
|
177
|
-
return sessionCookie ? `${this.sessionOptions.cookieName}=${sessionCookie}` : undefined;
|
|
178
|
-
}
|
|
179
|
-
/////////////////////////////////////////////////////////////////////////////
|
|
180
|
-
///////// ET-390 - Session reuse: end
|
|
181
|
-
/////////////////////////////////////////////////////////////////////////////
|
|
182
|
-
/////////////////////////////////////////////////////////////////////////////
|
|
183
22
|
///////// Custom getRequestOptions: start
|
|
184
23
|
/////////////////////////////////////////////////////////////////////////////
|
|
185
|
-
|
|
186
|
-
getRequestOptions(method, sessionCookie) {
|
|
24
|
+
getRequestOptions(method) {
|
|
187
25
|
const csrfOptions = {
|
|
188
26
|
method,
|
|
189
27
|
headers: {
|
|
@@ -195,19 +33,9 @@ class FlexPLMConnect {
|
|
|
195
33
|
if (this.staticHeaders) {
|
|
196
34
|
Object.assign(csrfOptions.headers, this.staticHeaders);
|
|
197
35
|
}
|
|
198
|
-
// Added for Session reuse - ET-390 - Starts
|
|
199
|
-
const cookie = this.cookieHeader(sessionCookie);
|
|
200
|
-
if (cookie) {
|
|
201
|
-
csrfOptions.headers['Cookie'] = cookie;
|
|
202
|
-
}
|
|
203
|
-
// Added for Session reuse - ET-390 - Ends
|
|
204
36
|
if (app_framework_1.Logger.isInfoOn()) {
|
|
205
37
|
const logOptions = JSON.parse(JSON.stringify(csrfOptions));
|
|
206
38
|
logOptions['headers']['Authorization'] = logOptions?.headers?.Authorization.substring(0, 9);
|
|
207
|
-
// Added for Session reuse - ET-390 - Starts
|
|
208
|
-
if (logOptions.headers['Cookie'])
|
|
209
|
-
logOptions.headers['Cookie'] = '***masked***';
|
|
210
|
-
// Added for Session reuse - ET-390 - Ends
|
|
211
39
|
console.info('csrfOptions: ' + JSON.stringify(logOptions));
|
|
212
40
|
}
|
|
213
41
|
return csrfOptions;
|
|
@@ -215,52 +43,13 @@ class FlexPLMConnect {
|
|
|
215
43
|
/////////////////////////////////////////////////////////////////////////////
|
|
216
44
|
///////// Custom getRequestOptions: end
|
|
217
45
|
/////////////////////////////////////////////////////////////////////////////
|
|
218
|
-
|
|
219
|
-
// Whenever a getCSRF is called without getNewSession, the default value of false will be used
|
|
220
|
-
// rejectedCookie: the cookie that was rejected by the caller, so a refresh already done by a concurrent event is not thrown away
|
|
221
|
-
async getCSRF(getNewSession = false, rejectedCookie) {
|
|
222
|
-
// Added for Session reuse - ET-390 - Starts
|
|
223
|
-
if (!this.sessionOptions.enabled)
|
|
224
|
-
return this.fetchCSRF(undefined);
|
|
225
|
-
for (let wait = this.waitForInFlightAcquisition(); wait; wait = this.waitForInFlightAcquisition())
|
|
226
|
-
await wait;
|
|
227
|
-
if (getNewSession) {
|
|
228
|
-
const current = this.getCachedSession();
|
|
229
|
-
if (!current || current === rejectedCookie)
|
|
230
|
-
this.clearSession();
|
|
231
|
-
}
|
|
232
|
-
const cached = this.getCachedSession();
|
|
233
|
-
if (cached) {
|
|
234
|
-
try {
|
|
235
|
-
return await this.fetchCSRF(cached);
|
|
236
|
-
}
|
|
237
|
-
catch (e) {
|
|
238
|
-
if (!(e instanceof SessionRejectedError))
|
|
239
|
-
throw e;
|
|
240
|
-
// if the cached cookie is rejected (401/403), drop it and acquire a new session (serialized)
|
|
241
|
-
if (this.getCachedSession() === cached)
|
|
242
|
-
this.clearSession();
|
|
243
|
-
return this.getCSRF();
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return this.runColdAcquisition(() => this.fetchCSRF(undefined));
|
|
247
|
-
// Added for Session reuse - ET-390 - Ends
|
|
248
|
-
}
|
|
249
|
-
async fetchCSRF(sentCookie) {
|
|
46
|
+
async getCSRF() {
|
|
250
47
|
const urlContext = this.config.urlContext;
|
|
251
48
|
const csrfEndpoint = this.config.csrfEndpoint;
|
|
252
49
|
const csrfURL = this.config.apiHost + urlContext + csrfEndpoint;
|
|
253
50
|
console.info('csrfURL: ' + csrfURL);
|
|
254
|
-
|
|
255
|
-
const csrfOptions = this.getRequestOptions('GET', sentCookie);
|
|
51
|
+
const csrfOptions = this.getRequestOptions('GET');
|
|
256
52
|
const response = await fetch(csrfURL, csrfOptions);
|
|
257
|
-
// Added for Session reuse - ET-390 - Starts
|
|
258
|
-
if (sentCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
259
|
-
console.warn('Cached FlexPLM session rejected on csrf call (status ' + response.status + '). Retrying with a new session.');
|
|
260
|
-
await this.discardBody(response);
|
|
261
|
-
throw new SessionRejectedError();
|
|
262
|
-
}
|
|
263
|
-
// Added for Session reuse - ET-390 - Ends
|
|
264
53
|
if (response.status >= 300) {
|
|
265
54
|
const message = 'Error connecting to FlexPLM:status: ' + response.status;
|
|
266
55
|
console.error(message);
|
|
@@ -269,24 +58,14 @@ class FlexPLMConnect {
|
|
|
269
58
|
e['httpResponseStatus'] = response.status;
|
|
270
59
|
throw e;
|
|
271
60
|
}
|
|
272
|
-
// Added for Session reuse - ET-390 - Starts
|
|
273
|
-
// If the cookie we sent was invalid, Windchill answers with a fresh Set-Cookie --> replace the cache.
|
|
274
|
-
let sessionCookie = sentCookie;
|
|
275
|
-
if (this.sessionOptions.enabled) {
|
|
276
|
-
sessionCookie = this.extractSessionCookie(response) ?? sentCookie;
|
|
277
|
-
if (sessionCookie)
|
|
278
|
-
this.storeSession(sessionCookie);
|
|
279
|
-
}
|
|
280
|
-
// Added for Session reuse - ET-390 - Ends
|
|
281
61
|
try {
|
|
282
62
|
const body = await response.json();
|
|
283
63
|
const nonce_key = body.items[0].attributes.nonce_key;
|
|
284
64
|
const nonce = body.items[0].attributes.nonce;
|
|
285
|
-
|
|
65
|
+
console.info('nonce_key: ' + nonce_key);
|
|
286
66
|
return {
|
|
287
67
|
nonce_key,
|
|
288
|
-
nonce
|
|
289
|
-
sessionCookie,
|
|
68
|
+
nonce
|
|
290
69
|
};
|
|
291
70
|
}
|
|
292
71
|
catch (e) {
|
|
@@ -300,7 +79,7 @@ class FlexPLMConnect {
|
|
|
300
79
|
if (this.payloadSendAsArray && !Array.isArray(payload)) {
|
|
301
80
|
payload = [payload];
|
|
302
81
|
}
|
|
303
|
-
const xferOptions = this.getRequestOptions('POST'
|
|
82
|
+
const xferOptions = this.getRequestOptions('POST');
|
|
304
83
|
xferOptions['body'] = JSON.stringify(payload);
|
|
305
84
|
xferOptions.headers[csrf.nonce_key] = csrf.nonce;
|
|
306
85
|
const urlContext = this.config.urlContext;
|
|
@@ -311,8 +90,6 @@ class FlexPLMConnect {
|
|
|
311
90
|
const logOptions = JSON.parse(JSON.stringify(xferOptions));
|
|
312
91
|
if (logOptions?.headers?.Authorization)
|
|
313
92
|
logOptions.headers.Authorization = logOptions?.headers?.Authorization.substring(0, 9);
|
|
314
|
-
if (logOptions?.headers?.Cookie)
|
|
315
|
-
logOptions.headers.Cookie = '***masked***';
|
|
316
93
|
if (csrf?.nonce_key)
|
|
317
94
|
logOptions.headers[csrf.nonce_key] = csrf.nonce ? `***masked(length:${csrf.nonce.length})***` : '***masked(empty)***';
|
|
318
95
|
console.info('csrfOptions: ' + JSON.stringify(logOptions));
|
|
@@ -330,22 +107,13 @@ class FlexPLMConnect {
|
|
|
330
107
|
if (app_framework_1.Logger.isInfoOn()) {
|
|
331
108
|
console.info('payload: ' + JSON.stringify(payload));
|
|
332
109
|
}
|
|
333
|
-
|
|
110
|
+
const csrf = await this.getCSRF();
|
|
334
111
|
if (!csrf) {
|
|
335
112
|
const message = 'Failed to get CSRF nonce';
|
|
336
113
|
console.error(message);
|
|
337
114
|
throw new Error(message);
|
|
338
115
|
}
|
|
339
|
-
|
|
340
|
-
// const response = await this.sendRequest(csrf, payload);
|
|
341
|
-
let response = await this.sendRequest(csrf, payload);
|
|
342
|
-
if (csrf.sessionCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
343
|
-
console.warn('FlexPLM rejected session/nonce (status ' + response.status + '). Getting a new session and retrying once.');
|
|
344
|
-
await this.discardBody(response);
|
|
345
|
-
csrf = await this.getCSRF(true, csrf.sessionCookie);
|
|
346
|
-
response = await this.sendRequest(csrf, payload);
|
|
347
|
-
}
|
|
348
|
-
// Added for Session reuse - ET-390 - Ends
|
|
116
|
+
const response = await this.sendRequest(csrf, payload);
|
|
349
117
|
const status = response.status;
|
|
350
118
|
if (status >= 300) {
|
|
351
119
|
const message = 'Error sending data to FlexPLM:status: ' + response.status;
|
|
@@ -398,35 +166,8 @@ class FlexPLMConnect {
|
|
|
398
166
|
const urlContext = includeUrlContext ? this.config.urlContext : '';
|
|
399
167
|
const path = urlPath || ('/servlet/rest' + this.vibeEventEndpoint);
|
|
400
168
|
const vibeEventsURL = this.config.apiHost + urlContext + path;
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
if (!this.sessionOptions.enabled) {
|
|
404
|
-
return { response: await fetch(vibeEventsURL, this.getRequestOptions('GET')), sent: undefined };
|
|
405
|
-
}
|
|
406
|
-
for (let wait = this.waitForInFlightAcquisition(); wait; wait = this.waitForInFlightAcquisition())
|
|
407
|
-
await wait;
|
|
408
|
-
const sent = this.getCachedSession();
|
|
409
|
-
const call = async () => {
|
|
410
|
-
const res = await fetch(vibeEventsURL, this.getRequestOptions('GET', sent));
|
|
411
|
-
// A rejected cookie must not be stored again; only keep a cookie that was accepted or newly issued.
|
|
412
|
-
if (!(sent && this.sessionOptions.retryStatuses.includes(res.status))) {
|
|
413
|
-
const cookie = this.extractSessionCookie(res) ?? sent;
|
|
414
|
-
if (cookie)
|
|
415
|
-
this.storeSession(cookie);
|
|
416
|
-
}
|
|
417
|
-
return { response: res, sent };
|
|
418
|
-
};
|
|
419
|
-
return sent ? call() : this.runColdAcquisition(call);
|
|
420
|
-
};
|
|
421
|
-
const first = await attempt();
|
|
422
|
-
let response = first.response;
|
|
423
|
-
if (first.sent && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
424
|
-
await this.discardBody(response);
|
|
425
|
-
if (this.getCachedSession() === first.sent)
|
|
426
|
-
this.clearSession();
|
|
427
|
-
response = (await attempt()).response;
|
|
428
|
-
}
|
|
429
|
-
// Added for Session reuse - ET-390 - Ends
|
|
169
|
+
const csrfOptions = this.getRequestOptions('GET');
|
|
170
|
+
const response = await fetch(vibeEventsURL, csrfOptions);
|
|
430
171
|
if (response.status >= 300) {
|
|
431
172
|
const message = 'Error connecting to FlexPLM:status: ' + response.status;
|
|
432
173
|
console.error(message);
|
|
@@ -28,27 +28,6 @@ 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
|
-
// The connector config is flat: keys look like 'flexplmConnect.sessionReuse.enabled'
|
|
43
|
-
const sessionConfig = (user, sessionReuse = { enabled: true }) => {
|
|
44
|
-
const flat = {};
|
|
45
|
-
Object.entries(sessionReuse).forEach(([k, v]) => { flat[`flexplmConnect.sessionReuse.${k}`] = v; });
|
|
46
|
-
return createConfig({ userName: () => user, ...flat });
|
|
47
|
-
};
|
|
48
|
-
const fetchMock = () => global.fetch;
|
|
49
|
-
const callHeaders = (i) => fetchMock().mock.calls[i][1].headers;
|
|
50
|
-
const callMethod = (i) => fetchMock().mock.calls[i][1].method;
|
|
51
|
-
// Added for Session reuse - ET-390 - Ends
|
|
52
31
|
describe('FlexPLMConnect.getRequest', () => {
|
|
53
32
|
it('should call default URL with urlContext when no params provided', async () => {
|
|
54
33
|
const connect = new flexplm_connect_1.FlexPLMConnect(createConfig());
|
|
@@ -142,482 +121,3 @@ describe('FlexPLMConnect.sendRequest', () => {
|
|
|
142
121
|
consoleInfoSpy.mockRestore();
|
|
143
122
|
});
|
|
144
123
|
});
|
|
145
|
-
// Added for Session reuse - ET-390 - Starts
|
|
146
|
-
describe('FlexPLMConnect session reuse', () => {
|
|
147
|
-
const payload = { id: '123' };
|
|
148
|
-
it('should capture JSESSIONID from the CSRF GET and send it on the POST', async () => {
|
|
149
|
-
const u = uniqueUser();
|
|
150
|
-
fetchMock()
|
|
151
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1; Path=/; HttpOnly']))
|
|
152
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
153
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
154
|
-
expect(fetchMock()).toHaveBeenCalledTimes(2);
|
|
155
|
-
expect(callHeaders(0).Cookie).toBeUndefined(); // first ever call: no cached session
|
|
156
|
-
expect(callHeaders(1).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
157
|
-
expect(callHeaders(1).CSRF_NONCE).toBe('n1');
|
|
158
|
-
});
|
|
159
|
-
it('reuses the cached session on the next event (new FlexPLMConnect instance)', async () => {
|
|
160
|
-
const u = uniqueUser();
|
|
161
|
-
fetchMock()
|
|
162
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1; Path=/'])) // event 1 GET
|
|
163
|
-
.mockResolvedValueOnce(resp(200, [], {})) // event 1 POST
|
|
164
|
-
.mockResolvedValueOnce(resp(200, [])) // event 2 GET (valid session -> no Set-Cookie)
|
|
165
|
-
.mockResolvedValueOnce(resp(200, [], {})); // event 2 POST
|
|
166
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
167
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
168
|
-
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1'); // GET reuses it
|
|
169
|
-
expect(callHeaders(3).Cookie).toBe('JSESSIONID=abc.tomcat1'); // POST reuses it
|
|
170
|
-
});
|
|
171
|
-
it('replaces the cached session when Windchill answers the GET with a new Set-Cookie', async () => {
|
|
172
|
-
const u = uniqueUser();
|
|
173
|
-
fetchMock()
|
|
174
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1; Path=/']))
|
|
175
|
-
.mockResolvedValueOnce(resp(200, [], {}))
|
|
176
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1; Path=/'])) // old one was stale -> new issued
|
|
177
|
-
.mockResolvedValueOnce(resp(200, [], {}))
|
|
178
|
-
.mockResolvedValueOnce(resp(200, []))
|
|
179
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
180
|
-
const connect = () => new flexplm_connect_1.FlexPLMConnect(sessionConfig(u));
|
|
181
|
-
await connect().sendToFlexPLM(payload);
|
|
182
|
-
await connect().sendToFlexPLM(payload);
|
|
183
|
-
await connect().sendToFlexPLM(payload);
|
|
184
|
-
expect(callHeaders(2).Cookie).toBe('JSESSIONID=old.tomcat1'); // event 2 GET sent the old one
|
|
185
|
-
expect(callHeaders(3).Cookie).toBe('JSESSIONID=new.tomcat1'); // POST uses the replacement
|
|
186
|
-
expect(callHeaders(4).Cookie).toBe('JSESSIONID=new.tomcat1'); // event 3 GET sends the replacement
|
|
187
|
-
});
|
|
188
|
-
it('retries once with a new session when the POST is rejected with 403', async () => {
|
|
189
|
-
const u = uniqueUser();
|
|
190
|
-
fetchMock()
|
|
191
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1; Path=/'])) // GET
|
|
192
|
-
.mockResolvedValueOnce(resp(403, [], {})) // POST rejected
|
|
193
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1; Path=/'])) // GET again, fresh session
|
|
194
|
-
.mockResolvedValueOnce(resp(200, [], { ok: true })); // POST succeeds
|
|
195
|
-
const result = await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
196
|
-
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
197
|
-
expect(callHeaders(2).Cookie).toBeUndefined(); // retry GET must NOT send the stale cookie
|
|
198
|
-
expect(callHeaders(3).Cookie).toBe('JSESSIONID=new.tomcat1');
|
|
199
|
-
expect(result.status).toBe(200);
|
|
200
|
-
expect(result.data).toEqual({ ok: true });
|
|
201
|
-
});
|
|
202
|
-
it('also retries on 401', async () => {
|
|
203
|
-
const u = uniqueUser();
|
|
204
|
-
fetchMock()
|
|
205
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
206
|
-
.mockResolvedValueOnce(resp(401, [], {}))
|
|
207
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
|
|
208
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
209
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
210
|
-
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
211
|
-
});
|
|
212
|
-
it('does not loop: a second 403 after the retry is surfaced as an error', async () => {
|
|
213
|
-
const u = uniqueUser();
|
|
214
|
-
fetchMock()
|
|
215
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
216
|
-
.mockResolvedValueOnce(resp(403, [], {}))
|
|
217
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
|
|
218
|
-
.mockResolvedValueOnce(resp(403, [], {}));
|
|
219
|
-
await expect(new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload))
|
|
220
|
-
.rejects.toThrow('Error sending data to FlexPLM:status: 403');
|
|
221
|
-
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
222
|
-
});
|
|
223
|
-
it('does not retry on non-auth errors such as 500 (event may already be processed)', async () => {
|
|
224
|
-
const u = uniqueUser();
|
|
225
|
-
fetchMock()
|
|
226
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
227
|
-
.mockResolvedValueOnce(resp(500, [], {}));
|
|
228
|
-
await expect(new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload))
|
|
229
|
-
.rejects.toThrow('Error sending data to FlexPLM:status: 500');
|
|
230
|
-
expect(fetchMock()).toHaveBeenCalledTimes(2);
|
|
231
|
-
});
|
|
232
|
-
it('retries the csrf GET without a cookie when the cached session is rejected there', async () => {
|
|
233
|
-
const u = uniqueUser();
|
|
234
|
-
fetchMock()
|
|
235
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1'])) // event 1 GET
|
|
236
|
-
.mockResolvedValueOnce(resp(200, [], {})) // event 1 POST
|
|
237
|
-
.mockResolvedValueOnce(resp(401, [])) // event 2 GET with cached cookie rejected
|
|
238
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1'])) // event 2 GET retry, no cookie
|
|
239
|
-
.mockResolvedValueOnce(resp(200, [], {})); // event 2 POST
|
|
240
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
241
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
242
|
-
expect(callHeaders(2).Cookie).toBe('JSESSIONID=old.tomcat1');
|
|
243
|
-
expect(callHeaders(3).Cookie).toBeUndefined();
|
|
244
|
-
expect(callHeaders(4).Cookie).toBe('JSESSIONID=new.tomcat1');
|
|
245
|
-
});
|
|
246
|
-
it('expires the cached session after ttlSeconds', async () => {
|
|
247
|
-
const u = uniqueUser();
|
|
248
|
-
const realNow = Date.now();
|
|
249
|
-
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(realNow);
|
|
250
|
-
fetchMock()
|
|
251
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
252
|
-
.mockResolvedValueOnce(resp(200, [], {}))
|
|
253
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=def.tomcat1']))
|
|
254
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
255
|
-
const cfg = sessionConfig(u, { enabled: true, ttlSeconds: 1 });
|
|
256
|
-
await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
|
|
257
|
-
nowSpy.mockReturnValue(realNow + 2000); // 2s later > 1s ttl
|
|
258
|
-
await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
|
|
259
|
-
expect(callHeaders(2).Cookie).toBeUndefined(); // expired -> GET sends no cookie
|
|
260
|
-
expect(callHeaders(3).Cookie).toBe('JSESSIONID=def.tomcat1');
|
|
261
|
-
nowSpy.mockRestore();
|
|
262
|
-
});
|
|
263
|
-
it('uses the last JSESSIONID when several Set-Cookie lines are returned', async () => {
|
|
264
|
-
const u = uniqueUser();
|
|
265
|
-
fetchMock()
|
|
266
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=first.tomcat1; Path=/', 'OTHER=x; Path=/', 'JSESSIONID=second.tomcat1; Path=/']))
|
|
267
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
268
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
269
|
-
expect(callHeaders(1).Cookie).toBe('JSESSIONID=second.tomcat1');
|
|
270
|
-
});
|
|
271
|
-
it('falls back to headers.get("set-cookie") when getSetCookie is not available', async () => {
|
|
272
|
-
const u = uniqueUser();
|
|
273
|
-
const legacy = {
|
|
274
|
-
status: 200,
|
|
275
|
-
json: jest.fn().mockResolvedValue(csrfBody),
|
|
276
|
-
text: jest.fn(),
|
|
277
|
-
headers: { get: (k) => (k.toLowerCase() === 'set-cookie' ? 'JSESSIONID=legacy.tomcat1; Path=/' : null) },
|
|
278
|
-
};
|
|
279
|
-
fetchMock().mockResolvedValueOnce(legacy).mockResolvedValueOnce(resp(200, [], {}));
|
|
280
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
281
|
-
expect(callHeaders(1).Cookie).toBe('JSESSIONID=legacy.tomcat1');
|
|
282
|
-
});
|
|
283
|
-
it('honours a custom cookieName', async () => {
|
|
284
|
-
const u = uniqueUser();
|
|
285
|
-
fetchMock()
|
|
286
|
-
.mockResolvedValueOnce(resp(200, ['MYSESSION=zzz; Path=/', 'JSESSIONID=ignored; Path=/']))
|
|
287
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
288
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'MYSESSION' })).sendToFlexPLM(payload);
|
|
289
|
-
expect(callHeaders(1).Cookie).toBe('MYSESSION=zzz');
|
|
290
|
-
});
|
|
291
|
-
it('accepts enabled as the string "true"', async () => {
|
|
292
|
-
const u = uniqueUser();
|
|
293
|
-
fetchMock()
|
|
294
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
295
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
296
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: 'true' })).sendToFlexPLM(payload);
|
|
297
|
-
expect(callHeaders(1).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
298
|
-
});
|
|
299
|
-
it('keeps separate sessions per user', async () => {
|
|
300
|
-
const a = uniqueUser();
|
|
301
|
-
const b = uniqueUser();
|
|
302
|
-
fetchMock()
|
|
303
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=aaa.tomcat1']))
|
|
304
|
-
.mockResolvedValueOnce(resp(200, [], {}))
|
|
305
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=bbb.tomcat1']))
|
|
306
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
307
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(a)).sendToFlexPLM(payload);
|
|
308
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(b)).sendToFlexPLM(payload);
|
|
309
|
-
expect(callHeaders(2).Cookie).toBeUndefined(); // user b must not see user a's cookie
|
|
310
|
-
expect(callHeaders(3).Cookie).toBe('JSESSIONID=bbb.tomcat1');
|
|
311
|
-
});
|
|
312
|
-
it('never logs the cookie value', async () => {
|
|
313
|
-
const isInfoOn = jest.spyOn(app_framework_1.Logger, 'isInfoOn').mockReturnValue(true);
|
|
314
|
-
const info = jest.spyOn(console, 'info').mockImplementation();
|
|
315
|
-
const u = uniqueUser();
|
|
316
|
-
fetchMock()
|
|
317
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=supersecret.tomcat1']))
|
|
318
|
-
.mockResolvedValueOnce(resp(200, [], {}))
|
|
319
|
-
.mockResolvedValueOnce(resp(200, []))
|
|
320
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
321
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
322
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload); // 2nd event sends Cookie on GET and POST
|
|
323
|
-
const logged = info.mock.calls.map(c => String(c[0] ?? '')).join('\n');
|
|
324
|
-
expect(logged).toContain('"Cookie":"***masked***"');
|
|
325
|
-
expect(logged).not.toContain('supersecret');
|
|
326
|
-
isInfoOn.mockRestore();
|
|
327
|
-
info.mockRestore();
|
|
328
|
-
});
|
|
329
|
-
describe('option normalisation', () => {
|
|
330
|
-
const rejectedOnceThenOk = (u, opts) => {
|
|
331
|
-
fetchMock()
|
|
332
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
333
|
-
.mockResolvedValueOnce(resp(403, [], {}))
|
|
334
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
|
|
335
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
336
|
-
return new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, ...opts })).sendToFlexPLM(payload);
|
|
337
|
-
};
|
|
338
|
-
it.each([
|
|
339
|
-
['a scalar number', 403],
|
|
340
|
-
['a comma separated string', '401, 403'],
|
|
341
|
-
['garbage (falls back to 401/403)', 'abc'],
|
|
342
|
-
['an empty array (falls back to 401/403)', []],
|
|
343
|
-
])('retryStatuses given as %s does not throw and still retries on 403', async (_label, retryStatuses) => {
|
|
344
|
-
await rejectedOnceThenOk(uniqueUser(), { retryStatuses });
|
|
345
|
-
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
346
|
-
});
|
|
347
|
-
it.each([['abc'], [0], [-5]])('invalid ttlSeconds (%p) falls back to the 900s default', async (ttlSeconds) => {
|
|
348
|
-
const u = uniqueUser();
|
|
349
|
-
const realNow = Date.now();
|
|
350
|
-
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(realNow);
|
|
351
|
-
fetchMock()
|
|
352
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
353
|
-
.mockResolvedValueOnce(resp(200, [], {}))
|
|
354
|
-
.mockResolvedValueOnce(resp(200, []))
|
|
355
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
356
|
-
const cfg = sessionConfig(u, { enabled: true, ttlSeconds });
|
|
357
|
-
await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
|
|
358
|
-
nowSpy.mockReturnValue(realNow + 2000);
|
|
359
|
-
await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
|
|
360
|
-
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
361
|
-
nowSpy.mockRestore();
|
|
362
|
-
});
|
|
363
|
-
it('a blank cookieName falls back to JSESSIONID', async () => {
|
|
364
|
-
const u = uniqueUser();
|
|
365
|
-
fetchMock()
|
|
366
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
367
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
368
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: ' ' })).sendToFlexPLM(payload);
|
|
369
|
-
expect(callHeaders(1).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
describe('retryStatuses restricted to client-error statuses', () => {
|
|
373
|
-
const postFailsWith = async (status, retryStatuses) => {
|
|
374
|
-
const u = uniqueUser();
|
|
375
|
-
fetchMock()
|
|
376
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
377
|
-
.mockResolvedValueOnce(resp(status, [], {}));
|
|
378
|
-
await expect(new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, retryStatuses })).sendToFlexPLM({ id: '1' }))
|
|
379
|
-
.rejects.toThrow('Error sending data to FlexPLM:status: ' + status);
|
|
380
|
-
expect(fetchMock()).toHaveBeenCalledTimes(2); // no retry
|
|
381
|
-
};
|
|
382
|
-
it.each([[500], ['500'], [[200, 500]]])('should ignore non-4xx retryStatuses (%p) and not retry a 500', async (retryStatuses) => {
|
|
383
|
-
await postFailsWith(500, retryStatuses);
|
|
384
|
-
});
|
|
385
|
-
it('should keep 4xx entries and drop others from a mixed retryStatuses list', async () => {
|
|
386
|
-
await postFailsWith(500, [401, 500]);
|
|
387
|
-
});
|
|
388
|
-
it('should not resend a successful event when retryStatuses is configured as 200', async () => {
|
|
389
|
-
const u = uniqueUser();
|
|
390
|
-
fetchMock()
|
|
391
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
392
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
393
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, retryStatuses: 200 })).sendToFlexPLM({ id: '1' });
|
|
394
|
-
expect(fetchMock()).toHaveBeenCalledTimes(2);
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
describe('session cache key includes session options', () => {
|
|
398
|
-
it('should not share a cached cookie between different cookieNames for the same user', async () => {
|
|
399
|
-
const u = uniqueUser();
|
|
400
|
-
fetchMock()
|
|
401
|
-
.mockResolvedValueOnce(resp(200, ['AAA=a1; Path=/']))
|
|
402
|
-
.mockResolvedValueOnce(resp(200, [], {}))
|
|
403
|
-
.mockResolvedValueOnce(resp(200, ['BBB=b1; Path=/']))
|
|
404
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
405
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'AAA' })).sendToFlexPLM({ id: '1' });
|
|
406
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'BBB' })).sendToFlexPLM({ id: '1' });
|
|
407
|
-
expect(callHeaders(2).Cookie).toBeUndefined(); // BBB config must not pick up the AAA session
|
|
408
|
-
expect(callHeaders(3).Cookie).toBe('BBB=b1');
|
|
409
|
-
});
|
|
410
|
-
it('should not share a cached session between different ttlSeconds for the same user', async () => {
|
|
411
|
-
const u = uniqueUser();
|
|
412
|
-
fetchMock()
|
|
413
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=short.tomcat1']))
|
|
414
|
-
.mockResolvedValueOnce(resp(200, [], {}))
|
|
415
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=long.tomcat1']))
|
|
416
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
417
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, ttlSeconds: 1 })).sendToFlexPLM({ id: '1' });
|
|
418
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, ttlSeconds: 900 })).sendToFlexPLM({ id: '1' });
|
|
419
|
-
expect(callHeaders(2).Cookie).toBeUndefined();
|
|
420
|
-
expect(callHeaders(3).Cookie).toBe('JSESSIONID=long.tomcat1');
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
describe('rejected response disposal', () => {
|
|
424
|
-
const withBody = (r) => Object.assign(r, { body: { cancel: jest.fn().mockResolvedValue(undefined) } });
|
|
425
|
-
it('should cancel the body of a POST rejected with 403 before retrying', async () => {
|
|
426
|
-
const u = uniqueUser();
|
|
427
|
-
const rejected = withBody(resp(403, [], {}));
|
|
428
|
-
fetchMock()
|
|
429
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
430
|
-
.mockResolvedValueOnce(rejected)
|
|
431
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
|
|
432
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
433
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM({ id: '1' });
|
|
434
|
-
expect(rejected.body.cancel).toHaveBeenCalledTimes(1);
|
|
435
|
-
});
|
|
436
|
-
it('should cancel the body of a CSRF GET rejected with the cached cookie before retrying', async () => {
|
|
437
|
-
const u = uniqueUser();
|
|
438
|
-
const rejected = withBody(resp(401, []));
|
|
439
|
-
fetchMock()
|
|
440
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
441
|
-
.mockResolvedValueOnce(resp(200, [], {}))
|
|
442
|
-
.mockResolvedValueOnce(rejected)
|
|
443
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
|
|
444
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
445
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM({ id: '1' });
|
|
446
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM({ id: '1' });
|
|
447
|
-
expect(rejected.body.cancel).toHaveBeenCalledTimes(1);
|
|
448
|
-
});
|
|
449
|
-
it('should cancel the body of a getRequest rejected with the cached cookie before retrying', async () => {
|
|
450
|
-
const u = uniqueUser();
|
|
451
|
-
const rejected = withBody(resp(401, []));
|
|
452
|
-
fetchMock()
|
|
453
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
454
|
-
.mockResolvedValueOnce(resp(200, [], {}))
|
|
455
|
-
.mockResolvedValueOnce(rejected)
|
|
456
|
-
.mockResolvedValueOnce(resp(200, [], { items: [] }));
|
|
457
|
-
const connect = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u));
|
|
458
|
-
await connect.sendToFlexPLM({ id: '1' });
|
|
459
|
-
await connect.getRequest();
|
|
460
|
-
expect(rejected.body.cancel).toHaveBeenCalledTimes(1);
|
|
461
|
-
});
|
|
462
|
-
it('should still retry when cancelling the body throws', async () => {
|
|
463
|
-
const u = uniqueUser();
|
|
464
|
-
const rejected = resp(403, [], {});
|
|
465
|
-
rejected.body = { cancel: jest.fn().mockRejectedValue(new Error('locked')) };
|
|
466
|
-
fetchMock()
|
|
467
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
468
|
-
.mockResolvedValueOnce(rejected)
|
|
469
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
|
|
470
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
471
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM({ id: '1' });
|
|
472
|
-
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
473
|
-
});
|
|
474
|
-
});
|
|
475
|
-
describe('cookie names containing regex metacharacters', () => {
|
|
476
|
-
it('extracts a cookie whose name contains "+"', async () => {
|
|
477
|
-
const u = uniqueUser();
|
|
478
|
-
fetchMock()
|
|
479
|
-
.mockResolvedValueOnce(resp(200, ['SID+=abc; Path=/']))
|
|
480
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
481
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'SID+' })).sendToFlexPLM(payload);
|
|
482
|
-
expect(callHeaders(1).Cookie).toBe('SID+=abc');
|
|
483
|
-
});
|
|
484
|
-
it('treats "." literally and does not match a look-alike cookie', async () => {
|
|
485
|
-
const u = uniqueUser();
|
|
486
|
-
fetchMock()
|
|
487
|
-
.mockResolvedValueOnce(resp(200, ['AxB=wrong; Path=/']))
|
|
488
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
489
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'A.B' })).sendToFlexPLM(payload);
|
|
490
|
-
expect(callHeaders(1).Cookie).toBeUndefined();
|
|
491
|
-
});
|
|
492
|
-
});
|
|
493
|
-
describe('concurrent session acquisition', () => {
|
|
494
|
-
const flush = () => new Promise(r => setImmediate(r));
|
|
495
|
-
const isColdGet = (call) => call[1].method === 'GET' && !call[1].headers.Cookie;
|
|
496
|
-
it('shares one session across concurrent events on a cold cache', async () => {
|
|
497
|
-
const u = uniqueUser();
|
|
498
|
-
let release;
|
|
499
|
-
const gate = new Promise(r => { release = r; });
|
|
500
|
-
fetchMock().mockImplementation(async (_url, opts) => {
|
|
501
|
-
if (opts.method === 'GET' && !opts.headers.Cookie) {
|
|
502
|
-
await gate;
|
|
503
|
-
return resp(200, ['JSESSIONID=abc.tomcat1']);
|
|
504
|
-
}
|
|
505
|
-
return resp(200, []);
|
|
506
|
-
});
|
|
507
|
-
const all = Promise.all([1, 2, 3].map(() => new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).getCSRF()));
|
|
508
|
-
await flush();
|
|
509
|
-
expect(fetchMock()).toHaveBeenCalledTimes(1); // the other two are waiting for the first acquisition
|
|
510
|
-
release();
|
|
511
|
-
const results = await all;
|
|
512
|
-
expect(fetchMock().mock.calls.filter(isColdGet)).toHaveLength(1);
|
|
513
|
-
results.forEach(r => expect(r.sessionCookie).toBe('abc.tomcat1'));
|
|
514
|
-
});
|
|
515
|
-
it('a waiter does not inherit the failure of the first acquisition', async () => {
|
|
516
|
-
const u = uniqueUser();
|
|
517
|
-
let release;
|
|
518
|
-
const gate = new Promise(r => { release = r; });
|
|
519
|
-
fetchMock()
|
|
520
|
-
.mockImplementationOnce(async () => { await gate; return resp(500, []); })
|
|
521
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']));
|
|
522
|
-
const first = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).getCSRF();
|
|
523
|
-
const firstResult = first.catch(e => e);
|
|
524
|
-
const second = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).getCSRF();
|
|
525
|
-
await flush();
|
|
526
|
-
release();
|
|
527
|
-
expect(await firstResult).toBeInstanceOf(Error);
|
|
528
|
-
expect((await second).sessionCookie).toBe('abc.tomcat1');
|
|
529
|
-
});
|
|
530
|
-
it('parallel events rejected with 403 create only one replacement session', async () => {
|
|
531
|
-
const u = uniqueUser();
|
|
532
|
-
fetchMock().mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']));
|
|
533
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).getCSRF(); // prime the cache with the old session
|
|
534
|
-
fetchMock().mockReset();
|
|
535
|
-
fetchMock().mockImplementation(async (_url, opts) => {
|
|
536
|
-
const cookie = opts.headers.Cookie;
|
|
537
|
-
if (opts.method === 'POST')
|
|
538
|
-
return cookie === 'JSESSIONID=new.tomcat1' ? resp(200, [], {}) : resp(403, [], {});
|
|
539
|
-
if (!cookie)
|
|
540
|
-
return resp(200, ['JSESSIONID=new.tomcat1']);
|
|
541
|
-
return resp(200, []);
|
|
542
|
-
});
|
|
543
|
-
await Promise.all([1, 2, 3].map(() => new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload)));
|
|
544
|
-
expect(fetchMock().mock.calls.filter(isColdGet)).toHaveLength(1);
|
|
545
|
-
});
|
|
546
|
-
});
|
|
547
|
-
});
|
|
548
|
-
describe('FlexPLMConnect session reuse - feature disabled (default)', () => {
|
|
549
|
-
const payload = { id: '123' };
|
|
550
|
-
it('sends no Cookie header and makes exactly one GET + one POST', async () => {
|
|
551
|
-
const u = uniqueUser();
|
|
552
|
-
fetchMock()
|
|
553
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1'])) // Set-Cookie present but must be ignored
|
|
554
|
-
.mockResolvedValueOnce(resp(200, [], {}))
|
|
555
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=def.tomcat1']))
|
|
556
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
557
|
-
const cfg = createConfig({ userName: () => u }); // no flexplmConnect at all
|
|
558
|
-
await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
|
|
559
|
-
await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
|
|
560
|
-
for (let i = 0; i < 4; i++) {
|
|
561
|
-
expect(callHeaders(i).Cookie).toBeUndefined();
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
it('does not retry on 403 (behaves exactly as before)', async () => {
|
|
565
|
-
const u = uniqueUser();
|
|
566
|
-
fetchMock()
|
|
567
|
-
.mockResolvedValueOnce(resp(200))
|
|
568
|
-
.mockResolvedValueOnce(resp(403, [], {}));
|
|
569
|
-
await expect(new flexplm_connect_1.FlexPLMConnect(createConfig({ userName: () => u })).sendToFlexPLM(payload))
|
|
570
|
-
.rejects.toThrow('Error sending data to FlexPLM:status: 403');
|
|
571
|
-
expect(fetchMock()).toHaveBeenCalledTimes(2);
|
|
572
|
-
});
|
|
573
|
-
it('explicit enabled:false behaves the same as not configured', async () => {
|
|
574
|
-
const u = uniqueUser();
|
|
575
|
-
fetchMock()
|
|
576
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
577
|
-
.mockResolvedValueOnce(resp(200, [], {}));
|
|
578
|
-
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: false })).sendToFlexPLM(payload);
|
|
579
|
-
expect(callHeaders(1).Cookie).toBeUndefined();
|
|
580
|
-
});
|
|
581
|
-
});
|
|
582
|
-
describe('FlexPLMConnect.getRequest session reuse', () => {
|
|
583
|
-
it('sends the cached cookie on image/GET requests', async () => {
|
|
584
|
-
const u = uniqueUser();
|
|
585
|
-
fetchMock()
|
|
586
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1'])) // csrf GET primes the cache
|
|
587
|
-
.mockResolvedValueOnce(resp(200, [], {})) // POST
|
|
588
|
-
.mockResolvedValueOnce(resp(200, [], { items: [] })); // getRequest
|
|
589
|
-
const connect = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u));
|
|
590
|
-
await connect.sendToFlexPLM({ id: '1' });
|
|
591
|
-
await connect.getRequest();
|
|
592
|
-
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
593
|
-
});
|
|
594
|
-
it('retries once without the cookie when the cached session is rejected on getRequest', async () => {
|
|
595
|
-
const u = uniqueUser();
|
|
596
|
-
fetchMock()
|
|
597
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
598
|
-
.mockResolvedValueOnce(resp(200, [], {}))
|
|
599
|
-
.mockResolvedValueOnce(resp(401, [])) // getRequest with cached cookie
|
|
600
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1'], { items: [] })); // retry, no cookie
|
|
601
|
-
const connect = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u));
|
|
602
|
-
await connect.sendToFlexPLM({ id: '1' });
|
|
603
|
-
await connect.getRequest();
|
|
604
|
-
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
605
|
-
expect(callHeaders(3).Cookie).toBeUndefined();
|
|
606
|
-
});
|
|
607
|
-
it('does not re-store the rejected cookie when the retry issues no Set-Cookie', async () => {
|
|
608
|
-
const u = uniqueUser();
|
|
609
|
-
fetchMock()
|
|
610
|
-
.mockResolvedValueOnce(resp(200, ['JSESSIONID=stale.tomcat1']))
|
|
611
|
-
.mockResolvedValueOnce(resp(200, [], {}))
|
|
612
|
-
.mockResolvedValueOnce(resp(401, [])) // getRequest with the cached cookie
|
|
613
|
-
.mockResolvedValueOnce(resp(200, [], { items: [] })) // retry succeeds, no Set-Cookie
|
|
614
|
-
.mockResolvedValueOnce(resp(200, [], { items: [] })); // next getRequest
|
|
615
|
-
const connect = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u));
|
|
616
|
-
await connect.sendToFlexPLM({ id: '1' });
|
|
617
|
-
await connect.getRequest();
|
|
618
|
-
await connect.getRequest();
|
|
619
|
-
expect(callHeaders(3).Cookie).toBeUndefined();
|
|
620
|
-
expect(callHeaders(4).Cookie).toBeUndefined(); // stale cookie must not be sent again
|
|
621
|
-
});
|
|
622
|
-
});
|
|
623
|
-
// Added for Session reuse - ET-390 - Ends
|