ok
Some checks failed
CI / validate (push) Has been cancelled

This commit is contained in:
isopropilick
2026-08-27 15:00:50 -06:00
parent b35387d841
commit 896c3a121a
19 changed files with 569 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
import fs from 'node:fs';
import path from 'node:path';
const CONFIG_FILE = 'qkforce.config.json';
const MAX_TARGET_URL_BYTES = 2048;
const FIXED_PROFILE = Object.freeze({
id: 'qkforce-wdio-cucumber-qkta',
language: 'typescript',
bdd: 'cucumber',
runner: 'webdriverio',
adapter: 'webdriverio-cucumber-node',
reporter: 'qk-test-analytics'
});
const fail = message => { throw new Error(`Invalid QKForce run configuration: ${message}`); };
const isObject = value => value !== null && typeof value === 'object' && !Array.isArray(value);
const rejectUnknownKeys = (value, allowed, location) => { for (const key of Object.keys(value)) if (!allowed.includes(key)) fail(`${location}.${key} is not supported`); };
const validateTargetUrl = value => {
if (typeof value !== 'string' || value.length === 0) fail('run.targetUrl is required');
if (Buffer.byteLength(value, 'utf8') > MAX_TARGET_URL_BYTES) fail('run.targetUrl exceeds the 2048-byte maximum');
let url;
try { url = new URL(value); } catch { fail('run.targetUrl must be an absolute URL'); }
if (url.username || url.password) fail('run.targetUrl must not contain credentials');
if (url.hash) fail('run.targetUrl must not contain a fragment');
if (url.search) fail('run.targetUrl must not contain a query string');
const loopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]';
if (url.protocol !== 'https:' && !(loopback && url.protocol === 'http:')) fail('run.targetUrl must use HTTPS unless it is an exact loopback development URL');
return url.toString();
};
export function validateQkforceRunConfig(candidate) {
if (!isObject(candidate)) fail('the document must be a JSON object');
rejectUnknownKeys(candidate, ['schemaVersion','profile','run'], 'config');
if (candidate.schemaVersion !== 1) fail('schemaVersion must be 1');
if (!isObject(candidate.profile)) fail('profile must be an object');
rejectUnknownKeys(candidate.profile, Object.keys(FIXED_PROFILE), 'profile');
for (const [key, expected] of Object.entries(FIXED_PROFILE)) if (candidate.profile[key] !== expected) fail(`profile.${key} must be ${expected}`);
if (!isObject(candidate.run)) fail('run must be an object');
rejectUnknownKeys(candidate.run, ['targetUrl'], 'run');
const targetUrl = validateTargetUrl(candidate.run.targetUrl);
return Object.freeze({ schemaVersion: 1, profile: FIXED_PROFILE, run: Object.freeze({ targetUrl }), baseUrl: targetUrl });
}
export function loadQkforceRunConfig(rootDirectory = process.cwd()) {
const configPath = path.resolve(rootDirectory, CONFIG_FILE);
let parsed;
try { parsed = JSON.parse(fs.readFileSync(configPath, 'utf8')); }
catch (error) { if (error && error.code === 'ENOENT') fail(`${CONFIG_FILE} is missing`); fail(`${CONFIG_FILE} is not valid JSON`); }
return validateQkforceRunConfig(parsed);
}
export { CONFIG_FILE, FIXED_PROFILE, MAX_TARGET_URL_BYTES };