summaryrefslogtreecommitdiff
path: root/src/__tests__
diff options
context:
space:
mode:
authorYuriy Andamasov <yuriy@andamasov.com>2026-05-23 17:49:28 +0300
committerGitHub <noreply@github.com>2026-05-23 17:49:28 +0300
commit422681512d98175eb558271589cf433c1c9a1020 (patch)
tree00ad5372e564a0c332f7dc26abe9836681f9361e /src/__tests__
parent49588447d6865c968143db7db1ab2b82b191e29e (diff)
parent5a47a44b717a912bd22d7e03921521cfec49074e (diff)
downloadphorge-elasticsearch-modern-production.tar.gz
phorge-elasticsearch-modern-production.zip
Merge pull request #16 from vyos/developmentHEADv0.1.0production
Release v0.1.0 — promote development to production
Diffstat (limited to 'src/__tests__')
-rw-r--r--src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php280
-rw-r--r--src/__tests__/VyOSElasticModernHostTestCase.php66
2 files changed, 346 insertions, 0 deletions
diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php
new file mode 100644
index 0000000..7e20fdc
--- /dev/null
+++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php
@@ -0,0 +1,280 @@
+<?php
+
+/**
+ * Test-double subclass of the concrete engine used by unit tests.
+ * Overrides host-dependent methods so tests run without a real
+ * PhabricatorSearchService; no actual Elasticsearch I/O occurs.
+ */
+final class VyOSElasticModernFulltextStorageEngineTestDouble
+ extends VyOSElasticModernFulltextStorageEngine {
+
+ public function reindexAbstractDocument(
+ PhabricatorSearchAbstractDocument $doc) {
+ // no-op in tests
+ }
+
+ public function executeSearch(PhabricatorSavedQuery $query) {
+ return array();
+ }
+
+ public function indexExists(?VyOSElasticModernHost $host = null) {
+ return false;
+ }
+
+ public function getIndexStats(?VyOSElasticModernHost $host = null) {
+ return array();
+ }
+
+}
+
+final class VyOSElasticModernFulltextStorageEngineTestCase
+ extends PhutilTestCase {
+
+ private function newEngine() {
+ return new VyOSElasticModernFulltextStorageEngineTestDouble();
+ }
+
+ public function testSetVersionAcceptsSevenAndAbove() {
+ foreach (array(7, 8, 9, 100) as $v) {
+ $engine = $this->newEngine();
+ $caught = null;
+ try {
+ $engine->setVersion($v);
+ } catch (Exception $e) {
+ $caught = $e;
+ }
+ $this->assertEqual(
+ null,
+ $caught,
+ pht('Expected no exception for version=%d.', $v));
+ $this->assertEqual($v, $engine->getVersion());
+ }
+ }
+
+ public function testSetVersionRejectsBelowSeven() {
+ foreach (array(0, 1, 2, 5, 6) as $v) {
+ $engine = $this->newEngine();
+ $caught = null;
+ try {
+ $engine->setVersion($v);
+ } catch (Exception $e) {
+ $caught = $e;
+ }
+ $this->assertTrue(
+ $caught !== null,
+ pht('Expected an exception for version=%d.', $v));
+ }
+ }
+
+ public function testGetDocumentUri() {
+ $engine = $this->newEngine()->setVersion(7);
+ $uri = $engine->getDocumentUri('TASK', 'PHID-TASK-abc');
+ $this->assertEqual('/_doc/PHID-TASK-abc', $uri);
+ }
+
+ public function testGetDocumentUriIgnoresType() {
+ // The typeless API does not encode the doc type in the URL.
+ $engine = $this->newEngine()->setVersion(7);
+ $uri_a = $engine->getDocumentUri('TASK', 'PHID-TASK-abc');
+ $uri_b = $engine->getDocumentUri('DREV', 'PHID-TASK-abc');
+ $this->assertEqual($uri_a, $uri_b);
+ }
+
+ public function testGetSearchUri() {
+ $engine = $this->newEngine()->setVersion(7);
+ $this->assertEqual(
+ '/_search',
+ $engine->getSearchUri(array('TASK', 'DREV')));
+ $this->assertEqual('/_search', $engine->getSearchUri(array('USER')));
+ $this->assertEqual('/_search', $engine->getSearchUri(array()));
+ }
+
+ public function testBuildTypeFilter() {
+ $engine = $this->newEngine()->setVersion(7);
+ $filter = $engine->buildTypeFilter(array('TASK', 'DREV'));
+ $expected = array(
+ 'terms' => array(
+ 'documentType' => array('TASK', 'DREV'),
+ ),
+ );
+ $this->assertEqual($expected, $filter);
+ }
+
+ public function testBuildTypeFilterEmpty() {
+ // Empty list still produces a filter; the caller is responsible
+ // for normalizing "no types specified" to "all indexable types"
+ // before calling this method. This matches the bundled engine's
+ // pattern (executeSearch() normalizes before URL construction).
+ $engine = $this->newEngine()->setVersion(7);
+ $filter = $engine->buildTypeFilter(array());
+ $this->assertEqual(
+ array('terms' => array('documentType' => array())),
+ $filter);
+ }
+
+ public function testBuildIndexMappingsShape() {
+ $engine = $this->newEngine()->setVersion(7);
+
+ $doc_types = array('TASK', 'DREV');
+ $fields = array('title', 'body', 'comment');
+ $relationships = array('authorPHID', 'projectPHID');
+ $mappings = $engine->buildIndexMappings(
+ $doc_types, $fields, $relationships, 'text');
+
+ // Single typeless mapping with one 'properties' block.
+ $this->assertTrue(isset($mappings['properties']));
+ $this->assertFalse(isset($mappings['TASK']));
+ $this->assertFalse(isset($mappings['DREV']));
+
+ // All three text fields have the multi-analyzer shape.
+ foreach (array('title', 'body', 'comment') as $field) {
+ $this->assertTrue(
+ isset($mappings['properties'][$field]),
+ pht('Field "%s" missing from mappings.', $field));
+ $this->assertEqual(
+ 'text',
+ $mappings['properties'][$field]['type'],
+ pht('Field "%s" should be type text.', $field));
+ $this->assertTrue(
+ isset($mappings['properties'][$field]['fields']['raw']),
+ pht('Field "%s" missing raw sub-field.', $field));
+ $this->assertTrue(
+ isset($mappings['properties'][$field]['fields']['keywords']),
+ pht('Field "%s" missing keywords sub-field.', $field));
+ $this->assertTrue(
+ isset($mappings['properties'][$field]['fields']['stems']),
+ pht('Field "%s" missing stems sub-field.', $field));
+ }
+
+ // Both relationships emit as keyword fields with doc_values:false.
+ foreach (array('authorPHID', 'projectPHID') as $rel) {
+ $this->assertEqual(
+ 'keyword',
+ $mappings['properties'][$rel]['type'],
+ pht('Relationship "%s" should be keyword type.', $rel));
+ $this->assertEqual(
+ false,
+ $mappings['properties'][$rel]['doc_values'],
+ pht('Relationship "%s" should have doc_values:false.', $rel));
+ $this->assertEqual(
+ 'date',
+ $mappings['properties'][$rel.'_ts']['type'],
+ pht('Relationship "%s" missing timestamp field.', $rel));
+ // No include_in_all anywhere.
+ $this->assertFalse(
+ isset($mappings['properties'][$rel]['include_in_all']),
+ pht('Relationship "%s" should not have include_in_all.', $rel));
+ }
+
+ // documentType is a keyword field inside properties.
+ $this->assertEqual(
+ 'keyword',
+ $mappings['properties']['documentType']['type']);
+
+ // Standard date fields present.
+ $this->assertEqual(
+ 'date',
+ $mappings['properties']['dateCreated']['type']);
+ $this->assertEqual(
+ 'date',
+ $mappings['properties']['lastModified']['type']);
+ }
+
+ public function testBuildIndexMappingsRejectsReservedFieldName() {
+ $engine = $this->newEngine()->setVersion(7);
+ $caught = null;
+ try {
+ $engine->buildIndexMappings(
+ array(), array('documentType'), array(), 'text');
+ } catch (Exception $e) {
+ $caught = $e;
+ }
+ $this->assertTrue(
+ $caught !== null,
+ pht('Expected exception when field name collides with reserved key.'));
+ }
+
+ public function testBuildIndexMappingsRejectsReservedRelationshipName() {
+ $engine = $this->newEngine()->setVersion(7);
+ $caught = null;
+ try {
+ $engine->buildIndexMappings(
+ array(), array(), array('lastModified'), 'text');
+ } catch (Exception $e) {
+ $caught = $e;
+ }
+ $this->assertTrue(
+ $caught !== null,
+ pht('Expected exception when relationship name collides with reserved key.'));
+ }
+
+ public function testBuildIndexMappingsRejectsFooTsClash() {
+ // A field named "foo_ts" would clash with the timestamp slot auto-generated
+ // for a relationship named "foo".
+ $engine = $this->newEngine()->setVersion(7);
+ $caught = null;
+ try {
+ $engine->buildIndexMappings(
+ array(), array('foo_ts'), array('foo'), 'text');
+ } catch (Exception $e) {
+ $caught = $e;
+ }
+ $this->assertTrue(
+ $caught !== null,
+ pht('Expected exception for field/relationship timestamp-slot collision.'));
+ }
+
+ public function testBuildIndexMappingsEmptyInputsYieldStandardFields() {
+ $engine = $this->newEngine()->setVersion(7);
+ $mappings = $engine->buildIndexMappings(array(), array(), array(), 'text');
+ $this->assertTrue(isset($mappings['properties']['documentType']));
+ $this->assertTrue(isset($mappings['properties']['dateCreated']));
+ $this->assertTrue(isset($mappings['properties']['lastModified']));
+ $this->assertEqual(
+ 'keyword', $mappings['properties']['documentType']['type']);
+ $this->assertEqual('date', $mappings['properties']['dateCreated']['type']);
+ $this->assertEqual('date', $mappings['properties']['lastModified']['type']);
+ }
+
+ public function testEngineIdentifier() {
+ $engine = $this->newEngine();
+ $this->assertEqual('elasticsearch-modern', $engine->getEngineIdentifier());
+ }
+
+ public function testHostType() {
+ $engine = $this->newEngine();
+ $host = $engine->getHostType();
+ $this->assertTrue($host instanceof VyOSElasticModernHost);
+ }
+
+ public function testGetTextFieldType() {
+ $engine = $this->newEngine()->setVersion(7);
+ $this->assertEqual('text', $engine->getTextFieldType());
+ }
+
+ public function testGetTimestampField() {
+ $engine = $this->newEngine()->setVersion(7);
+ $this->assertEqual('lastModified', $engine->getTimestampField());
+ }
+
+ public function testBuildSpecTreatsZeroQueryAsSearchTerm() {
+ // '0' is falsy in PHP but must be treated as a non-empty search term.
+ // strlen('0') == 1, so the query clause should be present and the
+ // default date-sorted path should NOT fire.
+ $engine = $this->newEngine()->setVersion(7);
+ $query = id(new PhabricatorSavedQuery())
+ ->setParameter('query', '0');
+
+ $method = new ReflectionMethod($engine, 'buildSpec');
+ $method->setAccessible(true);
+ $spec = $method->invoke($engine, $query, array('TASK'));
+
+ $this->assertFalse(isset($spec['sort']));
+ $this->assertEqual(
+ '0',
+ idxv(
+ $spec,
+ array('query', 'bool', 'must', 0, 'simple_query_string', 'query')));
+ }
+
+}
diff --git a/src/__tests__/VyOSElasticModernHostTestCase.php b/src/__tests__/VyOSElasticModernHostTestCase.php
new file mode 100644
index 0000000..8fcd9b6
--- /dev/null
+++ b/src/__tests__/VyOSElasticModernHostTestCase.php
@@ -0,0 +1,66 @@
+<?php
+
+final class VyOSElasticModernHostTestEngine
+ extends VyOSElasticModernFulltextStorageEngine {
+}
+
+final class VyOSElasticModernHostTestCase
+ extends PhutilTestCase {
+
+ public function testEngineIsConcreteForDispatch() {
+ // Must NOT be abstract: PhutilClassMapQuery → PhutilSymbolLoader::loadObjects()
+ // calls setConcreteOnly(true) which unsets abstract classes. A concrete (plain)
+ // class is required for cluster.search[].type:elasticsearch-modern dispatch.
+ $class = new ReflectionClass('VyOSElasticModernFulltextStorageEngine');
+ $this->assertFalse($class->isAbstract());
+ }
+
+ public function testDisplayName() {
+ $engine = new VyOSElasticModernHostTestEngine();
+ $host = new VyOSElasticModernHost($engine);
+ $this->assertEqual(
+ 'Elasticsearch (modern)',
+ (string)$host->getDisplayName());
+ }
+
+ public function testConfigDefaults() {
+ $engine = new VyOSElasticModernHostTestEngine();
+ $host = new VyOSElasticModernHost($engine);
+ $host->setConfig(array());
+ $this->assertEqual('http', $host->getProtocol());
+ $this->assertEqual('phabricator/', $host->getPath());
+ $this->assertEqual(7, $host->getVersion());
+ }
+
+ public function testConfigOverrides() {
+ $engine = new VyOSElasticModernHostTestEngine();
+ $host = new VyOSElasticModernHost($engine);
+ $host->setConfig(array(
+ 'host' => 'es.example.com',
+ 'port' => 9200,
+ 'protocol' => 'https',
+ 'path' => '/myphorge',
+ 'version' => 8,
+ ));
+ $this->assertEqual('es.example.com', $host->getHost());
+ $this->assertEqual(9200, $host->getPort());
+ $this->assertEqual('https', $host->getProtocol());
+ $this->assertEqual('/myphorge', $host->getPath());
+ $this->assertEqual(8, $host->getVersion());
+ }
+
+ public function testGetURI() {
+ $engine = new VyOSElasticModernHostTestEngine();
+ $host = new VyOSElasticModernHost($engine);
+ $host->setConfig(array(
+ 'host' => 'es.example.com',
+ 'port' => 9200,
+ 'protocol' => 'http',
+ 'path' => '/phabricator',
+ ));
+ $uri = (string)$host->getURI('/_doc/abc');
+ $this->assertTrue(strpos($uri, 'es.example.com') !== false);
+ $this->assertTrue(strpos($uri, '/_doc/abc') !== false);
+ }
+
+}