@appliqation/automation-sdk 2.1.0

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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +441 -0
  3. package/package.json +107 -0
  4. package/src/AppliqationClient.js +562 -0
  5. package/src/constants.js +245 -0
  6. package/src/core/AuthManager.js +353 -0
  7. package/src/core/HttpClient.js +475 -0
  8. package/src/index.d.ts +333 -0
  9. package/src/index.js +26 -0
  10. package/src/playwright/JwtBrowserAuth.js +240 -0
  11. package/src/playwright/fixture.js +92 -0
  12. package/src/playwright/global-setup.js +243 -0
  13. package/src/playwright/helpers/jwt-browser-auth.js +227 -0
  14. package/src/playwright/index.js +16 -0
  15. package/src/reporters/cypress/CypressReporter.js +387 -0
  16. package/src/reporters/cypress/UuidExtractor.js +139 -0
  17. package/src/reporters/cypress/index.js +30 -0
  18. package/src/reporters/jest/JestReporter.js +361 -0
  19. package/src/reporters/jest/UuidExtractor.js +174 -0
  20. package/src/reporters/jest/index.js +28 -0
  21. package/src/reporters/playwright/AppliqationReporter.js +654 -0
  22. package/src/reporters/playwright/helpers/DeviceOsDetector.js +435 -0
  23. package/src/reporters/playwright/helpers/UuidExtractor.js +290 -0
  24. package/src/reporters/playwright/index.d.ts +96 -0
  25. package/src/reporters/playwright/index.js +14 -0
  26. package/src/services/OrphanTestService.js +74 -0
  27. package/src/services/ResultService.js +252 -0
  28. package/src/services/RunMatrixService.js +309 -0
  29. package/src/utils/PayloadBuilder.js +280 -0
  30. package/src/utils/RunDataNormalizer.js +335 -0
  31. package/src/utils/UuidValidator.js +102 -0
  32. package/src/utils/errors.js +217 -0
  33. package/src/utils/index.js +17 -0
  34. package/src/utils/logger.js +124 -0
  35. package/src/utils/mapAppqUuid.js +83 -0
  36. package/src/utils/validator.js +157 -0
