readProjectFile('web/js/core/app-telemetry.js'); $ingest = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/FrontendTelemetryIngestService.php'); $settings = $this->readProjectFile('lib/Service/Settings/SettingsFrontendTelemetryGateway.php'); $jsMap = $this->parseJsEventKeyToTypeMap($js); $phpMap = $this->parsePhpEventTypeToKeyMap($ingest); $expectedPhpMap = array_flip($jsMap); ksort($expectedPhpMap); ksort($phpMap); $this->assertSame($expectedPhpMap, $phpMap); $settingsAllowedEvents = $this->parsePhpStringArrayConstant( $settings, 'FRONTEND_TELEMETRY_ALLOWED_EVENTS' ); sort($settingsAllowedEvents, SORT_STRING); $expectedAllowedEvents = array_keys($jsMap); sort($expectedAllowedEvents, SORT_STRING); $this->assertSame($expectedAllowedEvents, $settingsAllowedEvents); } public function testAllowedMetaKeysStayInSyncAcrossFrontendAndBackend(): void { $js = $this->readProjectFile('web/js/core/app-telemetry.js'); $ingest = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/FrontendTelemetryIngestService.php'); $jsKeys = $this->parseJsStringSet($js, 'ALLOWED_META_KEYS'); $phpKeys = $this->parsePhpStringArrayConstant($ingest, 'ALLOWED_META_KEYS'); sort($jsKeys, SORT_STRING); sort($phpKeys, SORT_STRING); $this->assertSame($jsKeys, $phpKeys); } public function testLengthLimitsStayInSyncAcrossFrontendAndBackend(): void { $js = $this->readProjectFile('web/js/core/app-telemetry.js'); $ingest = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/FrontendTelemetryIngestService.php'); $this->assertSame( $this->parseJsIntConstant($js, 'MAX_MESSAGE_LENGTH'), $this->parsePhpIntConstant($ingest, 'MESSAGE_MAX_LENGTH') ); $this->assertSame( $this->parseJsIntConstant($js, 'MAX_FINGERPRINT_LENGTH'), $this->parsePhpIntConstant($ingest, 'FINGERPRINT_MAX_LENGTH') ); $this->assertSame( $this->parseJsIntConstant($js, 'MAX_META_ENTRIES'), $this->parsePhpIntConstant($ingest, 'META_MAX_ENTRIES') ); $this->assertSame( $this->parseJsIntConstant($js, 'MAX_META_VALUE_LENGTH'), $this->parsePhpIntConstant($ingest, 'META_VALUE_MAX_LENGTH') ); } /** * @return array */ private function parseJsEventKeyToTypeMap(string $content): array { if (preg_match('/const\s+EVENT_KEY_TO_TYPE\s*=\s*Object\.freeze\(\{(.*?)\}\);/s', $content, $match) !== 1) { $this->fail('Could not parse EVENT_KEY_TO_TYPE from JS telemetry module.'); } $map = []; preg_match_all('/([a-z_]+)\s*:\s*\'([^\']+)\'/', $match[1], $pairs, PREG_SET_ORDER); foreach ($pairs as $pair) { $map[$pair[1]] = $pair[2]; } $this->assertNotSame([], $map, 'EVENT_KEY_TO_TYPE map is empty.'); return $map; } /** * @return array */ private function parsePhpEventTypeToKeyMap(string $content): array { $constValues = []; preg_match_all('/private const\s+([A-Z0-9_]+)\s*=\s*\'([^\']+)\';/', $content, $constants, PREG_SET_ORDER); foreach ($constants as $constant) { $constValues[$constant[1]] = $constant[2]; } if (preg_match('/private const EVENT_KEY_MAP = \[(.*?)\];/s', $content, $match) !== 1) { $this->fail('Could not parse EVENT_KEY_MAP from ingest service.'); } $map = []; preg_match_all('/self::([A-Z0-9_]+)\s*=>\s*\'([a-z_]+)\'/', $match[1], $pairs, PREG_SET_ORDER); foreach ($pairs as $pair) { $eventConstant = $pair[1]; $eventType = $constValues[$eventConstant] ?? ''; $this->assertNotSame('', $eventType, 'Missing event constant for EVENT_KEY_MAP entry: ' . $eventConstant); $map[$eventType] = $pair[2]; } $this->assertNotSame([], $map, 'EVENT_KEY_MAP is empty.'); return $map; } /** * @return list */ private function parseJsStringSet(string $content, string $constantName): array { $pattern = '/const\s+' . preg_quote($constantName, '/') . '\s*=\s*new Set\(\[(.*?)\]\);/s'; if (preg_match($pattern, $content, $match) !== 1) { $this->fail('Could not parse JS Set constant: ' . $constantName); } $values = []; preg_match_all('/\'([^\']+)\'/', $match[1], $items, PREG_SET_ORDER); foreach ($items as $item) { $values[] = $item[1]; } return array_values(array_unique($values)); } /** * @return list */ private function parsePhpStringArrayConstant(string $content, string $constantName): array { $pattern = '/private const\s+' . preg_quote($constantName, '/') . '\s*=\s*\[(.*?)\];/s'; if (preg_match($pattern, $content, $match) !== 1) { $this->fail('Could not parse PHP array constant: ' . $constantName); } $values = []; preg_match_all('/\'([^\']+)\'/', $match[1], $items, PREG_SET_ORDER); foreach ($items as $item) { $values[] = $item[1]; } return array_values(array_unique($values)); } private function parseJsIntConstant(string $content, string $constantName): int { $pattern = '/const\s+' . preg_quote($constantName, '/') . '\s*=\s*([0-9]+)\s*;/'; if (preg_match($pattern, $content, $match) !== 1) { $this->fail('Could not parse JS integer constant: ' . $constantName); } return (int) $match[1]; } private function parsePhpIntConstant(string $content, string $constantName): int { $pattern = '/private const\s+' . preg_quote($constantName, '/') . '\s*=\s*([0-9]+)\s*;/'; if (preg_match($pattern, $content, $match) !== 1) { $this->fail('Could not parse PHP integer constant: ' . $constantName); } return (int) $match[1]; } }