@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,245 @@
1
+ /**
2
+ * SDK Constants
3
+ * Centralized location for all magic numbers and configuration values
4
+ */
5
+
6
+ // ============================================================================
7
+ // HTTP & Network Configuration
8
+ // ============================================================================
9
+
10
+ /**
11
+ * Default HTTP timeout in milliseconds
12
+ * @constant {number}
13
+ */
14
+ const DEFAULT_TIMEOUT = 45000; // 45 seconds
15
+
16
+ /**
17
+ * Maximum number of retry attempts
18
+ * @constant {number}
19
+ */
20
+ const DEFAULT_RETRIES = 3;
21
+
22
+ /**
23
+ * Base retry delay in milliseconds (used for exponential backoff)
24
+ * @constant {number}
25
+ */
26
+ const DEFAULT_RETRY_DELAY = 1000; // 1 second
27
+
28
+ /**
29
+ * Maximum retry delay in milliseconds (cap for exponential backoff)
30
+ * @constant {number}
31
+ */
32
+ const MAX_RETRY_DELAY = 30000; // 30 seconds
33
+
34
+ /**
35
+ * Retry jitter factor (percentage as decimal)
36
+ * Adds randomness to prevent thundering herd
37
+ * @constant {number}
38
+ */
39
+ const RETRY_JITTER_FACTOR = 0.1; // 10%
40
+
41
+ /**
42
+ * Exponential backoff base multiplier
43
+ * @constant {number}
44
+ */
45
+ const BACKOFF_MULTIPLIER = 2;
46
+
47
+ // ============================================================================
48
+ // Connection Pooling Configuration
49
+ // ============================================================================
50
+
51
+ /**
52
+ * Enable HTTP keep-alive by default
53
+ * @constant {boolean}
54
+ */
55
+ const DEFAULT_KEEP_ALIVE = true;
56
+
57
+ /**
58
+ * Maximum number of concurrent sockets
59
+ * @constant {number}
60
+ */
61
+ const DEFAULT_MAX_SOCKETS = 50;
62
+
63
+ /**
64
+ * Maximum number of free sockets in the pool
65
+ * @constant {number}
66
+ */
67
+ const DEFAULT_MAX_FREE_SOCKETS = 10;
68
+
69
+ /**
70
+ * Keep-alive timeout in milliseconds
71
+ * @constant {number}
72
+ */
73
+ const DEFAULT_KEEP_ALIVE_MSECS = 1000; // 1 second
74
+
75
+ // ============================================================================
76
+ // Batch Processing Configuration
77
+ // ============================================================================
78
+
79
+ /**
80
+ * Default batch size for result submission
81
+ * @constant {number}
82
+ */
83
+ const DEFAULT_BATCH_SIZE = 50;
84
+
85
+ /**
86
+ * Enable batch submission by default
87
+ * @constant {boolean}
88
+ */
89
+ const DEFAULT_BATCH_SUBMIT = true;
90
+
91
+ /**
92
+ * Enable logging of orphan tests by default
93
+ * @constant {boolean}
94
+ */
95
+ const DEFAULT_LOG_ORPHANS = true;
96
+
97
+ // ============================================================================
98
+ // JWT Token Configuration
99
+ // ============================================================================
100
+
101
+ /**
102
+ * JWT token refresh threshold in minutes
103
+ * Refresh when token has less than this time remaining
104
+ * @constant {number}
105
+ */
106
+ const JWT_REFRESH_THRESHOLD_MINUTES = 55;
107
+
108
+ /**
109
+ * JWT token minimum validity in minutes
110
+ * Token must be valid for at least this long
111
+ * @constant {number}
112
+ */
113
+ const JWT_MIN_VALIDITY_MINUTES = 5;
114
+
115
+ // ============================================================================
116
+ // Log Levels
117
+ // ============================================================================
118
+
119
+ /**
120
+ * Available log levels
121
+ * @constant {Object}
122
+ */
123
+ const LOG_LEVELS = {
124
+ ERROR: 'ERROR',
125
+ WARN: 'WARN',
126
+ INFO: 'INFO',
127
+ DEBUG: 'DEBUG'
128
+ };
129
+
130
+ /**
131
+ * Default log level
132
+ * @constant {string}
133
+ */
134
+ const DEFAULT_LOG_LEVEL = LOG_LEVELS.INFO;
135
+
136
+ // ============================================================================
137
+ // Test Result Statuses
138
+ // ============================================================================
139
+
140
+ /**
141
+ * Test result statuses
142
+ * @constant {Object}
143
+ */
144
+ const TEST_STATUS = {
145
+ PASS: 'pass',
146
+ FAIL: 'fail',
147
+ SKIP: 'skip'
148
+ };
149
+
150
+ // ============================================================================
151
+ // HTTP Status Codes for Retry Logic
152
+ // ============================================================================
153
+
154
+ /**
155
+ * HTTP status code for rate limiting
156
+ * @constant {number}
157
+ */
158
+ const HTTP_TOO_MANY_REQUESTS = 429;
159
+
160
+ /**
161
+ * HTTP status code for request timeout
162
+ * @constant {number}
163
+ */
164
+ const HTTP_REQUEST_TIMEOUT = 408;
165
+
166
+ /**
167
+ * HTTP status code threshold for server errors
168
+ * @constant {number}
169
+ */
170
+ const HTTP_SERVER_ERROR_THRESHOLD = 500;
171
+
172
+ // ============================================================================
173
+ // Default Values
174
+ // ============================================================================
175
+
176
+ /**
177
+ * Default scenario ID for generic automation runs
178
+ * @constant {number}
179
+ */
180
+ const DEFAULT_SCENARIO_ID = 0;
181
+
182
+ /**
183
+ * Default environment name
184
+ * @constant {string}
185
+ */
186
+ const DEFAULT_ENVIRONMENT = 'Local';
187
+
188
+ /**
189
+ * Default device type
190
+ * @constant {string}
191
+ */
192
+ const DEFAULT_DEVICE = 'Desktop';
193
+
194
+ /**
195
+ * Default operating system
196
+ * @constant {string}
197
+ */
198
+ const DEFAULT_OS = 'Unknown';
199
+
200
+ // ============================================================================
201
+ // Exports
202
+ // ============================================================================
203
+
204
+ module.exports = {
205
+ // HTTP & Network
206
+ DEFAULT_TIMEOUT,
207
+ DEFAULT_RETRIES,
208
+ DEFAULT_RETRY_DELAY,
209
+ MAX_RETRY_DELAY,
210
+ RETRY_JITTER_FACTOR,
211
+ BACKOFF_MULTIPLIER,
212
+
213
+ // Connection Pooling
214
+ DEFAULT_KEEP_ALIVE,
215
+ DEFAULT_MAX_SOCKETS,
216
+ DEFAULT_MAX_FREE_SOCKETS,
217
+ DEFAULT_KEEP_ALIVE_MSECS,
218
+
219
+ // Batch Processing
220
+ DEFAULT_BATCH_SIZE,
221
+ DEFAULT_BATCH_SUBMIT,
222
+ DEFAULT_LOG_ORPHANS,
223
+
224
+ // JWT
225
+ JWT_REFRESH_THRESHOLD_MINUTES,
226
+ JWT_MIN_VALIDITY_MINUTES,
227
+
228
+ // Logging
229
+ LOG_LEVELS,
230
+ DEFAULT_LOG_LEVEL,
231
+
232
+ // Test Status
233
+ TEST_STATUS,
234
+
235
+ // HTTP Status Codes
236
+ HTTP_TOO_MANY_REQUESTS,
237
+ HTTP_REQUEST_TIMEOUT,
238
+ HTTP_SERVER_ERROR_THRESHOLD,
239
+
240
+ // Defaults
241
+ DEFAULT_SCENARIO_ID,
242
+ DEFAULT_ENVIRONMENT,
243
+ DEFAULT_DEVICE,
244
+ DEFAULT_OS
245
+ };
@@ -0,0 +1,353 @@
1
+ const axios = require('axios');
2
+ const logger = require('../utils/logger');
3
+
4
+ class AuthManager {
5
+ constructor(config) {
6
+ this.config = config;
7
+ this.csrfToken = null;
8
+ this.sessionCookie = null;
9
+ this.jwtToken = null;
10
+ this.jwtExpiry = null;
11
+ this.lastAuthTime = null;
12
+ this.tokenExpiryTime = null;
13
+ this.runConfig = null; // Store run configuration for JWT refresh
14
+
15
+ // Determine authentication mode
16
+ this.authMode = config.apiKey ? 'api_key' : 'csrf';
17
+ }
18
+
19
+ async getAuthHeaders() {
20
+ const headers = {
21
+ 'Content-Type': 'application/json',
22
+ 'Accept': 'application/json'
23
+ };
24
+
25
+ // API Key authentication (for automation)
26
+ if (this.authMode === 'api_key' && this.config.apiKey) {
27
+ headers['X-API-Key'] = this.config.apiKey;
28
+
29
+ // Add JWT token if available and not expired
30
+ if (this.jwtToken && !this.isJwtExpired()) {
31
+ headers['Authorization'] = `Bearer ${this.jwtToken}`;
32
+ }
33
+
34
+ return headers;
35
+ }
36
+
37
+ // Legacy CSRF authentication (backward compatible)
38
+ if (!this.csrfToken || this.isTokenExpired()) {
39
+ await this.authenticate();
40
+ }
41
+
42
+ headers['Authorization'] = `Basic ${Buffer.from(`${this.config.username}:${this.config.password}`).toString('base64')}`;
43
+
44
+ if (this.csrfToken) {
45
+ headers['X-CSRF-Token'] = this.csrfToken;
46
+ }
47
+
48
+ if (this.sessionCookie) {
49
+ headers['Cookie'] = this.sessionCookie;
50
+ }
51
+
52
+ return headers;
53
+ }
54
+
55
+ async authenticate() {
56
+ try {
57
+ logger.info('Authenticating with Drupal...');
58
+
59
+ // Step 1: Get CSRF token
60
+ await this.getCsrfToken();
61
+
62
+ // Step 2: Login to establish session
63
+ await this.login();
64
+
65
+ this.lastAuthTime = Date.now();
66
+ // Set token expiry to 1 hour from now
67
+ this.tokenExpiryTime = Date.now() + (60 * 60 * 1000);
68
+
69
+ logger.info('Authentication successful');
70
+ } catch (error) {
71
+ logger.error('Authentication failed', { error: error.message });
72
+ throw new Error(`Authentication failed: ${error.message}`);
73
+ }
74
+ }
75
+
76
+ async getCsrfToken() {
77
+ try {
78
+ const response = await axios.get(`${this.config.baseUrl}/session/token`, {
79
+ headers: {
80
+ 'Authorization': `Basic ${Buffer.from(`${this.config.username}:${this.config.password}`).toString('base64')}`
81
+ },
82
+ timeout: this.config.timeout || 30000
83
+ });
84
+
85
+ this.csrfToken = response.data;
86
+
87
+ // Extract session cookie if present
88
+ const setCookieHeader = response.headers['set-cookie'];
89
+ if (setCookieHeader) {
90
+ this.sessionCookie = setCookieHeader
91
+ .map(cookie => cookie.split(';')[0])
92
+ .join('; ');
93
+ }
94
+
95
+ logger.debug('CSRF token obtained successfully');
96
+ } catch (error) {
97
+ logger.error('Failed to get CSRF token', { error: error.message });
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ async login() {
103
+ try {
104
+ const loginData = {
105
+ name: this.config.username,
106
+ pass: this.config.password
107
+ };
108
+
109
+ const headers = {
110
+ 'Content-Type': 'application/json',
111
+ 'Authorization': `Basic ${Buffer.from(`${this.config.username}:${this.config.password}`).toString('base64')}`
112
+ };
113
+
114
+ if (this.csrfToken) {
115
+ headers['X-CSRF-Token'] = this.csrfToken;
116
+ }
117
+
118
+ const response = await axios.post(
119
+ `${this.config.baseUrl}/user/login?_format=json`,
120
+ loginData,
121
+ {
122
+ headers,
123
+ timeout: this.config.timeout || 30000
124
+ }
125
+ );
126
+
127
+ // Update session cookie from login response
128
+ const setCookieHeader = response.headers['set-cookie'];
129
+ if (setCookieHeader) {
130
+ this.sessionCookie = setCookieHeader
131
+ .map(cookie => cookie.split(';')[0])
132
+ .join('; ');
133
+ }
134
+
135
+ logger.debug('Login successful');
136
+ } catch (error) {
137
+ // Login might fail but Basic Auth could still work
138
+ logger.warn('Login request failed, continuing with Basic Auth', { error: error.message });
139
+ }
140
+ }
141
+
142
+ async refreshToken() {
143
+ logger.info('Refreshing authentication token...');
144
+ this.csrfToken = null;
145
+ this.sessionCookie = null;
146
+ await this.authenticate();
147
+ }
148
+
149
+ isTokenExpired() {
150
+ if (!this.tokenExpiryTime) return true;
151
+ return Date.now() > this.tokenExpiryTime;
152
+ }
153
+
154
+ getLastAuthTime() {
155
+ return this.lastAuthTime;
156
+ }
157
+
158
+ // Method to manually set CSRF token if needed
159
+ setCsrfToken(token) {
160
+ this.csrfToken = token;
161
+ this.tokenExpiryTime = Date.now() + (60 * 60 * 1000); // 1 hour expiry
162
+ }
163
+
164
+ // Method to clear authentication data
165
+ clearAuth() {
166
+ this.csrfToken = null;
167
+ this.sessionCookie = null;
168
+ this.jwtToken = null;
169
+ this.jwtExpiry = null;
170
+ this.lastAuthTime = null;
171
+ this.tokenExpiryTime = null;
172
+ }
173
+
174
+ // JWT token management methods (for API key authentication)
175
+ setJwtToken(token) {
176
+ this.jwtToken = token;
177
+ this.jwtExpiry = this.parseJwtExpiry(token);
178
+ logger.debug('JWT token stored', { expiresAt: new Date(this.jwtExpiry).toISOString() });
179
+ }
180
+
181
+ parseJwtExpiry(token) {
182
+ try {
183
+ const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
184
+ // Convert to milliseconds and subtract 5 minutes for safety margin
185
+ return (payload.exp * 1000) - (5 * 60 * 1000);
186
+ } catch (error) {
187
+ logger.warn('Failed to parse JWT expiry', { error: error.message });
188
+ // Default to 23 hours from now if parsing fails
189
+ return Date.now() + (23 * 60 * 60 * 1000);
190
+ }
191
+ }
192
+
193
+ isJwtExpired() {
194
+ if (!this.jwtExpiry) return true;
195
+ return Date.now() >= this.jwtExpiry;
196
+ }
197
+
198
+ /**
199
+ * Refresh JWT token by creating a new automation run
200
+ * Uses the stored run configuration and API key
201
+ * @returns {Promise<string>} New JWT token
202
+ */
203
+ async refreshJwtToken() {
204
+ if (!this.config.apiKey) {
205
+ throw new Error('Cannot refresh JWT: API key not configured');
206
+ }
207
+
208
+ if (!this.runConfig) {
209
+ throw new Error('Cannot refresh JWT: Run configuration not set. Call setRunConfig() first.');
210
+ }
211
+
212
+ try {
213
+ logger.info('Refreshing JWT token...', {
214
+ projectKey: this.runConfig.projectKey,
215
+ scenarioId: this.runConfig.scenarioId,
216
+ environment: this.runConfig.environment
217
+ });
218
+
219
+ const https = require('https');
220
+
221
+ // Prepare payload for run creation
222
+ const payload = {
223
+ project_key: this.runConfig.projectKey,
224
+ scenario_id: this.runConfig.scenarioId || 0,
225
+ environment: this.runConfig.environment || 'Local',
226
+ browsers: this.runConfig.browsers || ['Chrome'],
227
+ device: this.runConfig.device || 'Desktop',
228
+ os: this.runConfig.os || 'Linux',
229
+ title: this.runConfig.title || `Automation Run - ${new Date().toISOString()}`
230
+ };
231
+
232
+ // Make direct API call to get new JWT token
233
+ const response = await axios.post(
234
+ `${this.config.baseUrl}/api/automation/run/create`,
235
+ payload,
236
+ {
237
+ headers: {
238
+ 'X-API-Key': this.config.apiKey,
239
+ 'Content-Type': 'application/json'
240
+ },
241
+ httpsAgent: new https.Agent({
242
+ rejectUnauthorized: this.config.rejectUnauthorized !== undefined ? this.config.rejectUnauthorized : true
243
+ }),
244
+ timeout: this.config.timeout || 30000
245
+ }
246
+ );
247
+
248
+ if (!response.data || !response.data.jwt_token) {
249
+ throw new Error('Invalid response: JWT token not received');
250
+ }
251
+
252
+ // Store the new JWT token
253
+ this.setJwtToken(response.data.jwt_token);
254
+
255
+ logger.info('JWT token refreshed successfully', {
256
+ expiresAt: new Date(this.jwtExpiry).toISOString()
257
+ });
258
+
259
+ return response.data.jwt_token;
260
+ } catch (error) {
261
+ logger.error('Failed to refresh JWT token', {
262
+ error: error.message,
263
+ status: error.response?.status,
264
+ data: error.response?.data
265
+ });
266
+ throw new Error(`JWT refresh failed: ${error.message}`);
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Check if JWT token should be refreshed
272
+ * Returns true if token is within 5 minutes of expiring
273
+ * @returns {boolean} True if token should be refreshed
274
+ */
275
+ shouldRefreshJwt() {
276
+ if (!this.jwtExpiry) return false;
277
+ // Refresh if we're within 5 minutes of expiration
278
+ const refreshThreshold = Date.now() + (5 * 60 * 1000);
279
+ return refreshThreshold >= this.jwtExpiry;
280
+ }
281
+
282
+ /**
283
+ * Set run configuration for JWT token refresh
284
+ * This must be called before refreshJwtToken() can work
285
+ * @param {Object} config - Run configuration
286
+ * @param {string} config.projectKey - Project key
287
+ * @param {number} config.scenarioId - Scenario ID (optional, defaults to 0)
288
+ * @param {string} config.environment - Environment name
289
+ * @param {string[]} config.browsers - Browser names (optional)
290
+ * @param {string} config.device - Device type (optional)
291
+ * @param {string} config.os - Operating system (optional)
292
+ * @param {string} config.title - Run title (optional)
293
+ */
294
+ setRunConfig(config) {
295
+ this.runConfig = config;
296
+ logger.debug('Run configuration stored for JWT refresh', {
297
+ projectKey: config.projectKey,
298
+ scenarioId: config.scenarioId,
299
+ environment: config.environment
300
+ });
301
+ }
302
+
303
+ getAuthMode() {
304
+ return this.authMode;
305
+ }
306
+
307
+ hasApiKey() {
308
+ return !!this.config.apiKey;
309
+ }
310
+
311
+ /**
312
+ * Get JWT token for browser authentication
313
+ * Returns the stored JWT token if available and not expired
314
+ *
315
+ * @returns {string|null} JWT token or null
316
+ */
317
+ getJwtForBrowser() {
318
+ if (this.jwtToken && !this.isJwtExpired()) {
319
+ return this.jwtToken;
320
+ }
321
+ return null;
322
+ }
323
+
324
+ /**
325
+ * Get authenticated URL with JWT token for browser navigation
326
+ *
327
+ * @param {string} baseUrl - Base URL to navigate to
328
+ * @param {string} path - Optional path to append
329
+ * @returns {string|null} URL with JWT token or null if no token available
330
+ */
331
+ getAuthenticatedUrl(baseUrl, path = '') {
332
+ const token = this.getJwtForBrowser();
333
+ if (!token) {
334
+ return null;
335
+ }
336
+
337
+ const url = path ? `${baseUrl.replace(/\/$/, '')}/${path.replace(/^\//, '')}` : baseUrl;
338
+ const separator = url.includes('?') ? '&' : '?';
339
+ return `${url}${separator}qonsole_token=${encodeURIComponent(token)}`;
340
+ }
341
+
342
+ /**
343
+ * Check if browser authentication is available
344
+ * (requires JWT token to be set)
345
+ *
346
+ * @returns {boolean} True if JWT token is available for browser auth
347
+ */
348
+ hasBrowserAuth() {
349
+ return !!this.getJwtForBrowser();
350
+ }
351
+ }
352
+
353
+ module.exports = AuthManager;