@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,243 @@
1
+ /**
2
+ * Global Setup for JWT Browser Authentication
3
+ *
4
+ * This module automatically sets up browser authentication using JWT tokens
5
+ * instead of traditional username/password login (setup-auth.js).
6
+ *
7
+ * How it works:
8
+ * 1. Uses API Key to call /api/automation/run/create
9
+ * 2. Receives JWT token from backend
10
+ * 3. Navigates browser to app with ?qonsole_token={jwt}
11
+ * 4. Backend validates JWT and establishes session
12
+ * 5. Saves authenticated state to .auth/jwt.json
13
+ * 6. All tests use this authenticated state
14
+ *
15
+ * Benefits:
16
+ * - No username/password in .env
17
+ * - No manual setup step required
18
+ * - 1-hour token validity
19
+ * - Auto-refresh support
20
+ * - Works in CI/CD environments
21
+ *
22
+ * @module playwright/global-setup
23
+ */
24
+
25
+ const { chromium } = require('@playwright/test');
26
+ const axios = require('axios');
27
+ const https = require('https');
28
+ const fs = require('fs');
29
+ const path = require('path');
30
+
31
+ /**
32
+ * Setup JWT-based browser authentication
33
+ *
34
+ * @param {Object} config - Playwright config object
35
+ * @returns {Promise<void>}
36
+ */
37
+ async function globalSetup(config) {
38
+ console.log('\nšŸ” Setting up JWT browser authentication...\n');
39
+
40
+ // Extract SDK configuration from reporter config or use env
41
+ const sdkConfig = extractSDKConfig(config);
42
+
43
+ // Validate required configuration
44
+ if (!sdkConfig.apiKey) {
45
+ console.error('āŒ APPLIQATION_API_KEY is required for JWT authentication');
46
+ console.error(' Please add to your .env file:\n');
47
+ console.error(' APPLIQATION_API_KEY=appq_live_xxxxx\n');
48
+ throw new Error('Missing APPLIQATION_API_KEY');
49
+ }
50
+
51
+ if (!sdkConfig.baseUrl) {
52
+ console.error('āŒ APPLIQATION_BASE_URL is required');
53
+ throw new Error('Missing APPLIQATION_BASE_URL');
54
+ }
55
+
56
+ // Determine if JWT auth is enabled
57
+ const useJwtAuth = sdkConfig.useJwtAuth !== false; // Default: true
58
+
59
+ if (!useJwtAuth) {
60
+ console.log('ā„¹ļø JWT authentication disabled. Using legacy session auth.\n');
61
+ return;
62
+ }
63
+
64
+ try {
65
+ // Step 1: Get JWT token from API
66
+ console.log('šŸ“” Requesting JWT token from Appliqation backend...');
67
+ const jwtData = await getJWTToken(sdkConfig);
68
+
69
+ if (!jwtData.jwt_token) {
70
+ console.warn('āš ļø No JWT token received. Falling back to legacy auth.');
71
+ return;
72
+ }
73
+
74
+ console.log(`āœ… JWT token received (expires in 1 hour)`);
75
+ console.log(` Run ID: ${jwtData.run_id}\n`);
76
+
77
+ // Step 2: Setup browser with JWT
78
+ console.log('🌐 Setting up browser session with JWT...');
79
+ await setupBrowserWithJWT(sdkConfig, jwtData.jwt_token);
80
+
81
+ console.log('āœ… Browser authentication setup complete!');
82
+ console.log('šŸ“„ Authenticated state saved to: .auth/jwt.json\n');
83
+ console.log('✨ Tests will run with JWT authentication!\n');
84
+
85
+ // Store for use in tests
86
+ process.env.APPLIQATION_JWT_TOKEN = jwtData.jwt_token;
87
+ process.env.APPLIQATION_RUN_ID = jwtData.run_id;
88
+
89
+ } catch (error) {
90
+ console.error('\nāŒ JWT authentication setup failed:', error.message);
91
+ console.error('\nšŸ’” Troubleshooting:');
92
+ console.error(' 1. Check APPLIQATION_API_KEY is valid');
93
+ console.error(' 2. Check APPLIQATION_BASE_URL is accessible');
94
+ console.error(' 3. Check backend has QONSOLE_JWT_SECRET configured');
95
+ console.error(' 4. Try running: npm run setup-auth (legacy method)\n');
96
+
97
+ // Don't fail the test run - fall back to legacy auth
98
+ console.warn('āš ļø Falling back to legacy authentication method\n');
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Extract SDK configuration from Playwright config
104
+ *
105
+ * @param {Object} config - Playwright config
106
+ * @returns {Object} SDK configuration
107
+ */
108
+ function extractSDKConfig(config) {
109
+ // Try to find SDK config in reporter configuration
110
+ let sdkConfig = {};
111
+
112
+ if (config.reporter) {
113
+ const reporters = Array.isArray(config.reporter) ? config.reporter : [config.reporter];
114
+
115
+ for (const reporter of reporters) {
116
+ if (Array.isArray(reporter)) {
117
+ const [reporterPath, reporterConfig] = reporter;
118
+ if (reporterPath && reporterPath.includes('appliqation')) {
119
+ sdkConfig = reporterConfig || {};
120
+ break;
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ // Fallback to environment variables
127
+ return {
128
+ baseUrl: sdkConfig.baseUrl || process.env.APPLIQATION_BASE_URL,
129
+ apiKey: sdkConfig.apiKey || process.env.APPLIQATION_API_KEY,
130
+ projectKey: sdkConfig.projectKey || process.env.APPLIQATION_PROJECT_KEY,
131
+ scenarioId: sdkConfig.scenarioId || parseInt(process.env.APPLIQATION_SCENARIO_ID) || 0,
132
+ environment: sdkConfig.environment || process.env.APPLIQATION_ENVIRONMENT || 'Local',
133
+ appUrl: sdkConfig.appUrl || process.env.APPLIQATION_APP_URL || sdkConfig.baseUrl || process.env.APPLIQATION_BASE_URL,
134
+ useJwtAuth: sdkConfig.useJwtAuth !== undefined ? sdkConfig.useJwtAuth : process.env.APPLIQATION_USE_JWT_AUTH !== 'false',
135
+ runTitle: sdkConfig.runTitle || process.env.APPLIQATION_RUN_TITLE || `JWT Auth Setup - ${new Date().toISOString()}`,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Get JWT token from Appliqation API
141
+ *
142
+ * @param {Object} sdkConfig - SDK configuration
143
+ * @returns {Promise<Object>} JWT data { jwt_token, run_id }
144
+ */
145
+ async function getJWTToken(sdkConfig) {
146
+ const endpoint = `${sdkConfig.baseUrl.replace(/\/$/, '')}/api/automation/run/create`;
147
+
148
+ const payload = {
149
+ project_key: sdkConfig.projectKey,
150
+ scenario_id: sdkConfig.scenarioId || 0,
151
+ environment: sdkConfig.environment,
152
+ title: sdkConfig.runTitle,
153
+ browsers: ['Chrome'], // Default browser for setup
154
+ device: 'Desktop',
155
+ os: process.platform === 'darwin' ? 'macOS' : process.platform === 'win32' ? 'Windows' : 'Linux',
156
+ };
157
+
158
+ try {
159
+ const response = await axios.post(endpoint, payload, {
160
+ headers: {
161
+ 'Content-Type': 'application/json',
162
+ 'X-API-Key': sdkConfig.apiKey,
163
+ },
164
+ httpsAgent: new https.Agent({
165
+ rejectUnauthorized: false, // Allow self-signed certs in dev
166
+ }),
167
+ validateStatus: (status) => status < 500,
168
+ });
169
+
170
+ if (response.status !== 200 && response.status !== 201) {
171
+ throw new Error(`API returned status ${response.status}: ${response.data?.message || 'Unknown error'}`);
172
+ }
173
+
174
+ return {
175
+ jwt_token: response.data.jwt_token,
176
+ run_id: response.data.run_id,
177
+ metadata: response.data.metadata,
178
+ };
179
+
180
+ } catch (error) {
181
+ if (error.response) {
182
+ throw new Error(`API error: ${error.response.status} - ${error.response.data?.message || error.message}`);
183
+ }
184
+ throw new Error(`Network error: ${error.message}`);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Setup browser session with JWT token
190
+ *
191
+ * @param {Object} sdkConfig - SDK configuration
192
+ * @param {string} jwtToken - JWT token
193
+ * @returns {Promise<void>}
194
+ */
195
+ async function setupBrowserWithJWT(sdkConfig, jwtToken) {
196
+ const browser = await chromium.launch({
197
+ headless: true,
198
+ });
199
+
200
+ const context = await browser.newContext({
201
+ ignoreHTTPSErrors: true, // Allow self-signed certs in dev
202
+ });
203
+
204
+ const page = await context.newPage();
205
+
206
+ try {
207
+ // Navigate to app with JWT token in query parameter
208
+ const appUrl = sdkConfig.appUrl.replace(/\/$/, '');
209
+ const separator = appUrl.includes('?') ? '&' : '?';
210
+ const urlWithToken = `${appUrl}${separator}qonsole_token=${encodeURIComponent(jwtToken)}`;
211
+
212
+ console.log(` Navigating to: ${appUrl}...`);
213
+ await page.goto(urlWithToken, {
214
+ waitUntil: 'networkidle',
215
+ timeout: 30000,
216
+ });
217
+
218
+ // Wait a bit for session to be established
219
+ await page.waitForTimeout(2000);
220
+
221
+ // Verify authentication worked
222
+ const currentUrl = page.url();
223
+ if (currentUrl.includes('/user/login')) {
224
+ throw new Error('JWT authentication failed - redirected to login page');
225
+ }
226
+
227
+ // Save authenticated state
228
+ const authDir = path.resolve('.auth');
229
+ if (!fs.existsSync(authDir)) {
230
+ fs.mkdirSync(authDir, { recursive: true });
231
+ }
232
+
233
+ await context.storageState({ path: '.auth/jwt.json' });
234
+ console.log(' āœ“ Authenticated state saved');
235
+
236
+ } catch (error) {
237
+ throw new Error(`Browser setup failed: ${error.message}`);
238
+ } finally {
239
+ await browser.close();
240
+ }
241
+ }
242
+
243
+ module.exports = globalSetup;
@@ -0,0 +1,227 @@
1
+ /**
2
+ * JWT Browser Authentication Helper
3
+ *
4
+ * Provides utilities for managing JWT-based browser authentication in Playwright tests.
5
+ * Handles token injection, validation, and refresh for both Appliqation and external apps.
6
+ *
7
+ * @module playwright/helpers/jwt-browser-auth
8
+ */
9
+
10
+ const axios = require('axios');
11
+ const https = require('https');
12
+
13
+ class JWTBrowserAuth {
14
+ /**
15
+ * Create a JWT Browser Auth helper
16
+ *
17
+ * @param {Object} config - Configuration object
18
+ * @param {string} config.baseUrl - Appliqation backend URL
19
+ * @param {string} config.apiKey - API key for authentication
20
+ * @param {string} config.appUrl - Application URL to test (optional, defaults to baseUrl)
21
+ */
22
+ constructor(config) {
23
+ this.baseUrl = config.baseUrl;
24
+ this.apiKey = config.apiKey;
25
+ this.appUrl = config.appUrl || config.baseUrl;
26
+ this.jwtToken = null;
27
+ this.jwtExpiry = null;
28
+ this.runId = null;
29
+ }
30
+
31
+ /**
32
+ * Get current JWT token, refreshing if expired
33
+ *
34
+ * @returns {Promise<string>} JWT token
35
+ */
36
+ async getToken() {
37
+ if (this.jwtToken && !this.isTokenExpired()) {
38
+ return this.jwtToken;
39
+ }
40
+
41
+ // Token expired or doesn't exist, get new one
42
+ await this.refreshToken();
43
+ return this.jwtToken;
44
+ }
45
+
46
+ /**
47
+ * Check if current token is expired
48
+ *
49
+ * @returns {boolean} True if expired
50
+ */
51
+ isTokenExpired() {
52
+ if (!this.jwtExpiry) {
53
+ return true;
54
+ }
55
+
56
+ // Consider expired if less than 5 minutes remaining
57
+ const now = Math.floor(Date.now() / 1000);
58
+ return (this.jwtExpiry - now) < 300;
59
+ }
60
+
61
+ /**
62
+ * Refresh JWT token from API
63
+ *
64
+ * @returns {Promise<void>}
65
+ */
66
+ async refreshToken() {
67
+ const endpoint = `${this.baseUrl.replace(/\/$/, '')}/api/automation/run/create`;
68
+
69
+ try {
70
+ const response = await axios.post(
71
+ endpoint,
72
+ {
73
+ project_key: process.env.APPLIQATION_PROJECT_KEY,
74
+ scenario_id: parseInt(process.env.APPLIQATION_SCENARIO_ID) || 0,
75
+ environment: process.env.APPLIQATION_ENVIRONMENT || 'Local',
76
+ title: `JWT Refresh - ${new Date().toISOString()}`,
77
+ browsers: ['Chrome'],
78
+ device: 'Desktop',
79
+ os: process.platform === 'darwin' ? 'macOS' : process.platform === 'win32' ? 'Windows' : 'Linux',
80
+ },
81
+ {
82
+ headers: {
83
+ 'Content-Type': 'application/json',
84
+ 'X-API-Key': this.apiKey,
85
+ },
86
+ httpsAgent: new https.Agent({
87
+ rejectUnauthorized: false,
88
+ }),
89
+ }
90
+ );
91
+
92
+ this.jwtToken = response.data.jwt_token;
93
+ this.runId = response.data.run_id;
94
+ this.jwtExpiry = this.parseJwtExpiry(this.jwtToken);
95
+
96
+ } catch (error) {
97
+ throw new Error(`Failed to refresh JWT token: ${error.message}`);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Parse JWT token to extract expiry time
103
+ *
104
+ * @param {string} token - JWT token
105
+ * @returns {number|null} Unix timestamp of expiry
106
+ */
107
+ parseJwtExpiry(token) {
108
+ try {
109
+ const parts = token.split('.');
110
+ if (parts.length !== 3) {
111
+ return null;
112
+ }
113
+
114
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
115
+ return payload.exp || null;
116
+ } catch (error) {
117
+ console.warn('Failed to parse JWT expiry:', error.message);
118
+ return null;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Inject JWT into browser context via URL navigation
124
+ *
125
+ * @param {import('@playwright/test').Page} page - Playwright page
126
+ * @param {string} targetUrl - URL to navigate to (optional)
127
+ * @returns {Promise<void>}
128
+ */
129
+ async injectJwtIntoContext(page, targetUrl = null) {
130
+ const token = await this.getToken();
131
+ const url = targetUrl || this.appUrl;
132
+
133
+ // Add JWT as query parameter
134
+ const separator = url.includes('?') ? '&' : '?';
135
+ const urlWithToken = `${url}${separator}qonsole_token=${encodeURIComponent(token)}`;
136
+
137
+ await page.goto(urlWithToken, {
138
+ waitUntil: 'networkidle',
139
+ timeout: 30000,
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Validate that JWT session is active in browser
145
+ *
146
+ * @param {import('@playwright/test').Page} page - Playwright page
147
+ * @returns {Promise<boolean>} True if session is active
148
+ */
149
+ async validateJwtSession(page) {
150
+ try {
151
+ // Check if we're on login page (authentication failed)
152
+ const currentUrl = page.url();
153
+ if (currentUrl.includes('/user/login')) {
154
+ return false;
155
+ }
156
+
157
+ // Check for common authenticated elements
158
+ const isAuthenticated = await page.evaluate(() => {
159
+ // Check for user menu or authenticated indicators
160
+ const userMenu = document.querySelector('.user-menu, [data-user-menu], #user-menu');
161
+ const logoutLink = document.querySelector('a[href*="logout"]');
162
+ const loginForm = document.querySelector('form[action*="login"]');
163
+
164
+ return (userMenu !== null || logoutLink !== null) && loginForm === null;
165
+ });
166
+
167
+ return isAuthenticated;
168
+
169
+ } catch (error) {
170
+ console.warn('Failed to validate JWT session:', error.message);
171
+ return false;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Get authenticated URL with JWT token
177
+ *
178
+ * @param {string} path - Path to append to base URL (optional)
179
+ * @returns {Promise<string>} URL with JWT token
180
+ */
181
+ async getAuthenticatedUrl(path = '') {
182
+ const token = await this.getToken();
183
+ const baseUrl = this.appUrl.replace(/\/$/, '');
184
+ const fullPath = path.startsWith('/') ? path : `/${path}`;
185
+ const url = `${baseUrl}${fullPath}`;
186
+
187
+ const separator = url.includes('?') ? '&' : '?';
188
+ return `${url}${separator}qonsole_token=${encodeURIComponent(token)}`;
189
+ }
190
+
191
+ /**
192
+ * Refresh token if needed before long-running operations
193
+ *
194
+ * @returns {Promise<void>}
195
+ */
196
+ async refreshJwtIfNeeded() {
197
+ if (this.isTokenExpired()) {
198
+ console.log('šŸ”„ JWT token expired, refreshing...');
199
+ await this.refreshToken();
200
+ console.log('āœ… JWT token refreshed');
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Get current run ID
206
+ *
207
+ * @returns {string|null} Run ID
208
+ */
209
+ getRunId() {
210
+ return this.runId;
211
+ }
212
+
213
+ /**
214
+ * Create JWT auth helper from environment variables
215
+ *
216
+ * @returns {JWTBrowserAuth} Auth helper instance
217
+ */
218
+ static fromEnv() {
219
+ return new JWTBrowserAuth({
220
+ baseUrl: process.env.APPLIQATION_BASE_URL,
221
+ apiKey: process.env.APPLIQATION_API_KEY,
222
+ appUrl: process.env.APPLIQATION_APP_URL || process.env.APPLIQATION_BASE_URL,
223
+ });
224
+ }
225
+ }
226
+
227
+ module.exports = { JWTBrowserAuth };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Appliqation Playwright Integration
3
+ *
4
+ * Provides Playwright-specific utilities including:
5
+ * - JWT browser authentication with auto-refresh
6
+ * - Playwright test fixture with automatic token management
7
+ */
8
+
9
+ const JwtBrowserAuth = require('./JwtBrowserAuth');
10
+ const { test, expect } = require('./fixture');
11
+
12
+ module.exports = {
13
+ JwtBrowserAuth,
14
+ test,
15
+ expect
16
+ };