@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,562 @@
1
+ const HttpClient = require('./core/HttpClient');
2
+ const AuthManager = require('./core/AuthManager');
3
+ const RunMatrixService = require('./services/RunMatrixService');
4
+ const ResultService = require('./services/ResultService');
5
+ const OrphanTestService = require('./services/OrphanTestService');
6
+ const UuidValidator = require('./utils/UuidValidator');
7
+ const PayloadBuilder = require('./utils/PayloadBuilder');
8
+ const logger = require('./utils/logger');
9
+
10
+ /**
11
+ * Main Appliqation Automation SDK Client
12
+ *
13
+ * @example
14
+ * // Initialize with API key (recommended for automation)
15
+ * const client = new AppliqationClient({
16
+ * baseUrl: 'https://your-instance.appliqation.com',
17
+ * apiKey: 'appq_live_xxxxxxxxxxxxx',
18
+ * projectKey: 'your-project-key'
19
+ * });
20
+ *
21
+ * // Create run matrix
22
+ * const run = await client.createRun({
23
+ * scenarioId: 123,
24
+ * environment: 'Production',
25
+ * browsers: ['Chrome', 'Firefox'],
26
+ * device: 'Desktop',
27
+ * os: 'Windows 11'
28
+ * });
29
+ *
30
+ * // Submit test results
31
+ * await client.submitResult(run.runId, {
32
+ * uuid: '124-d1f9559c-b978-43cc-9c76-fd539c717cb4',
33
+ * status: 'passed',
34
+ * browser: 'Chrome'
35
+ * });
36
+ */
37
+ class AppliqationClient {
38
+ /**
39
+ * Create Appliqation client
40
+ * @param {Object} config - Client configuration
41
+ * @param {string} config.baseUrl - Appliqation instance URL
42
+ * @param {string} config.apiKey - API key for authentication (recommended)
43
+ * @param {string} config.projectKey - Project key (base64 encoded or plain)
44
+ * @param {string} [config.username] - Username for CSRF auth (legacy)
45
+ * @param {string} [config.password] - Password for CSRF auth (legacy)
46
+ * @param {string} [config.title] - Custom run title (or use APPLIQATION_RUN_TITLE env var)
47
+ * @param {Object} [config.options] - Additional options
48
+ * @param {number} [config.options.timeout=30000] - Request timeout in ms
49
+ * @param {number} [config.options.retries=3] - Number of retry attempts
50
+ * @param {boolean} [config.options.logOrphans=true] - Log orphan tests to backend
51
+ * @param {string} [config.options.logLevel='info'] - Logging level
52
+ */
53
+ constructor(config) {
54
+ // Validate configuration
55
+ this.validateConfig(config);
56
+
57
+ // Determine SSL enforcement based on environment
58
+ const isProduction = this._isProductionEnvironment();
59
+ let rejectUnauthorized = true; // Default to secure
60
+
61
+ if (config.rejectUnauthorized !== undefined) {
62
+ // User explicitly set rejectUnauthorized
63
+ if (isProduction && config.rejectUnauthorized === false) {
64
+ // WARNING: User is trying to disable SSL in production
65
+ logger.warn('SSL certificate verification is disabled in production environment!', {
66
+ environment: process.env.NODE_ENV,
67
+ ci: process.env.CI,
68
+ warning: 'This is a security risk. SSL verification should be enabled in production.'
69
+ });
70
+ }
71
+ rejectUnauthorized = config.rejectUnauthorized;
72
+ } else if (isProduction) {
73
+ // Production environment - force SSL verification
74
+ rejectUnauthorized = true;
75
+ logger.info('SSL certificate verification enforced for production environment');
76
+ } else {
77
+ // Development environment - default to secure but can be overridden
78
+ rejectUnauthorized = true;
79
+ }
80
+
81
+ // Store configuration
82
+ this.config = {
83
+ baseUrl: config.baseUrl.replace(/\/$/, ''), // Remove trailing slash
84
+ apiKey: config.apiKey,
85
+ projectKey: config.projectKey,
86
+ username: config.username,
87
+ password: config.password,
88
+ runTitle: config.runTitle || config.title || process.env.APPLIQATION_RUN_TITLE || null,
89
+ rejectUnauthorized: rejectUnauthorized,
90
+ options: {
91
+ timeout: config.options?.timeout || 30000,
92
+ retries: config.options?.retries || 3,
93
+ logOrphans: config.options?.logOrphans !== false,
94
+ logLevel: config.options?.logLevel || 'info'
95
+ }
96
+ };
97
+
98
+ // Configure logger
99
+ logger.setLevel(this.config.options.logLevel);
100
+
101
+ // Initialize core components
102
+ this.auth = new AuthManager(this.config);
103
+ this.http = new HttpClient(this.config, this.auth);
104
+
105
+ // Initialize services
106
+ this.runMatrix = new RunMatrixService(this.http, this.config);
107
+ this.results = new ResultService(this.http);
108
+ this.orphans = new OrphanTestService(this.http);
109
+
110
+ // Track current run context
111
+ this.currentRun = null;
112
+
113
+ logger.info('Appliqation client initialized', {
114
+ baseUrl: this.config.baseUrl,
115
+ authMode: this.config.apiKey ? 'api_key' : 'csrf'
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Create a new test run matrix
121
+ * @param {Object} options - Run configuration
122
+ * @param {number} options.scenarioId - Scenario node ID
123
+ * @param {number} options.testSetId - Test Set node ID (alternative to scenarioId)
124
+ * @param {string} options.type - 'scenario' or 'testset'
125
+ * @param {string} [options.environment='Local'] - Environment name
126
+ * @param {string[]} [options.browsers=['Chrome']] - Array of browser names
127
+ * @param {string} [options.device] - Device type (Desktop, Mobile, Tablet)
128
+ * @param {string} [options.os] - Operating system
129
+ * @param {string} [options.title] - Custom run title
130
+ * @returns {Promise<Object>} { runId, token, timestamp, metadata }
131
+ */
132
+ async createRun(options) {
133
+ try {
134
+ logger.info('Creating run matrix...', options);
135
+
136
+ const run = await this.runMatrix.create(options);
137
+
138
+ // Store current run context
139
+ this.currentRun = run;
140
+
141
+ // Store run configuration in AuthManager for JWT refresh capability
142
+ if (this.auth && this.config.apiKey) {
143
+ this.auth.setRunConfig({
144
+ projectKey: this.config.projectKey,
145
+ scenarioId: options.scenarioId || 0,
146
+ testSetId: options.testSetId,
147
+ environment: options.environment || 'Local',
148
+ browsers: options.browsers || ['Chrome'],
149
+ device: options.device,
150
+ os: options.os,
151
+ title: options.title || this.config.runTitle
152
+ });
153
+ logger.debug('Run configuration stored in AuthManager for JWT refresh');
154
+ }
155
+
156
+ logger.info('Run matrix created successfully', {
157
+ runId: run.runId,
158
+ timestamp: run.timestamp
159
+ });
160
+
161
+ return run;
162
+ } catch (error) {
163
+ logger.error('Failed to create run matrix', {
164
+ error: error.message,
165
+ options
166
+ });
167
+ throw error;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Create multiple runs for device/OS matrix testing
173
+ * @param {Array} configs - Array of run configurations
174
+ * @returns {Promise<Object>} { runs, errors, summary }
175
+ */
176
+ async createMultipleRuns(configs) {
177
+ try {
178
+ logger.info(`Creating ${configs.length} run matrices...`);
179
+
180
+ const result = await this.runMatrix.createMultiple(configs);
181
+
182
+ logger.info('Multiple runs created', {
183
+ total: result.summary.total,
184
+ success: result.summary.success,
185
+ failed: result.summary.failed
186
+ });
187
+
188
+ return result;
189
+ } catch (error) {
190
+ logger.error('Failed to create multiple runs', {
191
+ error: error.message
192
+ });
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Submit a single test result
199
+ * @param {string} runId - Run ID
200
+ * @param {Object} result - Test result
201
+ * @param {string} result.uuid - Test case UUID
202
+ * @param {string} result.status - Test status (passed, failed, skipped)
203
+ * @param {string} result.browser - Browser name
204
+ * @param {string} [result.parent_uuid] - Parent test case UUID
205
+ * @param {string} [result.comment] - Additional comments
206
+ * @param {string} [result.environment] - Environment name
207
+ * @returns {Promise<Object>} Response from server
208
+ */
209
+ async submitResult(runId, result) {
210
+ try {
211
+ // Validate UUID
212
+ const validation = UuidValidator.validateAndExtract(result.uuid);
213
+ if (!validation.valid) {
214
+ throw new Error(`Invalid UUID: ${validation.error}`);
215
+ }
216
+
217
+ // Validate result fields
218
+ const resultValidation = PayloadBuilder.validateResult(result);
219
+ if (!resultValidation.valid) {
220
+ throw new Error(`Invalid result: ${resultValidation.errors.join(', ')}`);
221
+ }
222
+
223
+ logger.debug('Submitting test result', {
224
+ runId,
225
+ uuid: result.uuid,
226
+ status: result.status,
227
+ browser: result.browser
228
+ });
229
+
230
+ const response = await this.results.submitSingle(runId, result);
231
+
232
+ return response;
233
+ } catch (error) {
234
+ logger.error('Failed to submit result', {
235
+ error: error.message,
236
+ runId,
237
+ uuid: result.uuid
238
+ });
239
+ throw error;
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Submit batch of test results with smart grouping
245
+ * @param {Array} results - Array of test results
246
+ * @param {Object} [options] - Batch options
247
+ * @param {number} [options.batchSize=50] - Results per batch
248
+ * @param {Function} [options.onProgress] - Progress callback
249
+ * @param {boolean} [options.retryFailures=true] - Retry failed submissions
250
+ * @returns {Promise<Object>} { success, failed, total }
251
+ */
252
+ async submitBatch(results, options = {}) {
253
+ try {
254
+ logger.info(`Submitting batch of ${results.length} results...`);
255
+
256
+ // Validate all results first
257
+ const validResults = [];
258
+ const invalidResults = [];
259
+
260
+ for (const result of results) {
261
+ const validation = UuidValidator.validateAndExtract(result.uuid);
262
+ if (validation.valid) {
263
+ validResults.push(result);
264
+ } else {
265
+ invalidResults.push({ result, error: validation.error });
266
+ logger.warn('Invalid UUID in batch', {
267
+ uuid: result.uuid,
268
+ error: validation.error
269
+ });
270
+ }
271
+ }
272
+
273
+ if (invalidResults.length > 0) {
274
+ logger.warn(`Skipping ${invalidResults.length} results with invalid UUIDs`);
275
+ }
276
+
277
+ const summary = await this.results.submitBatch(validResults, options);
278
+
279
+ logger.info('Batch submission completed', {
280
+ success: summary.success,
281
+ failed: summary.failed,
282
+ invalid: invalidResults.length,
283
+ total: results.length
284
+ });
285
+
286
+ return {
287
+ ...summary,
288
+ invalid: invalidResults.length,
289
+ invalidResults
290
+ };
291
+ } catch (error) {
292
+ logger.error('Failed to submit batch', {
293
+ error: error.message,
294
+ count: results.length
295
+ });
296
+ throw error;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Log orphan tests (tests without UUID mappings)
302
+ * @param {string} runId - Run ID
303
+ * @param {Array} orphanTests - Array of orphan test objects
304
+ * @returns {Promise<Object>} Response from server
305
+ */
306
+ async logOrphanTests(runId, orphanTests) {
307
+ try {
308
+ if (!this.config.options.logOrphans) {
309
+ logger.debug('Orphan test logging is disabled');
310
+ return { success: true, logged: 0 };
311
+ }
312
+
313
+ if (!orphanTests || orphanTests.length === 0) {
314
+ return { success: true, logged: 0 };
315
+ }
316
+
317
+ logger.info(`Logging ${orphanTests.length} orphan tests...`);
318
+
319
+ const response = await this.orphans.log(runId, orphanTests);
320
+
321
+ logger.info(`Orphan tests logged successfully`, {
322
+ runId,
323
+ count: orphanTests.length
324
+ });
325
+
326
+ return response;
327
+ } catch (error) {
328
+ logger.error('Failed to log orphan tests', {
329
+ error: error.message,
330
+ runId,
331
+ count: orphanTests.length
332
+ });
333
+ // Don't throw - orphan logging should not break the test run
334
+ return { success: false, error: error.message };
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Add browser to existing run matrix dynamically
340
+ * @param {string} runId - Run ID
341
+ * @param {string} browser - Browser name
342
+ * @param {number} nid - Optional scenario/testset node ID (defaults to 0)
343
+ * @returns {Promise<Object>} Response from server
344
+ */
345
+ async addBrowser(runId, browser, nid = 0) {
346
+ try {
347
+ logger.info('Adding browser to run matrix', { runId, browser, nid });
348
+
349
+ const response = await this.runMatrix.addBrowser(runId, browser, nid);
350
+
351
+ logger.info('Browser added successfully', { runId, browser });
352
+
353
+ return response;
354
+ } catch (error) {
355
+ logger.error('Failed to add browser', {
356
+ error: error.message,
357
+ runId,
358
+ browser
359
+ });
360
+ throw error;
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Get run matrix data
366
+ * @param {string} runId - Run ID
367
+ * @returns {Promise<Object>} Run matrix data
368
+ */
369
+ async getRun(runId) {
370
+ try {
371
+ const response = await this.runMatrix.get(runId);
372
+ return response;
373
+ } catch (error) {
374
+ logger.error('Failed to get run matrix', {
375
+ error: error.message,
376
+ runId
377
+ });
378
+ throw error;
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Get current run context (if set)
384
+ * @returns {Object|null} Current run data or null
385
+ */
386
+ getCurrentRun() {
387
+ return this.currentRun;
388
+ }
389
+
390
+ /**
391
+ * Set current run context manually
392
+ * @param {Object} run - Run data with runId
393
+ */
394
+ setCurrentRun(run) {
395
+ this.currentRun = run;
396
+ }
397
+
398
+ /**
399
+ * Validate UUID format
400
+ * @param {string} uuid - UUID to validate
401
+ * @returns {boolean} True if valid
402
+ */
403
+ validateUuid(uuid) {
404
+ return UuidValidator.validate(uuid);
405
+ }
406
+
407
+ /**
408
+ * Extract NID from UUID
409
+ * @param {string} uuid - UUID string
410
+ * @returns {number|null} NID or null if invalid
411
+ */
412
+ extractNid(uuid) {
413
+ return UuidValidator.extractNid(uuid);
414
+ }
415
+
416
+ /**
417
+ * Validate and extract UUID components
418
+ * @param {string} uuid - UUID string
419
+ * @returns {Object} { valid, nid, uuid, error }
420
+ */
421
+ parseUuid(uuid) {
422
+ return UuidValidator.validateAndExtract(uuid);
423
+ }
424
+
425
+ /**
426
+ * Validate client configuration
427
+ * @private
428
+ */
429
+ validateConfig(config) {
430
+ if (!config) {
431
+ throw new Error(
432
+ 'Configuration is required.\n' +
433
+ 'Please provide configuration object:\n' +
434
+ ' const client = new AppliqationClient({\n' +
435
+ ' baseUrl: process.env.APPLIQATION_BASE_URL,\n' +
436
+ ' apiKey: process.env.APPLIQATION_API_KEY,\n' +
437
+ ' projectKey: process.env.APPLIQATION_PROJECT_KEY\n' +
438
+ ' });'
439
+ );
440
+ }
441
+
442
+ if (!config.baseUrl) {
443
+ throw new Error(
444
+ 'baseUrl is required.\n' +
445
+ 'Please set the APPLIQATION_BASE_URL environment variable or provide baseUrl in configuration:\n' +
446
+ ' APPLIQATION_BASE_URL=https://your-instance.appliqation.com\n' +
447
+ 'Or in code:\n' +
448
+ ' { baseUrl: "https://your-instance.appliqation.com" }'
449
+ );
450
+ }
451
+
452
+ if (!config.projectKey) {
453
+ throw new Error(
454
+ 'projectKey is required.\n' +
455
+ 'Please set the APPLIQATION_PROJECT_KEY environment variable or provide projectKey in configuration:\n' +
456
+ ' APPLIQATION_PROJECT_KEY=your-project-key\n' +
457
+ 'Or in code:\n' +
458
+ ' { projectKey: "your-project-key" }\n' +
459
+ '\n' +
460
+ 'You can find your project key in the Appliqation dashboard under Project Settings.'
461
+ );
462
+ }
463
+
464
+ // Note: Both base64-encoded project keys AND numeric project IDs are now supported
465
+ // Numeric IDs are a temporary workaround for duplicate project key issues
466
+ // The backend will validate the project key format and access permissions
467
+
468
+ // Must have either API key or username/password
469
+ if (!config.apiKey && (!config.username || !config.password)) {
470
+ throw new Error(
471
+ 'Authentication credentials are required.\n' +
472
+ 'Please set the APPLIQATION_API_KEY environment variable (recommended):\n' +
473
+ ' APPLIQATION_API_KEY=appq_live_xxxxxxxxxxxx\n' +
474
+ 'Or in code:\n' +
475
+ ' { apiKey: "appq_live_xxxxxxxxxxxx" }\n' +
476
+ '\n' +
477
+ 'You can generate an API key in the Appliqation dashboard under Settings > API Keys.\n' +
478
+ '\n' +
479
+ 'Legacy CSRF authentication (username/password) is also supported but not recommended.'
480
+ );
481
+ }
482
+
483
+ // Validate URL format
484
+ try {
485
+ new URL(config.baseUrl);
486
+ } catch (error) {
487
+ throw new Error(
488
+ `Invalid baseUrl format: "${config.baseUrl}"\n` +
489
+ 'baseUrl must be a valid URL starting with http:// or https://\n' +
490
+ 'Example: https://your-instance.appliqation.com'
491
+ );
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Test connectivity to Appliqation instance
497
+ * @returns {Promise<boolean>} True if connection successful
498
+ */
499
+ async testConnection() {
500
+ try {
501
+ logger.info('Testing connection to Appliqation...');
502
+
503
+ // Try to authenticate
504
+ await this.auth.authenticate();
505
+
506
+ logger.info('Connection test successful');
507
+ return true;
508
+ } catch (error) {
509
+ logger.error('Connection test failed', {
510
+ error: error.message
511
+ });
512
+ throw new Error(`Connection test failed: ${error.message}`);
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Determine if the current environment is production
518
+ * @private
519
+ * @returns {boolean} True if production environment
520
+ */
521
+ _isProductionEnvironment() {
522
+ // Check for CI environments
523
+ if (process.env.CI === 'true' || process.env.CI === '1') {
524
+ return true;
525
+ }
526
+
527
+ // Check NODE_ENV
528
+ const nodeEnv = (process.env.NODE_ENV || '').toLowerCase();
529
+ if (nodeEnv === 'production' || nodeEnv === 'prod' || nodeEnv === 'staging') {
530
+ return true;
531
+ }
532
+
533
+ // Check for common CI environment variables
534
+ const ciEnvVars = [
535
+ 'GITHUB_ACTIONS',
536
+ 'GITLAB_CI',
537
+ 'CIRCLECI',
538
+ 'TRAVIS',
539
+ 'JENKINS_URL',
540
+ 'TEAMCITY_VERSION',
541
+ 'BUILDKITE'
542
+ ];
543
+
544
+ for (const envVar of ciEnvVars) {
545
+ if (process.env[envVar]) {
546
+ return true;
547
+ }
548
+ }
549
+
550
+ return false;
551
+ }
552
+
553
+ /**
554
+ * Get SDK version
555
+ * @returns {string} Version string
556
+ */
557
+ getVersion() {
558
+ return require('../package.json').version;
559
+ }
560
+ }
561
+
562
+ module.exports = AppliqationClient;