@flowspec-qa/runner-core 0.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/dist/api-client.d.ts +50 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +70 -0
- package/dist/api-client.js.map +1 -0
- package/dist/auth.d.ts +17 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +38 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +37 -0
- package/dist/config.js.map +1 -0
- package/dist/executor.d.ts +32 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +221 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/result-sanitizer.d.ts +21 -0
- package/dist/result-sanitizer.d.ts.map +1 -0
- package/dist/result-sanitizer.js +54 -0
- package/dist/result-sanitizer.js.map +1 -0
- package/dist/suite-manager.d.ts +25 -0
- package/dist/suite-manager.d.ts.map +1 -0
- package/dist/suite-manager.js +60 -0
- package/dist/suite-manager.js.map +1 -0
- package/dist/uploader.d.ts +16 -0
- package/dist/uploader.d.ts.map +1 -0
- package/dist/uploader.js +29 -0
- package/dist/uploader.js.map +1 -0
- package/package.json +23 -0
- package/src/api-client.ts +118 -0
- package/src/auth.ts +45 -0
- package/src/config.ts +52 -0
- package/src/executor.ts +275 -0
- package/src/index.ts +8 -0
- package/src/result-sanitizer.ts +60 -0
- package/src/suite-manager.ts +73 -0
- package/src/uploader.ts +45 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { config } from './config.js';
|
|
2
|
+
import type { TestResultItem } from './api-client.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sanitize test results before uploading to FlowSpecQA SaaS.
|
|
6
|
+
*
|
|
7
|
+
* Strips:
|
|
8
|
+
* - HTTP/HTTPS URLs (base URLs of the client's environments)
|
|
9
|
+
* - IP addresses
|
|
10
|
+
* - Authorization / Bearer tokens
|
|
11
|
+
* - Password patterns (key=value)
|
|
12
|
+
* - Anything matching custom patterns in config
|
|
13
|
+
*
|
|
14
|
+
* The test name, status, and duration are NEVER modified.
|
|
15
|
+
* Only `error` messages are sanitized.
|
|
16
|
+
*/
|
|
17
|
+
export function sanitizeResults(results: TestResultItem[]): TestResultItem[] {
|
|
18
|
+
const patterns = config.get('sanitizerPatterns').map((p) => new RegExp(p, 'gi'));
|
|
19
|
+
|
|
20
|
+
return results.map((r) => ({
|
|
21
|
+
...r,
|
|
22
|
+
error: r.error ? sanitizeString(r.error, patterns) : null,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sanitizeString(text: string, patterns: RegExp[]): string {
|
|
27
|
+
let sanitized = text;
|
|
28
|
+
for (const pattern of patterns) {
|
|
29
|
+
sanitized = sanitized.replace(pattern, '[REDACTED]');
|
|
30
|
+
}
|
|
31
|
+
// Additional pass: truncate very long error messages to 1500 chars
|
|
32
|
+
if (sanitized.length > 1500) {
|
|
33
|
+
sanitized = sanitized.slice(0, 1500) + '... [truncated]';
|
|
34
|
+
}
|
|
35
|
+
return sanitized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate that no obvious secrets leaked through (belt-and-suspenders check).
|
|
40
|
+
* Throws if any result still contains a pattern that looks like a secret.
|
|
41
|
+
*/
|
|
42
|
+
export function assertNoSecrets(results: TestResultItem[]): void {
|
|
43
|
+
const dangerPatterns = [
|
|
44
|
+
/https?:\/\/[a-z0-9.-]+\.(internal|local|corp|dev|staging|test)[/:]?/i,
|
|
45
|
+
/Bearer\s+[A-Za-z0-9.\-_]{20,}/,
|
|
46
|
+
/\bpassword\b/i,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
for (const r of results) {
|
|
50
|
+
if (!r.error) continue;
|
|
51
|
+
for (const p of dangerPatterns) {
|
|
52
|
+
if (p.test(r.error)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Security check failed: error message for test "${r.name}" may contain sensitive data. ` +
|
|
55
|
+
`Run with --skip-security-check to bypass (not recommended).`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import AdmZip from 'adm-zip';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { config } from './config.js';
|
|
5
|
+
import { FlowSpecQAApi } from './api-client.js';
|
|
6
|
+
|
|
7
|
+
export interface LocalSuite {
|
|
8
|
+
runId: string;
|
|
9
|
+
entityId: string;
|
|
10
|
+
framework: 'playwright' | 'cypress' | 'maestro';
|
|
11
|
+
suiteDir: string;
|
|
12
|
+
downloadedAt: Date;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Download a suite ZIP from FlowSpecQA SaaS and unzip it to the local cache.
|
|
17
|
+
* Returns the path to the unzipped suite directory.
|
|
18
|
+
*/
|
|
19
|
+
export async function downloadSuite(suiteId: string, entityId: string, framework: string): Promise<LocalSuite> {
|
|
20
|
+
const cacheDir = config.get('suiteCacheDir');
|
|
21
|
+
const suiteDir = path.join(cacheDir, entityId, suiteId);
|
|
22
|
+
|
|
23
|
+
// Avoid re-downloading if already cached
|
|
24
|
+
if (fs.existsSync(path.join(suiteDir, 'package.json'))) {
|
|
25
|
+
return {
|
|
26
|
+
runId: suiteId,
|
|
27
|
+
entityId,
|
|
28
|
+
framework: framework as LocalSuite['framework'],
|
|
29
|
+
suiteDir,
|
|
30
|
+
downloadedAt: fs.statSync(suiteDir).mtime,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const zipBuffer = await FlowSpecQAApi.downloadSuiteZip(suiteId);
|
|
35
|
+
fs.mkdirSync(suiteDir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
const zip = new AdmZip(zipBuffer);
|
|
38
|
+
zip.extractAllTo(suiteDir, /* overwrite */ true);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
runId: suiteId,
|
|
42
|
+
entityId,
|
|
43
|
+
framework: framework as LocalSuite['framework'],
|
|
44
|
+
suiteDir,
|
|
45
|
+
downloadedAt: new Date(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* List all locally cached suites.
|
|
51
|
+
*/
|
|
52
|
+
export function listCachedSuites(): Array<{ entityId: string; runId: string; suiteDir: string }> {
|
|
53
|
+
const cacheDir = config.get('suiteCacheDir');
|
|
54
|
+
if (!fs.existsSync(cacheDir)) return [];
|
|
55
|
+
|
|
56
|
+
const result: Array<{ entityId: string; runId: string; suiteDir: string }> = [];
|
|
57
|
+
for (const entityId of fs.readdirSync(cacheDir)) {
|
|
58
|
+
const entityDir = path.join(cacheDir, entityId);
|
|
59
|
+
if (!fs.statSync(entityDir).isDirectory()) continue;
|
|
60
|
+
for (const runId of fs.readdirSync(entityDir)) {
|
|
61
|
+
result.push({ entityId, runId, suiteDir: path.join(entityDir, runId) });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Remove a cached suite directory.
|
|
69
|
+
*/
|
|
70
|
+
export function evictSuite(entityId: string, runId: string): void {
|
|
71
|
+
const suiteDir = path.join(config.get('suiteCacheDir'), entityId, runId);
|
|
72
|
+
fs.rmSync(suiteDir, { recursive: true, force: true });
|
|
73
|
+
}
|
package/src/uploader.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { FlowSpecQAApi, type RunResultPayload } from './api-client.js';
|
|
2
|
+
import { sanitizeResults, assertNoSecrets } from './result-sanitizer.js';
|
|
3
|
+
import type { ExecutionResult } from './executor.js';
|
|
4
|
+
|
|
5
|
+
const RUNNER_VERSION = '0.1.0';
|
|
6
|
+
|
|
7
|
+
export interface UploadOptions {
|
|
8
|
+
runId: string;
|
|
9
|
+
entityId: string;
|
|
10
|
+
framework: string;
|
|
11
|
+
envLabel: string;
|
|
12
|
+
skipSecurityCheck?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sanitize + upload test results to FlowSpecQA SaaS.
|
|
17
|
+
* Returns the exec_id assigned by the server.
|
|
18
|
+
*/
|
|
19
|
+
export async function uploadResults(
|
|
20
|
+
execution: ExecutionResult,
|
|
21
|
+
options: UploadOptions
|
|
22
|
+
): Promise<{ execId: string }> {
|
|
23
|
+
// Step 1: Sanitize — strip URLs, IPs, tokens from error messages
|
|
24
|
+
const sanitized = sanitizeResults(execution.results);
|
|
25
|
+
|
|
26
|
+
// Step 2: Belt-and-suspenders security check
|
|
27
|
+
if (!options.skipSecurityCheck) {
|
|
28
|
+
assertNoSecrets(sanitized);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Step 3: Build payload
|
|
32
|
+
const payload: RunResultPayload = {
|
|
33
|
+
run_id: options.runId,
|
|
34
|
+
entity_id: options.entityId,
|
|
35
|
+
framework: options.framework,
|
|
36
|
+
env_label: options.envLabel,
|
|
37
|
+
results: sanitized,
|
|
38
|
+
started_at: execution.startedAt.toISOString(),
|
|
39
|
+
finished_at: execution.finishedAt.toISOString(),
|
|
40
|
+
runner_version: RUNNER_VERSION,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const response = await FlowSpecQAApi.submitRunResult(payload);
|
|
44
|
+
return { execId: response.exec_id };
|
|
45
|
+
}
|