UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

257 lines (236 loc) 10.8 kB
'use strict' const REQUIRED_SETTINGS_KEYS = [ 'code_coverage', 'tests_skipping', 'itr_enabled', 'require_git', 'early_flake_detection', 'flaky_test_retries_enabled', 'di_enabled', 'known_tests_enabled', 'test_management', 'impacted_tests_enabled', 'coverage_report_upload_enabled', ] const SETTINGS_BOOLEAN_KEYS = [ 'code_coverage', 'tests_skipping', 'itr_enabled', 'require_git', 'flaky_test_retries_enabled', 'di_enabled', 'known_tests_enabled', 'impacted_tests_enabled', 'coverage_report_upload_enabled', ] const EARLY_FLAKE_DETECTION_KEYS = ['enabled', 'faulty_session_threshold', 'slow_test_retries'] const TEST_MANAGEMENT_KEYS = ['attempt_to_fix_retries', 'enabled'] const RETRY_THRESHOLD_PATTERN = /^\d+(?:ms|s|m|h)$/ const MAX_VALIDATION_MODULES = 1000 const MAX_VALIDATION_SUITES = 10_000 const MAX_VALIDATION_TESTS = 100_000 const MAX_VALIDATION_RETRIES = 100 const MAX_VALIDATION_STRING_BYTES = 4096 function isObject (value) { return value !== null && typeof value === 'object' && !Array.isArray(value) } function assertObject (value, message) { if (!isObject(value)) { throw new Error(message) } } function assertHasKeys (value, keys, endpoint) { for (const key of keys) { if (!Object.hasOwn(value, key)) { throw new Error(`Invalid ${endpoint} response: missing ${key}`) } } } function getAttributes (response, endpoint) { const attributes = response?.data?.attributes assertObject(attributes, `Invalid ${endpoint} response: attributes must be an object`) return attributes } function validateSettingsResponse (response, options = {}) { const attributes = response?.data?.attributes ?? response assertObject(attributes, 'Invalid settings response: attributes must be an object') assertHasKeys(attributes, REQUIRED_SETTINGS_KEYS, 'settings') assertObject(attributes.early_flake_detection, 'Invalid settings response: early_flake_detection must be an object') assertObject(attributes.test_management, 'Invalid settings response: test_management must be an object') assertHasKeys(attributes.early_flake_detection, ['enabled'], 'settings early_flake_detection') assertHasKeys(attributes.test_management, ['enabled'], 'settings test_management') if (!options.validationMode) return assertOnlyKeys(attributes, REQUIRED_SETTINGS_KEYS, 'settings') assertOnlyKeys(attributes.early_flake_detection, EARLY_FLAKE_DETECTION_KEYS, 'settings early_flake_detection') assertOnlyKeys(attributes.test_management, TEST_MANAGEMENT_KEYS, 'settings test_management') assertBooleanFields(attributes, SETTINGS_BOOLEAN_KEYS, 'settings') assertBooleanFields(attributes.early_flake_detection, ['enabled'], 'settings early_flake_detection') assertBooleanFields(attributes.test_management, ['enabled'], 'settings test_management') assertOptionalBoundedNumber( attributes.early_flake_detection.faulty_session_threshold, 'settings early_flake_detection faulty_session_threshold' ) assertRetryMap(attributes.early_flake_detection.slow_test_retries) assertOptionalBoundedNumber( attributes.test_management.attempt_to_fix_retries, 'settings test_management attempt_to_fix_retries', { integer: true } ) } function validateKnownTestsResponse (response, options = {}) { const attributes = getAttributes(response, 'known tests') assertHasKeys(attributes, ['tests'], 'known tests') const { tests } = attributes if (tests === null) return assertObject(tests, 'Invalid known tests response: tests must be an object or null') let suiteCount = 0 let testCount = 0 const modules = Object.entries(tests) if (options.validationMode && modules.length > MAX_VALIDATION_MODULES) { throw new Error('Invalid known tests response: too many modules') } for (const [moduleName, suites] of modules) { if (options.validationMode) assertValidationString(moduleName, 'known tests module') assertObject(suites, 'Invalid known tests response: module suites must be objects') for (const [suiteName, testNames] of Object.entries(suites)) { suiteCount++ if (options.validationMode) { if (suiteCount > MAX_VALIDATION_SUITES) throw new Error('Invalid known tests response: too many suites') assertValidationString(suiteName, 'known tests suite') } if (!Array.isArray(testNames)) { throw new TypeError('Invalid known tests response: suite tests must be arrays') } for (const testName of testNames) { testCount++ if (typeof testName !== 'string') { throw new TypeError('Invalid known tests response: test names must be strings') } if (options.validationMode) { if (testCount > MAX_VALIDATION_TESTS) throw new Error('Invalid known tests response: too many tests') assertValidationString(testName, 'known test name') } } } } } function validateSkippableTestsResponse (response, options = {}) { assertObject(response, 'Invalid skippable tests response: response must be an object') if (!Array.isArray(response.data)) { throw new TypeError('Invalid skippable tests response: data must be an array') } if (options.validationMode && response.data.length > MAX_VALIDATION_TESTS) { throw new Error('Invalid skippable tests response: too many items') } if (response.meta !== undefined) { assertObject(response.meta, 'Invalid skippable tests response: meta must be an object') } if (response.meta?.coverage !== undefined) { assertObject(response.meta.coverage, 'Invalid skippable tests response: meta.coverage must be an object') } if (response.meta?.correlation_id !== undefined && typeof response.meta.correlation_id !== 'string') { throw new Error('Invalid skippable tests response: meta.correlation_id must be a string') } for (const item of response.data) { assertObject(item, 'Invalid skippable tests response: data entries must be objects') if (typeof item.type !== 'string') { throw new TypeError('Invalid skippable tests response: data entry type must be a string') } if (options.validationMode) assertValidationString(item.type, 'skippable test type') assertObject(item.attributes, 'Invalid skippable tests response: data entry attributes must be an object') if ((item.type === 'suite' || item.type === 'test') && typeof item.attributes.suite !== 'string') { throw new Error('Invalid skippable tests response: data entry suite must be a string') } if (options.validationMode && item.attributes.suite !== undefined) { assertValidationString(item.attributes.suite, 'skippable test suite') } if (item.type === 'test' && typeof item.attributes.name !== 'string') { throw new Error('Invalid skippable tests response: data entry name must be a string') } if (options.validationMode && item.attributes.name !== undefined) { assertValidationString(item.attributes.name, 'skippable test name') } } } function validateTestManagementTestsResponse (response, options = {}) { const attributes = getAttributes(response, 'test management tests') assertHasKeys(attributes, ['modules'], 'test management tests') assertObject(attributes.modules, 'Invalid test management tests response: modules must be an object') let suiteCount = 0 let testCount = 0 const modules = Object.entries(attributes.modules) if (options.validationMode && modules.length > MAX_VALIDATION_MODULES) { throw new Error('Invalid test management tests response: too many modules') } for (const [moduleName, testModule] of modules) { if (options.validationMode) assertValidationString(moduleName, 'test management module') assertObject(testModule, 'Invalid test management tests response: modules entries must be objects') assertObject(testModule.suites, 'Invalid test management tests response: module suites must be objects') for (const [suiteName, suite] of Object.entries(testModule.suites)) { suiteCount++ if (options.validationMode) { if (suiteCount > MAX_VALIDATION_SUITES) { throw new Error('Invalid test management tests response: too many suites') } assertValidationString(suiteName, 'test management suite') } assertObject(suite, 'Invalid test management tests response: suites entries must be objects') assertObject(suite.tests, 'Invalid test management tests response: suite tests must be objects') for (const [testName, test] of Object.entries(suite.tests)) { testCount++ if (options.validationMode) { if (testCount > MAX_VALIDATION_TESTS) { throw new Error('Invalid test management tests response: too many tests') } assertValidationString(testName, 'test management test') } assertObject(test, 'Invalid test management tests response: tests entries must be objects') assertObject(test.properties, 'Invalid test management tests response: test properties must be objects') } } } } function assertBooleanFields (object, keys, endpoint) { for (const key of keys) { if (typeof object[key] !== 'boolean') { throw new TypeError(`Invalid ${endpoint} response: ${key} must be a boolean`) } } } function assertOptionalBoundedNumber (value, endpoint, { integer = false } = {}) { if (value === undefined) return if (!Number.isFinite(value) || (integer && !Number.isInteger(value)) || value < 0 || value > MAX_VALIDATION_RETRIES) { throw new TypeError(`Invalid ${endpoint}: value must be between 0 and ${MAX_VALIDATION_RETRIES}`) } } function assertRetryMap (retries) { if (retries === undefined) return assertObject(retries, 'Invalid settings response: early_flake_detection slow_test_retries must be an object') for (const [threshold, count] of Object.entries(retries)) { assertValidationString(threshold, 'early flake detection retry threshold') if (!RETRY_THRESHOLD_PATTERN.test(threshold)) { throw new TypeError('Invalid early flake detection retry threshold: expected a duration such as 5s') } assertOptionalBoundedNumber(count, `early flake detection retry count ${threshold}`, { integer: true }) } } function assertOnlyKeys (object, keys, endpoint) { const allowed = new Set(keys) for (const key of Object.keys(object)) { if (!allowed.has(key)) { throw new Error(`Invalid ${endpoint} response: unexpected ${key}`) } } } function assertValidationString (value, field) { if (typeof value !== 'string' || Buffer.byteLength(value) > MAX_VALIDATION_STRING_BYTES) { throw new TypeError(`Invalid ${field}: value must be a bounded string`) } } module.exports = { validateKnownTestsResponse, validateSettingsResponse, validateSkippableTestsResponse, validateTestManagementTestsResponse, }