diff --git a/src/Plugin/EmulateElastic/OpenSearchDashboards/Handler.php b/src/Plugin/EmulateElastic/OpenSearchDashboards/Handler.php new file mode 100644 index 00000000..bb6c141e --- /dev/null +++ b/src/Plugin/EmulateElastic/OpenSearchDashboards/Handler.php @@ -0,0 +1,362 @@ +payload, $this->manticoreClient] + )->run(); + } + + /** + * Handle search requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleSearch(Payload $payload, Client $manticoreClient): TaskResult { + $request = json_decode($payload->body, true); + if (!is_array($request)) { + $request = []; + } + + $query = 'SELECT * FROM ' . $payload->table; + + if (isset($request['query'])) { + $query .= ' WHERE MATCH(\'@* ' . addslashes(json_encode($request['query'])) . '\')'; + } + + $size = $request['size'] ?? 10; + $query .= ' LIMIT ' . (int)$size; + + $result = $manticoreClient->sendRequest($query); + $data = $result->getResult()[0]['data'] ?? []; + + // Build OpenSearch response format + $response = [ + 'took' => 0, + 'timed_out' => false, + '_shards' => [ + 'total' => 1, + 'successful' => 1, + 'skipped' => 0, + 'failed' => 0, + ], + 'hits' => [ + 'total' => [ + 'value' => count($data), + 'relation' => 'eq', + ], + 'max_score' => 1.0, + 'hits' => [], + ], + ]; + + foreach ($data as $row) { + $response['hits']['hits'][] = [ + '_index' => $payload->table, + '_type' => '_doc', + '_id' => $row['id'] ?? uniqid(), + '_score' => 1.0, + '_source' => $row, + ]; + } + + return TaskResult::raw($response); + } + + /** + * Handle cat requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleCat(Payload $payload, Client $manticoreClient): TaskResult { + $query = 'SHOW TABLES'; + $result = $manticoreClient->sendRequest($query); + $data = $result->getResult()[0]['data'] ?? []; + + $response = []; + foreach ($data as $row) { + $tableName = $row['Index'] ?? ''; + $response[] = $tableName . "\t" . "open\t" . "1\t" . "1\t" . "0\t" . "0\t" . "0b"; + } + + return TaskResult::raw(implode("\n", $response)); + } + + /** + * Handle count requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleCount(Payload $payload, Client $manticoreClient): TaskResult { + $query = 'SELECT COUNT(*) as count FROM ' . $payload->table; + $result = $manticoreClient->sendRequest($query); + $data = $result->getResult()[0]['data'] ?? []; + $count = $data[0]['count'] ?? 0; + + $response = [ + 'count' => (int)$count, + '_shards' => [ + 'total' => 1, + 'successful' => 1, + 'skipped' => 0, + 'failed' => 0, + ], + ]; + + return TaskResult::raw($response); + } + + /** + * Handle license requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleLicense(Payload $payload, Client $manticoreClient): TaskResult { + $response = [ + 'license' => [ + 'status' => 'active', + 'uid' => '12345678-1234-1234-1234-123456789012', + 'type' => 'basic', + 'issue_date' => '2024-01-01T00:00:00.000Z', + 'issue_date_in_millis' => 1704067200000, + 'expiry_date' => '2025-01-01T00:00:00.000Z', + 'expiry_date_in_millis' => 1735689600000, + 'max_nodes' => 1000, + 'issued_to' => 'Manticore Search', + 'issuer' => 'Manticore Software', + 'start_date_in_millis' => 1704067200000, + ], + ]; + + return TaskResult::raw($response); + } + + /** + * Handle nodes requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleNodes(Payload $payload, Client $manticoreClient): TaskResult { + $response = [ + '_nodes' => [ + 'total' => 1, + 'successful' => 1, + 'failed' => 0, + ], + 'cluster_name' => 'manticore-cluster', + 'nodes' => [ + 'node-1' => [ + 'name' => 'manticore-node', + 'transport_address' => '127.0.0.1:9300', + 'host' => '127.0.0.1', + 'ip' => '127.0.0.1', + 'version' => '8.0.0', + 'build_hash' => 'abcdef123456', + 'roles' => ['data', 'ingest', 'master'], + 'attributes' => [], + 'os' => [ + 'name' => 'Linux', + 'arch' => 'x86_64', + 'version' => '5.4.0', + ], + 'process' => [ + 'refresh_interval_in_millis' => 1000, + 'id' => 12345, + ], + 'jvm' => [ + 'pid' => 12345, + 'version' => '11.0.0', + 'vm_name' => 'OpenJDK 64-Bit Server VM', + 'vm_version' => '11.0.0', + 'vm_vendor' => 'Oracle Corporation', + ], + ], + ], + ]; + + return TaskResult::raw($response); + } + + /** + * Handle xpack requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleXpack(Payload $payload, Client $manticoreClient): TaskResult { + $response = [ + 'build' => [ + 'hash' => 'abcdef123456', + 'date' => '2024-01-01T00:00:00.000Z', + ], + 'license' => [ + 'type' => 'basic', + 'mode' => 'basic', + 'status' => 'active', + ], + 'features' => [ + 'aggregations' => [ + 'available' => true, + 'enabled' => true, + ], + 'analytics' => [ + 'available' => true, + 'enabled' => true, + ], + 'ccr' => [ + 'available' => false, + 'enabled' => false, + ], + 'data_frame' => [ + 'available' => false, + 'enabled' => false, + ], + 'data_science' => [ + 'available' => false, + 'enabled' => false, + ], + 'graph' => [ + 'available' => false, + 'enabled' => false, + ], + 'ilm' => [ + 'available' => false, + 'enabled' => false, + ], + 'logstash' => [ + 'available' => false, + 'enabled' => false, + ], + 'ml' => [ + 'available' => false, + 'enabled' => false, + ], + 'monitoring' => [ + 'available' => false, + 'enabled' => false, + ], + 'rollup' => [ + 'available' => false, + 'enabled' => false, + ], + 'security' => [ + 'available' => false, + 'enabled' => false, + ], + 'sql' => [ + 'available' => true, + 'enabled' => true, + ], + 'watcher' => [ + 'available' => false, + 'enabled' => false, + ], + ], + 'tagline' => 'You know, for search', + ]; + + return TaskResult::raw($response); + } + + /** + * Handle OpenSearch Dashboards requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleOpenSearchDashboards(Payload $payload, Client $manticoreClient): TaskResult { + $response = [ + '_index' => $payload->table, + '_type' => '_doc', + '_id' => 'config:1.0.0', + '_version' => 1, + 'found' => true, + '_source' => [ + 'type' => 'config', + 'updated_at' => '2024-01-01T00:00:00.000Z', + 'config' => [ + 'buildNum' => 1000, + 'defaultIndex' => 'manticore-index', + ], + ], + ]; + + return TaskResult::raw($response); + } + + /** + * Handle default requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleDefault(Payload $payload, Client $manticoreClient): TaskResult { + $response = [ + 'acknowledged' => true, + 'status' => 'ok', + ]; + + return TaskResult::raw($response); + } +} \ No newline at end of file diff --git a/src/Plugin/EmulateElastic/OpenSearchDashboards/Payload.php b/src/Plugin/EmulateElastic/OpenSearchDashboards/Payload.php new file mode 100644 index 00000000..c8c7fbde --- /dev/null +++ b/src/Plugin/EmulateElastic/OpenSearchDashboards/Payload.php @@ -0,0 +1,183 @@ + + */ +final class Payload extends BasePayload { + + /** @var string $table */ + public string $table = ''; + + /** @var string $body */ + public string $body = ''; + + /** @var string $path */ + public string $path; + + /** @var string $requestTarget */ + public static string $requestTarget; + + /** + * Get description for this plugin + * @return string + */ + public static function getInfo(): string { + return 'Emulates OpenSearch Dashboards queries and generates responses' + . ' as if they were made by OpenSearch'; + } + + /** + * @param Request $request + * @return static + */ + public static function fromRequest(Request $request): static { + $self = new static(); + $pathParts = explode('/', ltrim($request->path, '/')); + $self->path = $request->path; + self::detectRequestTarget($pathParts, $self); + + // Set body for search requests + if (static::$requestTarget === '_search' || + in_array(static::$requestTarget, ['_doc', '_create', '_update', '_bulk'])) { + $self->body = $request->payload; + } + + $self->table = self::extractTableFromPath($pathParts); + return $self; + } + + /** + * @param Request $request + * @return bool + */ + public static function hasMatch(Request $request): bool { + $pathParts = explode('/', ltrim($request->path, '/')); + $pathParts = array_filter($pathParts); + $pathParts = array_values($pathParts); + + if (empty($pathParts)) { + return false; + } + + $opensearchDashboardsEndpoints = [ + '.opensearch_dashboards', + '.opensearch_dashboards_task_manager', + '_search', + '_cat', + '_count', + '_license', + '_nodes', + '_xpack', + '_update_by_query', + 'metric', + 'config', + 'space', + 'index-pattern', + 'settings', + 'telemetry', + 'stats' + ]; + + // Check if the first path part matches any OpenSearch Dashboards endpoint + if (in_array($pathParts[0], $opensearchDashboardsEndpoints)) { + return true; + } + + if (isset($pathParts[1]) && in_array($pathParts[1], ['_doc', '_create', '_update', '_search'])) { + return true; + } + + if (isset($pathParts[1]) && $pathParts[1] === '_field_caps') { + return true; + } + + return false; + } + + /** + * Detect the request target from path parts + * @param array $pathParts + * @param Payload $self + * @return void + */ + protected static function detectRequestTarget(array $pathParts, Payload $self): void { + if (empty($pathParts)) { + static::$requestTarget = ''; + return; + } + + $firstPart = $pathParts[0]; + $secondPart = $pathParts[1] ?? ''; + + if (in_array($firstPart, ['.opensearch_dashboards', '.opensearch_dashboards_task_manager'])) { + static::$requestTarget = $firstPart; + return; + } + + if (in_array($firstPart, ['_cat', '_count', '_license', '_nodes', '_xpack', '_search'])) { + static::$requestTarget = $firstPart; + return; + } + + if (in_array($secondPart, ['_doc', '_create', '_update', '_search'])) { + static::$requestTarget = $secondPart; + return; + } + + if ($secondPart === '_field_caps') { + static::$requestTarget = '_field_caps'; + return; + } + + // Default to the first part + static::$requestTarget = $firstPart; + } + + /** + * Extract table name from path parts + * @param array $pathParts + * @return string + */ + protected static function extractTableFromPath(array $pathParts): string { + if (empty($pathParts)) { + return ''; + } + + if (in_array($pathParts[0], ['.opensearch_dashboards', '.opensearch_dashboards_task_manager'])) { + return $pathParts[0]; + } + + if (isset($pathParts[1]) && in_array($pathParts[1], ['_doc', '_create', '_update', '_search'])) { + return $pathParts[0]; + } + + if (isset($pathParts[1]) && $pathParts[1] === '_field_caps') { + return $pathParts[0]; + } + + return $pathParts[0]; + } + + /** + * Get handler class name for this payload + * @return string + */ + public function getHandlerClassName(): string { + return Handler::class; + } +} \ No newline at end of file diff --git a/src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMap/OpenSearchDashboards.php b/src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMap/OpenSearchDashboards.php new file mode 100644 index 00000000..92ad8162 --- /dev/null +++ b/src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMap/OpenSearchDashboards.php @@ -0,0 +1,442 @@ + [ + '_id' => 'space:default', + '_index' => '.opensearch_dashboards_2', + '_primary_term' => 1, + '_seq_no' => 0, + '_source' => [ + 'migrationVersion' => [ + 'space' => '6.6.0', + ], + 'references' => [], + 'space' => [ + '_reserved' => true, + 'color' => '#00bfb3', + 'description' => 'This is your default space!', + 'disabledFeatures' => [], + 'name' => 'Default', + ], + 'type' => 'space', + 'updated_at' => '2024-05-27T13:55:01.278Z', + ], + '_type' => '_doc', + '_version' => 1, + 'found' => true, + ], + '.opensearch_dashboards/_doc/index-pattern' => [ + '_index' => '.opensearch_dashboards_2', + '_primary_term' => 1, + '_seq_no' => 0, + '_shards' => [ + 'failed' => 0, + 'successful' => 1, + 'total' => 1, + ], + '_type' => '_doc', + '_version' => 1, + 'result' => 'created', + ], + '.opensearch_dashboards/_doc/config%3A1.0.0' => [ + '_id' => 'config:1.0.0', + '_index' => '.opensearch_dashboards_2', + '_primary_term' => 1, + '_seq_no' => 0, + '_source' => [ + 'config' => [ + 'buildNum' => 1000, + 'defaultIndex' => 'manticore-index', + ], + 'references' => [], + 'type' => 'config', + 'updated_at' => '2024-05-27T13:55:27.747Z', + ], + '_type' => '_doc', + '_version' => 2, + 'found' => true, + ], + '.opensearch_dashboards' => [ + 'mappings' => [ + '_meta' => [ + 'migrationMappingPropertyHashes' => [ + 'action' => 'c0c235fba02ebd2a2412bcda79009b58', + 'action_task_params' => 'a9d49f184ee89641044be0ca2950fa3a', + 'alert' => 'e588043a01d3d43477e7cad7efa0f5d8', + 'apm-indices' => '9bb9b2bf1fa636ed8619cbab5ce6a1dd', + 'apm-services-telemetry' => '07ee1939fa4302c62ddc052ec03fed90', + 'canvas-element' => '7390014e1091044523666d97247392fc', + 'canvas-workpad' => 'b0a1706d356228dbdcb4a17e6b9eb231', + 'config' => '87aca8fdb053154f11383fce3dbf3edf', + 'dashboard' => 'd00f614b29a80360e1190193fd333bab', + 'file-upload-telemetry' => '0ed4d3e1983d1217a30982630897092e', + 'graph-workspace' => 'cd7ba1330e6682e9cc00b78850874be1', + 'index-pattern' => '66eccb05066c5a89924f48a9e9736499', + 'infrastructure-ui-source' => 'ddc0ecb18383f6b26101a2fadb2dab0c', + 'inventory-view' => '84b320fd67209906333ffce261128462', + 'kql-telemetry' => 'd12a98a6f19a2d273696597547e064ee', + 'lens' => '21c3ea0763beb1ecb0162529706b88c5', + 'lens-ui-telemetry' => '509bfa5978586998e05f9e303c07a327', + 'map' => '23d7aa4a720d4938ccde3983f87bd58d', + 'maps-telemetry' => '268da3a48066123fc5baf35abaa55014', + 'metrics-explorer-view' => '53c5365793677328df0ccb6138bf3cdd', + 'migrationVersion' => '4a1746014a75ade3a714e1db5763276f', + 'ml-telemetry' => '257fd1d4b4fdbb9cb4b8a3b27da201e9', + 'namespace' => '2f4316de49999235636386fe51dc06c1', + 'query' => '11aaeb7f5f7fa5bb43f25e18ce26e7d9', + 'references' => '7997cf5a56cc02bdc9c93361bde732b0', + 'space' => '4a1746014a75ade3a714e1db5763276f', + 'telemetry' => '257fd1d4b4fdbb9cb4b8a3b27da201e9', + 'url' => '11aaeb7f5f7fa5bb43f25e18ce26e7d9', + 'visualization' => 'd00f614b29a80360e1190193fd333bab', + ], + ], + 'properties' => [ + 'action' => [ + 'properties' => [ + 'actionTypeId' => [ + 'type' => 'keyword', + ], + 'config' => [ + 'type' => 'object', + ], + 'name' => [ + 'type' => 'keyword', + ], + 'secrets' => [ + 'type' => 'object', + ], + ], + ], + 'action_task_params' => [ + 'properties' => [ + 'actionId' => [ + 'type' => 'keyword', + ], + 'apiKey' => [ + 'type' => 'keyword', + ], + 'consumer' => [ + 'type' => 'keyword', + ], + 'executionId' => [ + 'type' => 'keyword', + ], + 'params' => [ + 'type' => 'object', + ], + 'relatedSavedObjects' => [ + 'properties' => [ + 'id' => [ + 'type' => 'keyword', + ], + 'namespace' => [ + 'type' => 'keyword', + ], + 'type' => [ + 'type' => 'keyword', + ], + 'typeId' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'source' => [ + 'type' => 'keyword', + ], + 'taskId' => [ + 'type' => 'keyword', + ], + ], + ], + 'alert' => [ + 'properties' => [ + 'actions' => [ + 'properties' => [ + 'actionRef' => [ + 'type' => 'keyword', + ], + 'actionTypeId' => [ + 'type' => 'keyword', + ], + 'group' => [ + 'type' => 'keyword', + ], + 'params' => [ + 'type' => 'object', + ], + ], + 'type' => 'object', + ], + 'alertTypeId' => [ + 'type' => 'keyword', + ], + 'apiKey' => [ + 'type' => 'keyword', + ], + 'apiKeyOwner' => [ + 'type' => 'keyword', + ], + 'consumer' => [ + 'type' => 'keyword', + ], + 'createdAt' => [ + 'type' => 'date', + ], + 'createdBy' => [ + 'type' => 'keyword', + ], + 'enabled' => [ + 'type' => 'boolean', + ], + 'executionStatus' => [ + 'properties' => [ + 'error' => [ + 'properties' => [ + 'message' => [ + 'type' => 'keyword', + ], + 'reason' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'lastExecutionDate' => [ + 'type' => 'date', + ], + 'status' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'meta' => [ + 'properties' => [ + 'versionApiKeyLastmodified' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'muteAll' => [ + 'type' => 'boolean', + ], + 'mutedInstanceIds' => [ + 'type' => 'keyword', + ], + 'name' => [ + 'type' => 'keyword', + ], + 'params' => [ + 'type' => 'object', + ], + 'schedule' => [ + 'properties' => [ + 'interval' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'scheduledTaskId' => [ + 'type' => 'keyword', + ], + 'tags' => [ + 'type' => 'keyword', + ], + 'throttle' => [ + 'type' => 'keyword', + ], + 'updatedAt' => [ + 'type' => 'date', + ], + 'updatedBy' => [ + 'type' => 'keyword', + ], + ], + ], + 'config' => [ + 'properties' => [ + 'buildNum' => [ + 'type' => 'long', + ], + 'defaultIndex' => [ + 'type' => 'keyword', + ], + ], + ], + 'dashboard' => [ + 'properties' => [ + 'hits' => [ + 'type' => 'long', + ], + 'kibanaSavedObjectMeta' => [ + 'properties' => [ + 'searchSourceJSON' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'optionsJSON' => [ + 'type' => 'keyword', + ], + 'panelsJSON' => [ + 'type' => 'keyword', + ], + 'refs' => [ + 'properties' => [ + 'id' => [ + 'type' => 'keyword', + ], + 'name' => [ + 'type' => 'keyword', + ], + 'type' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'title' => [ + 'type' => 'keyword', + ], + 'version' => [ + 'type' => 'long', + ], + ], + ], + 'index-pattern' => [ + 'properties' => [ + 'fieldFormatMap' => [ + 'type' => 'keyword', + ], + 'fields' => [ + 'type' => 'keyword', + ], + 'intervalName' => [ + 'type' => 'keyword', + ], + 'notExpandable' => [ + 'type' => 'boolean', + ], + 'references' => [ + 'type' => 'object', + ], + 'sourceFilters' => [ + 'type' => 'keyword', + ], + 'timeFieldName' => [ + 'type' => 'keyword', + ], + 'title' => [ + 'type' => 'keyword', + ], + ], + ], + 'space' => [ + 'properties' => [ + '_reserved' => [ + 'type' => 'boolean', + ], + 'color' => [ + 'type' => 'keyword', + ], + 'description' => [ + 'type' => 'keyword', + ], + 'disabledFeatures' => [ + 'type' => 'keyword', + ], + 'imageUrl' => [ + 'type' => 'keyword', + ], + 'name' => [ + 'type' => 'keyword', + ], + ], + ], + 'url' => [ + 'properties' => [ + 'accessCount' => [ + 'type' => 'long', + ], + 'accessDate' => [ + 'type' => 'date', + ], + 'createDate' => [ + 'type' => 'date', + ], + 'url' => [ + 'type' => 'keyword', + ], + ], + ], + 'visualization' => [ + 'properties' => [ + 'hits' => [ + 'type' => 'long', + ], + 'kibanaSavedObjectMeta' => [ + 'properties' => [ + 'searchSourceJSON' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'optionsJSON' => [ + 'type' => 'keyword', + ], + 'refs' => [ + 'properties' => [ + 'id' => [ + 'type' => 'keyword', + ], + 'name' => [ + 'type' => 'keyword', + ], + 'type' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'savedSearchRefName' => [ + 'type' => 'keyword', + ], + 'title' => [ + 'type' => 'keyword', + ], + 'uiStateJSON' => [ + 'type' => 'keyword', + ], + 'version' => [ + 'type' => 'long', + ], + 'visState' => [ + 'type' => 'keyword', + ], + ], + ], + ], + ], + 'settings' => [ + 'index' => [ + 'number_of_shards' => 1, + 'number_of_replicas' => 0, + ], + ], + ], +]; \ No newline at end of file diff --git a/src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMapLoaderTrait.php b/src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMapLoaderTrait.php new file mode 100644 index 00000000..c01f0222 --- /dev/null +++ b/src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMapLoaderTrait.php @@ -0,0 +1,65 @@ + */ + private static array $queryMaps = []; + + /** + * Load query map for a specific path + * @param string $path + * @return array|null + */ + protected static function loadQueryMap(string $path): ?array { + if (empty(self::$queryMaps)) { + self::loadAllQueryMaps(); + } + + return self::$queryMaps[$path] ?? null; + } + + /** + * Load all query maps + * @return void + */ + private static function loadAllQueryMaps(): void { + $queryMapFiles = [ + __DIR__ . '/QueryMap/OpenSearchDashboards.php', + ]; + + foreach ($queryMapFiles as $file) { + if (file_exists($file)) { + $map = include $file; + if (is_array($map)) { + self::$queryMaps = array_merge(self::$queryMaps, $map); + } + } + } + } + + /** + * Get all loaded query maps + * @return array + */ + protected static function getAllQueryMaps(): array { + if (empty(self::$queryMaps)) { + self::loadAllQueryMaps(); + } + + return self::$queryMaps; + } +} \ No newline at end of file diff --git a/test/Plugin/EmulateElastic/OpenSearchDashboards/OpenSearchDashboardsTest.php b/test/Plugin/EmulateElastic/OpenSearchDashboards/OpenSearchDashboardsTest.php new file mode 100644 index 00000000..84f41905 --- /dev/null +++ b/test/Plugin/EmulateElastic/OpenSearchDashboards/OpenSearchDashboardsTest.php @@ -0,0 +1,96 @@ +path = '/test-index/_search'; + $searchRequest->payload = '{"query":{"match_all":{}}}'; + $this->assertTrue(Payload::hasMatch($searchRequest)); + + // Test cat requests + $catRequest = new Request(); + $catRequest->path = '/_cat/indices'; + $this->assertTrue(Payload::hasMatch($catRequest)); + + // Test count requests + $countRequest = new Request(); + $countRequest->path = '/test-index/_count'; + $this->assertTrue(Payload::hasMatch($countRequest)); + + // Test license requests + $licenseRequest = new Request(); + $licenseRequest->path = '/_license'; + $this->assertTrue(Payload::hasMatch($licenseRequest)); + + // Test nodes requests + $nodesRequest = new Request(); + $nodesRequest->path = '/_nodes'; + $this->assertTrue(Payload::hasMatch($nodesRequest)); + + // Test xpack requests + $xpackRequest = new Request(); + $xpackRequest->path = '/_xpack'; + $this->assertTrue(Payload::hasMatch($xpackRequest)); + + // Test OpenSearch Dashboards requests + $dashboardsRequest = new Request(); + $dashboardsRequest->path = '/.opensearch_dashboards/_doc/config:1.0.0'; + $this->assertTrue(Payload::hasMatch($dashboardsRequest)); + + // Test non-matching requests + $nonMatchingRequest = new Request(); + $nonMatchingRequest->path = '/some/other/path'; + $this->assertFalse(Payload::hasMatch($nonMatchingRequest)); + } + + /** + * Test payload creation from request + * @return void + */ + public function testFromRequest(): void { + $request = new Request(); + $request->path = '/test-index/_search'; + $request->payload = '{"query":{"match_all":{}},"size":10}'; + + $payload = Payload::fromRequest($request); + + $this->assertEquals('/test-index/_search', $payload->path); + $this->assertEquals('test-index', $payload->table); + $this->assertEquals('{"query":{"match_all":{}},"size":10}', $payload->body); + $this->assertEquals('_search', Payload::$requestTarget); + } + + /** + * Test plugin info + * @return void + */ + public function testGetInfo(): void { + $info = Payload::getInfo(); + $this->assertStringContainsString('OpenSearch Dashboards', $info); + $this->assertStringContainsString('OpenSearch', $info); + } +} \ No newline at end of file