#!/usr/bin/env php 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();