mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 01:01:24 +00:00
281 lines
9.9 KiB
PHP
281 lines
9.9 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\Services\Search\Drivers\ManticoreSearchDriver;
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
use ReflectionClass;
|
|
|
|
class ManticoreSearchQueryTest extends TestCase
|
|
{
|
|
#[Test]
|
|
#[DataProvider('negationQueriesProvider')]
|
|
public function it_preserves_negation_operators(string $input, string $expected): void
|
|
{
|
|
$result = ManticoreSearchDriver::prepareUserSearchQuery($input);
|
|
$this->assertSame($expected, $result);
|
|
}
|
|
|
|
public static function negationQueriesProvider(): array
|
|
{
|
|
return [
|
|
'negation with !' => ['!circus', '!circus'],
|
|
'negation with -' => ['-circus', '-circus'],
|
|
'word + negation' => ['dead !circus', 'dead !circus'],
|
|
'word + hyphen negation' => ['dead -circus', 'dead -circus'],
|
|
'multiple negations' => ['!foo !bar', '!foo !bar'],
|
|
];
|
|
}
|
|
|
|
#[Test]
|
|
#[DataProvider('phraseQueriesProvider')]
|
|
public function it_preserves_phrase_search(string $input, string $expected): void
|
|
{
|
|
$result = ManticoreSearchDriver::prepareUserSearchQuery($input);
|
|
$this->assertSame($expected, $result);
|
|
}
|
|
|
|
public static function phraseQueriesProvider(): array
|
|
{
|
|
return [
|
|
'exact phrase' => ['"exact phrase"', '"exact phrase"'],
|
|
'negated phrase with !' => ['!"exact phrase"', '!"exact phrase"'],
|
|
'negated phrase with -' => ['-"exact phrase"', '-"exact phrase"'],
|
|
'word + phrase' => ['hello "world peace"', 'hello "world peace"'],
|
|
];
|
|
}
|
|
|
|
#[Test]
|
|
#[DataProvider('orQueriesProvider')]
|
|
public function it_preserves_or_operator(string $input, string $expected): void
|
|
{
|
|
$result = ManticoreSearchDriver::prepareUserSearchQuery($input);
|
|
$this->assertSame($expected, $result);
|
|
}
|
|
|
|
public static function orQueriesProvider(): array
|
|
{
|
|
return [
|
|
'basic OR' => ['cats | dogs', 'cats | dogs'],
|
|
'OR with negation' => ['cats | -dogs', 'cats | -dogs'],
|
|
];
|
|
}
|
|
|
|
#[Test]
|
|
#[DataProvider('wildcardQueriesProvider')]
|
|
public function it_preserves_wildcards(string $input, string $expected): void
|
|
{
|
|
$result = ManticoreSearchDriver::prepareUserSearchQuery($input);
|
|
$this->assertSame($expected, $result);
|
|
}
|
|
|
|
public static function wildcardQueriesProvider(): array
|
|
{
|
|
return [
|
|
'suffix wildcard' => ['test*', 'test*'],
|
|
'prefix wildcard' => ['*fix', '*fix'],
|
|
'both wildcards' => ['*mid*', '*mid*'],
|
|
];
|
|
}
|
|
|
|
#[Test]
|
|
#[DataProvider('groupingQueriesProvider')]
|
|
public function it_preserves_grouping_parens(string $input, string $expected): void
|
|
{
|
|
$result = ManticoreSearchDriver::prepareUserSearchQuery($input);
|
|
$this->assertSame($expected, $result);
|
|
}
|
|
|
|
public static function groupingQueriesProvider(): array
|
|
{
|
|
return [
|
|
'basic grouping' => ['(cats | dogs)', '(cats | dogs)'],
|
|
'grouping with negation' => ['(cats | dogs) -birds', '(cats | dogs) -birds'],
|
|
];
|
|
}
|
|
|
|
#[Test]
|
|
#[DataProvider('escapingQueriesProvider')]
|
|
public function it_still_escapes_dangerous_characters(string $input, string $expected): void
|
|
{
|
|
$result = ManticoreSearchDriver::prepareUserSearchQuery($input);
|
|
$this->assertSame($expected, $result);
|
|
}
|
|
|
|
public static function escapingQueriesProvider(): array
|
|
{
|
|
return [
|
|
'escapes @' => ['@field test', '\@field test'],
|
|
'escapes ~' => ['test~2', 'test\~2'],
|
|
'escapes $' => ['test$', 'test\$'],
|
|
'mid-word hyphen escaped' => ['spider-man', 'spider\-man'],
|
|
'mid-word ! escaped' => ['wow!great', 'wow\!great'],
|
|
];
|
|
}
|
|
|
|
#[Test]
|
|
#[DataProvider('edgeCaseQueriesProvider')]
|
|
public function it_handles_edge_cases(string $input, string $expected): void
|
|
{
|
|
$result = ManticoreSearchDriver::prepareUserSearchQuery($input);
|
|
$this->assertSame($expected, $result);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_scopes_multi_word_queries_to_the_requested_field(): void
|
|
{
|
|
$reflection = new ReflectionClass(ManticoreSearchDriver::class);
|
|
$method = $reflection->getMethod('scopePreparedQueryToField');
|
|
|
|
$this->assertSame(
|
|
'@searchname (Resurrection 2025)',
|
|
$method->invoke(null, 'Resurrection 2025', '@searchname')
|
|
);
|
|
$this->assertSame(
|
|
'@(searchname,plainsearchname) (Resurrection 2025)',
|
|
$method->invoke(null, 'Resurrection 2025', '@(searchname,plainsearchname)')
|
|
);
|
|
$this->assertSame(
|
|
'(Resurrection 2025)',
|
|
$method->invoke(null, 'Resurrection 2025')
|
|
);
|
|
}
|
|
|
|
public static function edgeCaseQueriesProvider(): array
|
|
{
|
|
return [
|
|
'empty string' => ['', ''],
|
|
'only whitespace' => [' ', ''],
|
|
'just asterisk' => ['*', ''],
|
|
'plain word' => ['circus', 'circus'],
|
|
'multiple words' => ['hello world', 'hello world'],
|
|
];
|
|
}
|
|
|
|
#[Test]
|
|
public function escape_string_still_escapes_everything(): void
|
|
{
|
|
// Verify the original escapeString still escapes operators (for non-search use cases)
|
|
$result = ManticoreSearchDriver::escapeString('!circus');
|
|
$this->assertStringContainsString('\!', $result);
|
|
$this->assertStringContainsString('circus', $result);
|
|
}
|
|
|
|
#[Test]
|
|
#[DataProvider('negationDetectionProvider')]
|
|
public function it_detects_negation_operators(array|string $input, bool $expected): void
|
|
{
|
|
$result = ManticoreSearchDriver::queryHasNegation($input);
|
|
$this->assertSame($expected, $result);
|
|
}
|
|
|
|
public static function negationDetectionProvider(): array
|
|
{
|
|
return [
|
|
'bang negation string' => ['!harry', true],
|
|
'hyphen negation string' => ['-harry', true],
|
|
'word + negation string' => ['dead !circus', true],
|
|
'word + hyphen negation string' => ['dead -circus', true],
|
|
'plain word string' => ['harry', false],
|
|
'multiple words string' => ['harry potter', false],
|
|
'mid-word hyphen string' => ['spider-man', false],
|
|
'empty string' => ['', false],
|
|
'negation in array value' => [['searchname' => '!harry'], true],
|
|
'plain word in array value' => [['searchname' => 'harry'], false],
|
|
'multiple fields with negation' => [['searchname' => 'potter', 'name' => '!harry'], true],
|
|
'empty array' => [[], false],
|
|
'array with -1 values' => [['searchname' => '-1'], false],
|
|
];
|
|
}
|
|
|
|
#[Test]
|
|
public function it_passes_release_search_limit_to_search_indexes(): void
|
|
{
|
|
$driver = new class extends ManticoreSearchDriver
|
|
{
|
|
public int $capturedLimit = 0;
|
|
|
|
public function __construct() {}
|
|
|
|
public function searchIndexes(string $rt_index, ?string $searchString, array $column = [], array $searchArray = [], int $limit = 1000): array
|
|
{
|
|
$this->capturedLimit = $limit;
|
|
|
|
return ['id' => [101, 202]];
|
|
}
|
|
};
|
|
|
|
$result = $driver->searchReleases(['searchname' => 'harry potter'], 75);
|
|
|
|
$this->assertSame(75, $driver->capturedLimit);
|
|
$this->assertSame([101, 202], $result);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_clamps_search_limit_to_configured_max_matches(): void
|
|
{
|
|
$reflection = new ReflectionClass(ManticoreSearchDriver::class);
|
|
$driver = $reflection->newInstanceWithoutConstructor();
|
|
|
|
$configProperty = $reflection->getProperty('config');
|
|
$configProperty->setValue($driver, ['max_matches' => 500]);
|
|
|
|
$method = $reflection->getMethod('normalizeSearchLimit');
|
|
|
|
$this->assertSame(500, $method->invoke($driver, 750));
|
|
$this->assertSame(1, $method->invoke($driver, 0));
|
|
}
|
|
|
|
#[Test]
|
|
public function search_releases_filtered_uses_stable_id_tiebreak_sort_for_manticore(): void
|
|
{
|
|
$driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ManticoreSearchDriver.php');
|
|
|
|
$this->assertIsString($driverSource);
|
|
$this->assertStringContainsString("\$query->sort('id', \$order);", $driverSource);
|
|
}
|
|
|
|
#[Test]
|
|
public function search_releases_filtered_scopes_field_queries_with_grouping_parentheses(): void
|
|
{
|
|
$driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ManticoreSearchDriver.php');
|
|
|
|
$this->assertIsString($driverSource);
|
|
$this->assertStringContainsString(
|
|
"\$terms[] = '@@relaxed '.self::scopePreparedQueryToField(\$prepared, '@'.\$key);",
|
|
$driverSource
|
|
);
|
|
$this->assertStringNotContainsString(
|
|
"\$terms[] = '@@relaxed @'.\$key.' '.\$prepared;",
|
|
$driverSource
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function search_releases_with_category_filter_scopes_searchname_with_grouping_parentheses(): void
|
|
{
|
|
$driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ManticoreSearchDriver.php');
|
|
|
|
$this->assertIsString($driverSource);
|
|
$this->assertStringContainsString(
|
|
"\$searchExpr = '@@relaxed '.self::scopePreparedQueryToField(\$preparedSearch, '@searchname');",
|
|
$driverSource
|
|
);
|
|
$this->assertStringNotContainsString(
|
|
"\$searchExpr = '@@relaxed @searchname '.\$preparedSearch;",
|
|
$driverSource
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function search_releases_filtered_uses_stable_id_tiebreak_sort_for_elasticsearch(): void
|
|
{
|
|
$driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ElasticSearchDriver.php');
|
|
|
|
$this->assertIsString($driverSource);
|
|
$this->assertStringContainsString("['id' => ['order' => \$order]],", $driverSource);
|
|
}
|
|
}
|