54 lines
2.9 KiB
JavaScript
54 lines
2.9 KiB
JavaScript
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 };
|