@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,217 @@
1
+ /**
2
+ * Centralized Error Handling
3
+ * Standardized error classes and error handling utilities
4
+ */
5
+
6
+ /**
7
+ * Base SDK Error class
8
+ */
9
+ class AppliqationError extends Error {
10
+ constructor(message, code, details = {}) {
11
+ super(message);
12
+ this.name = this.constructor.name;
13
+ this.code = code;
14
+ this.details = details;
15
+ Error.captureStackTrace(this, this.constructor);
16
+ }
17
+
18
+ toJSON() {
19
+ return {
20
+ name: this.name,
21
+ message: this.message,
22
+ code: this.code,
23
+ details: this.details
24
+ };
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Configuration Error
30
+ * Thrown when SDK configuration is invalid
31
+ */
32
+ class ConfigurationError extends AppliqationError {
33
+ constructor(message, details = {}) {
34
+ super(message, 'CONFIGURATION_ERROR', details);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Authentication Error
40
+ * Thrown when authentication fails
41
+ */
42
+ class AuthenticationError extends AppliqationError {
43
+ constructor(message, details = {}) {
44
+ super(message, 'AUTHENTICATION_ERROR', details);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Network Error
50
+ * Thrown when network requests fail
51
+ */
52
+ class NetworkError extends AppliqationError {
53
+ constructor(message, details = {}) {
54
+ super(message, 'NETWORK_ERROR', details);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Validation Error
60
+ * Thrown when data validation fails
61
+ */
62
+ class ValidationError extends AppliqationError {
63
+ constructor(message, details = {}) {
64
+ super(message, 'VALIDATION_ERROR', details);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * API Error
70
+ * Thrown when API returns an error response
71
+ */
72
+ class ApiError extends AppliqationError {
73
+ constructor(message, statusCode, details = {}) {
74
+ super(message, 'API_ERROR', { ...details, statusCode });
75
+ this.statusCode = statusCode;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Timeout Error
81
+ * Thrown when requests timeout
82
+ */
83
+ class TimeoutError extends AppliqationError {
84
+ constructor(message, timeout, details = {}) {
85
+ super(message, 'TIMEOUT_ERROR', { ...details, timeout });
86
+ this.timeout = timeout;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Error Handler Utility
92
+ */
93
+ class ErrorHandler {
94
+ /**
95
+ * Handle HTTP errors and convert to SDK errors
96
+ * @param {Error} error - Original error
97
+ * @param {string} context - Context where error occurred
98
+ * @returns {AppliqationError} Standardized error
99
+ */
100
+ static handleHttpError(error, context = 'HTTP request') {
101
+ // Timeout errors
102
+ if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
103
+ return new TimeoutError(
104
+ `${context} timed out`,
105
+ error.config?.timeout,
106
+ { originalError: error.message }
107
+ );
108
+ }
109
+
110
+ // Network errors (no response)
111
+ if (!error.response) {
112
+ return new NetworkError(
113
+ `${context} failed: ${error.message}`,
114
+ {
115
+ code: error.code,
116
+ syscall: error.syscall,
117
+ originalError: error.message
118
+ }
119
+ );
120
+ }
121
+
122
+ // API errors (with response)
123
+ const { status, statusText, data } = error.response;
124
+
125
+ // Authentication errors
126
+ if (status === 401 || status === 403) {
127
+ return new AuthenticationError(
128
+ `Authentication failed: ${statusText}`,
129
+ {
130
+ status,
131
+ statusText,
132
+ response: data
133
+ }
134
+ );
135
+ }
136
+
137
+ // Validation errors
138
+ if (status === 400 || status === 422) {
139
+ return new ValidationError(
140
+ `Validation failed: ${data?.message || statusText}`,
141
+ {
142
+ status,
143
+ statusText,
144
+ response: data
145
+ }
146
+ );
147
+ }
148
+
149
+ // Generic API errors
150
+ return new ApiError(
151
+ `${context} failed: ${statusText}`,
152
+ status,
153
+ {
154
+ statusText,
155
+ response: data
156
+ }
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Check if error is retryable
162
+ * @param {Error} error - Error to check
163
+ * @returns {boolean} True if error is retryable
164
+ */
165
+ static isRetryable(error) {
166
+ // Network errors are retryable
167
+ if (error instanceof NetworkError) {
168
+ return true;
169
+ }
170
+
171
+ // Timeout errors are retryable
172
+ if (error instanceof TimeoutError) {
173
+ return true;
174
+ }
175
+
176
+ // Certain API errors are retryable (5xx, 429, 408)
177
+ if (error instanceof ApiError) {
178
+ const { statusCode } = error;
179
+ return statusCode >= 500 || statusCode === 429 || statusCode === 408;
180
+ }
181
+
182
+ return false;
183
+ }
184
+
185
+ /**
186
+ * Format error for logging
187
+ * @param {Error} error - Error to format
188
+ * @returns {Object} Formatted error object
189
+ */
190
+ static formatForLogging(error) {
191
+ if (error instanceof AppliqationError) {
192
+ return {
193
+ name: error.name,
194
+ code: error.code,
195
+ message: error.message,
196
+ details: error.details
197
+ };
198
+ }
199
+
200
+ return {
201
+ name: error.name,
202
+ message: error.message,
203
+ stack: error.stack
204
+ };
205
+ }
206
+ }
207
+
208
+ module.exports = {
209
+ AppliqationError,
210
+ ConfigurationError,
211
+ AuthenticationError,
212
+ NetworkError,
213
+ ValidationError,
214
+ ApiError,
215
+ TimeoutError,
216
+ ErrorHandler
217
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Utilities export module
3
+ */
4
+
5
+ const UuidValidator = require('./UuidValidator');
6
+ const PayloadBuilder = require('./PayloadBuilder');
7
+ const RunDataNormalizer = require('./RunDataNormalizer');
8
+ const { mapAppqUuid } = require('./mapAppqUuid');
9
+ const logger = require('./logger');
10
+
11
+ module.exports = {
12
+ UuidValidator,
13
+ PayloadBuilder,
14
+ RunDataNormalizer,
15
+ mapAppqUuid,
16
+ logger
17
+ };
@@ -0,0 +1,124 @@
1
+ const LOG_LEVELS = {
2
+ ERROR: 0,
3
+ WARN: 1,
4
+ INFO: 2,
5
+ DEBUG: 3
6
+ };
7
+
8
+ // Sensitive data patterns to sanitize
9
+ const SENSITIVE_PATTERNS = [
10
+ // API Keys
11
+ { pattern: /(appq_\w+_[a-f0-9]{32})/gi, replacement: 'appq_***_REDACTED' },
12
+ { pattern: /("apiKey"\s*:\s*")([^"]+)(")/gi, replacement: '$1***REDACTED***$3' },
13
+ { pattern: /("X-API-Key"\s*:\s*")([^"]+)(")/gi, replacement: '$1***REDACTED***$3' },
14
+
15
+ // JWT Tokens
16
+ { pattern: /(eyJ[A-Za-z0-9-_]+\.eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_.+/=]+)/g, replacement: 'eyJ***REDACTED***' },
17
+ { pattern: /("token"\s*:\s*")([^"]+)(")/gi, replacement: '$1***REDACTED***$3' },
18
+ { pattern: /("jwt"\s*:\s*")([^"]+)(")/gi, replacement: '$1***REDACTED***$3' },
19
+
20
+ // Authorization Headers
21
+ { pattern: /("Authorization"\s*:\s*")(Bearer\s+)?([^"]+)(")/gi, replacement: '$1$2***REDACTED***$4' },
22
+ { pattern: /("authorization"\s*:\s*")(Bearer\s+)?([^"]+)(")/gi, replacement: '$1$2***REDACTED***$4' },
23
+
24
+ // Passwords
25
+ { pattern: /("password"\s*:\s*")([^"]+)(")/gi, replacement: '$1***REDACTED***$3' },
26
+ { pattern: /("pass"\s*:\s*")([^"]+)(")/gi, replacement: '$1***REDACTED***$3' },
27
+
28
+ // Session IDs
29
+ { pattern: /("sessionId"\s*:\s*")([^"]+)(")/gi, replacement: '$1***REDACTED***$3' },
30
+ { pattern: /("session"\s*:\s*")([^"]+)(")/gi, replacement: '$1***REDACTED***$3' }
31
+ ];
32
+
33
+ class Logger {
34
+ constructor(level = 'INFO') {
35
+ this.level = LOG_LEVELS[level.toUpperCase()] || LOG_LEVELS.INFO;
36
+ this.enableConsole = process.env.NODE_ENV !== 'test';
37
+ }
38
+
39
+ /**
40
+ * Sanitize sensitive data from logs
41
+ * @param {any} data - Data to sanitize (string, object, or array)
42
+ * @returns {any} Sanitized data
43
+ */
44
+ sanitize(data) {
45
+ if (data === null || data === undefined) {
46
+ return data;
47
+ }
48
+
49
+ // Handle strings
50
+ if (typeof data === 'string') {
51
+ let sanitized = data;
52
+ SENSITIVE_PATTERNS.forEach(({ pattern, replacement }) => {
53
+ sanitized = sanitized.replace(pattern, replacement);
54
+ });
55
+ return sanitized;
56
+ }
57
+
58
+ // Handle objects and arrays by converting to JSON and back
59
+ if (typeof data === 'object') {
60
+ try {
61
+ let jsonString = JSON.stringify(data);
62
+ SENSITIVE_PATTERNS.forEach(({ pattern, replacement }) => {
63
+ jsonString = jsonString.replace(pattern, replacement);
64
+ });
65
+ return JSON.parse(jsonString);
66
+ } catch (error) {
67
+ // If JSON parsing fails, return original data
68
+ return data;
69
+ }
70
+ }
71
+
72
+ return data;
73
+ }
74
+
75
+ formatMessage(level, message, meta = {}) {
76
+ const timestamp = new Date().toISOString();
77
+ // Sanitize both message and meta
78
+ const sanitizedMessage = this.sanitize(message);
79
+ const sanitizedMeta = this.sanitize(meta);
80
+ const metaString = Object.keys(sanitizedMeta).length > 0 ? JSON.stringify(sanitizedMeta) : '';
81
+ return `[${timestamp}] [${level}] ${sanitizedMessage} ${metaString}`.trim();
82
+ }
83
+
84
+ error(message, meta = {}) {
85
+ if (this.level >= LOG_LEVELS.ERROR) {
86
+ const formatted = this.formatMessage('ERROR', message, meta);
87
+ if (this.enableConsole) console.error(formatted);
88
+ }
89
+ }
90
+
91
+ warn(message, meta = {}) {
92
+ if (this.level >= LOG_LEVELS.WARN) {
93
+ const formatted = this.formatMessage('WARN', message, meta);
94
+ if (this.enableConsole) console.warn(formatted);
95
+ }
96
+ }
97
+
98
+ info(message, meta = {}) {
99
+ if (this.level >= LOG_LEVELS.INFO) {
100
+ const formatted = this.formatMessage('INFO', message, meta);
101
+ if (this.enableConsole) console.log(formatted);
102
+ }
103
+ }
104
+
105
+ debug(message, meta = {}) {
106
+ if (this.level >= LOG_LEVELS.DEBUG) {
107
+ const formatted = this.formatMessage('DEBUG', message, meta);
108
+ if (this.enableConsole) console.debug(formatted);
109
+ }
110
+ }
111
+
112
+ setLevel(level) {
113
+ this.level = LOG_LEVELS[level.toUpperCase()] || LOG_LEVELS.INFO;
114
+ }
115
+
116
+ setConsoleEnabled(enabled) {
117
+ this.enableConsole = enabled;
118
+ }
119
+ }
120
+
121
+ // Export singleton instance
122
+ const logger = new Logger(process.env.LOG_LEVEL || 'INFO');
123
+
124
+ module.exports = logger;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * UUID Mapping Utility for Appliqation SDK
3
+ *
4
+ * This utility provides a simple, framework-agnostic way to map test UUIDs
5
+ * to framework-specific test metadata (currently supports Playwright).
6
+ */
7
+
8
+ /**
9
+ * Adds Appliqation UUID annotation to a Playwright test.
10
+ *
11
+ * This helper function keeps UUID mapping centralized and makes tests more
12
+ * framework-agnostic by abstracting the annotation mechanism.
13
+ *
14
+ * @param {import('@playwright/test').TestInfo} testInfo - Playwright test info object
15
+ * @param {string} uuid - Appliqation test UUID in format: nid-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
16
+ *
17
+ * @throws {Error} If testInfo is not provided or invalid
18
+ *
19
+ * @example
20
+ * // In your Playwright test file
21
+ * const { test } = require('@playwright/test');
22
+ * const { mapAppqUuid } = require('@appliqation/automation-sdk/utils');
23
+ *
24
+ * test('login functionality', async ({ page }, testInfo) => {
25
+ * // Map this test to Appliqation test case
26
+ * mapAppqUuid(testInfo, '1141-6d2827f3-1bc2-405c-95b4-6a09e8b64148');
27
+ *
28
+ * await page.goto('/login');
29
+ * // ... test implementation
30
+ * });
31
+ *
32
+ * @example
33
+ * // UUID format examples
34
+ * mapAppqUuid(testInfo, '1141-6d2827f3-1bc2-405c-95b4-6a09e8b64148'); // Standard format
35
+ * mapAppqUuid(testInfo, '1234-abc12345-6789-def0-1234-567890abcdef'); // Another valid UUID
36
+ */
37
+ function mapAppqUuid(testInfo, uuid) {
38
+ // Validate testInfo parameter
39
+ if (!testInfo) {
40
+ throw new Error(
41
+ '[Appliqation SDK] mapAppqUuid requires a valid testInfo object. ' +
42
+ 'Make sure to pass the testInfo parameter from your test function.'
43
+ );
44
+ }
45
+
46
+ // Validate testInfo has annotations array
47
+ if (!testInfo.annotations || !Array.isArray(testInfo.annotations)) {
48
+ throw new Error(
49
+ '[Appliqation SDK] testInfo.annotations is not available. ' +
50
+ 'Ensure you are using a compatible version of Playwright (@playwright/test >= 1.30.0).'
51
+ );
52
+ }
53
+
54
+ // Validate UUID parameter
55
+ if (!uuid || typeof uuid !== 'string') {
56
+ console.warn(
57
+ '[Appliqation SDK] mapAppqUuid called without a valid UUID. ' +
58
+ 'Test results may not be properly linked to Appliqation test cases.'
59
+ );
60
+ return;
61
+ }
62
+
63
+ // Trim whitespace from UUID
64
+ const trimmedUuid = uuid.trim();
65
+
66
+ // Basic UUID format validation (nid-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
67
+ const uuidPattern = /^\d+-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
68
+ if (!uuidPattern.test(trimmedUuid)) {
69
+ console.warn(
70
+ `[Appliqation SDK] UUID format appears invalid: "${trimmedUuid}". ` +
71
+ 'Expected format: nid-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (e.g., 1141-6d2827f3-1bc2-405c-95b4-6a09e8b64148)'
72
+ );
73
+ }
74
+
75
+ // Add UUID as Playwright annotation
76
+ // The SDK's UuidExtractor will read this annotation when reporting results
77
+ testInfo.annotations.push({
78
+ type: 'appliqation-uuid',
79
+ description: trimmedUuid
80
+ });
81
+ }
82
+
83
+ module.exports = { mapAppqUuid };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Validates the payload for test result insertion
3
+ * @param {Object} payload - The test result payload
4
+ * @returns {Object} - Validation result with isValid flag and errors array
5
+ */
6
+ function validatePayload(payload) {
7
+ const errors = [];
8
+
9
+ if (!payload) {
10
+ errors.push('Payload is required');
11
+ return { isValid: false, errors };
12
+ }
13
+
14
+ // Required fields validation
15
+ if (!payload.run_id) {
16
+ errors.push('run_id is required');
17
+ } else if (typeof payload.run_id !== 'string') {
18
+ errors.push('run_id must be a string');
19
+ }
20
+
21
+ if (!payload.uuid) {
22
+ errors.push('uuid is required');
23
+ } else if (typeof payload.uuid !== 'string') {
24
+ errors.push('uuid must be a string');
25
+ } else if (!validateUuidFormat(payload.uuid)) {
26
+ errors.push('uuid must be in format: nid-uuid (e.g., "124-d1f9559c-b978-43cc-9c76-fd539c717cb4")');
27
+ }
28
+
29
+ if (!payload.status) {
30
+ errors.push('status is required');
31
+ } else if (typeof payload.status !== 'string') {
32
+ errors.push('status must be a string');
33
+ } else if (!validateStatus(payload.status)) {
34
+ errors.push('status must be one of: Pass, Fail, Skip, Pending');
35
+ }
36
+
37
+ // Optional fields validation
38
+ if (payload.browser && typeof payload.browser !== 'string') {
39
+ errors.push('browser must be a string');
40
+ }
41
+
42
+ if (payload.comment && typeof payload.comment !== 'string') {
43
+ errors.push('comment must be a string');
44
+ }
45
+
46
+ // Additional validation for run_id format (UUID)
47
+ if (payload.run_id && !validateRunIdFormat(payload.run_id)) {
48
+ errors.push('run_id must be a valid UUID format');
49
+ }
50
+
51
+ return {
52
+ isValid: errors.length === 0,
53
+ errors
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Validates UUID format for run_id
59
+ * @param {string} runId - The run ID to validate
60
+ * @returns {boolean} - True if valid UUID format
61
+ */
62
+ function validateRunIdFormat(runId) {
63
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
64
+ return uuidRegex.test(runId);
65
+ }
66
+
67
+ /**
68
+ * Validates UUID format for test case UUID
69
+ * @param {string} uuid - The UUID to validate (format: nid-uuid)
70
+ * @returns {boolean} - True if valid format
71
+ */
72
+ function validateUuidFormat(uuid) {
73
+ // Format should be: nid-uuid (e.g., "124-d1f9559c-b978-43cc-9c76-fd539c717cb4")
74
+ const parts = uuid.split('-');
75
+ if (parts.length < 6) return false; // nid + 5 UUID parts
76
+
77
+ const nid = parts[0];
78
+ const uuidPart = parts.slice(1).join('-');
79
+
80
+ // Validate nid is numeric
81
+ if (!/^\d+$/.test(nid)) return false;
82
+
83
+ // Validate UUID part
84
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
85
+ return uuidRegex.test(uuidPart);
86
+ }
87
+
88
+ /**
89
+ * Validates test status
90
+ * @param {string} status - The test status
91
+ * @returns {boolean} - True if valid status
92
+ */
93
+ function validateStatus(status) {
94
+ const validStatuses = ['Pass', 'Fail', 'Skip', 'Pending', 'pass', 'fail', 'skip', 'pending'];
95
+ return validStatuses.includes(status);
96
+ }
97
+
98
+ /**
99
+ * Sanitizes and normalizes payload data
100
+ * @param {Object} payload - The payload to sanitize
101
+ * @returns {Object} - Sanitized payload
102
+ */
103
+ function sanitizePayload(payload) {
104
+ const sanitized = { ...payload };
105
+
106
+ // Normalize status to proper case
107
+ if (sanitized.status) {
108
+ sanitized.status = sanitized.status.toLowerCase();
109
+ sanitized.status = sanitized.status.charAt(0).toUpperCase() + sanitized.status.slice(1);
110
+ }
111
+
112
+ // Trim string fields
113
+ if (sanitized.browser) {
114
+ sanitized.browser = sanitized.browser.trim();
115
+ }
116
+
117
+ if (sanitized.comment) {
118
+ sanitized.comment = sanitized.comment.trim();
119
+ }
120
+
121
+ if (sanitized.run_id) {
122
+ sanitized.run_id = sanitized.run_id.trim();
123
+ }
124
+
125
+ if (sanitized.uuid) {
126
+ sanitized.uuid = sanitized.uuid.trim();
127
+ }
128
+
129
+ return sanitized;
130
+ }
131
+
132
+ /**
133
+ * Creates a payload from test framework data
134
+ * @param {Object} testData - Test data from framework
135
+ * @param {Object} config - Configuration options
136
+ * @returns {Object} - Formatted payload
137
+ */
138
+ function createPayload(testData, config) {
139
+ const payload = {
140
+ run_id: testData.run_id || config.run_id,
141
+ uuid: testData.uuid || config.uuid,
142
+ browser: testData.browser || config.browser || 'Unknown',
143
+ status: testData.status || 'Fail',
144
+ comment: testData.comment || testData.error || ''
145
+ };
146
+
147
+ return sanitizePayload(payload);
148
+ }
149
+
150
+ module.exports = {
151
+ validatePayload,
152
+ validateRunIdFormat,
153
+ validateUuidFormat,
154
+ validateStatus,
155
+ sanitizePayload,
156
+ createPayload
157
+ };