@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(v => (typeof v === 'string' ? v.trim() : v))
27
- .filter(v => v !== '' && v !== null && v !== undefined)
39
+ .map(status => (typeof status === 'string' ? status.trim() : status))
40
+ .filter(status => status !== '' && status !== null && status !== undefined)
28
41
  .map(Number)
29
- .filter(n => Number.isInteger(n) && n >= 100 && n <= 599);
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
- return [this.config.apiHost, this.config.urlContext, this.config.userName(), this.config.plmEnviornment].join('|');
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
- sessionCache.set(this.getSessionKey(), { cookie, expiresAt: Date.now() + this.sessionOptions.ttlMs });
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('reuses the cached session on the next event (new FlexPLMConnect instance)', async () => {
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('replaces the cached session when Windchill answers the GET with a new Set-Cookie', async () => {
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('retries once with a new session when the POST is rejected with 403', async () => {
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 retries on 401', async () => {
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('does not loop: a second 403 after the retry is surfaced as an error', async () => {
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('does not retry on non-auth errors such as 500 (event may already be processed)', async () => {
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('retries the csrf GET without a cookie when the cached session is rejected there', async () => {
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('expires the cached session after ttlSeconds', async () => {
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('uses the last JSESSIONID when several Set-Cookie lines are returned', async () => {
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('falls back to headers.get("set-cookie") when getSetCookie is not available', async () => {
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('honours a custom cookieName', async () => {
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('accepts enabled as the string "true"', async () => {
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('keeps separate sessions per user', async () => {
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 logs the cookie value', async () => {
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
- ])('retryStatuses given as %s does not throw and still retries on 403', async (_label, retryStatuses) => {
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]])('invalid ttlSeconds (%p) falls back to the 900s default', async (ttlSeconds) => {
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('a blank cookieName falls back to JSESSIONID', async () => {
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('extracts a cookie whose name contains "+"', async () => {
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('treats "." literally and does not match a look-alike cookie', async () => {
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('shares one session across concurrent events on a cold cache', async () => {
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 does not inherit the failure of the first acquisition', async () => {
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('parallel events rejected with 403 create only one replacement session', async () => {
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('sends no Cookie header and makes exactly one GET + one POST', async () => {
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('does not retry on 403 (behaves exactly as before)', async () => {
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('explicit enabled:false behaves the same as not configured', async () => {
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('sends the cached cookie on image/GET requests', async () => {
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('retries once without the cookie when the cached session is rejected on getRequest', async () => {
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('does not re-store the rejected cookie when the retry issues no Set-Cookie', async () => {
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']))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/flexplm",
3
- "version": "1.7.7-alpha.7ad4e7e",
3
+ "version": "1.7.7-alpha.85f6463",
4
4
  "description": "Library used for integration with flexplm.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",