@contrail/flexplm 1.7.6 → 1.7.7-alpha.1ad98c2

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.
@@ -5,10 +5,19 @@ export declare class FlexPLMConnect {
5
5
  private staticHeaders;
6
6
  payloadSendAsArray: boolean;
7
7
  constructor(_config: FCConfig, endPoint?: any, payloadAsArray?: any);
8
+ private get sessionOptions();
9
+ private getSessionKey;
10
+ private getCachedSession;
11
+ private storeSession;
12
+ private clearSession;
13
+ /** Returns the session cookie value Windchill issued on this response, if any (last one wins). */
14
+ private extractSessionCookie;
15
+ private cookieHeader;
8
16
  private getRequestOptions;
9
- getCSRF(): Promise<{
17
+ getCSRF(getNewSession?: boolean): Promise<{
10
18
  nonce_key: any;
11
19
  nonce: any;
20
+ sessionCookie: string;
12
21
  }>;
13
22
  sendRequest(csrf: any, payload: any): Promise<Response>;
14
23
  protected processRequest(payload: any): Promise<FlexPLMResponseData>;
@@ -2,6 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FlexPLMConnect = void 0;
4
4
  const app_framework_1 = require("@contrail/app-framework");
5
+ // Module-level on purpose: a new FlexPLMConnect is constructed for every event. Declaring for Session resuing use case.
6
+ const sessionCache = new Map();
7
+ // Added for Session reuse - ET-390 - Ends
5
8
  class FlexPLMConnect {
6
9
  constructor(_config, endPoint = undefined, payloadAsArray = undefined) {
7
10
  this.vibeEventEndpoint = '';
@@ -19,9 +22,95 @@ class FlexPLMConnect {
19
22
  }
20
23
  }
21
24
  /////////////////////////////////////////////////////////////////////////////
25
+ ///////// ET-390 - Session reuse: start
26
+ /////////////////////////////////////////////////////////////////////////////
27
+ /*
28
+ private get sessionOptions() {
29
+ const sessReuseBool = this.config?.flexplmConnect?.sessionReuse;
30
+ console.info('sessReuseBool value is --> ' + sessReuseBool);
31
+ console.info('enabled value is --> ' + sessReuseBool?.enabled);
32
+ return {
33
+ enabled: sessReuseBool?.enabled === true || String(sessReuseBool?.enabled).toLowerCase() === 'true',
34
+ ttlMs: (sessReuseBool?.ttlSeconds ?? 900) * 1000,
35
+ cookieName: sessReuseBool?.cookieName ?? 'JSESSIONID',
36
+ retryStatuses: (sessReuseBool?.retryStatuses ?? [401, 403]) as number[],
37
+ };
38
+ }
39
+ */
40
+ get sessionOptions() {
41
+ const cfg = this.config ?? {};
42
+ const sessReuseEntries = (name) => cfg[`flexplmConnect.sessionReuse.${name}`];
43
+ const enabled = sessReuseEntries('enabled');
44
+ console.info('sessionReuse.enabled value is --> ' + enabled);
45
+ return {
46
+ enabled: enabled === true || String(enabled).toLowerCase() === 'true',
47
+ ttlMs: (sessReuseEntries('ttlSeconds') ?? 900) * 1000,
48
+ cookieName: sessReuseEntries('cookieName') ?? 'JSESSIONID',
49
+ retryStatuses: (sessReuseEntries('retryStatuses') ?? [401, 403]),
50
+ };
51
+ }
52
+ getSessionKey() {
53
+ return [this.config.apiHost, this.config.urlContext, this.config.userName(), this.config.plmEnviornment].join('|');
54
+ }
55
+ getCachedSession() {
56
+ if (!this.sessionOptions.enabled)
57
+ return undefined;
58
+ const key = this.getSessionKey();
59
+ const cachedSessValue = sessionCache.get(key);
60
+ if (!cachedSessValue)
61
+ return undefined;
62
+ if (cachedSessValue.expiresAt <= Date.now()) {
63
+ sessionCache.delete(key);
64
+ return undefined;
65
+ }
66
+ console.info('cachedSessValue.cookie: ' + cachedSessValue.cookie);
67
+ return cachedSessValue.cookie;
68
+ }
69
+ storeSession(cookie) {
70
+ // sliding expiry: Windchill's session timeout is also sliding
71
+ console.info('cookie: ' + cookie);
72
+ sessionCache.set(this.getSessionKey(), { cookie, expiresAt: Date.now() + this.sessionOptions.ttlMs });
73
+ }
74
+ clearSession() {
75
+ sessionCache.delete(this.getSessionKey());
76
+ }
77
+ /** Returns the session cookie value Windchill issued on this response, if any (last one wins). */
78
+ extractSessionCookie(response) {
79
+ const respHeader = response?.headers;
80
+ console.info('respHeader: ' + respHeader);
81
+ if (!respHeader)
82
+ return undefined;
83
+ let raw = [];
84
+ if (typeof respHeader.getSetCookie === 'function') {
85
+ raw = respHeader.getSetCookie();
86
+ }
87
+ else if (typeof respHeader.get === 'function' && respHeader.get('set-cookie')) {
88
+ raw = [respHeader.get('set-cookie')];
89
+ }
90
+ const name = this.sessionOptions.cookieName;
91
+ const regexFilter = new RegExp('(?:^|[,\\s])' + name + '=([^;,\\s]+)', 'g');
92
+ let jSession;
93
+ for (const line of raw) {
94
+ let cookieNameMatcher = regexFilter.exec(line);
95
+ while (cookieNameMatcher !== null) {
96
+ jSession = cookieNameMatcher[1];
97
+ cookieNameMatcher = regexFilter.exec(line);
98
+ }
99
+ }
100
+ console.info('jSession: ' + jSession);
101
+ return jSession;
102
+ }
103
+ cookieHeader(sessionCookie) {
104
+ return sessionCookie ? `${this.sessionOptions.cookieName}=${sessionCookie}` : undefined;
105
+ }
106
+ /////////////////////////////////////////////////////////////////////////////
107
+ ///////// ET-390 - Session reuse: end
108
+ /////////////////////////////////////////////////////////////////////////////
109
+ /////////////////////////////////////////////////////////////////////////////
22
110
  ///////// Custom getRequestOptions: start
23
111
  /////////////////////////////////////////////////////////////////////////////
24
- getRequestOptions(method) {
112
+ // ET-390 - adding a new parameter sessionCookie to the getRequestOptions function
113
+ getRequestOptions(method, sessionCookie) {
25
114
  const csrfOptions = {
26
115
  method,
27
116
  headers: {
@@ -33,23 +122,53 @@ class FlexPLMConnect {
33
122
  if (this.staticHeaders) {
34
123
  Object.assign(csrfOptions.headers, this.staticHeaders);
35
124
  }
125
+ // Added for Session reuse - ET-390 - Starts
126
+ const cookie = this.cookieHeader(sessionCookie);
127
+ console.info('sessionReuse--cookie --> ' + cookie);
128
+ if (cookie) {
129
+ csrfOptions.headers['Cookie'] = cookie;
130
+ }
131
+ // Added for Session reuse - ET-390 - Ends
36
132
  if (app_framework_1.Logger.isInfoOn()) {
37
133
  const logOptions = JSON.parse(JSON.stringify(csrfOptions));
38
134
  logOptions['headers']['Authorization'] = logOptions?.headers?.Authorization.substring(0, 9);
135
+ // Added for Session reuse - ET-390 - Starts
136
+ if (logOptions.headers['Cookie'])
137
+ logOptions.headers['Cookie'] = '***masked***';
138
+ // Added for Session reuse - ET-390 - Ends
39
139
  console.info('csrfOptions: ' + JSON.stringify(logOptions));
40
140
  }
141
+ console.info('csrfOptions --> ' + csrfOptions);
41
142
  return csrfOptions;
42
143
  }
43
144
  /////////////////////////////////////////////////////////////////////////////
44
145
  ///////// Custom getRequestOptions: end
45
146
  /////////////////////////////////////////////////////////////////////////////
46
- async getCSRF() {
147
+ // ET-390 - adding a new parameter getNewSession to the getCSRF function
148
+ // Whenever a getCSRF is called without getNewSession, the default value of false will be used
149
+ async getCSRF(getNewSession = false) {
47
150
  const urlContext = this.config.urlContext;
48
151
  const csrfEndpoint = this.config.csrfEndpoint;
49
152
  const csrfURL = this.config.apiHost + urlContext + csrfEndpoint;
50
153
  console.info('csrfURL: ' + csrfURL);
51
- const csrfOptions = this.getRequestOptions('GET');
52
- const response = await fetch(csrfURL, csrfOptions);
154
+ // Added for Session reuse - ET-390 - Starts
155
+ if (getNewSession)
156
+ this.clearSession();
157
+ let sentCookie = this.getCachedSession();
158
+ // getRequestOptions will be called with additional sentCookie parameter
159
+ const csrfOptions = this.getRequestOptions('GET', sentCookie);
160
+ // Added for Session reuse - ET-390 - Ends
161
+ let response = await fetch(csrfURL, csrfOptions);
162
+ console.info('response.status: ' + response.status);
163
+ // Added for Session reuse - ET-390 - Starts
164
+ // if the response status is 401 or 403, then get a new session id
165
+ if (sentCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
166
+ console.warn('Cached FlexPLM session rejected on csrf call (status ' + response.status + '). Retrying with a new session.');
167
+ this.clearSession();
168
+ sentCookie = undefined;
169
+ response = await fetch(csrfURL, this.getRequestOptions('GET'));
170
+ }
171
+ // Added for Session reuse - ET-390 - Ends
53
172
  if (response.status >= 300) {
54
173
  const message = 'Error connecting to FlexPLM:status: ' + response.status;
55
174
  console.error(message);
@@ -58,14 +177,26 @@ class FlexPLMConnect {
58
177
  e['httpResponseStatus'] = response.status;
59
178
  throw e;
60
179
  }
180
+ // Added for Session reuse - ET-390 - Starts
181
+ // If the cookie we sent was invalid, Windchill answers with a fresh Set-Cookie --> replace the cache.
182
+ let sessionCookie = sentCookie;
183
+ if (this.sessionOptions.enabled) {
184
+ sessionCookie = this.extractSessionCookie(response) ?? sentCookie;
185
+ if (sessionCookie)
186
+ this.storeSession(sessionCookie);
187
+ }
188
+ // Added for Session reuse - ET-390 - Ends
61
189
  try {
62
190
  const body = await response.json();
63
191
  const nonce_key = body.items[0].attributes.nonce_key;
64
192
  const nonce = body.items[0].attributes.nonce;
65
193
  console.info('nonce_key: ' + nonce_key);
194
+ console.info('sessionCookie: ' + sessionCookie);
195
+ //ET-390 - adding sessionCookie to the return
66
196
  return {
67
197
  nonce_key,
68
- nonce
198
+ nonce,
199
+ sessionCookie,
69
200
  };
70
201
  }
71
202
  catch (e) {
@@ -79,7 +210,7 @@ class FlexPLMConnect {
79
210
  if (this.payloadSendAsArray && !Array.isArray(payload)) {
80
211
  payload = [payload];
81
212
  }
82
- const xferOptions = this.getRequestOptions('POST');
213
+ const xferOptions = this.getRequestOptions('POST', csrf?.sessionCookie);
83
214
  xferOptions['body'] = JSON.stringify(payload);
84
215
  xferOptions.headers[csrf.nonce_key] = csrf.nonce;
85
216
  const urlContext = this.config.urlContext;
@@ -90,6 +221,8 @@ class FlexPLMConnect {
90
221
  const logOptions = JSON.parse(JSON.stringify(xferOptions));
91
222
  if (logOptions?.headers?.Authorization)
92
223
  logOptions.headers.Authorization = logOptions?.headers?.Authorization.substring(0, 9);
224
+ if (logOptions?.headers?.Cookie)
225
+ logOptions.headers.Cookie = '***masked***';
93
226
  if (csrf?.nonce_key)
94
227
  logOptions.headers[csrf.nonce_key] = csrf.nonce ? `***masked(length:${csrf.nonce.length})***` : '***masked(empty)***';
95
228
  console.info('csrfOptions: ' + JSON.stringify(logOptions));
@@ -107,13 +240,21 @@ class FlexPLMConnect {
107
240
  if (app_framework_1.Logger.isInfoOn()) {
108
241
  console.info('payload: ' + JSON.stringify(payload));
109
242
  }
110
- const csrf = await this.getCSRF();
243
+ let csrf = await this.getCSRF();
111
244
  if (!csrf) {
112
245
  const message = 'Failed to get CSRF nonce';
113
246
  console.error(message);
114
247
  throw new Error(message);
115
248
  }
116
- const response = await this.sendRequest(csrf, payload);
249
+ /// Added for Session reuse - ET-390 - Starts
250
+ // const response = await this.sendRequest(csrf, payload);
251
+ let response = await this.sendRequest(csrf, payload);
252
+ if (csrf.sessionCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
253
+ console.warn('FlexPLM rejected session/nonce (status ' + response.status + '). Getting a new session and retrying once.');
254
+ csrf = await this.getCSRF(true);
255
+ response = await this.sendRequest(csrf, payload);
256
+ }
257
+ // Added for Session reuse - ET-390 - Ends
117
258
  const status = response.status;
118
259
  if (status >= 300) {
119
260
  const message = 'Error sending data to FlexPLM:status: ' + response.status;
@@ -166,8 +307,22 @@ class FlexPLMConnect {
166
307
  const urlContext = includeUrlContext ? this.config.urlContext : '';
167
308
  const path = urlPath || ('/servlet/rest' + this.vibeEventEndpoint);
168
309
  const vibeEventsURL = this.config.apiHost + urlContext + path;
169
- const csrfOptions = this.getRequestOptions('GET');
170
- const response = await fetch(vibeEventsURL, csrfOptions);
310
+ // Added for Session reuse - ET-390 - Starts
311
+ const cached = this.getCachedSession();
312
+ const csrfOptions = this.getRequestOptions('GET', cached);
313
+ let response = await fetch(vibeEventsURL, csrfOptions);
314
+ if (cached && this.sessionOptions.retryStatuses.includes(response.status)) {
315
+ this.clearSession();
316
+ response = await fetch(vibeEventsURL, this.getRequestOptions('GET'));
317
+ }
318
+ if (this.sessionOptions.enabled) {
319
+ const cachedCookie = this.extractSessionCookie(response) ?? cached;
320
+ if (cachedCookie)
321
+ this.storeSession(cachedCookie);
322
+ }
323
+ // 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);
171
326
  if (response.status >= 300) {
172
327
  const message = 'Error connecting to FlexPLM:status: ' + response.status;
173
328
  console.error(message);
@@ -28,6 +28,22 @@ const createConfig = (overrides) => ({
28
28
  useDistinctRestEndPointForImages: false,
29
29
  ...overrides,
30
30
  });
31
+ // Added for Session reuse - ET-390 - Starts
32
+ const csrfBody = { items: [{ attributes: { nonce_key: 'CSRF_NONCE', nonce: 'n1' } }] };
33
+ // Response with Set-Cookie support. setCookie = list of raw Set-Cookie header lines.
34
+ const resp = (status, setCookie = [], json = csrfBody) => ({
35
+ status,
36
+ json: jest.fn().mockResolvedValue(json),
37
+ text: jest.fn().mockResolvedValue('error text'),
38
+ headers: { getSetCookie: () => setCookie, get: () => null },
39
+ });
40
+ let userCounter = 0;
41
+ const uniqueUser = () => 'sess-user-' + (++userCounter);
42
+ const sessionConfig = (user, sessionReuse = { enabled: true }) => createConfig({ userName: () => user, flexplmConnect: { sessionReuse } });
43
+ const fetchMock = () => global.fetch;
44
+ const callHeaders = (i) => fetchMock().mock.calls[i][1].headers;
45
+ const callMethod = (i) => fetchMock().mock.calls[i][1].method;
46
+ // Added for Session reuse - ET-390 - Ends
31
47
  describe('FlexPLMConnect.getRequest', () => {
32
48
  it('should call default URL with urlContext when no params provided', async () => {
33
49
  const connect = new flexplm_connect_1.FlexPLMConnect(createConfig());
@@ -121,3 +137,249 @@ describe('FlexPLMConnect.sendRequest', () => {
121
137
  consoleInfoSpy.mockRestore();
122
138
  });
123
139
  });
140
+ // Added for Session reuse - ET-390 - Starts
141
+ describe('FlexPLMConnect session reuse', () => {
142
+ const payload = { id: '123' };
143
+ it('captures JSESSIONID from the csrf GET and sends it on the POST', async () => {
144
+ const u = uniqueUser();
145
+ fetchMock()
146
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1; Path=/; HttpOnly']))
147
+ .mockResolvedValueOnce(resp(200, [], {}));
148
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
149
+ expect(fetchMock()).toHaveBeenCalledTimes(2);
150
+ expect(callHeaders(0).Cookie).toBeUndefined(); // first ever call: no cached session
151
+ expect(callHeaders(1).Cookie).toBe('JSESSIONID=abc.tomcat1');
152
+ expect(callHeaders(1).CSRF_NONCE).toBe('n1');
153
+ });
154
+ it('reuses the cached session on the next event (new FlexPLMConnect instance)', async () => {
155
+ const u = uniqueUser();
156
+ fetchMock()
157
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1; Path=/'])) // event 1 GET
158
+ .mockResolvedValueOnce(resp(200, [], {})) // event 1 POST
159
+ .mockResolvedValueOnce(resp(200, [])) // event 2 GET (valid session -> no Set-Cookie)
160
+ .mockResolvedValueOnce(resp(200, [], {})); // event 2 POST
161
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
162
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
163
+ expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1'); // GET reuses it
164
+ expect(callHeaders(3).Cookie).toBe('JSESSIONID=abc.tomcat1'); // POST reuses it
165
+ });
166
+ it('replaces the cached session when Windchill answers the GET with a new Set-Cookie', async () => {
167
+ const u = uniqueUser();
168
+ fetchMock()
169
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1; Path=/']))
170
+ .mockResolvedValueOnce(resp(200, [], {}))
171
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1; Path=/'])) // old one was stale -> new issued
172
+ .mockResolvedValueOnce(resp(200, [], {}))
173
+ .mockResolvedValueOnce(resp(200, []))
174
+ .mockResolvedValueOnce(resp(200, [], {}));
175
+ const connect = () => new flexplm_connect_1.FlexPLMConnect(sessionConfig(u));
176
+ await connect().sendToFlexPLM(payload);
177
+ await connect().sendToFlexPLM(payload);
178
+ await connect().sendToFlexPLM(payload);
179
+ expect(callHeaders(2).Cookie).toBe('JSESSIONID=old.tomcat1'); // event 2 GET sent the old one
180
+ expect(callHeaders(3).Cookie).toBe('JSESSIONID=new.tomcat1'); // POST uses the replacement
181
+ expect(callHeaders(4).Cookie).toBe('JSESSIONID=new.tomcat1'); // event 3 GET sends the replacement
182
+ });
183
+ it('retries once with a new session when the POST is rejected with 403', async () => {
184
+ const u = uniqueUser();
185
+ fetchMock()
186
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1; Path=/'])) // GET
187
+ .mockResolvedValueOnce(resp(403, [], {})) // POST rejected
188
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1; Path=/'])) // GET again, fresh session
189
+ .mockResolvedValueOnce(resp(200, [], { ok: true })); // POST succeeds
190
+ const result = await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
191
+ expect(fetchMock()).toHaveBeenCalledTimes(4);
192
+ expect(callHeaders(2).Cookie).toBeUndefined(); // retry GET must NOT send the stale cookie
193
+ expect(callHeaders(3).Cookie).toBe('JSESSIONID=new.tomcat1');
194
+ expect(result.status).toBe(200);
195
+ expect(result.data).toEqual({ ok: true });
196
+ });
197
+ it('also retries on 401', async () => {
198
+ const u = uniqueUser();
199
+ fetchMock()
200
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
201
+ .mockResolvedValueOnce(resp(401, [], {}))
202
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
203
+ .mockResolvedValueOnce(resp(200, [], {}));
204
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
205
+ expect(fetchMock()).toHaveBeenCalledTimes(4);
206
+ });
207
+ it('does not loop: a second 403 after the retry is surfaced as an error', async () => {
208
+ const u = uniqueUser();
209
+ fetchMock()
210
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1']))
211
+ .mockResolvedValueOnce(resp(403, [], {}))
212
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1']))
213
+ .mockResolvedValueOnce(resp(403, [], {}));
214
+ await expect(new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload))
215
+ .rejects.toThrow('Error sending data to FlexPLM:status: 403');
216
+ expect(fetchMock()).toHaveBeenCalledTimes(4);
217
+ });
218
+ it('does not retry on non-auth errors such as 500 (event may already be processed)', async () => {
219
+ const u = uniqueUser();
220
+ fetchMock()
221
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
222
+ .mockResolvedValueOnce(resp(500, [], {}));
223
+ await expect(new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload))
224
+ .rejects.toThrow('Error sending data to FlexPLM:status: 500');
225
+ expect(fetchMock()).toHaveBeenCalledTimes(2);
226
+ });
227
+ it('retries the csrf GET without a cookie when the cached session is rejected there', async () => {
228
+ const u = uniqueUser();
229
+ fetchMock()
230
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1'])) // event 1 GET
231
+ .mockResolvedValueOnce(resp(200, [], {})) // event 1 POST
232
+ .mockResolvedValueOnce(resp(401, [])) // event 2 GET with cached cookie rejected
233
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1'])) // event 2 GET retry, no cookie
234
+ .mockResolvedValueOnce(resp(200, [], {})); // event 2 POST
235
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
236
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
237
+ expect(callHeaders(2).Cookie).toBe('JSESSIONID=old.tomcat1');
238
+ expect(callHeaders(3).Cookie).toBeUndefined();
239
+ expect(callHeaders(4).Cookie).toBe('JSESSIONID=new.tomcat1');
240
+ });
241
+ it('expires the cached session after ttlSeconds', async () => {
242
+ const u = uniqueUser();
243
+ const realNow = Date.now();
244
+ const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(realNow);
245
+ fetchMock()
246
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
247
+ .mockResolvedValueOnce(resp(200, [], {}))
248
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=def.tomcat1']))
249
+ .mockResolvedValueOnce(resp(200, [], {}));
250
+ const cfg = sessionConfig(u, { enabled: true, ttlSeconds: 1 });
251
+ await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
252
+ nowSpy.mockReturnValue(realNow + 2000); // 2s later > 1s ttl
253
+ await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
254
+ expect(callHeaders(2).Cookie).toBeUndefined(); // expired -> GET sends no cookie
255
+ expect(callHeaders(3).Cookie).toBe('JSESSIONID=def.tomcat1');
256
+ nowSpy.mockRestore();
257
+ });
258
+ it('uses the last JSESSIONID when several Set-Cookie lines are returned', async () => {
259
+ const u = uniqueUser();
260
+ fetchMock()
261
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=first.tomcat1; Path=/', 'OTHER=x; Path=/', 'JSESSIONID=second.tomcat1; Path=/']))
262
+ .mockResolvedValueOnce(resp(200, [], {}));
263
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
264
+ expect(callHeaders(1).Cookie).toBe('JSESSIONID=second.tomcat1');
265
+ });
266
+ it('falls back to headers.get("set-cookie") when getSetCookie is not available', async () => {
267
+ const u = uniqueUser();
268
+ const legacy = {
269
+ status: 200,
270
+ json: jest.fn().mockResolvedValue(csrfBody),
271
+ text: jest.fn(),
272
+ headers: { get: (k) => (k.toLowerCase() === 'set-cookie' ? 'JSESSIONID=legacy.tomcat1; Path=/' : null) },
273
+ };
274
+ fetchMock().mockResolvedValueOnce(legacy).mockResolvedValueOnce(resp(200, [], {}));
275
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
276
+ expect(callHeaders(1).Cookie).toBe('JSESSIONID=legacy.tomcat1');
277
+ });
278
+ it('honours a custom cookieName', async () => {
279
+ const u = uniqueUser();
280
+ fetchMock()
281
+ .mockResolvedValueOnce(resp(200, ['MYSESSION=zzz; Path=/', 'JSESSIONID=ignored; Path=/']))
282
+ .mockResolvedValueOnce(resp(200, [], {}));
283
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: true, cookieName: 'MYSESSION' })).sendToFlexPLM(payload);
284
+ expect(callHeaders(1).Cookie).toBe('MYSESSION=zzz');
285
+ });
286
+ it('accepts enabled as the string "true"', async () => {
287
+ const u = uniqueUser();
288
+ fetchMock()
289
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
290
+ .mockResolvedValueOnce(resp(200, [], {}));
291
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: 'true' })).sendToFlexPLM(payload);
292
+ expect(callHeaders(1).Cookie).toBe('JSESSIONID=abc.tomcat1');
293
+ });
294
+ it('keeps separate sessions per user', async () => {
295
+ const a = uniqueUser();
296
+ const b = uniqueUser();
297
+ fetchMock()
298
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=aaa.tomcat1']))
299
+ .mockResolvedValueOnce(resp(200, [], {}))
300
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=bbb.tomcat1']))
301
+ .mockResolvedValueOnce(resp(200, [], {}));
302
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(a)).sendToFlexPLM(payload);
303
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(b)).sendToFlexPLM(payload);
304
+ expect(callHeaders(2).Cookie).toBeUndefined(); // user b must not see user a's cookie
305
+ expect(callHeaders(3).Cookie).toBe('JSESSIONID=bbb.tomcat1');
306
+ });
307
+ it('never logs the cookie value', async () => {
308
+ const isInfoOn = jest.spyOn(app_framework_1.Logger, 'isInfoOn').mockReturnValue(true);
309
+ const info = jest.spyOn(console, 'info').mockImplementation();
310
+ const u = uniqueUser();
311
+ fetchMock()
312
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=supersecret.tomcat1']))
313
+ .mockResolvedValueOnce(resp(200, [], {}))
314
+ .mockResolvedValueOnce(resp(200, []))
315
+ .mockResolvedValueOnce(resp(200, [], {}));
316
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload);
317
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u)).sendToFlexPLM(payload); // 2nd event sends Cookie on GET and POST
318
+ const logged = info.mock.calls.map(c => String(c[0] ?? '')).join('\n');
319
+ expect(logged).toContain('"Cookie":"***masked***"');
320
+ expect(logged).not.toContain('supersecret');
321
+ isInfoOn.mockRestore();
322
+ info.mockRestore();
323
+ });
324
+ });
325
+ describe('FlexPLMConnect session reuse - feature disabled (default)', () => {
326
+ const payload = { id: '123' };
327
+ it('sends no Cookie header and makes exactly one GET + one POST', async () => {
328
+ const u = uniqueUser();
329
+ fetchMock()
330
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1'])) // Set-Cookie present but must be ignored
331
+ .mockResolvedValueOnce(resp(200, [], {}))
332
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=def.tomcat1']))
333
+ .mockResolvedValueOnce(resp(200, [], {}));
334
+ const cfg = createConfig({ userName: () => u }); // no flexplmConnect at all
335
+ await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
336
+ await new flexplm_connect_1.FlexPLMConnect(cfg).sendToFlexPLM(payload);
337
+ for (let i = 0; i < 4; i++) {
338
+ expect(callHeaders(i).Cookie).toBeUndefined();
339
+ }
340
+ });
341
+ it('does not retry on 403 (behaves exactly as before)', async () => {
342
+ const u = uniqueUser();
343
+ fetchMock()
344
+ .mockResolvedValueOnce(resp(200))
345
+ .mockResolvedValueOnce(resp(403, [], {}));
346
+ await expect(new flexplm_connect_1.FlexPLMConnect(createConfig({ userName: () => u })).sendToFlexPLM(payload))
347
+ .rejects.toThrow('Error sending data to FlexPLM:status: 403');
348
+ expect(fetchMock()).toHaveBeenCalledTimes(2);
349
+ });
350
+ it('explicit enabled:false behaves the same as not configured', async () => {
351
+ const u = uniqueUser();
352
+ fetchMock()
353
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
354
+ .mockResolvedValueOnce(resp(200, [], {}));
355
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig(u, { enabled: false })).sendToFlexPLM(payload);
356
+ expect(callHeaders(1).Cookie).toBeUndefined();
357
+ });
358
+ });
359
+ describe('FlexPLMConnect.getRequest session reuse (only if you applied step 2g)', () => {
360
+ it('sends the cached cookie on image/GET requests', async () => {
361
+ const u = uniqueUser();
362
+ fetchMock()
363
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1'])) // csrf GET primes the cache
364
+ .mockResolvedValueOnce(resp(200, [], {})) // POST
365
+ .mockResolvedValueOnce(resp(200, [], { items: [] })); // getRequest
366
+ const connect = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u));
367
+ await connect.sendToFlexPLM({ id: '1' });
368
+ await connect.getRequest();
369
+ expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1');
370
+ });
371
+ it('retries once without the cookie when the cached session is rejected on getRequest', async () => {
372
+ const u = uniqueUser();
373
+ fetchMock()
374
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
375
+ .mockResolvedValueOnce(resp(200, [], {}))
376
+ .mockResolvedValueOnce(resp(401, [])) // getRequest with cached cookie
377
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1'], { items: [] })); // retry, no cookie
378
+ const connect = new flexplm_connect_1.FlexPLMConnect(sessionConfig(u));
379
+ await connect.sendToFlexPLM({ id: '1' });
380
+ await connect.getRequest();
381
+ expect(callHeaders(2).Cookie).toBe('JSESSIONID=abc.tomcat1');
382
+ expect(callHeaders(3).Cookie).toBeUndefined();
383
+ });
384
+ });
385
+ // Added for Session reuse - ET-390 - Ends
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/flexplm",
3
- "version": "1.7.6",
3
+ "version": "1.7.7-alpha.1ad98c2",
4
4
  "description": "Library used for integration with flexplm.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",