143 lines
5.7 KiB
PHP
Executable File
143 lines
5.7 KiB
PHP
Executable File
#!/usr/bin/env php
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use RedMCP\ListQueryNormalizer;
|
|
|
|
require __DIR__ . '/../vendor/autoload.php';
|
|
|
|
final class QueryNormalizerTest
|
|
{
|
|
private int $assertions = 0;
|
|
|
|
public function run(): void
|
|
{
|
|
$this->testDefaultPagingAndRawOverride();
|
|
$this->testIssueFriendlyFilters();
|
|
$this->testSortShortcutsAndStructuredSort();
|
|
$this->testDatePresetsAndRanges();
|
|
$this->testUserParams();
|
|
$this->testSearchParams();
|
|
|
|
fwrite(STDOUT, "OK {$this->assertions} assertions\n");
|
|
}
|
|
|
|
private function testDefaultPagingAndRawOverride(): void
|
|
{
|
|
$params = ListQueryNormalizer::listParams([
|
|
'limit' => 500,
|
|
'page' => 3,
|
|
'params' => ['limit' => 75],
|
|
]);
|
|
|
|
$this->assertSame(75, $params['limit'], 'raw params override normalized limit');
|
|
$this->assertSame(200, $params['offset'], 'page offset uses clamped normalized limit before raw override');
|
|
}
|
|
|
|
private function testIssueFriendlyFilters(): void
|
|
{
|
|
$filters = ListQueryNormalizer::issueFilters([
|
|
'project_id' => 'customer-service',
|
|
'status' => 'open',
|
|
'assigned_to_id' => 25,
|
|
'updated' => 'since 2026-04-01',
|
|
'filters' => ['assigned_to_id' => 'me'],
|
|
]);
|
|
|
|
$this->assertSame('customer-service', $filters['project_id'], 'project_id is copied');
|
|
$this->assertSame('open', $filters['status_id'], 'open status alias maps to Redmine open status');
|
|
$this->assertSame('me', $filters['assigned_to_id'], 'raw filters override normalized filters');
|
|
$this->assertSame('>=2026-04-01', $filters['updated_on'], 'since date maps to Redmine lower bound');
|
|
}
|
|
|
|
private function testSortShortcutsAndStructuredSort(): void
|
|
{
|
|
$newest = ListQueryNormalizer::listParams(['sort' => 'newest']);
|
|
$this->assertSame('updated_on:desc', $newest['sort'], 'newest shortcut sorts by updated_on descending');
|
|
|
|
$priority = ListQueryNormalizer::listParams(['sort' => 'priority']);
|
|
$this->assertSame('priority:desc,updated_on:desc', $priority['sort'], 'priority shortcut includes updated_on tie-breaker');
|
|
|
|
$structured = ListQueryNormalizer::listParams([
|
|
'sort' => [
|
|
['field' => 'created_on', 'direction' => 'desc'],
|
|
['field' => 'id', 'direction' => 'asc'],
|
|
],
|
|
]);
|
|
$this->assertSame('created_on:desc,id:asc', $structured['sort'], 'structured sort converts to Redmine sort string');
|
|
}
|
|
|
|
private function testDatePresetsAndRanges(): void
|
|
{
|
|
$clock = new DateTimeImmutable('2026-04-25 12:00:00', new DateTimeZone('UTC'));
|
|
|
|
$today = ListQueryNormalizer::issueFilters(['created' => 'today'], $clock);
|
|
$this->assertSame('2026-04-25', $today['created_on'], 'today maps to exact date');
|
|
|
|
$range = ListQueryNormalizer::issueFilters(['created' => '2026-04-01..2026-04-25'], $clock);
|
|
$this->assertSame('><2026-04-01|2026-04-25', $range['created_on'], 'range string maps to Redmine between syntax');
|
|
|
|
$objectRange = ListQueryNormalizer::issueFilters(['due' => ['from' => '2026-05-01', 'to' => '2026-05-31']], $clock);
|
|
$this->assertSame('><2026-05-01|2026-05-31', $objectRange['due_date'], 'object range maps to due_date');
|
|
|
|
$lastSeven = ListQueryNormalizer::issueFilters(['updated' => 'last 7 days'], $clock);
|
|
$this->assertSame('><2026-04-19|2026-04-25', $lastSeven['updated_on'], 'last N days includes today');
|
|
|
|
$freeText = ListQueryNormalizer::issueFilters(['created' => 'April 2 2026'], $clock);
|
|
$this->assertSame('2026-04-02', $freeText['created_on'], 'simple free text date parses');
|
|
}
|
|
|
|
private function testSearchParams(): void
|
|
{
|
|
$params = ListQueryNormalizer::searchParams([
|
|
'project_id' => 'customer-service',
|
|
'all_words' => true,
|
|
'titles_only' => false,
|
|
'open_issues' => true,
|
|
'sort' => 'oldest',
|
|
'page' => 2,
|
|
'params' => ['offset' => 5],
|
|
]);
|
|
|
|
$this->assertSame('customer-service', $params['project_id'], 'search project_id is copied');
|
|
$this->assertSame('1', $params['all_words'], 'true search flags map to Redmine 1');
|
|
$this->assertSame('0', $params['titles_only'], 'false search flags map to Redmine 0');
|
|
$this->assertSame('1', $params['open_issues'], 'open_issues flag maps to Redmine 1');
|
|
$this->assertSame('created_on:asc', $params['sort'], 'oldest shortcut maps to created_on ascending');
|
|
$this->assertSame(5, $params['offset'], 'raw search params override normalized paging');
|
|
}
|
|
|
|
private function testUserParams(): void
|
|
{
|
|
$params = ListQueryNormalizer::userParams([
|
|
'status' => 'active',
|
|
'name' => 'danny',
|
|
'group_id' => 4,
|
|
'sort' => 'oldest',
|
|
]);
|
|
|
|
$this->assertSame(1, $params['status'], 'active user status maps to Redmine active status id');
|
|
$this->assertSame('danny', $params['name'], 'user name filter is copied');
|
|
$this->assertSame(4, $params['group_id'], 'user group filter is copied');
|
|
$this->assertSame('created_on:asc', $params['sort'], 'user list accepts sort shortcuts');
|
|
}
|
|
|
|
/**
|
|
* @param mixed $expected
|
|
* @param mixed $actual
|
|
*/
|
|
private function assertSame($expected, $actual, string $message): void
|
|
{
|
|
$this->assertions++;
|
|
if ($expected === $actual) {
|
|
return;
|
|
}
|
|
|
|
fwrite(STDERR, "FAIL: {$message}\nExpected: " . var_export($expected, true) . "\nActual: " . var_export($actual, true) . "\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
(new QueryNormalizerTest())->run();
|