@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/index.d.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript definitions for @appliqation/automation-sdk
|
|
3
|
+
* @version 2.1.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Core Types
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Test result status
|
|
12
|
+
*/
|
|
13
|
+
export type TestStatus = 'pass' | 'fail' | 'skip';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Log levels
|
|
17
|
+
*/
|
|
18
|
+
export type LogLevel = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Browser types
|
|
22
|
+
*/
|
|
23
|
+
export type Browser = 'Chrome' | 'Firefox' | 'Safari' | 'Edge' | 'Chromium' | 'WebKit' | string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Device types
|
|
27
|
+
*/
|
|
28
|
+
export type Device = 'Desktop' | 'Mobile' | 'Tablet' | string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Operating systems
|
|
32
|
+
*/
|
|
33
|
+
export type OperatingSystem = 'Windows' | 'macOS' | 'Linux' | 'iOS' | 'Android' | string;
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Configuration Interfaces
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* SDK configuration options
|
|
41
|
+
*/
|
|
42
|
+
export interface AppliqationConfig {
|
|
43
|
+
/** Base URL of the Appliqation instance */
|
|
44
|
+
baseUrl: string;
|
|
45
|
+
|
|
46
|
+
/** API key for authentication (recommended) */
|
|
47
|
+
apiKey?: string;
|
|
48
|
+
|
|
49
|
+
/** Project key */
|
|
50
|
+
projectKey?: string;
|
|
51
|
+
|
|
52
|
+
/** Username for legacy CSRF authentication */
|
|
53
|
+
username?: string;
|
|
54
|
+
|
|
55
|
+
/** Password for legacy CSRF authentication */
|
|
56
|
+
password?: string;
|
|
57
|
+
|
|
58
|
+
/** Scenario ID (optional, 0 for generic automation runs) */
|
|
59
|
+
scenarioId?: number;
|
|
60
|
+
|
|
61
|
+
/** Test set ID (alternative to scenarioId) */
|
|
62
|
+
testSetId?: number;
|
|
63
|
+
|
|
64
|
+
/** Environment name (e.g., 'Local', 'Development', 'Staging', 'Production') */
|
|
65
|
+
environment?: string;
|
|
66
|
+
|
|
67
|
+
/** Custom run title */
|
|
68
|
+
title?: string;
|
|
69
|
+
|
|
70
|
+
/** Alternative to title */
|
|
71
|
+
runTitle?: string;
|
|
72
|
+
|
|
73
|
+
/** Browser list */
|
|
74
|
+
browsers?: Browser[];
|
|
75
|
+
|
|
76
|
+
/** Device type */
|
|
77
|
+
device?: Device;
|
|
78
|
+
|
|
79
|
+
/** Operating system */
|
|
80
|
+
os?: OperatingSystem;
|
|
81
|
+
|
|
82
|
+
/** Automatically create run on initialization */
|
|
83
|
+
autoCreateRun?: boolean;
|
|
84
|
+
|
|
85
|
+
/** Enable batch submission of results */
|
|
86
|
+
batchSubmit?: boolean;
|
|
87
|
+
|
|
88
|
+
/** Batch size for result submission */
|
|
89
|
+
batchSize?: number;
|
|
90
|
+
|
|
91
|
+
/** SSL certificate verification (default: true) */
|
|
92
|
+
rejectUnauthorized?: boolean;
|
|
93
|
+
|
|
94
|
+
/** Additional options */
|
|
95
|
+
options?: {
|
|
96
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
97
|
+
timeout?: number;
|
|
98
|
+
|
|
99
|
+
/** Number of retries for failed requests (default: 3) */
|
|
100
|
+
retries?: number;
|
|
101
|
+
|
|
102
|
+
/** Base retry delay in milliseconds (default: 1000) */
|
|
103
|
+
retryDelay?: number;
|
|
104
|
+
|
|
105
|
+
/** Maximum retry delay in milliseconds (default: 30000) */
|
|
106
|
+
maxRetryDelay?: number;
|
|
107
|
+
|
|
108
|
+
/** Retry jitter factor (default: 0.1) */
|
|
109
|
+
retryJitter?: number;
|
|
110
|
+
|
|
111
|
+
/** Log orphan tests (tests without UUID mapping) (default: true) */
|
|
112
|
+
logOrphans?: boolean;
|
|
113
|
+
|
|
114
|
+
/** Log level (default: 'INFO') */
|
|
115
|
+
logLevel?: LogLevel;
|
|
116
|
+
|
|
117
|
+
/** Enable HTTP keep-alive (default: true) */
|
|
118
|
+
keepAlive?: boolean;
|
|
119
|
+
|
|
120
|
+
/** Maximum number of sockets (default: 50) */
|
|
121
|
+
maxSockets?: number;
|
|
122
|
+
|
|
123
|
+
/** Maximum number of free sockets (default: 10) */
|
|
124
|
+
maxFreeSockets?: number;
|
|
125
|
+
|
|
126
|
+
/** Keep-alive timeout in milliseconds (default: 1000) */
|
|
127
|
+
keepAliveMsecs?: number;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// Result Interfaces
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Test result object
|
|
137
|
+
*/
|
|
138
|
+
export interface TestResult {
|
|
139
|
+
/** Test UUID (format: "{testSetId}-{uuid}") */
|
|
140
|
+
uuid: string;
|
|
141
|
+
|
|
142
|
+
/** Test status */
|
|
143
|
+
status: TestStatus;
|
|
144
|
+
|
|
145
|
+
/** Optional comment */
|
|
146
|
+
comment?: string;
|
|
147
|
+
|
|
148
|
+
/** Optional attachments */
|
|
149
|
+
attachments?: string[];
|
|
150
|
+
|
|
151
|
+
/** Optional metadata */
|
|
152
|
+
metadata?: Record<string, any>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* API response
|
|
157
|
+
*/
|
|
158
|
+
export interface ApiResponse<T = any> {
|
|
159
|
+
/** Success status */
|
|
160
|
+
success: boolean;
|
|
161
|
+
|
|
162
|
+
/** Response data */
|
|
163
|
+
data?: T;
|
|
164
|
+
|
|
165
|
+
/** HTTP status code */
|
|
166
|
+
status?: number;
|
|
167
|
+
|
|
168
|
+
/** Response headers */
|
|
169
|
+
headers?: Record<string, string>;
|
|
170
|
+
|
|
171
|
+
/** Error message (if success is false) */
|
|
172
|
+
message?: string;
|
|
173
|
+
|
|
174
|
+
/** Error details */
|
|
175
|
+
error?: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Run matrix response
|
|
180
|
+
*/
|
|
181
|
+
export interface RunMatrixResponse {
|
|
182
|
+
/** Run ID */
|
|
183
|
+
runId: string;
|
|
184
|
+
|
|
185
|
+
/** Run timestamp */
|
|
186
|
+
timestamp: number;
|
|
187
|
+
|
|
188
|
+
/** Environment */
|
|
189
|
+
environment: string;
|
|
190
|
+
|
|
191
|
+
/** Browser list */
|
|
192
|
+
browsers: Browser[];
|
|
193
|
+
|
|
194
|
+
/** Device type */
|
|
195
|
+
device?: Device;
|
|
196
|
+
|
|
197
|
+
/** Operating system */
|
|
198
|
+
os?: OperatingSystem;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Batch submission summary
|
|
203
|
+
*/
|
|
204
|
+
export interface BatchSubmissionSummary {
|
|
205
|
+
/** Number of successful submissions */
|
|
206
|
+
success: number;
|
|
207
|
+
|
|
208
|
+
/** Number of failed submissions */
|
|
209
|
+
failed: number;
|
|
210
|
+
|
|
211
|
+
/** Total number of results */
|
|
212
|
+
total: number;
|
|
213
|
+
|
|
214
|
+
/** Number of invalid results */
|
|
215
|
+
invalid?: number;
|
|
216
|
+
|
|
217
|
+
/** Invalid results details */
|
|
218
|
+
invalidResults?: Array<{ result: TestResult; error: string }>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// AppliqationClient Class
|
|
223
|
+
// ============================================================================
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Main SDK client class
|
|
227
|
+
*/
|
|
228
|
+
export class AppliqationClient {
|
|
229
|
+
/**
|
|
230
|
+
* Create a new Appliqation client
|
|
231
|
+
*/
|
|
232
|
+
constructor(config: AppliqationConfig);
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Create a new automation run
|
|
236
|
+
*/
|
|
237
|
+
createRun(config?: Partial<AppliqationConfig>): Promise<RunMatrixResponse>;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Submit a single test result
|
|
241
|
+
*/
|
|
242
|
+
reportResult(
|
|
243
|
+
uuid: string,
|
|
244
|
+
status: TestStatus,
|
|
245
|
+
options?: { comment?: string; attachments?: string[]; metadata?: Record<string, any> }
|
|
246
|
+
): Promise<ApiResponse>;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Submit multiple test results in batch
|
|
250
|
+
*/
|
|
251
|
+
reportBatchResults(results: TestResult[]): Promise<BatchSubmissionSummary>;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Log orphan tests (tests without UUID mappings)
|
|
255
|
+
*/
|
|
256
|
+
logOrphanTests(runId: string, orphanTests: any[]): Promise<ApiResponse>;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get current run ID
|
|
260
|
+
*/
|
|
261
|
+
getRunId(): string | null;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Test connection to Appliqation instance
|
|
265
|
+
*/
|
|
266
|
+
testConnection(): Promise<boolean>;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get SDK version
|
|
270
|
+
*/
|
|
271
|
+
getVersion(): string;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============================================================================
|
|
275
|
+
// Exports
|
|
276
|
+
// ============================================================================
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Main client class (default export)
|
|
280
|
+
*/
|
|
281
|
+
export default AppliqationClient;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Named exports
|
|
285
|
+
*/
|
|
286
|
+
export { AppliqationClient };
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Utility exports
|
|
290
|
+
*/
|
|
291
|
+
export class UuidValidator {
|
|
292
|
+
static validate(uuid: string): { valid: boolean; error?: string };
|
|
293
|
+
static isValid(uuid: string): boolean;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export class PayloadBuilder {
|
|
297
|
+
static buildResultPayload(
|
|
298
|
+
uuid: string,
|
|
299
|
+
status: TestStatus,
|
|
300
|
+
options?: { comment?: string; attachments?: string[]; metadata?: Record<string, any> }
|
|
301
|
+
): TestResult;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export const logger: {
|
|
305
|
+
error(message: string, meta?: Record<string, any>): void;
|
|
306
|
+
warn(message: string, meta?: Record<string, any>): void;
|
|
307
|
+
info(message: string, meta?: Record<string, any>): void;
|
|
308
|
+
debug(message: string, meta?: Record<string, any>): void;
|
|
309
|
+
setLevel(level: LogLevel): void;
|
|
310
|
+
setConsoleEnabled(enabled: boolean): void;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Map a test UUID to Playwright testInfo for result reporting
|
|
315
|
+
*
|
|
316
|
+
* @param testInfo - Playwright test info object from test function
|
|
317
|
+
* @param uuid - Appliqation test UUID (format: nid-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
|
|
318
|
+
*
|
|
319
|
+
* @throws {Error} If testInfo is not provided or invalid
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```typescript
|
|
323
|
+
* import { test } from '@playwright/test';
|
|
324
|
+
* import { mapAppqUuid } from '@appliqation/automation-sdk/utils';
|
|
325
|
+
*
|
|
326
|
+
* test('login functionality', async ({ page }, testInfo) => {
|
|
327
|
+
* mapAppqUuid(testInfo, '1141-6d2827f3-1bc2-405c-95b4-6a09e8b64148');
|
|
328
|
+
* await page.goto('/login');
|
|
329
|
+
* // ... test implementation
|
|
330
|
+
* });
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
export function mapAppqUuid(testInfo: any, uuid: string): void;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appliqation Automation SDK
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for the SDK - New Architecture with API Key Authentication
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const AppliqationClient = require('./AppliqationClient');
|
|
8
|
+
const UuidValidator = require('./utils/UuidValidator');
|
|
9
|
+
const PayloadBuilder = require('./utils/PayloadBuilder');
|
|
10
|
+
const logger = require('./utils/logger');
|
|
11
|
+
const playwright = require('./playwright');
|
|
12
|
+
|
|
13
|
+
// Export main client class
|
|
14
|
+
module.exports = AppliqationClient;
|
|
15
|
+
|
|
16
|
+
// Export utilities as named exports
|
|
17
|
+
module.exports.AppliqationClient = AppliqationClient;
|
|
18
|
+
module.exports.UuidValidator = UuidValidator;
|
|
19
|
+
module.exports.PayloadBuilder = PayloadBuilder;
|
|
20
|
+
module.exports.logger = logger;
|
|
21
|
+
|
|
22
|
+
// Export Playwright utilities
|
|
23
|
+
module.exports.playwright = playwright;
|
|
24
|
+
|
|
25
|
+
// Export default
|
|
26
|
+
module.exports.default = AppliqationClient;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const https = require('https');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const jwt = require('jsonwebtoken');
|
|
5
|
+
const logger = require('../utils/logger');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* JWT Browser Authentication Manager
|
|
9
|
+
*
|
|
10
|
+
* Handles browser JWT authentication with automatic token refresh
|
|
11
|
+
* for Playwright test frameworks.
|
|
12
|
+
*
|
|
13
|
+
* Features:
|
|
14
|
+
* - Automatic JWT token refresh when approaching expiry
|
|
15
|
+
* - Browser cookie management
|
|
16
|
+
* - Storage state synchronization
|
|
17
|
+
* - Zero-configuration setup
|
|
18
|
+
*/
|
|
19
|
+
class JwtBrowserAuth {
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.baseUrl = config.baseUrl;
|
|
22
|
+
this.apiKey = config.apiKey;
|
|
23
|
+
this.projectKey = config.projectKey;
|
|
24
|
+
this.storageStatePath = config.storageStatePath || '.auth/user.json';
|
|
25
|
+
this.refreshThresholdMinutes = config.refreshThresholdMinutes || 5;
|
|
26
|
+
this.rejectUnauthorized = config.rejectUnauthorized !== undefined ? config.rejectUnauthorized : true;
|
|
27
|
+
|
|
28
|
+
this.jwtToken = null;
|
|
29
|
+
this.jwtExpiry = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Decode JWT token to extract expiry time
|
|
34
|
+
*/
|
|
35
|
+
decodeToken(token) {
|
|
36
|
+
try {
|
|
37
|
+
return jwt.decode(token);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
logger.error('Failed to decode JWT token', { error: error.message });
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get browser JWT token from API
|
|
46
|
+
*/
|
|
47
|
+
async getBrowserJwt() {
|
|
48
|
+
try {
|
|
49
|
+
logger.info('Requesting browser JWT token...');
|
|
50
|
+
|
|
51
|
+
const response = await axios.post(`${this.baseUrl}/api/auth/jwt/browser`, {
|
|
52
|
+
api_key: this.apiKey
|
|
53
|
+
}, {
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
timeout: 30000,
|
|
56
|
+
httpsAgent: new https.Agent({ rejectUnauthorized: this.rejectUnauthorized })
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!response.data || !response.data.jwt_token) {
|
|
60
|
+
throw new Error('No JWT token received from server');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.jwtToken = response.data.jwt_token;
|
|
64
|
+
|
|
65
|
+
// Decode token to get actual expiry
|
|
66
|
+
const decoded = this.decodeToken(this.jwtToken);
|
|
67
|
+
if (decoded && decoded.exp) {
|
|
68
|
+
this.jwtExpiry = decoded.exp * 1000;
|
|
69
|
+
} else {
|
|
70
|
+
const expiresIn = response.data.expires_in || 3600;
|
|
71
|
+
this.jwtExpiry = Date.now() + (expiresIn * 1000);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
logger.info('Browser JWT token received', {
|
|
75
|
+
expiresAt: new Date(this.jwtExpiry).toISOString(),
|
|
76
|
+
minutesUntilExpiry: this.getTimeUntilExpiry()
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return response.data;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
logger.error('Failed to get browser JWT token', { error: error.message });
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if JWT token should be refreshed
|
|
88
|
+
*/
|
|
89
|
+
shouldRefresh() {
|
|
90
|
+
if (!this.jwtExpiry) return true;
|
|
91
|
+
const thresholdMs = this.refreshThresholdMinutes * 60 * 1000;
|
|
92
|
+
const timeUntilExpiry = this.jwtExpiry - Date.now();
|
|
93
|
+
return timeUntilExpiry <= thresholdMs;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get time until token expiry in minutes
|
|
98
|
+
*/
|
|
99
|
+
getTimeUntilExpiry() {
|
|
100
|
+
if (!this.jwtExpiry) return 0;
|
|
101
|
+
return Math.max(0, Math.floor((this.jwtExpiry - Date.now()) / 1000 / 60));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Refresh browser JWT token in the page context
|
|
106
|
+
*/
|
|
107
|
+
async refreshBrowserJwt(page) {
|
|
108
|
+
try {
|
|
109
|
+
logger.debug('Checking JWT token expiry...', {
|
|
110
|
+
minutesUntilExpiry: this.getTimeUntilExpiry()
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!this.shouldRefresh()) {
|
|
114
|
+
logger.debug('JWT token is still valid, no refresh needed');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
logger.info('Refreshing browser JWT token...');
|
|
119
|
+
|
|
120
|
+
// Get new JWT token
|
|
121
|
+
const response = await this.getBrowserJwt();
|
|
122
|
+
const newToken = response.jwt_token;
|
|
123
|
+
|
|
124
|
+
// Update cookie in the browser context
|
|
125
|
+
const context = page.context();
|
|
126
|
+
const domain = new URL(this.baseUrl).hostname;
|
|
127
|
+
|
|
128
|
+
// Get existing cookies
|
|
129
|
+
const cookies = await context.cookies();
|
|
130
|
+
const otherCookies = cookies.filter(c => c.name !== 'appliqation_jwt');
|
|
131
|
+
|
|
132
|
+
// Clear all cookies and re-add them with new JWT
|
|
133
|
+
await context.clearCookies();
|
|
134
|
+
|
|
135
|
+
if (otherCookies.length > 0) {
|
|
136
|
+
await context.addCookies(otherCookies);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Add new JWT cookie
|
|
140
|
+
await context.addCookies([{
|
|
141
|
+
name: 'appliqation_jwt',
|
|
142
|
+
value: newToken,
|
|
143
|
+
domain: domain,
|
|
144
|
+
path: '/',
|
|
145
|
+
httpOnly: true,
|
|
146
|
+
secure: this.baseUrl.startsWith('https'),
|
|
147
|
+
sameSite: 'Lax'
|
|
148
|
+
}]);
|
|
149
|
+
|
|
150
|
+
logger.info('Browser JWT token refreshed successfully', {
|
|
151
|
+
minutesUntilExpiry: this.getTimeUntilExpiry()
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Update storage state file
|
|
155
|
+
await this.updateStorageState(context);
|
|
156
|
+
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logger.error('Failed to refresh browser JWT token', { error: error.message });
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Update the storage state file with new cookies
|
|
165
|
+
*/
|
|
166
|
+
async updateStorageState(context) {
|
|
167
|
+
try {
|
|
168
|
+
await context.storageState({ path: this.storageStatePath });
|
|
169
|
+
logger.debug('Updated storage state', { path: this.storageStatePath });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
logger.warn('Failed to update storage state', { error: error.message });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Load JWT info from storage state file
|
|
177
|
+
*/
|
|
178
|
+
loadFromStorageState() {
|
|
179
|
+
try {
|
|
180
|
+
if (!fs.existsSync(this.storageStatePath)) {
|
|
181
|
+
logger.debug('No storage state file found', { path: this.storageStatePath });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const storageState = JSON.parse(fs.readFileSync(this.storageStatePath, 'utf8'));
|
|
186
|
+
const jwtCookie = storageState.cookies?.find(c => c.name === 'appliqation_jwt');
|
|
187
|
+
|
|
188
|
+
if (jwtCookie) {
|
|
189
|
+
this.jwtToken = jwtCookie.value;
|
|
190
|
+
const decoded = this.decodeToken(this.jwtToken);
|
|
191
|
+
if (decoded && decoded.exp) {
|
|
192
|
+
this.jwtExpiry = decoded.exp * 1000;
|
|
193
|
+
logger.debug('Loaded JWT from storage state', {
|
|
194
|
+
minutesUntilExpiry: this.getTimeUntilExpiry()
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
logger.warn('Failed to load JWT from storage state', { error: error.message });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Setup initial browser authentication
|
|
205
|
+
* Creates JWT token and saves to storage state
|
|
206
|
+
*/
|
|
207
|
+
async setupInitialAuth(browserContext) {
|
|
208
|
+
try {
|
|
209
|
+
logger.info('Setting up initial browser JWT authentication...');
|
|
210
|
+
|
|
211
|
+
// Get browser JWT token
|
|
212
|
+
const response = await this.getBrowserJwt();
|
|
213
|
+
const jwtToken = response.jwt_token;
|
|
214
|
+
|
|
215
|
+
// Set JWT token as cookie
|
|
216
|
+
const domain = new URL(this.baseUrl).hostname;
|
|
217
|
+
await browserContext.addCookies([{
|
|
218
|
+
name: 'appliqation_jwt',
|
|
219
|
+
value: jwtToken,
|
|
220
|
+
domain: domain,
|
|
221
|
+
path: '/',
|
|
222
|
+
httpOnly: true,
|
|
223
|
+
secure: this.baseUrl.startsWith('https'),
|
|
224
|
+
sameSite: 'Lax'
|
|
225
|
+
}]);
|
|
226
|
+
|
|
227
|
+
logger.info('Browser JWT authentication setup complete', {
|
|
228
|
+
domain,
|
|
229
|
+
minutesUntilExpiry: this.getTimeUntilExpiry()
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return jwtToken;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
logger.error('Failed to setup initial browser authentication', { error: error.message });
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = JwtBrowserAuth;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Import Playwright test dynamically to avoid circular dependencies
|
|
2
|
+
let baseTest;
|
|
3
|
+
try {
|
|
4
|
+
baseTest = require('@playwright/test').test;
|
|
5
|
+
} catch (error) {
|
|
6
|
+
// Fallback if @playwright/test is not available
|
|
7
|
+
console.warn('⚠️ @playwright/test not found. JWT auto-refresh will not be available.');
|
|
8
|
+
baseTest = null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const JwtBrowserAuth = require('./JwtBrowserAuth');
|
|
12
|
+
require('dotenv').config();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Appliqation Playwright Fixture with Auto JWT Refresh
|
|
16
|
+
*
|
|
17
|
+
* This fixture automatically handles JWT browser authentication with auto-refresh.
|
|
18
|
+
* Simply import this instead of @playwright/test and authentication is handled automatically.
|
|
19
|
+
*
|
|
20
|
+
* USAGE:
|
|
21
|
+
* ------
|
|
22
|
+
* // In your test file:
|
|
23
|
+
* const { test, expect } = require('@appliqation/automation-sdk/playwright');
|
|
24
|
+
*
|
|
25
|
+
* test('my test', async ({ page }) => {
|
|
26
|
+
* // JWT is automatically refreshed before each test if needed
|
|
27
|
+
* await page.goto('/dashboard');
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* CONFIGURATION:
|
|
31
|
+
* --------------
|
|
32
|
+
* Set these environment variables in your .env file:
|
|
33
|
+
* - APPLIQATION_BASE_URL
|
|
34
|
+
* - APPLIQATION_API_KEY
|
|
35
|
+
* - APPLIQATION_PROJECT_KEY
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// Create the fixture only if Playwright is available
|
|
39
|
+
let test, expect;
|
|
40
|
+
|
|
41
|
+
if (baseTest) {
|
|
42
|
+
test = baseTest.extend({
|
|
43
|
+
page: async ({ page }, use) => {
|
|
44
|
+
// Get configuration from environment
|
|
45
|
+
const baseUrl = process.env.APPLIQATION_BASE_URL;
|
|
46
|
+
const apiKey = process.env.APPLIQATION_API_KEY;
|
|
47
|
+
const projectKey = process.env.APPLIQATION_PROJECT_KEY;
|
|
48
|
+
|
|
49
|
+
// If API key is not configured, skip JWT refresh
|
|
50
|
+
if (!apiKey) {
|
|
51
|
+
await use(page);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Initialize JWT browser auth manager
|
|
56
|
+
const jwtAuth = new JwtBrowserAuth({
|
|
57
|
+
baseUrl,
|
|
58
|
+
apiKey,
|
|
59
|
+
projectKey,
|
|
60
|
+
storageStatePath: '.auth/user.json',
|
|
61
|
+
refreshThresholdMinutes: 5
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Load existing JWT from storage state
|
|
65
|
+
jwtAuth.loadFromStorageState();
|
|
66
|
+
|
|
67
|
+
// Refresh JWT if needed before test runs
|
|
68
|
+
try {
|
|
69
|
+
await jwtAuth.refreshBrowserJwt(page);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Log error but don't fail the test
|
|
72
|
+
// The test might still work with existing token
|
|
73
|
+
console.warn('⚠️ JWT refresh failed, continuing with existing token:', error.message);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Run the test
|
|
77
|
+
await use(page);
|
|
78
|
+
|
|
79
|
+
// No cleanup needed - JWT is managed automatically
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect = require('@playwright/test').expect;
|
|
84
|
+
} else {
|
|
85
|
+
test = null;
|
|
86
|
+
expect = null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = {
|
|
90
|
+
test,
|
|
91
|
+
expect
|
|
92
|
+
};
|