@appliqation/automation-sdk 2.7.0 → 2.8.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/README.md +299 -1273
- package/package.json +5 -34
- package/src/AppliqationClient.js +125 -25
- package/src/cli/auth-setup.js +71 -8
- package/src/constants.js +21 -1
- package/src/core/AuthManager.js +5 -0
- package/src/index.d.ts +102 -6
- package/src/login/index.d.ts +2 -1
- package/src/login/index.js +2 -1
- package/src/playwright/fixture.js +4 -1
- package/src/reporters/playwright/AppliqationReporter.js +159 -11
- package/src/services/OrphanTestService.js +21 -2
- package/src/services/ResultService.js +76 -45
- package/src/services/RunMatrixService.js +21 -1
- package/src/services/ScopeValidator.js +314 -0
- package/src/services/TaggingService.js +5 -5
- package/src/utils/PayloadBuilder.js +112 -21
- package/src/utils/logger.js +59 -5
- package/src/reporters/cypress/CypressReporter.js +0 -434
- package/src/reporters/cypress/UuidExtractor.js +0 -139
- package/src/reporters/cypress/index.js +0 -30
- package/src/reporters/jest/JestReporter.js +0 -408
- package/src/reporters/jest/UuidExtractor.js +0 -174
- package/src/reporters/jest/index.js +0 -28
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appliqation/automation-sdk",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Appliqation Automation SDK — API
|
|
3
|
+
"version": "2.8.0",
|
|
4
|
+
"description": "Appliqation Automation SDK for Playwright — API-key auth, scope-validated result reporting, portable storageState (setupAuth), and customer-defined login flows (defineLogin) for gated apps",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
7
7
|
"bin": {
|
|
8
|
-
"appq-auth-setup": "
|
|
8
|
+
"appq-auth-setup": "src/cli/auth-setup.js"
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
@@ -32,14 +32,6 @@
|
|
|
32
32
|
"require": "./src/playwright/fixture.js",
|
|
33
33
|
"import": "./src/playwright/fixture.js"
|
|
34
34
|
},
|
|
35
|
-
"./cypress": {
|
|
36
|
-
"require": "./src/reporters/cypress/index.js",
|
|
37
|
-
"import": "./src/reporters/cypress/index.js"
|
|
38
|
-
},
|
|
39
|
-
"./jest": {
|
|
40
|
-
"require": "./src/reporters/jest/index.js",
|
|
41
|
-
"import": "./src/reporters/jest/index.js"
|
|
42
|
-
},
|
|
43
35
|
"./utils": {
|
|
44
36
|
"require": "./src/utils/index.js",
|
|
45
37
|
"import": "./src/utils/index.js"
|
|
@@ -65,9 +57,7 @@
|
|
|
65
57
|
"lint": "eslint src/",
|
|
66
58
|
"lint:fix": "eslint src/ --fix",
|
|
67
59
|
"example:basic": "node examples/basic-usage.js",
|
|
68
|
-
"example:playwright": "cd examples/playwright
|
|
69
|
-
"example:cypress": "cd examples/cypress-basic && npm test",
|
|
70
|
-
"example:jest": "cd examples/jest-basic && npm test",
|
|
60
|
+
"example:playwright": "cd examples/playwright && npx playwright test",
|
|
71
61
|
"docs": "echo 'Documentation generation coming soon'"
|
|
72
62
|
},
|
|
73
63
|
"keywords": [
|
|
@@ -76,10 +66,6 @@
|
|
|
76
66
|
"automation",
|
|
77
67
|
"test-management",
|
|
78
68
|
"playwright",
|
|
79
|
-
"cypress",
|
|
80
|
-
"jest",
|
|
81
|
-
"selenium",
|
|
82
|
-
"webdriver",
|
|
83
69
|
"api-key",
|
|
84
70
|
"sdk",
|
|
85
71
|
"test-reporting",
|
|
@@ -91,31 +77,16 @@
|
|
|
91
77
|
"license": "MIT",
|
|
92
78
|
"dependencies": {
|
|
93
79
|
"axios": "^1.6.0",
|
|
94
|
-
"dotenv": "^16.3.1",
|
|
95
80
|
"jsonwebtoken": "^9.0.3"
|
|
96
81
|
},
|
|
97
82
|
"devDependencies": {
|
|
98
83
|
"@playwright/test": "^1.40.0",
|
|
99
|
-
"cypress": "^13.0.0",
|
|
100
84
|
"eslint": "^8.57.0",
|
|
101
85
|
"jest": "^29.7.0",
|
|
102
86
|
"playwright": "^1.40.0"
|
|
103
87
|
},
|
|
104
88
|
"peerDependencies": {
|
|
105
|
-
"@playwright/test": ">=1.30.0"
|
|
106
|
-
"cypress": ">=10.0.0",
|
|
107
|
-
"jest": ">=29.0.0"
|
|
108
|
-
},
|
|
109
|
-
"peerDependenciesMeta": {
|
|
110
|
-
"@playwright/test": {
|
|
111
|
-
"optional": true
|
|
112
|
-
},
|
|
113
|
-
"cypress": {
|
|
114
|
-
"optional": true
|
|
115
|
-
},
|
|
116
|
-
"jest": {
|
|
117
|
-
"optional": true
|
|
118
|
-
}
|
|
89
|
+
"@playwright/test": ">=1.30.0"
|
|
119
90
|
},
|
|
120
91
|
"engines": {
|
|
121
92
|
"node": ">=18.0.0"
|
package/src/AppliqationClient.js
CHANGED
|
@@ -8,7 +8,13 @@ const ProjectInfoService = require('./services/ProjectInfoService');
|
|
|
8
8
|
const UuidValidator = require('./utils/UuidValidator');
|
|
9
9
|
const PayloadBuilder = require('./utils/PayloadBuilder');
|
|
10
10
|
const logger = require('./utils/logger');
|
|
11
|
-
const {
|
|
11
|
+
const { ConfigurationError } = require('./utils/errors');
|
|
12
|
+
const { DEFAULT_APPLIQATION_BASE_URL, AUTO_TAG_NAME } = require('./constants');
|
|
13
|
+
|
|
14
|
+
// H2 — environment names must match the backend's InputValidator regex
|
|
15
|
+
// (appq_mongo InputValidator). Mismatch causes a 400 mid-run; reject at
|
|
16
|
+
// startup instead so misconfiguration fails immediately and obviously.
|
|
17
|
+
const ENVIRONMENT_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Main Appliqation Automation SDK Client
|
|
@@ -49,22 +55,51 @@ class AppliqationClient {
|
|
|
49
55
|
constructor(config = {}) {
|
|
50
56
|
const resolved = AppliqationClient.resolveConfig(config);
|
|
51
57
|
|
|
52
|
-
//
|
|
58
|
+
// H2 — fail-fast schema validation. Hard errors throw at construction so
|
|
59
|
+
// misconfiguration surfaces immediately instead of after a full test run.
|
|
60
|
+
// Soft warnings (missing apiKey, etc.) still just log so no-op mode is
|
|
61
|
+
// preserved when the user intentionally runs without reporting.
|
|
53
62
|
const validation = this.validateConfig(resolved);
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
if (validation.errors.length > 0) {
|
|
64
|
+
throw new ConfigurationError(
|
|
65
|
+
`Invalid Appliqation SDK configuration:\n - ${validation.errors.join('\n - ')}`,
|
|
66
|
+
{ errors: validation.errors }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
for (const warning of validation.warnings) {
|
|
70
|
+
logger.warn(warning);
|
|
59
71
|
}
|
|
60
72
|
|
|
61
|
-
//
|
|
73
|
+
// H3 — TLS enforcement. Disabling cert validation is allowed in local
|
|
74
|
+
// dev (self-signed certs) but actively dangerous in CI/production: a
|
|
75
|
+
// MITM attacker can intercept result submissions and alter verdicts.
|
|
76
|
+
// The previous behaviour only warned; we now throw to make the misuse
|
|
77
|
+
// impossible to ship. Customers who genuinely need to bypass TLS in a
|
|
78
|
+
// CI environment must explicitly set APPLIQATION_INSECURE=allow.
|
|
62
79
|
const isProduction = this._isProductionEnvironment();
|
|
63
80
|
let rejectUnauthorized = true;
|
|
64
81
|
|
|
65
82
|
if (resolved.rejectUnauthorized !== undefined) {
|
|
66
83
|
if (isProduction && resolved.rejectUnauthorized === false) {
|
|
67
|
-
|
|
84
|
+
const overrideAllowed = process.env.APPLIQATION_INSECURE === 'allow';
|
|
85
|
+
if (!overrideAllowed) {
|
|
86
|
+
throw new ConfigurationError(
|
|
87
|
+
'rejectUnauthorized=false is not permitted in CI/production. '
|
|
88
|
+
+ 'Disabling TLS verification exposes result submissions to MITM. '
|
|
89
|
+
+ 'Use a properly trusted certificate, or set '
|
|
90
|
+
+ 'APPLIQATION_INSECURE=allow to acknowledge the risk and override.',
|
|
91
|
+
{
|
|
92
|
+
detectedAs: 'production',
|
|
93
|
+
ci: process.env.CI,
|
|
94
|
+
nodeEnv: process.env.NODE_ENV
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
logger.warn(
|
|
99
|
+
'TLS verification is DISABLED via APPLIQATION_INSECURE=allow in a '
|
|
100
|
+
+ 'production-like environment. Result submissions are vulnerable '
|
|
101
|
+
+ 'to interception.'
|
|
102
|
+
);
|
|
68
103
|
}
|
|
69
104
|
rejectUnauthorized = resolved.rejectUnauthorized;
|
|
70
105
|
}
|
|
@@ -84,10 +119,8 @@ class AppliqationClient {
|
|
|
84
119
|
logOrphans: resolved.options?.logOrphans !== false,
|
|
85
120
|
logLevel: resolved.options?.logLevel || 'info',
|
|
86
121
|
autoTag: resolved.options?.autoTag !== false,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
process.env.APPLIQATION_AUTO_TAG_NAME ||
|
|
90
|
-
'Appq_automated',
|
|
122
|
+
// Fixed, not configurable — see AUTO_TAG_NAME's doc comment.
|
|
123
|
+
autoTagName: AUTO_TAG_NAME,
|
|
91
124
|
autoTagBatchSize: resolved.options?.autoTagBatchSize || 50,
|
|
92
125
|
autoTagRetries: resolved.options?.autoTagRetries || 2
|
|
93
126
|
}
|
|
@@ -351,6 +384,20 @@ class AppliqationClient {
|
|
|
351
384
|
}
|
|
352
385
|
}
|
|
353
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Fetch the UUID membership of a Test Set, for scope validation.
|
|
389
|
+
*/
|
|
390
|
+
async fetchTestSetScope(testSetId) {
|
|
391
|
+
try {
|
|
392
|
+
const response = await this.http.get(`/api/automation/testset/${testSetId}/scope`);
|
|
393
|
+
const uuids = response?.data?.data;
|
|
394
|
+
return new Set(Array.isArray(uuids) ? uuids : []);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
logger.error('Failed to fetch testset scope', { error: error.message, testSetId });
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
354
401
|
validateUuid(uuid) {
|
|
355
402
|
return UuidValidator.validate(uuid);
|
|
356
403
|
}
|
|
@@ -364,36 +411,89 @@ class AppliqationClient {
|
|
|
364
411
|
}
|
|
365
412
|
|
|
366
413
|
/**
|
|
367
|
-
* Validate client configuration
|
|
414
|
+
* Validate client configuration.
|
|
415
|
+
*
|
|
416
|
+
* Hard errors (returned in `errors`) cause the constructor to throw
|
|
417
|
+
* ConfigurationError. They cover misconfiguration that would otherwise
|
|
418
|
+
* fail silently or mid-run: malformed URL, non-positive numerics, invalid
|
|
419
|
+
* environment name format. Soft warnings (returned in `warnings`) are
|
|
420
|
+
* logged but don't block construction — missing apiKey, etc.
|
|
421
|
+
*
|
|
368
422
|
* @private
|
|
369
|
-
* @returns {{
|
|
423
|
+
* @returns {{ errors: string[], warnings: string[] }}
|
|
370
424
|
*/
|
|
371
425
|
validateConfig(config) {
|
|
426
|
+
const errors = [];
|
|
372
427
|
const warnings = [];
|
|
373
428
|
|
|
374
429
|
if (!config) {
|
|
375
|
-
|
|
376
|
-
return {
|
|
430
|
+
errors.push('No configuration object provided');
|
|
431
|
+
return { errors, warnings };
|
|
377
432
|
}
|
|
378
433
|
|
|
434
|
+
// apiKey — warn only (no-op mode is intentional behaviour)
|
|
379
435
|
if (!config.apiKey) {
|
|
380
436
|
warnings.push('[Appliqation] API key required. Set APPLIQATION_API_KEY in .env');
|
|
437
|
+
} else if (typeof config.apiKey !== 'string' || config.apiKey.trim() === '') {
|
|
438
|
+
errors.push(`apiKey must be a non-empty string (got: ${typeof config.apiKey})`);
|
|
381
439
|
}
|
|
382
440
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
logger.debug('projectKey not set — will auto-discover from API key');
|
|
386
|
-
}
|
|
387
|
-
|
|
441
|
+
// baseUrl — hard error on malformed URL (silent failure otherwise:
|
|
442
|
+
// every HTTP call fails with a cryptic axios error mid-run).
|
|
388
443
|
if (config.baseUrl) {
|
|
389
444
|
try {
|
|
390
|
-
new URL(config.baseUrl);
|
|
445
|
+
const url = new URL(config.baseUrl);
|
|
446
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
447
|
+
errors.push(`baseUrl must use http or https (got: "${url.protocol}")`);
|
|
448
|
+
}
|
|
391
449
|
} catch (error) {
|
|
392
|
-
|
|
450
|
+
errors.push(`Invalid baseUrl: "${config.baseUrl}" is not a valid URL`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// environment — hard error on invalid format (backend rejects 400 mid-run)
|
|
455
|
+
if (config.environment !== undefined && config.environment !== null && config.environment !== '') {
|
|
456
|
+
if (typeof config.environment !== 'string') {
|
|
457
|
+
errors.push(`environment must be a string (got: ${typeof config.environment})`);
|
|
458
|
+
} else if (!ENVIRONMENT_NAME_PATTERN.test(config.environment)) {
|
|
459
|
+
errors.push(
|
|
460
|
+
`environment "${config.environment}" contains invalid characters. `
|
|
461
|
+
+ 'Allowed: letters, digits, dot, underscore, hyphen.'
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// options.timeout — must be a positive integer if specified
|
|
467
|
+
if (config.options && config.options.timeout !== undefined) {
|
|
468
|
+
const t = config.options.timeout;
|
|
469
|
+
if (!Number.isInteger(t) || t <= 0) {
|
|
470
|
+
errors.push(`options.timeout must be a positive integer (got: ${t})`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// options.retries — must be a non-negative integer if specified
|
|
475
|
+
if (config.options && config.options.retries !== undefined) {
|
|
476
|
+
const r = config.options.retries;
|
|
477
|
+
if (!Number.isInteger(r) || r < 0) {
|
|
478
|
+
errors.push(`options.retries must be a non-negative integer (got: ${r})`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// options.batchSize / options.autoTagBatchSize — must be positive integers
|
|
483
|
+
for (const field of ['batchSize', 'autoTagBatchSize']) {
|
|
484
|
+
if (config.options && config.options[field] !== undefined) {
|
|
485
|
+
const v = config.options[field];
|
|
486
|
+
if (!Number.isInteger(v) || v <= 0) {
|
|
487
|
+
errors.push(`options.${field} must be a positive integer (got: ${v})`);
|
|
488
|
+
}
|
|
393
489
|
}
|
|
394
490
|
}
|
|
395
491
|
|
|
396
|
-
|
|
492
|
+
if (!config.projectKey) {
|
|
493
|
+
logger.debug('projectKey not set — will auto-discover from API key');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return { errors, warnings };
|
|
397
497
|
}
|
|
398
498
|
|
|
399
499
|
/**
|
package/src/cli/auth-setup.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Customer setup (one-time, per project):
|
|
10
10
|
* 1. Configure roles in Appliqation Project Settings → Auth Config
|
|
11
|
-
* 2. Write `tests/
|
|
11
|
+
* 2. Write `tests/automan/auth/login.ts` in their repo (default path;
|
|
12
|
+
* configurable in Project Settings):
|
|
12
13
|
*
|
|
13
14
|
* import { defineLogin } from '@appliqation/automation-sdk/login';
|
|
14
15
|
* export default defineLogin(async (page, { username, password }) => {
|
|
@@ -50,7 +51,7 @@ const {
|
|
|
50
51
|
} = require('../utils/setupAuth');
|
|
51
52
|
|
|
52
53
|
const FATAL_EXIT = 1;
|
|
53
|
-
const DEFAULT_LOGIN_FILE = 'tests/
|
|
54
|
+
const DEFAULT_LOGIN_FILE = 'tests/automan/auth/login.ts';
|
|
54
55
|
|
|
55
56
|
function parseArgs(argv) {
|
|
56
57
|
const args = {};
|
|
@@ -78,12 +79,21 @@ function fail(message) {
|
|
|
78
79
|
* Resolve the customer's login.ts path. Priority:
|
|
79
80
|
* 1. --login-file CLI flag (explicit override)
|
|
80
81
|
* 2. APPQ_LOGIN_FILE env var (CI override without changing CLI args)
|
|
81
|
-
* 3. Convention: tests/
|
|
82
|
+
* 3. Convention: tests/automan/auth/login.ts relative to CWD
|
|
82
83
|
*
|
|
83
84
|
* Throws clearly if the resolved path doesn't exist — customers who
|
|
84
85
|
* haven't created the file yet need a path-specific error, not a
|
|
85
86
|
* cryptic dynamic-import failure.
|
|
86
87
|
*/
|
|
88
|
+
// H4 — only allow loading login scripts written in JS or TS. Blocks an
|
|
89
|
+
// attacker from convincing the CLI to require() arbitrary file types
|
|
90
|
+
// (.json with prototype pollution, .node native addons, .yaml via a
|
|
91
|
+
// custom hook, etc).
|
|
92
|
+
const ALLOWED_LOGIN_FILE_EXTENSIONS = new Set([
|
|
93
|
+
'.ts', '.tsx', '.mts', '.cts',
|
|
94
|
+
'.js', '.mjs', '.cjs', '.jsx'
|
|
95
|
+
]);
|
|
96
|
+
|
|
87
97
|
function resolveLoginFile(args) {
|
|
88
98
|
const candidate = args['login-file']
|
|
89
99
|
|| process.env.APPQ_LOGIN_FILE
|
|
@@ -91,9 +101,48 @@ function resolveLoginFile(args) {
|
|
|
91
101
|
const absolute = path.isAbsolute(candidate)
|
|
92
102
|
? candidate
|
|
93
103
|
: path.resolve(process.cwd(), candidate);
|
|
94
|
-
|
|
104
|
+
|
|
105
|
+
// H4 — directory-traversal guard. The CLI loads the resolved file via
|
|
106
|
+
// require() / dynamic import(), so a path outside the project root is
|
|
107
|
+
// an arbitrary-code-execution vector in a compromised CI environment
|
|
108
|
+
// (malicious PR, supply chain). Require the login file to live inside
|
|
109
|
+
// the current working directory tree.
|
|
110
|
+
const projectRoot = process.cwd();
|
|
111
|
+
const rootWithSep = projectRoot.endsWith(path.sep) ? projectRoot : projectRoot + path.sep;
|
|
112
|
+
// Realpath to defeat symlink escapes; fall back to absolute if the
|
|
113
|
+
// file doesn't exist yet so the existence error below still fires
|
|
114
|
+
// with the user's intended path.
|
|
115
|
+
let canonical;
|
|
116
|
+
try {
|
|
117
|
+
canonical = fs.realpathSync(absolute);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
canonical = absolute;
|
|
120
|
+
}
|
|
121
|
+
if (canonical !== projectRoot && !canonical.startsWith(rootWithSep)) {
|
|
95
122
|
fail(
|
|
96
|
-
`Login file
|
|
123
|
+
`Login file must live inside the project directory.\n`
|
|
124
|
+
+ ` Project root: ${projectRoot}\n`
|
|
125
|
+
+ ` Login file: ${canonical}\n`
|
|
126
|
+
+ `\n`
|
|
127
|
+
+ ` Resolving outside the project tree would let any process that\n`
|
|
128
|
+
+ ` controls --login-file or APPQ_LOGIN_FILE load arbitrary code\n`
|
|
129
|
+
+ ` through this CLI. Move the file under the project root.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// H4 — extension allowlist.
|
|
134
|
+
const ext = path.extname(canonical).toLowerCase();
|
|
135
|
+
if (!ALLOWED_LOGIN_FILE_EXTENSIONS.has(ext)) {
|
|
136
|
+
fail(
|
|
137
|
+
`Login file extension "${ext}" is not allowed.\n`
|
|
138
|
+
+ ` Allowed: ${Array.from(ALLOWED_LOGIN_FILE_EXTENSIONS).join(', ')}\n`
|
|
139
|
+
+ ` Got: ${canonical}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!fs.existsSync(canonical)) {
|
|
144
|
+
fail(
|
|
145
|
+
`Login file not found: ${canonical}\n`
|
|
97
146
|
+ ` Create it (default path: ${DEFAULT_LOGIN_FILE}) and export your login flow as a default export:\n`
|
|
98
147
|
+ `\n`
|
|
99
148
|
+ ` import { defineLogin } from '@appliqation/automation-sdk/login';\n`
|
|
@@ -103,7 +152,7 @@ function resolveLoginFile(args) {
|
|
|
103
152
|
+ ` });\n`
|
|
104
153
|
);
|
|
105
154
|
}
|
|
106
|
-
return
|
|
155
|
+
return canonical;
|
|
107
156
|
}
|
|
108
157
|
|
|
109
158
|
/**
|
|
@@ -220,7 +269,13 @@ async function main() {
|
|
|
220
269
|
}
|
|
221
270
|
|
|
222
271
|
const targetPath = authStatePath({ project_id, role });
|
|
223
|
-
|
|
272
|
+
// H1 — restrict the auth-state directory to owner-only. Playwright's
|
|
273
|
+
// storageState file contains cookies, localStorage, and sessionStorage
|
|
274
|
+
// — equivalent to a live authenticated browser session. Default
|
|
275
|
+
// mkdir permissions inherit the umask (typically 0755 / world-readable
|
|
276
|
+
// on shared CI runners and developer machines); force 0700 so no
|
|
277
|
+
// other local user can read peer sessions.
|
|
278
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o700 });
|
|
224
279
|
|
|
225
280
|
console.log(`→ Launching Chromium for login as role "${role}" against ${baseURL}`);
|
|
226
281
|
const browser = await chromium.launch({ headless: true });
|
|
@@ -233,7 +288,15 @@ async function main() {
|
|
|
233
288
|
try {
|
|
234
289
|
await loginFn(page, { username, password, role, baseURL });
|
|
235
290
|
await context.storageState({ path: targetPath });
|
|
236
|
-
|
|
291
|
+
// H1 — Playwright writes storageState with umask-inherited
|
|
292
|
+
// permissions (typically 0644). Tighten to 0600 so even other
|
|
293
|
+
// local users on the same machine cannot read it.
|
|
294
|
+
try {
|
|
295
|
+
fs.chmodSync(targetPath, 0o600);
|
|
296
|
+
} catch (chmodErr) {
|
|
297
|
+
console.warn(`⚠️ Could not tighten permissions on ${targetPath}: ${chmodErr.message}`);
|
|
298
|
+
}
|
|
299
|
+
console.log(`✓ Auth state saved to ${targetPath} (mode 0600)`);
|
|
237
300
|
console.log(` Tests using setupAuth({ project_id: ${project_id}, role: '${role}' }) will pick this up.`);
|
|
238
301
|
} catch (err) {
|
|
239
302
|
fail(`Login failed: ${err.message}`);
|
package/src/constants.js
CHANGED
|
@@ -204,6 +204,23 @@ const DEFAULT_OS = 'Unknown';
|
|
|
204
204
|
*/
|
|
205
205
|
const DEFAULT_APPLIQATION_BASE_URL = 'https://appq.appliqation.io';
|
|
206
206
|
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// Auto-Tagging Configuration
|
|
209
|
+
// ============================================================================
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Tag applied to a test case the first time the SDK submits an accepted
|
|
213
|
+
* result for it. Marks the test case as automation-covered.
|
|
214
|
+
*
|
|
215
|
+
* Fixed, not configurable — appq's own backend (AutomationResultManager's
|
|
216
|
+
* autoTagScenario/autoTagTestset) writes this exact literal when it
|
|
217
|
+
* auto-tags at run creation. A configurable client-side name would let the
|
|
218
|
+
* SDK and backend disagree and produce two different tags for the same
|
|
219
|
+
* "automated" signal.
|
|
220
|
+
* @constant {string}
|
|
221
|
+
*/
|
|
222
|
+
const AUTO_TAG_NAME = 'Appq_Auto';
|
|
223
|
+
|
|
207
224
|
// ============================================================================
|
|
208
225
|
// Exports
|
|
209
226
|
// ============================================================================
|
|
@@ -251,5 +268,8 @@ module.exports = {
|
|
|
251
268
|
DEFAULT_OS,
|
|
252
269
|
|
|
253
270
|
// Base URL
|
|
254
|
-
DEFAULT_APPLIQATION_BASE_URL
|
|
271
|
+
DEFAULT_APPLIQATION_BASE_URL,
|
|
272
|
+
|
|
273
|
+
// Auto-Tagging
|
|
274
|
+
AUTO_TAG_NAME
|
|
255
275
|
};
|
package/src/core/AuthManager.js
CHANGED
|
@@ -180,6 +180,11 @@ class AuthManager {
|
|
|
180
180
|
|
|
181
181
|
parseJwtExpiry(token) {
|
|
182
182
|
try {
|
|
183
|
+
// Signature not verified — token is issued by the Appliqation API server
|
|
184
|
+
// we just authenticated against, so origin is trusted. We only decode
|
|
185
|
+
// the payload to read `exp` for refresh scheduling. Verifying the HMAC
|
|
186
|
+
// here would require the server-side signing secret, which the SDK
|
|
187
|
+
// does not (and must not) hold.
|
|
183
188
|
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
|
|
184
189
|
// Convert to milliseconds and subtract 5 minutes for safety margin
|
|
185
190
|
return (payload.exp * 1000) - (5 * 60 * 1000);
|
package/src/index.d.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TypeScript definitions for @appliqation/automation-sdk
|
|
3
|
-
* @version 2.
|
|
3
|
+
* @version 2.8.0
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Scope-mismatch resolution strategy (audit finding C5).
|
|
8
|
+
*
|
|
9
|
+
* When `scenarioId` or `testSetId` is set in config and the executed
|
|
10
|
+
* suite contains UUIDs outside that scope, the SDK applies one of
|
|
11
|
+
* these strategies before submitting results.
|
|
12
|
+
* 'cancel' — fail fast, submit nothing, exit 1 (default in CI)
|
|
13
|
+
* 'adhoc' — override config, create an ad-hoc run, submit everything
|
|
14
|
+
* 'filter' — submit only in-scope results, drop the rest locally
|
|
15
|
+
*/
|
|
16
|
+
export type ScopeMismatchStrategy = 'cancel' | 'adhoc' | 'filter';
|
|
17
|
+
|
|
6
18
|
// ============================================================================
|
|
7
19
|
// Core Types
|
|
8
20
|
// ============================================================================
|
|
@@ -49,10 +61,16 @@ export interface AppliqationConfig {
|
|
|
49
61
|
/** Project key */
|
|
50
62
|
projectKey?: string;
|
|
51
63
|
|
|
52
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* @deprecated Legacy CSRF authentication. Slated for removal in a
|
|
66
|
+
* future major version (audit finding D4). Use `apiKey` instead.
|
|
67
|
+
*/
|
|
53
68
|
username?: string;
|
|
54
69
|
|
|
55
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* @deprecated Legacy CSRF authentication. Slated for removal in a
|
|
72
|
+
* future major version (audit finding D4). Use `apiKey` instead.
|
|
73
|
+
*/
|
|
56
74
|
password?: string;
|
|
57
75
|
|
|
58
76
|
/** Scenario ID (optional, 0 for generic automation runs) */
|
|
@@ -88,9 +106,25 @@ export interface AppliqationConfig {
|
|
|
88
106
|
/** Batch size for result submission */
|
|
89
107
|
batchSize?: number;
|
|
90
108
|
|
|
91
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* SSL certificate verification (default: true).
|
|
111
|
+
*
|
|
112
|
+
* H3 — setting this to `false` in a production-like environment
|
|
113
|
+
* (`CI=true`, `NODE_ENV=production`/`prod`/`staging`, or any well-known
|
|
114
|
+
* CI env var) will throw `ConfigurationError` at client construction.
|
|
115
|
+
* Explicit override via `APPLIQATION_INSECURE=allow` for the rare case
|
|
116
|
+
* where a customer genuinely needs to bypass TLS in CI.
|
|
117
|
+
*/
|
|
92
118
|
rejectUnauthorized?: boolean;
|
|
93
119
|
|
|
120
|
+
/**
|
|
121
|
+
* C5 — scope-mismatch resolution strategy. Applied when the executed
|
|
122
|
+
* suite contains UUIDs outside the configured `scenarioId`/`testSetId`.
|
|
123
|
+
* Also settable via `APPLIQATION_ON_SCOPE_MISMATCH` env var.
|
|
124
|
+
* Defaults to 'cancel' in CI (fail safe), interactive prompt in TTY.
|
|
125
|
+
*/
|
|
126
|
+
onScopeMismatch?: ScopeMismatchStrategy;
|
|
127
|
+
|
|
94
128
|
/** Additional options */
|
|
95
129
|
options?: {
|
|
96
130
|
/** Request timeout in milliseconds (default: 30000) */
|
|
@@ -232,9 +266,16 @@ export class AppliqationClient {
|
|
|
232
266
|
constructor(config: AppliqationConfig);
|
|
233
267
|
|
|
234
268
|
/**
|
|
235
|
-
* Create a new automation run
|
|
269
|
+
* Create a new automation run.
|
|
270
|
+
*
|
|
271
|
+
* The optional `uuids` field (C4 / E3) pre-populates the run
|
|
272
|
+
* document's `data[]` array so the UI grid and orphan detector know
|
|
273
|
+
* which TCs belong to the run before any result lands. Pass plain
|
|
274
|
+
* string UUIDs matching PR appq/#532's schema (not `{uuid}` objects).
|
|
236
275
|
*/
|
|
237
|
-
createRun(
|
|
276
|
+
createRun(
|
|
277
|
+
config?: Partial<AppliqationConfig> & { uuids?: string[] }
|
|
278
|
+
): Promise<RunMatrixResponse>;
|
|
238
279
|
|
|
239
280
|
/**
|
|
240
281
|
* Submit a single test result
|
|
@@ -299,6 +340,61 @@ export class PayloadBuilder {
|
|
|
299
340
|
status: TestStatus,
|
|
300
341
|
options?: { comment?: string; attachments?: string[]; metadata?: Record<string, any> }
|
|
301
342
|
): TestResult;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* S8 — validate a fully assembled automation result payload against
|
|
346
|
+
* backend shape constraints (InputValidator regexes + status enum).
|
|
347
|
+
* Runs client-side immediately before submission so schema drift is
|
|
348
|
+
* caught with a precise field-path error instead of a generic 400.
|
|
349
|
+
*/
|
|
350
|
+
static validateAutomationResultPayload(payload: {
|
|
351
|
+
run_id?: unknown;
|
|
352
|
+
test_case_uuid?: unknown;
|
|
353
|
+
status?: unknown;
|
|
354
|
+
source?: unknown;
|
|
355
|
+
browser?: unknown;
|
|
356
|
+
environment?: unknown;
|
|
357
|
+
duration?: unknown;
|
|
358
|
+
timestamp?: unknown;
|
|
359
|
+
[key: string]: unknown;
|
|
360
|
+
}): { valid: boolean; errors: string[] };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* C5 — Pre-execution scope validator. Verifies UUIDs about to execute
|
|
365
|
+
* match the configured scenarioId/testSetId; applies cancel/adhoc/filter
|
|
366
|
+
* strategy on mismatch. Wired automatically by AppliqationReporter in
|
|
367
|
+
* `onBegin`; exposed for programmatic use.
|
|
368
|
+
*/
|
|
369
|
+
export class ScopeValidator {
|
|
370
|
+
constructor(options: {
|
|
371
|
+
scenarioId?: number | string;
|
|
372
|
+
testSetId?: number | string;
|
|
373
|
+
strategy?: ScopeMismatchStrategy;
|
|
374
|
+
fetchTestSetScope?: (testSetId: number | string) => Promise<Set<string> | string[]>;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
static readonly STRATEGY: {
|
|
378
|
+
CANCEL: 'cancel';
|
|
379
|
+
ADHOC: 'adhoc';
|
|
380
|
+
FILTER: 'filter';
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
isScoped(): boolean;
|
|
384
|
+
extractUuidsFromSuite(suite: unknown): string[];
|
|
385
|
+
validate(uuids: string[]): Promise<{
|
|
386
|
+
hasMismatch: boolean;
|
|
387
|
+
inScope: string[];
|
|
388
|
+
outOfScope: Array<{ uuid: string; actualScenarioId?: number }>;
|
|
389
|
+
reason: 'scenario' | 'testset' | null;
|
|
390
|
+
}>;
|
|
391
|
+
resolve(validation: unknown): Promise<{
|
|
392
|
+
action: 'cancel' | 'adhoc' | 'filter' | 'pass';
|
|
393
|
+
submitUuids: string[];
|
|
394
|
+
droppedUuids: string[];
|
|
395
|
+
overrideToAdhoc: boolean;
|
|
396
|
+
message: string;
|
|
397
|
+
}>;
|
|
302
398
|
}
|
|
303
399
|
|
|
304
400
|
export const logger: {
|
package/src/login/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TypeScript definitions for @appliqation/automation-sdk/login.
|
|
3
3
|
*
|
|
4
|
-
* Customers writing `tests/
|
|
4
|
+
* Customers writing `tests/automan/auth/login.ts` (default path;
|
|
5
|
+
* configurable in Project Settings) import these
|
|
5
6
|
* types for autocomplete + compile-time checking on the login flow
|
|
6
7
|
* function shape. Same file gets imported (untyped, dynamically) by
|
|
7
8
|
* the Appliqation executor's LoginHelper and the appq-auth-setup CLI.
|
package/src/login/index.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* supplied login flows.
|
|
4
4
|
*
|
|
5
5
|
* Customers define their SUT's login flow as a Playwright function in
|
|
6
|
-
* their own repo (canonical path: `tests/
|
|
6
|
+
* their own repo (canonical path: `tests/automan/auth/login.ts`, default;
|
|
7
|
+
* configurable in Project Settings).
|
|
7
8
|
* The function is imported by:
|
|
8
9
|
* 1. Appliqation's executor (LoginHelper) — pulled from MongoDB
|
|
9
10
|
* after a GitHub-webhook upsert into automan_canonical_scripts
|
|
@@ -9,7 +9,10 @@ try {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const JwtBrowserAuth = require('./JwtBrowserAuth');
|
|
12
|
-
|
|
12
|
+
// L3 — dotenv removed from the fixture. In enterprise CI, env vars are
|
|
13
|
+
// injected by the platform (GitHub Actions, Jenkins, GitLab); a stray
|
|
14
|
+
// committed .env file would otherwise silently override them. Env var
|
|
15
|
+
// loading is the customer's responsibility in playwright.config.ts.
|
|
13
16
|
const { DEFAULT_APPLIQATION_BASE_URL } = require('../constants');
|
|
14
17
|
|
|
15
18
|
/**
|