@contrail/flexplm 1.7.7-alpha.7ad4e7e → 1.7.7-alpha.85f6463
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.
|
@@ -15,6 +15,8 @@ export declare class FlexPLMConnect {
|
|
|
15
15
|
/** Runs a cold acquisition and registers it so concurrent callers wait for it instead of opening their own session. */
|
|
16
16
|
private runColdAcquisition;
|
|
17
17
|
private getSessionKey;
|
|
18
|
+
/** Releases the connection of a rejected response before it is replaced by a retry. */
|
|
19
|
+
private discardBody;
|
|
18
20
|
private getCachedSession;
|
|
19
21
|
private storeSession;
|
|
20
22
|
private clearSession;
|
|
@@ -4,6 +4,19 @@ exports.FlexPLMConnect = void 0;
|
|
|
4
4
|
const app_framework_1 = require("@contrail/app-framework");
|
|
5
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
|
+
};
|
|
7
20
|
// In-flight cold session acquisitions per key, so concurrent events share one FlexPLM session instead of each creating their own.
|
|
8
21
|
const inFlightAcquisitions = new Map();
|
|
9
22
|
const DEFAULT_SESSION_COOKIE_NAME = 'JSESSIONID';
|
|
@@ -23,10 +36,11 @@ const parseRetryStatuses = (value) => {
|
|
|
23
36
|
else if (value !== undefined && value !== null)
|
|
24
37
|
list = [value];
|
|
25
38
|
const statuses = list
|
|
26
|
-
.map(
|
|
27
|
-
.filter(
|
|
39
|
+
.map(status => (typeof status === 'string' ? status.trim() : status))
|
|
40
|
+
.filter(status => status !== '' && status !== null && status !== undefined)
|
|
28
41
|
.map(Number)
|
|
29
|
-
|
|
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);
|
|
30
44
|
return statuses.length ? statuses : DEFAULT_RETRY_STATUSES;
|
|
31
45
|
};
|
|
32
46
|
const parseTtlSeconds = (value) => {
|
|
@@ -112,7 +126,21 @@ class FlexPLMConnect {
|
|
|
112
126
|
}
|
|
113
127
|
}
|
|
114
128
|
getSessionKey() {
|
|
115
|
-
|
|
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
|
+
}
|
|
116
144
|
}
|
|
117
145
|
getCachedSession() {
|
|
118
146
|
if (!this.sessionOptions.enabled)
|
|
@@ -129,7 +157,11 @@ class FlexPLMConnect {
|
|
|
129
157
|
}
|
|
130
158
|
storeSession(cookie) {
|
|
131
159
|
// sliding expiry: Windchill's session timeout is also sliding
|
|
132
|
-
|
|
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 });
|
|
133
165
|
}
|
|
134
166
|
clearSession() {
|
|
135
167
|
sessionCache.delete(this.getSessionKey());
|
|
@@ -186,6 +218,7 @@ class FlexPLMConnect {
|
|
|
186
218
|
csrfOptions.headers['Cookie'] = cookie;
|
|
187
219
|
}
|
|
188
220
|
// Added for Session reuse - ET-390 - Ends
|
|
221
|
+
console.info('cookie: ' + cookie);
|
|
189
222
|
if (app_framework_1.Logger.isInfoOn()) {
|
|
190
223
|
const logOptions = JSON.parse(JSON.stringify(csrfOptions));
|
|
191
224
|
logOptions['headers']['Authorization'] = logOptions?.headers?.Authorization.substring(0, 9);
|
|
@@ -211,7 +244,7 @@ class FlexPLMConnect {
|
|
|
211
244
|
await wait;
|
|
212
245
|
if (getNewSession) {
|
|
213
246
|
const current = this.getCachedSession();
|
|
214
|
-
if (!current || current === rejectedCookie)
|
|
247
|
+
if (rejectedCookie === undefined || !current || current === rejectedCookie)
|
|
215
248
|
this.clearSession();
|
|
216
249
|
}
|
|
217
250
|
const cached = this.getCachedSession();
|
|
@@ -242,6 +275,7 @@ class FlexPLMConnect {
|
|
|
242
275
|
// Added for Session reuse - ET-390 - Starts
|
|
243
276
|
if (sentCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
244
277
|
console.warn('Cached FlexPLM session rejected on csrf call (status ' + response.status + '). Retrying with a new session.');
|
|
278
|
+
await this.discardBody(response);
|
|
245
279
|
throw new SessionRejectedError();
|
|
246
280
|
}
|
|
247
281
|
// Added for Session reuse - ET-390 - Ends
|
|
@@ -262,6 +296,7 @@ class FlexPLMConnect {
|
|
|
262
296
|
this.storeSession(sessionCookie);
|
|
263
297
|
}
|
|
264
298
|
// Added for Session reuse - ET-390 - Ends
|
|
299
|
+
console.info('sessionCookie: ' + sessionCookie);
|
|
265
300
|
try {
|
|
266
301
|
const body = await response.json();
|
|
267
302
|
const nonce_key = body.items[0].attributes.nonce_key;
|
|
@@ -325,6 +360,7 @@ class FlexPLMConnect {
|
|
|
325
360
|
let response = await this.sendRequest(csrf, payload);
|
|
326
361
|
if (csrf.sessionCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
327
362
|
console.warn('FlexPLM rejected session/nonce (status ' + response.status + '). Getting a new session and retrying once.');
|
|
363
|
+
await this.discardBody(response);
|
|
328
364
|
csrf = await this.getCSRF(true, csrf.sessionCookie);
|
|
329
365
|
response = await this.sendRequest(csrf, payload);
|
|
330
366
|
}
|
|
@@ -404,6 +440,7 @@ class FlexPLMConnect {
|
|
|
404
440
|
const first = await attempt();
|
|
405
441
|
let response = first.response;
|
|
406
442
|
if (first.sent && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
443
|
+
await this.discardBody(response);
|
|
407
444
|
if (this.getCachedSession() === first.sent)
|
|
408
445
|
this.clearSession();
|
|
409
446
|
response = (await attempt()).response;
|
|
@@ -156,7 +156,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
156
156
|
expect(callHeaders(1).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
157
157
|
expect(callHeaders(1).CSRF_NONCE).toBe('n1');
|
|
158
158
|
});
|
|
159
|
-
it('
|
|
159
|
+
it('should reuse the cached session on the next event (new FlexPLMConnect instance)', async () => {
|
|
160
160
|
const u = uniqueUser();
|
|
161
161
|
fetchMock()
|
|
162
162
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1; Path=/'])) // event 1 GET
|
|
@@ -168,7 +168,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
168
168
|
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1'); // GET reuses it
|
|
169
169
|
expect(callHeaders(3).Cookie).toBe('JSESSIONID=abc.tomcat1'); // POST reuses it
|
|
170
170
|
});
|
|
171
|
-
it('
|
|
171
|
+
it('should replace the cached session when Windchill answers the GET with a new Set-Cookie', async () => {
|
|
172
172
|
const u = uniqueUser();
|
|
173
173
|
fetchMock()
|
|
174
174
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1; Path=/']))
|
|
@@ -185,7 +185,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
185
185
|
expect(callHeaders(3).Cookie).toBe('JSESSIONID=new.tomcat1'); // POST uses the replacement
|
|
186
186
|
expect(callHeaders(4).Cookie).toBe('JSESSIONID=new.tomcat1'); // event 3 GET sends the replacement
|
|
187
187
|
});
|
|
188
|
-
it('
|
|
188
|
+
it('should retry once with a new session when the POST is rejected with 403', async () => {
|
|
189
189
|
const u = uniqueUser();
|
|
190
190
|
fetchMock()
|
|
191
191
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1; Path=/'])) // GET
|
|
@@ -199,7 +199,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
199
199
|
expect(result.status).toBe(200);
|
|
200
200
|
expect(result.data).toEqual({ ok: true });
|
|
201
201
|
});
|
|
202
|
-
it('also
|
|
202
|
+
it('should also retry on 401', async () => {
|
|
203
203
|
const u = uniqueUser();
|
|
204
204
|
fetchMock()
|
|
205
205
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
@@ -209,7 +209,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
209
209
|
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
210
210
|
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
211
211
|
});
|
|
212
|
-
it('
|
|
212
|
+
it('should not loop: a second 403 after the retry is surfaced as an error', async () => {
|
|
213
213
|
const u = uniqueUser();
|
|
214
214
|
fetchMock()
|
|
215
215
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
|
|
@@ -220,7 +220,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
220
220
|
.rejects.toThrow('Error sending data to FlexPLM:status: 403');
|
|
221
221
|
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
222
222
|
});
|
|
223
|
-
it('
|
|
223
|
+
it('should not retry on non-auth errors such as 500 (event may already be processed)', async () => {
|
|
224
224
|
const u = uniqueUser();
|
|
225
225
|
fetchMock()
|
|
226
226
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
@@ -229,7 +229,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
229
229
|
.rejects.toThrow('Error sending data to FlexPLM:status: 500');
|
|
230
230
|
expect(fetchMock()).toHaveBeenCalledTimes(2);
|
|
231
231
|
});
|
|
232
|
-
it('
|
|
232
|
+
it('should retry the csrf GET without a cookie when the cached session is rejected there', async () => {
|
|
233
233
|
const u = uniqueUser();
|
|
234
234
|
fetchMock()
|
|
235
235
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1'])) // event 1 GET
|
|
@@ -243,7 +243,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
243
243
|
expect(callHeaders(3).Cookie).toBeUndefined();
|
|
244
244
|
expect(callHeaders(4).Cookie).toBe('JSESSIONID=new.tomcat1');
|
|
245
245
|
});
|
|
246
|
-
it('
|
|
246
|
+
it('should expire the cached session after ttlSeconds', async () => {
|
|
247
247
|
const u = uniqueUser();
|
|
248
248
|
const realNow = Date.now();
|
|
249
249
|
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(realNow);
|
|
@@ -260,7 +260,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
260
260
|
expect(callHeaders(3).Cookie).toBe('JSESSIONID=def.tomcat1');
|
|
261
261
|
nowSpy.mockRestore();
|
|
262
262
|
});
|
|
263
|
-
it('
|
|
263
|
+
it('should use the last JSESSIONID when several Set-Cookie lines are returned', async () => {
|
|
264
264
|
const u = uniqueUser();
|
|
265
265
|
fetchMock()
|
|
266
266
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=first.tomcat1; Path=/', 'OTHER=x; Path=/', 'JSESSIONID=second.tomcat1; Path=/']))
|
|
@@ -268,7 +268,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
268
268
|
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
269
269
|
expect(callHeaders(1).Cookie).toBe('JSESSIONID=second.tomcat1');
|
|
270
270
|
});
|
|
271
|
-
it('
|
|
271
|
+
it('should fall back to headers.get("set-cookie") when getSetCookie is not available', async () => {
|
|
272
272
|
const u = uniqueUser();
|
|
273
273
|
const legacy = {
|
|
274
274
|
status: 200,
|
|
@@ -280,7 +280,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
280
280
|
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
|
|
281
281
|
expect(callHeaders(1).Cookie).toBe('JSESSIONID=legacy.tomcat1');
|
|
282
282
|
});
|
|
283
|
-
it('
|
|
283
|
+
it('should honour a custom cookieName', async () => {
|
|
284
284
|
const u = uniqueUser();
|
|
285
285
|
fetchMock()
|
|
286
286
|
.mockResolvedValueOnce(resp(200, ['MYSESSION=zzz; Path=/', 'JSESSIONID=ignored; Path=/']))
|
|
@@ -288,7 +288,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
288
288
|
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'MYSESSION' })).sendToFlexPLM(payload);
|
|
289
289
|
expect(callHeaders(1).Cookie).toBe('MYSESSION=zzz');
|
|
290
290
|
});
|
|
291
|
-
it('
|
|
291
|
+
it('should accept enabled as the string "true"', async () => {
|
|
292
292
|
const u = uniqueUser();
|
|
293
293
|
fetchMock()
|
|
294
294
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
@@ -296,7 +296,39 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
296
296
|
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: 'true' })).sendToFlexPLM(payload);
|
|
297
297
|
expect(callHeaders(1).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
298
298
|
});
|
|
299
|
-
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 () => {
|
|
300
332
|
const a = uniqueUser();
|
|
301
333
|
const b = uniqueUser();
|
|
302
334
|
fetchMock()
|
|
@@ -309,7 +341,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
309
341
|
expect(callHeaders(2).Cookie).toBeUndefined(); // user b must not see user a's cookie
|
|
310
342
|
expect(callHeaders(3).Cookie).toBe('JSESSIONID=bbb.tomcat1');
|
|
311
343
|
});
|
|
312
|
-
it('never
|
|
344
|
+
it('should never log the cookie value', async () => {
|
|
313
345
|
const isInfoOn = jest.spyOn(app_framework_1.Logger, 'isInfoOn').mockReturnValue(true);
|
|
314
346
|
const info = jest.spyOn(console, 'info').mockImplementation();
|
|
315
347
|
const u = uniqueUser();
|
|
@@ -340,11 +372,11 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
340
372
|
['a comma separated string', '401, 403'],
|
|
341
373
|
['garbage (falls back to 401/403)', 'abc'],
|
|
342
374
|
['an empty array (falls back to 401/403)', []],
|
|
343
|
-
])('
|
|
375
|
+
])('should not throw and still retry on 403 when retryStatuses is %s', async (_label, retryStatuses) => {
|
|
344
376
|
await rejectedOnceThenOk(uniqueUser(), { retryStatuses });
|
|
345
377
|
expect(fetchMock()).toHaveBeenCalledTimes(4);
|
|
346
378
|
});
|
|
347
|
-
it.each([['abc'], [0], [-5]])('
|
|
379
|
+
it.each([['abc'], [0], [-5]])('should fall back to the 900s default when ttlSeconds is invalid (%p)', async (ttlSeconds) => {
|
|
348
380
|
const u = uniqueUser();
|
|
349
381
|
const realNow = Date.now();
|
|
350
382
|
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(realNow);
|
|
@@ -360,7 +392,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
360
392
|
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
361
393
|
nowSpy.mockRestore();
|
|
362
394
|
});
|
|
363
|
-
it('
|
|
395
|
+
it('should fall back to JSESSIONID when cookieName is blank', async () => {
|
|
364
396
|
const u = uniqueUser();
|
|
365
397
|
fetchMock()
|
|
366
398
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
@@ -369,8 +401,111 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
369
401
|
expect(callHeaders(1).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
370
402
|
});
|
|
371
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
|
+
});
|
|
372
507
|
describe('cookie names containing regex metacharacters', () => {
|
|
373
|
-
it('
|
|
508
|
+
it('should extract a cookie whose name contains "+"', async () => {
|
|
374
509
|
const u = uniqueUser();
|
|
375
510
|
fetchMock()
|
|
376
511
|
.mockResolvedValueOnce(resp(200, ['SID+=abc; Path=/']))
|
|
@@ -378,7 +513,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
378
513
|
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'SID+' })).sendToFlexPLM(payload);
|
|
379
514
|
expect(callHeaders(1).Cookie).toBe('SID+=abc');
|
|
380
515
|
});
|
|
381
|
-
it('
|
|
516
|
+
it('should treat "." literally and not match a look-alike cookie', async () => {
|
|
382
517
|
const u = uniqueUser();
|
|
383
518
|
fetchMock()
|
|
384
519
|
.mockResolvedValueOnce(resp(200, ['AxB=wrong; Path=/']))
|
|
@@ -390,7 +525,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
390
525
|
describe('concurrent session acquisition', () => {
|
|
391
526
|
const flush = () => new Promise(r => setImmediate(r));
|
|
392
527
|
const isColdGet = (call) => call[1].method === 'GET' && !call[1].headers.Cookie;
|
|
393
|
-
it('
|
|
528
|
+
it('should share one session across concurrent events on a cold cache', async () => {
|
|
394
529
|
const u = uniqueUser();
|
|
395
530
|
let release;
|
|
396
531
|
const gate = new Promise(r => { release = r; });
|
|
@@ -409,7 +544,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
409
544
|
expect(fetchMock().mock.calls.filter(isColdGet)).toHaveLength(1);
|
|
410
545
|
results.forEach(r => expect(r.sessionCookie).toBe('abc.tomcat1'));
|
|
411
546
|
});
|
|
412
|
-
it('a waiter
|
|
547
|
+
it('should not make a waiter inherit the failure of the first acquisition', async () => {
|
|
413
548
|
const u = uniqueUser();
|
|
414
549
|
let release;
|
|
415
550
|
const gate = new Promise(r => { release = r; });
|
|
@@ -424,7 +559,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
424
559
|
expect(await firstResult).toBeInstanceOf(Error);
|
|
425
560
|
expect((await second).sessionCookie).toBe('abc.tomcat1');
|
|
426
561
|
});
|
|
427
|
-
it('
|
|
562
|
+
it('should create only one replacement session for parallel events rejected with 403', async () => {
|
|
428
563
|
const u = uniqueUser();
|
|
429
564
|
fetchMock().mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']));
|
|
430
565
|
await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).getCSRF(); // prime the cache with the old session
|
|
@@ -444,7 +579,7 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
444
579
|
});
|
|
445
580
|
describe('FlexPLMConnect session reuse - feature disabled (default)', () => {
|
|
446
581
|
const payload = { id: '123' };
|
|
447
|
-
it('
|
|
582
|
+
it('should send no Cookie header and make exactly one GET + one POST', async () => {
|
|
448
583
|
const u = uniqueUser();
|
|
449
584
|
fetchMock()
|
|
450
585
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1'])) // Set-Cookie present but must be ignored
|
|
@@ -458,7 +593,7 @@ describe('FlexPLMConnect session reuse - feature disabled (default)', () => {
|
|
|
458
593
|
expect(callHeaders(i).Cookie).toBeUndefined();
|
|
459
594
|
}
|
|
460
595
|
});
|
|
461
|
-
it('
|
|
596
|
+
it('should not retry on 403 (behaves exactly as before)', async () => {
|
|
462
597
|
const u = uniqueUser();
|
|
463
598
|
fetchMock()
|
|
464
599
|
.mockResolvedValueOnce(resp(200))
|
|
@@ -467,7 +602,7 @@ describe('FlexPLMConnect session reuse - feature disabled (default)', () => {
|
|
|
467
602
|
.rejects.toThrow('Error sending data to FlexPLM:status: 403');
|
|
468
603
|
expect(fetchMock()).toHaveBeenCalledTimes(2);
|
|
469
604
|
});
|
|
470
|
-
it('
|
|
605
|
+
it('should behave the same as not configured when enabled is false', async () => {
|
|
471
606
|
const u = uniqueUser();
|
|
472
607
|
fetchMock()
|
|
473
608
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
@@ -477,7 +612,7 @@ describe('FlexPLMConnect session reuse - feature disabled (default)', () => {
|
|
|
477
612
|
});
|
|
478
613
|
});
|
|
479
614
|
describe('FlexPLMConnect.getRequest session reuse', () => {
|
|
480
|
-
it('
|
|
615
|
+
it('should send the cached cookie on image/GET requests', async () => {
|
|
481
616
|
const u = uniqueUser();
|
|
482
617
|
fetchMock()
|
|
483
618
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1'])) // csrf GET primes the cache
|
|
@@ -488,7 +623,7 @@ describe('FlexPLMConnect.getRequest session reuse', () => {
|
|
|
488
623
|
await connect.getRequest();
|
|
489
624
|
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
490
625
|
});
|
|
491
|
-
it('
|
|
626
|
+
it('should retry once without the cookie when the cached session is rejected on getRequest', async () => {
|
|
492
627
|
const u = uniqueUser();
|
|
493
628
|
fetchMock()
|
|
494
629
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
@@ -501,7 +636,7 @@ describe('FlexPLMConnect.getRequest session reuse', () => {
|
|
|
501
636
|
expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1');
|
|
502
637
|
expect(callHeaders(3).Cookie).toBeUndefined();
|
|
503
638
|
});
|
|
504
|
-
it('
|
|
639
|
+
it('should not re-store the rejected cookie when the retry issues no Set-Cookie', async () => {
|
|
505
640
|
const u = uniqueUser();
|
|
506
641
|
fetchMock()
|
|
507
642
|
.mockResolvedValueOnce(resp(200, ['JSESSIONID=stale.tomcat1']))
|