@@ -0,0 +1,475 @@
1
+ const axios = require('axios');
2
+ const https = require('https');
3
+ const http = require('http');
4
+ const logger = require('../utils/logger');
5
+ const {
6
+ DEFAULT_TIMEOUT,
7
+ DEFAULT_RETRIES,
8
+ DEFAULT_RETRY_DELAY,
9
+ MAX_RETRY_DELAY,
10
+ RETRY_JITTER_FACTOR,
11
+ BACKOFF_MULTIPLIER,
12
+ DEFAULT_KEEP_ALIVE,
13
+ DEFAULT_MAX_SOCKETS,
14
+ DEFAULT_MAX_FREE_SOCKETS,
15
+ DEFAULT_KEEP_ALIVE_MSECS,
16
+ HTTP_TOO_MANY_REQUESTS,
17
+ HTTP_REQUEST_TIMEOUT,
18
+ HTTP_SERVER_ERROR_THRESHOLD
19
+ } = require('../constants');
20
+
21
+ /**
22
+ * Generic HTTP client for making requests to Appliqation API
23
+ */
24
+ class HttpClient {
25
+ constructor(config, auth = null) {
26
+ this.config = {
27
+ baseUrl: config.baseUrl,
28
+ timeout: config.timeout || DEFAULT_TIMEOUT,
29
+ retries: config.retries || DEFAULT_RETRIES,
30
+ retryDelay: config.retryDelay || DEFAULT_RETRY_DELAY,
31
+ rejectUnauthorized: config.rejectUnauthorized !== undefined ? config.rejectUnauthorized : true, // Default to secure
32
+ // Connection pooling settings
33
+ keepAlive: config.keepAlive !== undefined ? config.keepAlive : DEFAULT_KEEP_ALIVE,
34
+ maxSockets: config.maxSockets || DEFAULT_MAX_SOCKETS,
35
+ maxFreeSockets: config.maxFreeSockets || DEFAULT_MAX_FREE_SOCKETS,
36
+ keepAliveMsecs: config.keepAliveMsecs || DEFAULT_KEEP_ALIVE_MSECS,
37
+ ...config
38
+ };
39
+
40
+ // Validate required baseUrl
41
+ if (!this.config.baseUrl) {
42
+ throw new Error('baseUrl is required. Please set APPLIQATION_BASE_URL environment variable or provide baseUrl in configuration.');
43
+ }
44
+
45
+ // Store auth manager reference
46
+ this.auth = auth;
47
+
48
+ // Create HTTP/HTTPS agents with connection pooling
49
+ const agentOptions = {
50
+ keepAlive: this.config.keepAlive,
51
+ maxSockets: this.config.maxSockets,
52
+ maxFreeSockets: this.config.maxFreeSockets,
53
+ keepAliveMsecs: this.config.keepAliveMsecs,
54
+ rejectUnauthorized: this.config.rejectUnauthorized
55
+ };
56
+
57
+ this.httpAgent = new http.Agent(agentOptions);
58
+ this.httpsAgent = new https.Agent(agentOptions);
59
+
60
+ logger.debug('HTTP connection pooling configured', {
61
+ keepAlive: agentOptions.keepAlive,
62
+ maxSockets: agentOptions.maxSockets,
63
+ maxFreeSockets: agentOptions.maxFreeSockets
64
+ });
65
+
66
+ this.axios = axios.create({
67
+ baseURL: this.config.baseUrl,
68
+ timeout: this.config.timeout,
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ 'Accept': 'application/json'
72
+ },
73
+ // Use connection pooling agents
74
+ httpAgent: this.httpAgent,
75
+ httpsAgent: this.httpsAgent
76
+ });
77
+
78
+ this.setupInterceptors();
79
+
80
+ // Set API key if provided in config
81
+ if (this.config.apiKey) {
82
+ this.setApiKey(this.config.apiKey);
83
+ logger.debug('API key set in HttpClient');
84
+ }
85
+ }
86
+
87
+ setupInterceptors() {
88
+ // Response interceptor for error handling and JWT refresh
89
+ this.axios.interceptors.response.use(
90
+ (response) => response,
91
+ async (error) => {
92
+ const originalRequest = error.config;
93
+
94
+ // Handle 401 errors with automatic JWT token refresh
95
+ if (error.response?.status === 401 &&
96
+ this.auth &&
97
+ this.auth.hasApiKey() &&
98
+ !originalRequest._retry) {
99
+
100
+ // Mark this request as already retried to prevent infinite loops
101
+ originalRequest._retry = true;
102
+
103
+ logger.info('401 Unauthorized detected - attempting JWT token refresh...', {
104
+ url: originalRequest.url,
105
+ method: originalRequest.method
106
+ });
107
+
108
+ try {
109
+ // Attempt to refresh JWT token using AuthManager
110
+ await this.auth.refreshJwtToken();
111
+
112
+ logger.info('JWT token refreshed successfully, retrying original request');
113
+
114
+ // Retry the original request with the new token
115
+ return this.axios(originalRequest);
116
+ } catch (refreshError) {
117
+ logger.error('Failed to refresh JWT token', {
118
+ error: refreshError.message,
119
+ originalUrl: originalRequest.url
120
+ });
121
+
122
+ // If refresh fails, reject with the refresh error
123
+ return Promise.reject(refreshError);
124
+ }
125
+ }
126
+
127
+ // For non-401 errors or when refresh is not applicable, log and reject
128
+ if (error.response?.status === 401) {
129
+ logger.warn('Authentication failed (401) - JWT refresh not available or already attempted');
130
+ }
131
+
132
+ return Promise.reject(error);
133
+ }
134
+ );
135
+ }
136
+
137
+ /**
138
+ * Make a GET request
139
+ * @param {string} url - The URL path
140
+ * @param {Object} config - Axios config options
141
+ * @returns {Promise<Object>} Response data
142
+ */
143
+ async get(url, config = {}) {
144
+ try {
145
+ logger.debug(`GET ${url}`);
146
+ const response = await this.retryRequest(() =>
147
+ this.axios.get(url, config)
148
+ );
149
+ return {
150
+ success: true,
151
+ data: response.data,
152
+ status: response.status,
153
+ headers: response.headers
154
+ };
155
+ } catch (error) {
156
+ return this.handleError(error, 'GET', url);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Make a POST request
162
+ * @param {string} url - The URL path
163
+ * @param {Object} data - Request body
164
+ * @param {Object} config - Axios config options
165
+ * @returns {Promise<Object>} Response data
166
+ */
167
+ async post(url, data, config = {}) {
168
+ const startTime = Date.now();
169
+ try {
170
+ logger.debug(`POST ${url}`, { dataSize: JSON.stringify(data).length });
171
+ logger.debug(`POST request starting at ${new Date().toISOString()}`);
172
+
173
+ // Detailed logging for debugging project key issues
174
+ logger.debug(`POST ${url} - Request Body:`, JSON.stringify(data, null, 2));
175
+ logger.debug(`POST ${url} - Request Headers:`, JSON.stringify(this.axios.defaults.headers.common, null, 2));
176
+
177
+ const response = await this.retryRequest(() =>
178
+ this.axios.post(url, data, config)
179
+ );
180
+
181
+ const duration = Date.now() - startTime;
182
+ logger.debug(`POST ${url} completed in ${duration}ms`, {
183
+ status: response.status,
184
+ timestamp: new Date().toISOString()
185
+ });
186
+
187
+ // Detailed response logging
188
+ logger.debug(`POST ${url} - Response Status: ${response.status}`);
189
+ logger.debug(`POST ${url} - Response Body:`, JSON.stringify(response.data, null, 2));
190
+
191
+ return {
192
+ success: true,
193
+ data: response.data,
194
+ status: response.status,
195
+ headers: response.headers
196
+ };
197
+ } catch (error) {
198
+ const duration = Date.now() - startTime;
199
+ logger.error(`POST ${url} failed after ${duration}ms`);
200
+ logger.error(`POST ${url} - Error Details:`, {
201
+ message: error.message,
202
+ status: error.response?.status,
203
+ statusText: error.response?.statusText,
204
+ responseData: error.response?.data
205
+ });
206
+ return this.handleError(error, 'POST', url);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Make a PUT request
212
+ * @param {string} url - The URL path
213
+ * @param {Object} data - Request body
214
+ * @param {Object} config - Axios config options
215
+ * @returns {Promise<Object>} Response data
216
+ */
217
+ async put(url, data, config = {}) {
218
+ try {
219
+ logger.debug(`PUT ${url}`);
220
+ const response = await this.retryRequest(() =>
221
+ this.axios.put(url, data, config)
222
+ );
223
+ return {
224
+ success: true,
225
+ data: response.data,
226
+ status: response.status,
227
+ headers: response.headers
228
+ };
229
+ } catch (error) {
230
+ return this.handleError(error, 'PUT', url);
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Make a DELETE request
236
+ * @param {string} url - The URL path
237
+ * @param {Object} config - Axios config options
238
+ * @returns {Promise<Object>} Response data
239
+ */
240
+ async delete(url, config = {}) {
241
+ try {
242
+ logger.debug(`DELETE ${url}`);
243
+ const response = await this.retryRequest(() =>
244
+ this.axios.delete(url, config)
245
+ );
246
+ return {
247
+ success: true,
248
+ data: response.data,
249
+ status: response.status,
250
+ headers: response.headers
251
+ };
252
+ } catch (error) {
253
+ return this.handleError(error, 'DELETE', url);
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Set authorization header
259
+ * @param {string} token - Authorization token
260
+ */
261
+ setAuthToken(token) {
262
+ if (token) {
263
+ this.axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
264
+ } else {
265
+ delete this.axios.defaults.headers.common['Authorization'];
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Set API key header
271
+ * @param {string} apiKey - API key
272
+ */
273
+ setApiKey(apiKey) {
274
+ if (apiKey) {
275
+ this.axios.defaults.headers.common['X-API-Key'] = apiKey;
276
+ } else {
277
+ delete this.axios.defaults.headers.common['X-API-Key'];
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Set custom header
283
+ * @param {string} key - Header name
284
+ * @param {string} value - Header value
285
+ */
286
+ setHeader(key, value) {
287
+ if (value) {
288
+ this.axios.defaults.headers.common[key] = value;
289
+ } else {
290
+ delete this.axios.defaults.headers.common[key];
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Retry a request with exponential backoff
296
+ * @param {Function} requestFn - The request function to retry
297
+ * @param {number} attempt - Current attempt number
298
+ * @returns {Promise<Object>} Response
299
+ */
300
+ async retryRequest(requestFn, attempt = 1) {
301
+ try {
302
+ return await requestFn();
303
+ } catch (error) {
304
+ if (attempt < this.config.retries && this.shouldRetry(error)) {
305
+ const delay = this.calculateBackoff(attempt);
306
+
307
+ logger.warn(`Request failed, retrying... (${attempt}/${this.config.retries})`, {
308
+ error: error.message,
309
+ retryAfterMs: delay,
310
+ attempt: attempt
311
+ });
312
+
313
+ await this.delay(delay);
314
+ return this.retryRequest(requestFn, attempt + 1);
315
+ }
316
+ throw error;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Calculate exponential backoff delay with jitter
322
+ * Formula: min(maxDelay, baseDelay * (2 ^ attempt)) + random jitter
323
+ * @param {number} attempt - Current retry attempt (1-indexed)
324
+ * @returns {number} Delay in milliseconds
325
+ */
326
+ calculateBackoff(attempt) {
327
+ const baseDelay = this.config.retryDelay || DEFAULT_RETRY_DELAY;
328
+ const maxDelay = this.config.maxRetryDelay || MAX_RETRY_DELAY;
329
+ const jitterFactor = this.config.retryJitter !== undefined ? this.config.retryJitter : RETRY_JITTER_FACTOR;
330
+
331
+ // Exponential backoff: baseDelay * (BACKOFF_MULTIPLIER ^ (attempt - 1))
332
+ // attempt 1: 1000 * 1 = 1000ms
333
+ // attempt 2: 1000 * 2 = 2000ms
334
+ // attempt 3: 1000 * 4 = 4000ms
335
+ const exponentialDelay = baseDelay * Math.pow(BACKOFF_MULTIPLIER, attempt - 1);
336
+
337
+ // Cap at maxDelay
338
+ const cappedDelay = Math.min(exponentialDelay, maxDelay);
339
+
340
+ // Add random jitter to prevent thundering herd
341
+ // jitter range: -jitterFactor to +jitterFactor
342
+ const jitter = cappedDelay * jitterFactor * (2 * Math.random() - 1);
343
+ const finalDelay = Math.max(0, Math.round(cappedDelay + jitter));
344
+
345
+ logger.debug('Backoff calculated', {
346
+ attempt,
347
+ baseDelay,
348
+ exponentialDelay,
349
+ cappedDelay,
350
+ jitter: Math.round(jitter),
351
+ finalDelay
352
+ });
353
+
354
+ return finalDelay;
355
+ }
356
+
357
+ /**
358
+ * Determine if request should be retried
359
+ * @param {Error} error - The error object
360
+ * @returns {boolean} True if should retry
361
+ */
362
+ shouldRetry(error) {
363
+ // Don't retry 4xx client errors (except 429 Too Many Requests and 408 Timeout)
364
+ if (error.response) {
365
+ const status = error.response.status;
366
+
367
+ // Retry server errors (5xx)
368
+ if (status >= HTTP_SERVER_ERROR_THRESHOLD) {
369
+ return true;
370
+ }
371
+
372
+ // Retry specific 4xx errors
373
+ if (status === HTTP_TOO_MANY_REQUESTS || status === HTTP_REQUEST_TIMEOUT) {
374
+ return true;
375
+ }
376
+
377
+ // Don't retry other 4xx errors
378
+ return false;
379
+ }
380
+
381
+ // Retry network errors (no response)
382
+ return true;
383
+ }
384
+
385
+ /**
386
+ * Delay for specified milliseconds
387
+ * @param {number} ms - Milliseconds to delay
388
+ * @returns {Promise<void>}
389
+ */
390
+ delay(ms) {
391
+ return new Promise(resolve => setTimeout(resolve, ms));
392
+ }
393
+
394
+ /**
395
+ * Handle error responses
396
+ * @param {Error} error - The error object
397
+ * @param {string} method - HTTP method
398
+ * @param {string} url - Request URL
399
+ * @returns {Object} Error response
400
+ */
401
+ handleError(error, method, url) {
402
+ const errorInfo = {
403
+ method,
404
+ url,
405
+ message: error.message,
406
+ status: error.response?.status || 500,
407
+ data: error.response?.data || null
408
+ };
409
+
410
+ logger.error(`HTTP ${method} failed: ${url}`, errorInfo);
411
+
412
+ // Re-throw error with preserved response data for upstream handlers
413
+ const enhancedError = new Error(error.message);
414
+ enhancedError.response = error.response; // Preserve full response for detailed error handling
415
+ enhancedError.status = errorInfo.status;
416
+ enhancedError.data = errorInfo.data;
417
+
418
+ throw enhancedError;
419
+ }
420
+
421
+ /**
422
+ * Test connection to server
423
+ * @returns {Promise<Object>} Connection test result
424
+ */
425
+ async testConnection() {
426
+ try {
427
+ const response = await this.axios.get('/');
428
+ return {
429
+ success: true,
430
+ message: 'Connection successful',
431
+ status: response.status
432
+ };
433
+ } catch (error) {
434
+ return {
435
+ success: false,
436
+ message: `Connection failed: ${error.message}`,
437
+ status: error.response?.status || 500
438
+ };
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Get HTTP client statistics
444
+ * @returns {Object} Client stats
445
+ */
446
+ getStats() {
447
+ return {
448
+ baseUrl: this.config.baseUrl,
449
+ timeout: this.config.timeout,
450
+ retries: this.config.retries,
451
+ retryDelay: this.config.retryDelay,
452
+ keepAlive: this.config.keepAlive,
453
+ maxSockets: this.config.maxSockets,
454
+ maxFreeSockets: this.config.maxFreeSockets
455
+ };
456
+ }
457
+
458
+ /**
459
+ * Cleanup and destroy HTTP agents
460
+ * Closes all open connections in the pool
461
+ */
462
+ destroy() {
463
+ logger.debug('Destroying HTTP client and closing connection pools');
464
+
465
+ if (this.httpAgent) {
466
+ this.httpAgent.destroy();
467
+ }
468
+
469
+ if (this.httpsAgent) {
470
+ this.httpsAgent.destroy();
471
+ }
472
+ }
473
+ }
474
+
475
+ module.exports = HttpClient;