@contrail/flexplm 1.7.7-alpha.1ad98c2 → 1.7.7-alpha.5994a89
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.
|
@@ -6,6 +6,10 @@ 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;
|
|
9
13
|
taskId?: string;
|
|
10
14
|
apiHost: string;
|
|
11
15
|
userName(): string;
|
|
@@ -6,7 +6,17 @@ export declare class FlexPLMConnect {
|
|
|
6
6
|
payloadSendAsArray: boolean;
|
|
7
7
|
constructor(_config: FCConfig, endPoint?: any, payloadAsArray?: any);
|
|
8
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;
|
|
9
17
|
private getSessionKey;
|
|
18
|
+
/** Releases the connection of a rejected response before it is replaced by a retry. */
|
|
19
|
+
private discardBody;
|
|
10
20
|
private getCachedSession;
|
|
11
21
|
private storeSession;
|
|
12
22
|
private clearSession;
|
|
@@ -14,11 +24,8 @@ export declare class FlexPLMConnect {
|
|
|
14
24
|
private extractSessionCookie;
|
|
15
25
|
private cookieHeader;
|
|
16
26
|
private getRequestOptions;
|
|
17
|
-
getCSRF(getNewSession?: boolean):
|
|
18
|
-
|
|
19
|
-
nonce: any;
|
|
20
|
-
sessionCookie: string;
|
|
21
|
-
}>;
|
|
27
|
+
getCSRF(getNewSession?: boolean, rejectedCookie?: string): any;
|
|
28
|
+
private fetchCSRF;
|
|
22
29
|
sendRequest(csrf: any, payload: any): Promise<Response>;
|
|
23
30
|
protected processRequest(payload: any): Promise<FlexPLMResponseData>;
|
|
24
31
|
sendToFlexPLM(payload: PayloadType): Promise<FlexPLMResponseData>;
|
|
@@ -2,8 +2,55 @@
|
|
|
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
|
|
5
|
+
// Module-level on purpose: a new FlexPLMConnect is constructed for every event. Declaring for Session reusing use case.
|
|
6
6
|
const sessionCache = new Map();
|
|
7
|
+
// Upper bound so users/option combinations that are never requested again cannot accumulate cookies forever.
|
|
8
|
+
const MAX_CACHED_SESSIONS = 100;
|
|
9
|
+
const pruneSessionCache = () => {
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
for (const [key, session] of sessionCache) {
|
|
12
|
+
if (session.expiresAt <= now)
|
|
13
|
+
sessionCache.delete(key);
|
|
14
|
+
}
|
|
15
|
+
// Still full: drop the oldest entries (Map keeps insertion order).
|
|
16
|
+
while (sessionCache.size >= MAX_CACHED_SESSIONS) {
|
|
17
|
+
sessionCache.delete(sessionCache.keys().next().value);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
// In-flight cold session acquisitions per key, so concurrent events share one FlexPLM session instead of each creating their own.
|
|
21
|
+
const inFlightAcquisitions = new Map();
|
|
22
|
+
const DEFAULT_SESSION_COOKIE_NAME = 'JSESSIONID';
|
|
23
|
+
const DEFAULT_SESSION_TTL_SECONDS = 900;
|
|
24
|
+
const DEFAULT_RETRY_STATUSES = [401, 403];
|
|
25
|
+
const COOKIE_NAME_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
26
|
+
// Thrown internally when a cached cookie was rejected, so the caller can re-acquire under the lock.
|
|
27
|
+
class SessionRejectedError extends Error {
|
|
28
|
+
}
|
|
29
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
30
|
+
const parseRetryStatuses = (value) => {
|
|
31
|
+
let list = [];
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
list = value;
|
|
34
|
+
else if (typeof value === 'string')
|
|
35
|
+
list = value.split(',');
|
|
36
|
+
else if (value !== undefined && value !== null)
|
|
37
|
+
list = [value];
|
|
38
|
+
const statuses = list
|
|
39
|
+
.map(status => (typeof status === 'string' ? status.trim() : status))
|
|
40
|
+
.filter(status => status !== '' && status !== null && status !== undefined)
|
|
41
|
+
.map(Number)
|
|
42
|
+
// Only client-error statuses can mean "session rejected". 2xx/5xx would resend events the server already processed.
|
|
43
|
+
.filter(status => Number.isInteger(status) && status >= 400 && status <= 499);
|
|
44
|
+
return statuses.length ? statuses : DEFAULT_RETRY_STATUSES;
|
|
45
|
+
};
|
|
46
|
+
const parseTtlSeconds = (value) => {
|
|
47
|
+
const ttl = Number(value);
|
|
48
|
+
return Number.isFinite(ttl) && ttl > 0 ? ttl : DEFAULT_SESSION_TTL_SECONDS;
|
|
49
|
+
};
|
|
50
|
+
const parseCookieName = (value) => {
|
|
51
|
+
const name = typeof value === 'string' ? value.trim() : '';
|
|
52
|
+
return COOKIE_NAME_TOKEN.test(name) ? name : DEFAULT_SESSION_COOKIE_NAME;
|
|
53
|
+
};
|
|
7
54
|
// Added for Session reuse - ET-390 - Ends
|
|
8
55
|
class FlexPLMConnect {
|
|
9
56
|
constructor(_config, endPoint = undefined, payloadAsArray = undefined) {
|
|
@@ -41,16 +88,59 @@ class FlexPLMConnect {
|
|
|
41
88
|
const cfg = this.config ?? {};
|
|
42
89
|
const sessReuseEntries = (name) => cfg[`flexplmConnect.sessionReuse.${name}`];
|
|
43
90
|
const enabled = sessReuseEntries('enabled');
|
|
44
|
-
console.info('sessionReuse.enabled value is --> ' + enabled);
|
|
45
91
|
return {
|
|
46
92
|
enabled: enabled === true || String(enabled).toLowerCase() === 'true',
|
|
47
|
-
ttlMs: (sessReuseEntries('ttlSeconds')
|
|
48
|
-
cookieName: sessReuseEntries('cookieName')
|
|
49
|
-
retryStatuses: (sessReuseEntries('retryStatuses')
|
|
93
|
+
ttlMs: parseTtlSeconds(sessReuseEntries('ttlSeconds')) * 1000,
|
|
94
|
+
cookieName: parseCookieName(sessReuseEntries('cookieName')),
|
|
95
|
+
retryStatuses: parseRetryStatuses(sessReuseEntries('retryStatuses')),
|
|
50
96
|
};
|
|
51
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Waits until no cold session acquisition is running for this key.
|
|
100
|
+
* Callers must `await` this only when it returns a promise: awaiting even a resolved promise yields,
|
|
101
|
+
* and the check-then-register in runColdAcquisition has to stay synchronous to be race free.
|
|
102
|
+
*/
|
|
103
|
+
waitForInFlightAcquisition() {
|
|
104
|
+
const key = this.getSessionKey();
|
|
105
|
+
if (!inFlightAcquisitions.has(key))
|
|
106
|
+
return undefined;
|
|
107
|
+
return (async () => {
|
|
108
|
+
let pending = inFlightAcquisitions.get(key);
|
|
109
|
+
while (pending) {
|
|
110
|
+
await pending.catch(() => undefined);
|
|
111
|
+
pending = inFlightAcquisitions.get(key);
|
|
112
|
+
}
|
|
113
|
+
})();
|
|
114
|
+
}
|
|
115
|
+
/** Runs a cold acquisition and registers it so concurrent callers wait for it instead of opening their own session. */
|
|
116
|
+
async runColdAcquisition(acquire) {
|
|
117
|
+
const key = this.getSessionKey();
|
|
118
|
+
const acquisition = acquire();
|
|
119
|
+
inFlightAcquisitions.set(key, acquisition);
|
|
120
|
+
try {
|
|
121
|
+
return await acquisition;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
if (inFlightAcquisitions.get(key) === acquisition)
|
|
125
|
+
inFlightAcquisitions.delete(key);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
52
128
|
getSessionKey() {
|
|
53
|
-
|
|
129
|
+
// Options that change the cached value are part of the key; JSON avoids '|' collisions between fields.
|
|
130
|
+
const { cookieName, ttlMs } = this.sessionOptions;
|
|
131
|
+
return JSON.stringify([
|
|
132
|
+
this.config.apiHost, this.config.urlContext, this.config.userName(), this.config.plmEnviornment, cookieName, ttlMs,
|
|
133
|
+
]);
|
|
134
|
+
}
|
|
135
|
+
/** Releases the connection of a rejected response before it is replaced by a retry. */
|
|
136
|
+
async discardBody(response) {
|
|
137
|
+
try {
|
|
138
|
+
if (typeof response?.body?.cancel === 'function')
|
|
139
|
+
await response.body.cancel();
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
// best effort: nothing to do if the body is already consumed or locked
|
|
143
|
+
}
|
|
54
144
|
}
|
|
55
145
|
getCachedSession() {
|
|
56
146
|
if (!this.sessionOptions.enabled)
|
|
@@ -63,13 +153,15 @@ class FlexPLMConnect {
|
|
|
63
153
|
sessionCache.delete(key);
|
|
64
154
|
return undefined;
|
|
65
155
|
}
|
|
66
|
-
console.info('cachedSessValue.cookie: ' + cachedSessValue.cookie);
|
|
67
156
|
return cachedSessValue.cookie;
|
|
68
157
|
}
|
|
69
158
|
storeSession(cookie) {
|
|
70
159
|
// sliding expiry: Windchill's session timeout is also sliding
|
|
71
|
-
|
|
72
|
-
|
|
160
|
+
const key = this.getSessionKey();
|
|
161
|
+
// delete first so a refreshed entry moves to the newest position (oldest-first eviction then acts as LRU)
|
|
162
|
+
sessionCache.delete(key);
|
|
163
|
+
pruneSessionCache();
|
|
164
|
+
sessionCache.set(key, { cookie, expiresAt: Date.now() + this.sessionOptions.ttlMs });
|
|
73
165
|
}
|
|
74
166
|
clearSession() {
|
|
75
167
|
sessionCache.delete(this.getSessionKey());
|
|
@@ -77,7 +169,6 @@ class FlexPLMConnect {
|
|
|
77
169
|
/** Returns the session cookie value Windchill issued on this response, if any (last one wins). */
|
|
78
170
|
extractSessionCookie(response) {
|
|
79
171
|
const respHeader = response?.headers;
|
|
80
|
-
console.info('respHeader: ' + respHeader);
|
|
81
172
|
if (!respHeader)
|
|
82
173
|
return undefined;
|
|
83
174
|
let raw = [];
|
|
@@ -88,7 +179,7 @@ class FlexPLMConnect {
|
|
|
88
179
|
raw = [respHeader.get('set-cookie')];
|
|
89
180
|
}
|
|
90
181
|
const name = this.sessionOptions.cookieName;
|
|
91
|
-
const regexFilter = new RegExp('(?:^|[,\\s])' + name + '=([^;,\\s]+)', 'g');
|
|
182
|
+
const regexFilter = new RegExp('(?:^|[,\\s])' + escapeRegExp(name) + '=([^;,\\s]+)', 'g');
|
|
92
183
|
let jSession;
|
|
93
184
|
for (const line of raw) {
|
|
94
185
|
let cookieNameMatcher = regexFilter.exec(line);
|
|
@@ -97,7 +188,6 @@ class FlexPLMConnect {
|
|
|
97
188
|
cookieNameMatcher = regexFilter.exec(line);
|
|
98
189
|
}
|
|
99
190
|
}
|
|
100
|
-
console.info('jSession: ' + jSession);
|
|
101
191
|
return jSession;
|
|
102
192
|
}
|
|
103
193
|
cookieHeader(sessionCookie) {
|
|
@@ -124,7 +214,6 @@ class FlexPLMConnect {
|
|
|
124
214
|
}
|
|
125
215
|
// Added for Session reuse - ET-390 - Starts
|
|
126
216
|
const cookie = this.cookieHeader(sessionCookie);
|
|
127
|
-
console.info('sessionReuse--cookie --> ' + cookie);
|
|
128
217
|
if (cookie) {
|
|
129
218
|
csrfOptions.headers['Cookie'] = cookie;
|
|
130
219
|
}
|
|
@@ -138,7 +227,6 @@ class FlexPLMConnect {
|
|
|
138
227
|
// Added for Session reuse - ET-390 - Ends
|
|
139
228
|
console.info('csrfOptions: ' + JSON.stringify(logOptions));
|
|
140
229
|
}
|
|
141
|
-
console.info('csrfOptions --> ' + csrfOptions);
|
|
142
230
|
return csrfOptions;
|
|
143
231
|
}
|
|
144
232
|
/////////////////////////////////////////////////////////////////////////////
|
|
@@ -146,27 +234,48 @@ class FlexPLMConnect {
|
|
|
146
234
|
/////////////////////////////////////////////////////////////////////////////
|
|
147
235
|
// ET-390 - adding a new parameter getNewSession to the getCSRF function
|
|
148
236
|
// Whenever a getCSRF is called without getNewSession, the default value of false will be used
|
|
149
|
-
|
|
237
|
+
// rejectedCookie: the cookie that was rejected by the caller, so a refresh already done by a concurrent event is not thrown away
|
|
238
|
+
async getCSRF(getNewSession = false, rejectedCookie) {
|
|
239
|
+
// Added for Session reuse - ET-390 - Starts
|
|
240
|
+
if (!this.sessionOptions.enabled)
|
|
241
|
+
return this.fetchCSRF(undefined);
|
|
242
|
+
for (let wait = this.waitForInFlightAcquisition(); wait; wait = this.waitForInFlightAcquisition())
|
|
243
|
+
await wait;
|
|
244
|
+
if (getNewSession) {
|
|
245
|
+
const current = this.getCachedSession();
|
|
246
|
+
if (rejectedCookie === undefined || !current || current === rejectedCookie)
|
|
247
|
+
this.clearSession();
|
|
248
|
+
}
|
|
249
|
+
const cached = this.getCachedSession();
|
|
250
|
+
if (cached) {
|
|
251
|
+
try {
|
|
252
|
+
return await this.fetchCSRF(cached);
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
if (!(e instanceof SessionRejectedError))
|
|
256
|
+
throw e;
|
|
257
|
+
// if the cached cookie is rejected (401/403), drop it and acquire a new session (serialized)
|
|
258
|
+
if (this.getCachedSession() === cached)
|
|
259
|
+
this.clearSession();
|
|
260
|
+
return this.getCSRF();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return this.runColdAcquisition(() => this.fetchCSRF(undefined));
|
|
264
|
+
// Added for Session reuse - ET-390 - Ends
|
|
265
|
+
}
|
|
266
|
+
async fetchCSRF(sentCookie) {
|
|
150
267
|
const urlContext = this.config.urlContext;
|
|
151
268
|
const csrfEndpoint = this.config.csrfEndpoint;
|
|
152
269
|
const csrfURL = this.config.apiHost + urlContext + csrfEndpoint;
|
|
153
270
|
console.info('csrfURL: ' + csrfURL);
|
|
154
|
-
// Added for Session reuse - ET-390 - Starts
|
|
155
|
-
if (getNewSession)
|
|
156
|
-
this.clearSession();
|
|
157
|
-
let sentCookie = this.getCachedSession();
|
|
158
271
|
// getRequestOptions will be called with additional sentCookie parameter
|
|
159
272
|
const csrfOptions = this.getRequestOptions('GET', sentCookie);
|
|
160
|
-
|
|
161
|
-
let response = await fetch(csrfURL, csrfOptions);
|
|
162
|
-
console.info('response.status: ' + response.status);
|
|
273
|
+
const response = await fetch(csrfURL, csrfOptions);
|
|
163
274
|
// Added for Session reuse - ET-390 - Starts
|
|
164
|
-
// if the response status is 401 or 403, then get a new session id
|
|
165
275
|
if (sentCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
166
276
|
console.warn('Cached FlexPLM session rejected on csrf call (status ' + response.status + '). Retrying with a new session.');
|
|
167
|
-
this.
|
|
168
|
-
|
|
169
|
-
response = await fetch(csrfURL, this.getRequestOptions('GET'));
|
|
277
|
+
await this.discardBody(response);
|
|
278
|
+
throw new SessionRejectedError();
|
|
170
279
|
}
|
|
171
280
|
// Added for Session reuse - ET-390 - Ends
|
|
172
281
|
if (response.status >= 300) {
|
|
@@ -190,9 +299,7 @@ class FlexPLMConnect {
|
|
|
190
299
|
const body = await response.json();
|
|
191
300
|
const nonce_key = body.items[0].attributes.nonce_key;
|
|
192
301
|
const nonce = body.items[0].attributes.nonce;
|
|
193
|
-
|
|
194
|
-
console.info('sessionCookie: ' + sessionCookie);
|
|
195
|
-
//ET-390 - adding sessionCookie to the return
|
|
302
|
+
// ET-390 - adding sessionCookie to the return
|
|
196
303
|
return {
|
|
197
304
|
nonce_key,
|
|
198
305
|
nonce,
|
|
@@ -251,7 +358,8 @@ class FlexPLMConnect {
|
|
|
251
358
|
let response = await this.sendRequest(csrf, payload);
|
|
252
359
|
if (csrf.sessionCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
253
360
|
console.warn('FlexPLM rejected session/nonce (status ' + response.status + '). Getting a new session and retrying once.');
|
|
254
|
-
|
|
361
|
+
await this.discardBody(response);
|
|
362
|
+
csrf = await this.getCSRF(true, csrf.sessionCookie);
|
|
255
363
|
response = await this.sendRequest(csrf, payload);
|
|
256
364
|
}
|
|
257
365
|
// Added for Session reuse - ET-390 - Ends
|
|
@@ -308,21 +416,34 @@ class FlexPLMConnect {
|
|
|
308
416
|
const path = urlPath || ('/servlet/rest' + this.vibeEventEndpoint);
|
|
309
417
|
const vibeEventsURL = this.config.apiHost + urlContext + path;
|
|
310
418
|
// Added for Session reuse - ET-390 - Starts
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
this.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
this.
|
|
419
|
+
const attempt = async () => {
|
|
420
|
+
if (!this.sessionOptions.enabled) {
|
|
421
|
+
return { response: await fetch(vibeEventsURL, this.getRequestOptions('GET')), sent: undefined };
|
|
422
|
+
}
|
|
423
|
+
for (let wait = this.waitForInFlightAcquisition(); wait; wait = this.waitForInFlightAcquisition())
|
|
424
|
+
await wait;
|
|
425
|
+
const sent = this.getCachedSession();
|
|
426
|
+
const call = async () => {
|
|
427
|
+
const res = await fetch(vibeEventsURL, this.getRequestOptions('GET', sent));
|
|
428
|
+
// A rejected cookie must not be stored again; only keep a cookie that was accepted or newly issued.
|
|
429
|
+
if (!(sent && this.sessionOptions.retryStatuses.includes(res.status))) {
|
|
430
|
+
const cookie = this.extractSessionCookie(res) ?? sent;
|
|
431
|
+
if (cookie)
|
|
432
|
+
this.storeSession(cookie);
|
|
433
|
+
}
|
|
434
|
+
return { response: res, sent };
|
|
435
|
+
};
|
|
436
|
+
return sent ? call() : this.runColdAcquisition(call);
|
|
437
|
+
};
|
|
438
|
+
const first = await attempt();
|
|
439
|
+
let response = first.response;
|
|
440
|
+
if (first.sent && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
441
|
+
await this.discardBody(response);
|
|
442
|
+
if (this.getCachedSession() === first.sent)
|
|
443
|
+
this.clearSession();
|
|
444
|
+
response = (await attempt()).response;
|
|
322
445
|
}
|
|
323
446
|
// Added for Session reuse - ET-390 - Ends
|
|
324
|
-
console.info('if loop entry 1: ' + this.sessionOptions.retryStatuses.includes(response.status));
|
|
325
|
-
console.info('response.status1: ' + response.status);
|
|
326
447
|
if (response.status >= 300) {
|
|
327
448
|
const message = 'Error connecting to FlexPLM:status: ' + response.status;
|
|
328
449
|
console.error(message);
|
|
@@ -39,7 +39,12 @@ const resp = (status, setCookie = [], json = csrfBody) => ({
|
|
|
39
39
|
});
|
|
40
40
|
let userCounter = 0;
|
|
41
41
|
const uniqueUser = () => 'sess-user-' + (++userCounter);
|
|
42
|
-
|
|
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
|
+
};
|
|
43
48
|
const fetchMock = () => global.fetch;
|
|
44
49
|
const callHeaders = (i) => fetchMock().mock.calls[i][1].headers;
|
|
45
50
|
const callMethod = (i) => fetchMock().mock.calls[i][1].method;
|
|
@@ -140,7 +145,7 @@ describe('FlexPLMConnect.sendRequest', () => {
|
|
|
140
145
|
// Added for Session reuse - ET-390 - Starts
|
|
141
146
|
describe('FlexPLMConnect session reuse', () => {
|
|
142
147
|
const payload = { id: '123' };
|
|
143
|
-
it('
|
|
148
|
+
it('should capture JSESSIONID from the CSRF GET and send it on the POST', async () => {
|
|
144
149
|
const u = uniqueUser();
|
|
145
150
|
fetchMock()
|
|
146
151
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1; Path=/; HttpOnly']))
|
|
@@ -151,7 +156,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
151
156
|
expect(callHeaders(1).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
152
157
|
expect(callHeaders(1).CSRF_NONCE).toBe('n1');
|
|
153
158
|
});
|
|
154
|
-
it('
|
|
159
|
+
it('should reuse the cached session on the next event (new FlexPLMConnect instance)', async () => {
|
|
155
160
|
const u = uniqueUser();
|
|
156
161
|
fetchMock()
|
|
157
162
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1; Path=/'])) // event 1 GET
|
|
@@ -163,7 +168,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
163
168
|
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1'); // GET reuses it
|
|
164
169
|
expect(callHeaders(3).Cookie).toBe('JSESSIONID=abc.tomcat1'); // POST reuses it
|
|
165
170
|
});
|
|
166
|
-
it('
|
|
171
|
+
it('should replace the cached session when Windchill answers the GET with a new Set-Cookie', async () => {
|
|
167
172
|
const u = uniqueUser();
|
|
168
173
|
fetchMock()
|
|
169
174
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1; Path=/']))
|
|
@@ -180,7 +185,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
180
185
|
expect(callHeaders(3).Cookie).toBe('JSESSIONID=new.tomcat1'); // POST uses the replacement
|
|
181
186
|
expect(callHeaders(4).Cookie).toBe('JSESSIONID=new.tomcat1'); // event 3 GET sends the replacement
|
|
182
187
|
});
|
|
183
|
-
it('
|
|
188
|
+
it('should retry once with a new session when the POST is rejected with 403', async () => {
|
|
184
189
|
const u = uniqueUser();
|
|
185
190
|
fetchMock()
|
|
186
191
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1; Path=/'])) // GET
|
|
@@ -194,7 +199,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
194
199
|
expect(result.status).toBe(200);
|
|
195
200
|
expect(result.data).toEqual({ ok: true });
|
|
196
201
|
});
|
|
197
|
-
it('also
|
|
202
|
+
it('should also retry on 401', async () => {
|
|
198
203
|
const u = uniqueUser();
|
|
199
204
|
fetchMock()
|
|
200
205
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
@@ -204,7 +209,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
204
209
|
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
205
210
|
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
206
211
|
});
|
|
207
|
-
it('
|
|
212
|
+
it('should not loop: a second 403 after the retry is surfaced as an error', async () => {
|
|
208
213
|
const u = uniqueUser();
|
|
209
214
|
fetchMock()
|
|
210
215
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
@@ -215,7 +220,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
215
220
|
.rejects.toThrow('Error sending data to FlexPLM:status: 403');
|
|
216
221
|
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
217
222
|
});
|
|
218
|
-
it('
|
|
223
|
+
it('should not retry on non-auth errors such as 500 (event may already be processed)', async () => {
|
|
219
224
|
const u = uniqueUser();
|
|
220
225
|
fetchMock()
|
|
221
226
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
@@ -224,7 +229,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
224
229
|
.rejects.toThrow('Error sending data to FlexPLM:status: 500');
|
|
225
230
|
expect(fetchMock()).toHaveBeenCalledTimes(2);
|
|
226
231
|
});
|
|
227
|
-
it('
|
|
232
|
+
it('should retry the csrf GET without a cookie when the cached session is rejected there', async () => {
|
|
228
233
|
const u = uniqueUser();
|
|
229
234
|
fetchMock()
|
|
230
235
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1'])) // event 1 GET
|
|
@@ -238,7 +243,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
238
243
|
expect(callHeaders(3).Cookie).toBeUndefined();
|
|
239
244
|
expect(callHeaders(4).Cookie).toBe('JSESSIONID=new.tomcat1');
|
|
240
245
|
});
|
|
241
|
-
it('
|
|
246
|
+
it('should expire the cached session after ttlSeconds', async () => {
|
|
242
247
|
const u = uniqueUser();
|
|
243
248
|
const realNow = Date.now();
|
|
244
249
|
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(realNow);
|
|
@@ -255,7 +260,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
255
260
|
expect(callHeaders(3).Cookie).toBe('JSESSIONID=def.tomcat1');
|
|
256
261
|
nowSpy.mockRestore();
|
|
257
262
|
});
|
|
258
|
-
it('
|
|
263
|
+
it('should use the last JSESSIONID when several Set-Cookie lines are returned', async () => {
|
|
259
264
|
const u = uniqueUser();
|
|
260
265
|
fetchMock()
|
|
261
266
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=first.tomcat1; Path=/', 'OTHER=x; Path=/', 'JSESSIONID=second.tomcat1; Path=/']))
|
|
@@ -263,7 +268,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
263
268
|
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
264
269
|
expect(callHeaders(1).Cookie).toBe('JSESSIONID=second.tomcat1');
|
|
265
270
|
});
|
|
266
|
-
it('
|
|
271
|
+
it('should fall back to headers.get("set-cookie") when getSetCookie is not available', async () => {
|
|
267
272
|
const u = uniqueUser();
|
|
268
273
|
const legacy = {
|
|
269
274
|
status: 200,
|
|
@@ -275,7 +280,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
275
280
|
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
276
281
|
expect(callHeaders(1).Cookie).toBe('JSESSIONID=legacy.tomcat1');
|
|
277
282
|
});
|
|
278
|
-
it('
|
|
283
|
+
it('should honour a custom cookieName', async () => {
|
|
279
284
|
const u = uniqueUser();
|
|
280
285
|
fetchMock()
|
|
281
286
|
.mockResolvedValueOnce(resp(200, ['MYSESSION=zzz; Path=/', 'JSESSIONID=ignored; Path=/']))
|
|
@@ -283,7 +288,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
283
288
|
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'MYSESSION' })).sendToFlexPLM(payload);
|
|
284
289
|
expect(callHeaders(1).Cookie).toBe('MYSESSION=zzz');
|
|
285
290
|
});
|
|
286
|
-
it('
|
|
291
|
+
it('should accept enabled as the string "true"', async () => {
|
|
287
292
|
const u = uniqueUser();
|
|
288
293
|
fetchMock()
|
|
289
294
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
@@ -291,7 +296,39 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
291
296
|
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: 'true' })).sendToFlexPLM(payload);
|
|
292
297
|
expect(callHeaders(1).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
293
298
|
});
|
|
294
|
-
it('
|
|
299
|
+
it('should evict expired sessions of other users when a new session is stored', async () => {
|
|
300
|
+
const userA = uniqueUser();
|
|
301
|
+
const userB = uniqueUser();
|
|
302
|
+
const realNow = Date.now();
|
|
303
|
+
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(realNow);
|
|
304
|
+
fetchMock()
|
|
305
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=a.tomcat1'])) // A GET
|
|
306
|
+
.mockResolvedValueOnce(resp(200, [], {})) // A POST
|
|
307
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=b.tomcat1'])) // B GET
|
|
308
|
+
.mockResolvedValueOnce(resp(200, [], {})) // B POST
|
|
309
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=a2.tomcat1'])) // A GET again
|
|
310
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
311
|
+
const cfg = (u) => sessionConfig(u, { enabled: true, ttlSeconds: 1 });
|
|
312
|
+
await new flexplm_connect_1.FlexPLMConnect(cfg(userA)).sendToFlexPLM(payload);
|
|
313
|
+
nowSpy.mockReturnValue(realNow + 2000); // A has expired
|
|
314
|
+
await new flexplm_connect_1.FlexPLMConnect(cfg(userB)).sendToFlexPLM(payload); // storing B sweeps A
|
|
315
|
+
nowSpy.mockReturnValue(realNow); // rewind: only a swept A has no entry left
|
|
316
|
+
await new flexplm_connect_1.FlexPLMConnect(cfg(userA)).sendToFlexPLM(payload);
|
|
317
|
+
expect(callHeaders(4).Cookie).toBeUndefined();
|
|
318
|
+
nowSpy.mockRestore();
|
|
319
|
+
});
|
|
320
|
+
it('should not hold more than the maximum number of cached sessions', async () => {
|
|
321
|
+
const first = uniqueUser();
|
|
322
|
+
fetchMock().mockImplementation(async () => resp(200, ['JSESSIONID=s.tomcat1']));
|
|
323
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(first)).sendToFlexPLM(payload);
|
|
324
|
+
for (let i = 0; i < 100; i++) {
|
|
325
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(uniqueUser())).sendToFlexPLM(payload);
|
|
326
|
+
}
|
|
327
|
+
fetchMock().mockClear();
|
|
328
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(first)).sendToFlexPLM(payload);
|
|
329
|
+
expect(callHeaders(0).Cookie).toBeUndefined(); // oldest entry was evicted
|
|
330
|
+
});
|
|
331
|
+
it('should keep separate sessions per user', async () => {
|
|
295
332
|
const a = uniqueUser();
|
|
296
333
|
const b = uniqueUser();
|
|
297
334
|
fetchMock()
|
|
@@ -304,7 +341,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
304
341
|
expect(callHeaders(2).Cookie).toBeUndefined(); // user b must not see user a's cookie
|
|
305
342
|
expect(callHeaders(3).Cookie).toBe('JSESSIONID=bbb.tomcat1');
|
|
306
343
|
});
|
|
307
|
-
it('never
|
|
344
|
+
it('should never log the cookie value', async () => {
|
|
308
345
|
const isInfoOn = jest.spyOn(app_framework_1.Logger, 'isInfoOn').mockReturnValue(true);
|
|
309
346
|
const info = jest.spyOn(console, 'info').mockImplementation();
|
|
310
347
|
const u = uniqueUser();
|
|
@@ -321,10 +358,228 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
321
358
|
isInfoOn.mockRestore();
|
|
322
359
|
info.mockRestore();
|
|
323
360
|
});
|
|
361
|
+
describe('option normalisation', () => {
|
|
362
|
+
const rejectedOnceThenOk = (u, opts) => {
|
|
363
|
+
fetchMock()
|
|
364
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
365
|
+
.mockResolvedValueOnce(resp(403, [], {}))
|
|
366
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
|
|
367
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
368
|
+
return new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, ...opts })).sendToFlexPLM(payload);
|
|
369
|
+
};
|
|
370
|
+
it.each([
|
|
371
|
+
['a scalar number', 403],
|
|
372
|
+
['a comma separated string', '401, 403'],
|
|
373
|
+
['garbage (falls back to 401/403)', 'abc'],
|
|
374
|
+
['an empty array (falls back to 401/403)', []],
|
|
375
|
+
])('should not throw and still retry on 403 when retryStatuses is %s', async (_label, retryStatuses) => {
|
|
376
|
+
await rejectedOnceThenOk(uniqueUser(), { retryStatuses });
|
|
377
|
+
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
378
|
+
});
|
|
379
|
+
it.each([['abc'], [0], [-5]])('should fall back to the 900s default when ttlSeconds is invalid (%p)', async (ttlSeconds) => {
|
|
380
|
+
const u = uniqueUser();
|
|
381
|
+
const realNow = Date.now();
|
|
382
|
+
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(realNow);
|
|
383
|
+
fetchMock()
|
|
384
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
385
|
+
.mockResolvedValueOnce(resp(200, [], {}))
|
|
386
|
+
.mockResolvedValueOnce(resp(200, []))
|
|
387
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
388
|
+
const cfg = sessionConfig(u, { enabled: true, ttlSeconds });
|
|
389
|
+
await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
|
|
390
|
+
nowSpy.mockReturnValue(realNow + 2000);
|
|
391
|
+
await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
|
|
392
|
+
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
393
|
+
nowSpy.mockRestore();
|
|
394
|
+
});
|
|
395
|
+
it('should fall back to JSESSIONID when cookieName is blank', async () => {
|
|
396
|
+
const u = uniqueUser();
|
|
397
|
+
fetchMock()
|
|
398
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
399
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
400
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: ' ' })).sendToFlexPLM(payload);
|
|
401
|
+
expect(callHeaders(1).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
describe('retryStatuses restricted to client-error statuses', () => {
|
|
405
|
+
const postFailsWith = async (status, retryStatuses) => {
|
|
406
|
+
const u = uniqueUser();
|
|
407
|
+
fetchMock()
|
|
408
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
409
|
+
.mockResolvedValueOnce(resp(status, [], {}));
|
|
410
|
+
await expect(new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, retryStatuses })).sendToFlexPLM({ id: '1' }))
|
|
411
|
+
.rejects.toThrow('Error sending data to FlexPLM:status: ' + status);
|
|
412
|
+
expect(fetchMock()).toHaveBeenCalledTimes(2); // no retry
|
|
413
|
+
};
|
|
414
|
+
it.each([[500], ['500'], [[200, 500]]])('should ignore non-4xx retryStatuses (%p) and not retry a 500', async (retryStatuses) => {
|
|
415
|
+
await postFailsWith(500, retryStatuses);
|
|
416
|
+
});
|
|
417
|
+
it('should keep 4xx entries and drop others from a mixed retryStatuses list', async () => {
|
|
418
|
+
await postFailsWith(500, [401, 500]);
|
|
419
|
+
});
|
|
420
|
+
it('should not resend a successful event when retryStatuses is configured as 200', async () => {
|
|
421
|
+
const u = uniqueUser();
|
|
422
|
+
fetchMock()
|
|
423
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
424
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
425
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, retryStatuses: 200 })).sendToFlexPLM({ id: '1' });
|
|
426
|
+
expect(fetchMock()).toHaveBeenCalledTimes(2);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
describe('session cache key includes session options', () => {
|
|
430
|
+
it('should not share a cached cookie between different cookieNames for the same user', async () => {
|
|
431
|
+
const u = uniqueUser();
|
|
432
|
+
fetchMock()
|
|
433
|
+
.mockResolvedValueOnce(resp(200, ['AAA=a1; Path=/']))
|
|
434
|
+
.mockResolvedValueOnce(resp(200, [], {}))
|
|
435
|
+
.mockResolvedValueOnce(resp(200, ['BBB=b1; Path=/']))
|
|
436
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
437
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'AAA' })).sendToFlexPLM({ id: '1' });
|
|
438
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'BBB' })).sendToFlexPLM({ id: '1' });
|
|
439
|
+
expect(callHeaders(2).Cookie).toBeUndefined(); // BBB config must not pick up the AAA session
|
|
440
|
+
expect(callHeaders(3).Cookie).toBe('BBB=b1');
|
|
441
|
+
});
|
|
442
|
+
it('should not share a cached session between different ttlSeconds for the same user', async () => {
|
|
443
|
+
const u = uniqueUser();
|
|
444
|
+
fetchMock()
|
|
445
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=short.tomcat1']))
|
|
446
|
+
.mockResolvedValueOnce(resp(200, [], {}))
|
|
447
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=long.tomcat1']))
|
|
448
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
449
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, ttlSeconds: 1 })).sendToFlexPLM({ id: '1' });
|
|
450
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, ttlSeconds: 900 })).sendToFlexPLM({ id: '1' });
|
|
451
|
+
expect(callHeaders(2).Cookie).toBeUndefined();
|
|
452
|
+
expect(callHeaders(3).Cookie).toBe('JSESSIONID=long.tomcat1');
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
describe('rejected response disposal', () => {
|
|
456
|
+
const withBody = (r) => Object.assign(r, { body: { cancel: jest.fn().mockResolvedValue(undefined) } });
|
|
457
|
+
it('should cancel the body of a POST rejected with 403 before retrying', async () => {
|
|
458
|
+
const u = uniqueUser();
|
|
459
|
+
const rejected = withBody(resp(403, [], {}));
|
|
460
|
+
fetchMock()
|
|
461
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
462
|
+
.mockResolvedValueOnce(rejected)
|
|
463
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
|
|
464
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
465
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM({ id: '1' });
|
|
466
|
+
expect(rejected.body.cancel).toHaveBeenCalledTimes(1);
|
|
467
|
+
});
|
|
468
|
+
it('should cancel the body of a CSRF GET rejected with the cached cookie before retrying', async () => {
|
|
469
|
+
const u = uniqueUser();
|
|
470
|
+
const rejected = withBody(resp(401, []));
|
|
471
|
+
fetchMock()
|
|
472
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
473
|
+
.mockResolvedValueOnce(resp(200, [], {}))
|
|
474
|
+
.mockResolvedValueOnce(rejected)
|
|
475
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
|
|
476
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
477
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM({ id: '1' });
|
|
478
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM({ id: '1' });
|
|
479
|
+
expect(rejected.body.cancel).toHaveBeenCalledTimes(1);
|
|
480
|
+
});
|
|
481
|
+
it('should cancel the body of a getRequest rejected with the cached cookie before retrying', async () => {
|
|
482
|
+
const u = uniqueUser();
|
|
483
|
+
const rejected = withBody(resp(401, []));
|
|
484
|
+
fetchMock()
|
|
485
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
486
|
+
.mockResolvedValueOnce(resp(200, [], {}))
|
|
487
|
+
.mockResolvedValueOnce(rejected)
|
|
488
|
+
.mockResolvedValueOnce(resp(200, [], { items: [] }));
|
|
489
|
+
const connect = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u));
|
|
490
|
+
await connect.sendToFlexPLM({ id: '1' });
|
|
491
|
+
await connect.getRequest();
|
|
492
|
+
expect(rejected.body.cancel).toHaveBeenCalledTimes(1);
|
|
493
|
+
});
|
|
494
|
+
it('should still retry when cancelling the body throws', async () => {
|
|
495
|
+
const u = uniqueUser();
|
|
496
|
+
const rejected = resp(403, [], {});
|
|
497
|
+
rejected.body = { cancel: jest.fn().mockRejectedValue(new Error('locked')) };
|
|
498
|
+
fetchMock()
|
|
499
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
500
|
+
.mockResolvedValueOnce(rejected)
|
|
501
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
|
|
502
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
503
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM({ id: '1' });
|
|
504
|
+
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
describe('cookie names containing regex metacharacters', () => {
|
|
508
|
+
it('should extract a cookie whose name contains "+"', async () => {
|
|
509
|
+
const u = uniqueUser();
|
|
510
|
+
fetchMock()
|
|
511
|
+
.mockResolvedValueOnce(resp(200, ['SID+=abc; Path=/']))
|
|
512
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
513
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'SID+' })).sendToFlexPLM(payload);
|
|
514
|
+
expect(callHeaders(1).Cookie).toBe('SID+=abc');
|
|
515
|
+
});
|
|
516
|
+
it('should treat "." literally and not match a look-alike cookie', async () => {
|
|
517
|
+
const u = uniqueUser();
|
|
518
|
+
fetchMock()
|
|
519
|
+
.mockResolvedValueOnce(resp(200, ['AxB=wrong; Path=/']))
|
|
520
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
521
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'A.B' })).sendToFlexPLM(payload);
|
|
522
|
+
expect(callHeaders(1).Cookie).toBeUndefined();
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
describe('concurrent session acquisition', () => {
|
|
526
|
+
const flush = () => new Promise(r => setImmediate(r));
|
|
527
|
+
const isColdGet = (call) => call[1].method === 'GET' && !call[1].headers.Cookie;
|
|
528
|
+
it('should share one session across concurrent events on a cold cache', async () => {
|
|
529
|
+
const u = uniqueUser();
|
|
530
|
+
let release;
|
|
531
|
+
const gate = new Promise(r => { release = r; });
|
|
532
|
+
fetchMock().mockImplementation(async (_url, opts) => {
|
|
533
|
+
if (opts.method === 'GET' && !opts.headers.Cookie) {
|
|
534
|
+
await gate;
|
|
535
|
+
return resp(200, ['JSESSIONID=abc.tomcat1']);
|
|
536
|
+
}
|
|
537
|
+
return resp(200, []);
|
|
538
|
+
});
|
|
539
|
+
const all = Promise.all([1, 2, 3].map(() => new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).getCSRF()));
|
|
540
|
+
await flush();
|
|
541
|
+
expect(fetchMock()).toHaveBeenCalledTimes(1); // the other two are waiting for the first acquisition
|
|
542
|
+
release();
|
|
543
|
+
const results = await all;
|
|
544
|
+
expect(fetchMock().mock.calls.filter(isColdGet)).toHaveLength(1);
|
|
545
|
+
results.forEach(r => expect(r.sessionCookie).toBe('abc.tomcat1'));
|
|
546
|
+
});
|
|
547
|
+
it('should not make a waiter inherit the failure of the first acquisition', async () => {
|
|
548
|
+
const u = uniqueUser();
|
|
549
|
+
let release;
|
|
550
|
+
const gate = new Promise(r => { release = r; });
|
|
551
|
+
fetchMock()
|
|
552
|
+
.mockImplementationOnce(async () => { await gate; return resp(500, []); })
|
|
553
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']));
|
|
554
|
+
const first = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).getCSRF();
|
|
555
|
+
const firstResult = first.catch(e => e);
|
|
556
|
+
const second = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).getCSRF();
|
|
557
|
+
await flush();
|
|
558
|
+
release();
|
|
559
|
+
expect(await firstResult).toBeInstanceOf(Error);
|
|
560
|
+
expect((await second).sessionCookie).toBe('abc.tomcat1');
|
|
561
|
+
});
|
|
562
|
+
it('should create only one replacement session for parallel events rejected with 403', async () => {
|
|
563
|
+
const u = uniqueUser();
|
|
564
|
+
fetchMock().mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']));
|
|
565
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).getCSRF(); // prime the cache with the old session
|
|
566
|
+
fetchMock().mockReset();
|
|
567
|
+
fetchMock().mockImplementation(async (_url, opts) => {
|
|
568
|
+
const cookie = opts.headers.Cookie;
|
|
569
|
+
if (opts.method === 'POST')
|
|
570
|
+
return cookie === 'JSESSIONID=new.tomcat1' ? resp(200, [], {}) : resp(403, [], {});
|
|
571
|
+
if (!cookie)
|
|
572
|
+
return resp(200, ['JSESSIONID=new.tomcat1']);
|
|
573
|
+
return resp(200, []);
|
|
574
|
+
});
|
|
575
|
+
await Promise.all([1, 2, 3].map(() => new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload)));
|
|
576
|
+
expect(fetchMock().mock.calls.filter(isColdGet)).toHaveLength(1);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
324
579
|
});
|
|
325
580
|
describe('FlexPLMConnect session reuse - feature disabled (default)', () => {
|
|
326
581
|
const payload = { id: '123' };
|
|
327
|
-
it('
|
|
582
|
+
it('should send no Cookie header and make exactly one GET + one POST', async () => {
|
|
328
583
|
const u = uniqueUser();
|
|
329
584
|
fetchMock()
|
|
330
585
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1'])) // Set-Cookie present but must be ignored
|
|
@@ -338,7 +593,7 @@ describe('FlexPLMConnect session reuse - feature disabled (default)', () => {
|
|
|
338
593
|
expect(callHeaders(i).Cookie).toBeUndefined();
|
|
339
594
|
}
|
|
340
595
|
});
|
|
341
|
-
it('
|
|
596
|
+
it('should not retry on 403 (behaves exactly as before)', async () => {
|
|
342
597
|
const u = uniqueUser();
|
|
343
598
|
fetchMock()
|
|
344
599
|
.mockResolvedValueOnce(resp(200))
|
|
@@ -347,7 +602,7 @@ describe('FlexPLMConnect session reuse - feature disabled (default)', () => {
|
|
|
347
602
|
.rejects.toThrow('Error sending data to FlexPLM:status: 403');
|
|
348
603
|
expect(fetchMock()).toHaveBeenCalledTimes(2);
|
|
349
604
|
});
|
|
350
|
-
it('
|
|
605
|
+
it('should behave the same as not configured when enabled is false', async () => {
|
|
351
606
|
const u = uniqueUser();
|
|
352
607
|
fetchMock()
|
|
353
608
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
@@ -356,8 +611,8 @@ describe('FlexPLMConnect session reuse - feature disabled (default)', () => {
|
|
|
356
611
|
expect(callHeaders(1).Cookie).toBeUndefined();
|
|
357
612
|
});
|
|
358
613
|
});
|
|
359
|
-
describe('FlexPLMConnect.getRequest session reuse
|
|
360
|
-
it('
|
|
614
|
+
describe('FlexPLMConnect.getRequest session reuse', () => {
|
|
615
|
+
it('should send the cached cookie on image/GET requests', async () => {
|
|
361
616
|
const u = uniqueUser();
|
|
362
617
|
fetchMock()
|
|
363
618
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1'])) // csrf GET primes the cache
|
|
@@ -368,7 +623,7 @@ describe('FlexPLMConnect.getRequest session reuse (only if you applied step 2g)'
|
|
|
368
623
|
await connect.getRequest();
|
|
369
624
|
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
370
625
|
});
|
|
371
|
-
it('
|
|
626
|
+
it('should retry once without the cookie when the cached session is rejected on getRequest', async () => {
|
|
372
627
|
const u = uniqueUser();
|
|
373
628
|
fetchMock()
|
|
374
629
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
@@ -381,5 +636,20 @@ describe('FlexPLMConnect.getRequest session reuse (only if you applied step 2g)'
|
|
|
381
636
|
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
382
637
|
expect(callHeaders(3).Cookie).toBeUndefined();
|
|
383
638
|
});
|
|
639
|
+
it('should not re-store the rejected cookie when the retry issues no Set-Cookie', async () => {
|
|
640
|
+
const u = uniqueUser();
|
|
641
|
+
fetchMock()
|
|
642
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=stale.tomcat1']))
|
|
643
|
+
.mockResolvedValueOnce(resp(200, [], {}))
|
|
644
|
+
.mockResolvedValueOnce(resp(401, [])) // getRequest with the cached cookie
|
|
645
|
+
.mockResolvedValueOnce(resp(200, [], { items: [] })) // retry succeeds, no Set-Cookie
|
|
646
|
+
.mockResolvedValueOnce(resp(200, [], { items: [] })); // next getRequest
|
|
647
|
+
const connect = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u));
|
|
648
|
+
await connect.sendToFlexPLM({ id: '1' });
|
|
649
|
+
await connect.getRequest();
|
|
650
|
+
await connect.getRequest();
|
|
651
|
+
expect(callHeaders(3).Cookie).toBeUndefined();
|
|
652
|
+
expect(callHeaders(4).Cookie).toBeUndefined(); // stale cookie must not be sent again
|
|
653
|
+
});
|
|
384
654
|
});
|
|
385
655
|
// Added for Session reuse - ET-390 - Ends
|