@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.
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/package.json +107 -0
- package/src/AppliqationClient.js +562 -0
- package/src/constants.js +245 -0
- package/src/core/AuthManager.js +353 -0
- package/src/core/HttpClient.js +475 -0
- package/src/index.d.ts +333 -0
- package/src/index.js +26 -0
- package/src/playwright/JwtBrowserAuth.js +240 -0
- package/src/playwright/fixture.js +92 -0
- package/src/playwright/global-setup.js +243 -0
- package/src/playwright/helpers/jwt-browser-auth.js +227 -0
- package/src/playwright/index.js +16 -0
- package/src/reporters/cypress/CypressReporter.js +387 -0
- package/src/reporters/cypress/UuidExtractor.js +139 -0
- package/src/reporters/cypress/index.js +30 -0
- package/src/reporters/jest/JestReporter.js +361 -0
- package/src/reporters/jest/UuidExtractor.js +174 -0
- package/src/reporters/jest/index.js +28 -0
- package/src/reporters/playwright/AppliqationReporter.js +654 -0
- package/src/reporters/playwright/helpers/DeviceOsDetector.js +435 -0
- package/src/reporters/playwright/helpers/UuidExtractor.js +290 -0
- package/src/reporters/playwright/index.d.ts +96 -0
- package/src/reporters/playwright/index.js +14 -0
- package/src/services/OrphanTestService.js +74 -0
- package/src/services/ResultService.js +252 -0
- package/src/services/RunMatrixService.js +309 -0
- package/src/utils/PayloadBuilder.js +280 -0
- package/src/utils/RunDataNormalizer.js +335 -0
- package/src/utils/UuidValidator.js +102 -0
- package/src/utils/errors.js +217 -0
- package/src/utils/index.js +17 -0
- package/src/utils/logger.js +124 -0
- package/src/utils/mapAppqUuid.js +83 -0
- package/src/utils/validator.js +157 -0
package/src/constants.js
ADDED
|
@@ -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;
|