Files
nimbus/tests/App/Modules/Routes/Services/Uri/VersionedUriUnitTest.php
Mazen Touati 23bf3b7691 fix(routes): support composed prefixes (#18)
also refactors and centralizes prefixes cleanup
2025-11-11 20:05:15 +01:00

378 lines
12 KiB
PHP

<?php
namespace Sunchayn\Nimbus\Tests\App\Modules\Routes\Services\Uri;
use Generator;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;
use Sunchayn\Nimbus\Modules\Routes\Services\Uri\VersionedUri;
#[CoversClass(VersionedUri::class)]
class VersionedUriUnitTest extends TestCase
{
#[DataProvider('versionExtractionProvider')]
public function test_it_extracts_version_correctly(
string $value,
string $routesPrefix,
string $expectedVersion
): void {
$uri = new VersionedUri(value: $value, routesPrefix: $routesPrefix);
$this->assertEquals($expectedVersion, $uri->getVersion());
}
public static function versionExtractionProvider(): Generator
{
yield 'simple versioned uri with v prefix' => [
'value' => '/v1/users',
'routesPrefix' => '',
'expectedVersion' => 'v1',
];
yield 'versioned uri with numeric version' => [
'value' => '/v2/posts',
'routesPrefix' => '',
'expectedVersion' => 'v2',
];
yield 'versioned uri with semantic version' => [
'value' => '/1.0/users',
'routesPrefix' => '',
'expectedVersion' => '1.0',
];
yield 'versioned uri with semantic version multiple digits' => [
'value' => '/2.5/users',
'routesPrefix' => '',
'expectedVersion' => '2.5',
];
yield 'versioned uri with prefix' => [
'value' => '/api/v1/users',
'routesPrefix' => 'api',
'expectedVersion' => 'v1',
];
yield 'versioned uri with prefix and semantic version' => [
'value' => '/api/1.0/users',
'routesPrefix' => 'api',
'expectedVersion' => '1.0',
];
yield 'non-versioned uri returns default version' => [
'value' => '/users',
'routesPrefix' => '',
'expectedVersion' => 'v1',
];
yield 'non-versioned uri with prefix returns default version' => [
'value' => '/api/users',
'routesPrefix' => 'api',
'expectedVersion' => 'v1',
];
yield 'uri with invalid version format returns default version' => [
'value' => '/version1/users',
'routesPrefix' => '',
'expectedVersion' => 'v1',
];
yield 'uri with v but no number returns default version' => [
'value' => '/v/users',
'routesPrefix' => '',
'expectedVersion' => 'v1',
];
yield 'uri with v and letters returns default version' => [
'value' => '/vabc/users',
'routesPrefix' => '',
'expectedVersion' => 'v1',
];
yield 'empty uri returns default version' => [
'value' => '',
'routesPrefix' => '',
'expectedVersion' => 'v1',
];
yield 'only slashes returns default version' => [
'value' => '///',
'routesPrefix' => '',
'expectedVersion' => 'v1',
];
yield 'only prefix returns default version' => [
'value' => '/api',
'routesPrefix' => 'api',
'expectedVersion' => 'v1',
];
yield 'only version returns that version' => [
'value' => '/v1',
'routesPrefix' => '',
'expectedVersion' => 'v1',
];
yield 'prefix and only version' => [
'value' => '/api/v1',
'routesPrefix' => 'api',
'expectedVersion' => 'v1',
];
yield 'version without leading slash' => [
'value' => 'v1/users',
'routesPrefix' => '',
'expectedVersion' => 'v1',
];
yield 'multiple consecutive slashes with version' => [
'value' => '//v1//users',
'routesPrefix' => '',
'expectedVersion' => 'v1',
];
yield 'version with trailing slash' => [
'value' => '/v1/',
'routesPrefix' => '',
'expectedVersion' => 'v1',
];
yield 'case sensitive prefix mismatch' => [
'value' => '/API/v1/users', // <- API is treated as resource, not prefix
'routesPrefix' => 'api',
'expectedVersion' => 'v1', // <- Default value.
];
yield 'version-like string in middle is not detected' => [
'value' => '/users/v2/posts', // <- only first part after prefix is checked
'routesPrefix' => '',
'expectedVersion' => 'v1', // <- Default value.
];
yield 'three digit semantic version returns empty' => [
'value' => '/1.0.0/users', // <- regex only matches X.Y format
'routesPrefix' => '',
'expectedVersion' => 'v1', // <- Default value.
];
yield 'v with multiple digits' => [
'value' => '/v123/users',
'routesPrefix' => '',
'expectedVersion' => 'v123',
];
yield 'semantic version with large numbers' => [
'value' => '/99.99/users',
'routesPrefix' => '',
'expectedVersion' => '99.99',
];
}
#[DataProvider('resourceExtractionProvider')]
public function test_it_extracts_resource_correctly(
string $value,
string $routesPrefix,
string $expectedResource
): void {
$uri = new VersionedUri(value: $value, routesPrefix: $routesPrefix);
$this->assertEquals($expectedResource, $uri->getResource());
}
public static function resourceExtractionProvider(): Generator
{
yield 'versioned uri extracts resource after version' => [
'value' => '/v1/users',
'routesPrefix' => '',
'expectedResource' => 'users',
];
yield 'versioned uri with semantic version extracts resource' => [
'value' => '/1.0/users',
'routesPrefix' => '',
'expectedResource' => 'users',
];
yield 'versioned uri with prefix extracts resource' => [
'value' => '/api/v1/users',
'routesPrefix' => 'api',
'expectedResource' => 'users',
];
yield 'versioned uri with composed prefix extracts resource' => [
'value' => '/cms/api/v1/users',
'routesPrefix' => 'cms/api',
'expectedResource' => 'users',
];
yield 'versioned uri with multiple segments extracts first resource' => [
'value' => '/v1/users/123/profile',
'routesPrefix' => '',
'expectedResource' => 'users',
];
yield 'non-versioned uri extracts resource' => [
'value' => '/users',
'routesPrefix' => '',
'expectedResource' => 'users',
];
yield 'non-versioned uri with prefix extracts resource' => [
'value' => '/api/users',
'routesPrefix' => 'api',
'expectedResource' => 'users',
];
yield 'non-versioned uri with multiple segments' => [
'value' => '/users/123',
'routesPrefix' => '',
'expectedResource' => 'users',
];
yield 'empty uri returns empty resource' => [
'value' => '',
'routesPrefix' => '',
'expectedResource' => '',
];
yield 'only slashes returns empty resource' => [
'value' => '///',
'routesPrefix' => '',
'expectedResource' => '',
];
yield 'only prefix returns empty resource' => [
'value' => '/api',
'routesPrefix' => 'api',
'expectedResource' => '',
];
yield 'only version returns empty resource' => [
'value' => '/v1',
'routesPrefix' => '',
'expectedResource' => '', // <- no resource after version
];
yield 'prefix and only version returns empty resource' => [
'value' => '/api/v1',
'routesPrefix' => 'api',
'expectedResource' => '',
];
yield 'version without leading slash' => [
'value' => 'v1/users',
'routesPrefix' => '',
'expectedResource' => 'users',
];
yield 'multiple consecutive slashes with version' => [
'value' => '//v1//users',
'routesPrefix' => '',
'expectedResource' => 'users',
];
yield 'resource without leading slash' => [
'value' => 'users/123',
'routesPrefix' => '',
'expectedResource' => 'users',
];
yield 'versioned uri with trailing slash' => [
'value' => '/v1/users/',
'routesPrefix' => '',
'expectedResource' => 'users',
];
yield 'single character resource with version' => [
'value' => '/v1/a',
'routesPrefix' => '',
'expectedResource' => 'a',
];
yield 'numeric resource with version' => [
'value' => '/v1/123',
'routesPrefix' => '',
'expectedResource' => '123',
];
yield 'resource with special characters and version' => [
'value' => '/v1/user-profile',
'routesPrefix' => '',
'expectedResource' => 'user-profile',
];
yield 'case sensitive prefix mismatch' => [
'value' => '/API/v1/users',
'routesPrefix' => 'api',
'expectedResource' => 'API', // <- API is treated as resource
];
yield 'prefix as part of resource name' => [
'value' => '/api/v1/api-users',
'routesPrefix' => 'api',
'expectedResource' => 'api-users',
];
yield 'deeply nested versioned uri' => [
'value' => '/api/v1/users/123/posts/456',
'routesPrefix' => 'api',
'expectedResource' => 'users',
];
yield 'whitespace in resource with version' => [
'value' => '/v1/ users /123',
'routesPrefix' => '',
'expectedResource' => ' users ',
];
yield 'url encoded characters in resource' => [
'value' => '/v1/user%20name',
'routesPrefix' => '',
'expectedResource' => 'user%20name',
];
yield 'invalid version format treats first part as resource' => [
'value' => '/version1/users',
'routesPrefix' => '',
'expectedResource' => 'version1', // <- not a valid version format
];
yield 'three digit semantic version treats it as resource' => [
'value' => '/1.0.0/users',
'routesPrefix' => '',
'expectedResource' => '1.0.0', // <- doesn't match version regex
];
}
#[TestWith(['/v1/users', ''])]
#[TestWith(['/api/v2/posts', 'api'])]
#[TestWith(['/1.0/resources', ''])]
public function test_it_can_call_get_version_multiple_times(string $value, string $routesPrefix): void
{
$uri = new VersionedUri(value: $value, routesPrefix: $routesPrefix);
// This asserts against mutating the original value.
$firstCall = $uri->getVersion();
$secondCall = $uri->getVersion();
$this->assertEquals($firstCall, $secondCall);
$this->assertNotEmpty($firstCall);
}
#[TestWith(['/v1/users'])]
#[TestWith(['/api/v2/posts'])]
#[TestWith(['/users'])]
public function test_it_can_call_get_resource_multiple_times(string $value): void
{
$uri = new VersionedUri(value: $value, routesPrefix: '');
// This asserts against mutating the original value.
$firstCall = $uri->getResource();
$secondCall = $uri->getResource();
$this->assertEquals($firstCall, $secondCall);
}
}