jetzt auch mit einheitstests

This commit is contained in:
lubiana 2025-06-28 15:55:01 +02:00
parent d68e7f45b3
commit e883913d3a
Signed by: lubiana
SSH key fingerprint: SHA256:vW1EA0fRR3Fw+dD/sM0K+x3Il2gSry6YRYHqOeQwrfk
10 changed files with 3952 additions and 16 deletions

View file

@ -8,6 +8,11 @@
"App\\": "src/" "App\\": "src/"
} }
}, },
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"require": { "require": {
"php": ">=8.4", "php": ">=8.4",
"symfony/uid": "^7.0", "symfony/uid": "^7.0",
@ -18,11 +23,13 @@
"rector/rector": "^2.1", "rector/rector": "^2.1",
"phpstan/phpstan": "^2.1", "phpstan/phpstan": "^2.1",
"phpstan/extension-installer": "^1.4", "phpstan/extension-installer": "^1.4",
"phpstan/phpstan-strict-rules": "^2.0" "phpstan/phpstan-strict-rules": "^2.0",
"pestphp/pest": "^3.8"
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {
"phpstan/extension-installer": true "phpstan/extension-installer": true,
"pestphp/pest-plugin": true
} }
}, },
"scripts": { "scripts": {
@ -30,6 +37,7 @@
"rector", "rector",
"ecs --fix || ecs --fix" "ecs --fix || ecs --fix"
], ],
"phpstan": "phpstan" "phpstan": "phpstan",
"test": "pest --mutate --parallel"
} }
} }

3677
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
use PhpCsFixer\Fixer\Import\OrderedImportsFixer;
use PhpCsFixer\Fixer\Import\NoUnusedImportsFixer; use PhpCsFixer\Fixer\Import\NoUnusedImportsFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig; use Symplify\EasyCodingStandard\Config\ECSConfig;
@ -9,11 +10,9 @@ return ECSConfig::configure()
->withPaths([ ->withPaths([
__DIR__ . '/public', __DIR__ . '/public',
__DIR__ . '/src', __DIR__ . '/src',
__DIR__ . '/tests',
]) ])
->withRootFiles() ->withRootFiles()
->withRules([
NoUnusedImportsFixer::class,
])
->withPhpCsFixerSets( ->withPhpCsFixerSets(
per: true, per: true,
php84Migration: true, php84Migration: true,
@ -25,4 +24,8 @@ return ECSConfig::configure()
strict: true, strict: true,
cleanCode: true, cleanCode: true,
) )
->withRules([
NoUnusedImportsFixer::class,
OrderedImportsFixer::class,
])
; ;

18
phpunit.xml Normal file
View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
<directory>src</directory>
</include>
</source>
</phpunit>

View file

@ -8,6 +8,7 @@ return RectorConfig::configure()
->withPaths([ ->withPaths([
__DIR__ . '/public', __DIR__ . '/public',
__DIR__ . '/src', __DIR__ . '/src',
__DIR__ . '/tests',
]) ])
->withRootFiles() ->withRootFiles()
->withImportNames(removeUnusedImports: true) ->withImportNames(removeUnusedImports: true)

View file

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App; namespace App;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Uid\Ulid; use Symfony\Component\Uid\Ulid;
@ -23,7 +22,7 @@ final readonly class App
'/v6' => new PlaintextResponse(Uuid::v6()->toString()), '/v6' => new PlaintextResponse(Uuid::v6()->toString()),
'/v7' => new PlaintextResponse(Uuid::v7()->toString()), '/v7' => new PlaintextResponse(Uuid::v7()->toString()),
'/ulid' => new PlaintextResponse(Ulid::generate()), '/ulid' => new PlaintextResponse(Ulid::generate()),
'/' => new PlaintextResponse( default => new PlaintextResponse(
<<<TXT <<<TXT
UUID/ULID Webservice UUID/ULID Webservice
@ -31,15 +30,9 @@ final readonly class App
/v1 - Generate a UUID v1 (time-based) /v1 - Generate a UUID v1 (time-based)
curl {$this->base}v1 curl {$this->base}v1
/v3 - Generate a UUID v3 (name-based, MD5 hash)
curl {$this->base}v3
/v4 - Generate a UUID v4 (random) /v4 - Generate a UUID v4 (random)
curl {$this->base}v4 curl {$this->base}v4
/v5 - Generate a UUID v5 (name-based, SHA-1 hash)
curl {$this->base}v5
/v6 - Generate a UUID v6 (reordered time-based) /v6 - Generate a UUID v6 (reordered time-based)
curl {$this->base}v6 curl {$this->base}v6
@ -52,9 +45,9 @@ final readonly class App
source: https://git.hannover.ccc.de/lubiana/uuid source: https://git.hannover.ccc.de/lubiana/uuid
TXT TXT
), ),
default => new RedirectResponse('/', 301),
}; };
} }
public function run(?Request $request = null): void public function run(?Request $request = null): void
{ {
$request ??= Request::createFromGlobals(); $request ??= Request::createFromGlobals();

138
tests/Feature/AppTest.php Normal file
View file

@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
use App\App;
use Symfony\Component\Uid\UuidV1;
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Uid\UuidV7;
use Symfony\Component\Uid\UuidV6;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\Ulid;
covers(App::class);
test('App returns correct version for endpoint', function (string $endpoint, string $class): void {
$app = new App('https://example.com/');
$request = Request::create('/' . $endpoint);
$response = $app->handle($request);
expect($response->getStatusCode())->toBe(200);
expect($response->headers->get('Content-Type'))->toBe('text/plain');
$uuid = $response->getContent();
expect(Uuid::isValid($uuid))->toBeTrue();
expect(Uuid::fromString($uuid))->toBeInstanceOf($class);
})->with([
[
'endpoint' => 'v1',
'class' => UuidV1::class,
],
[
'endpoint' => 'v4',
'class' => UuidV4::class,
],
[
'endpoint' => 'v6',
'class' => UuidV6::class,
],
[
'endpoint' => 'v7',
'class' => UuidV7::class,
],
]);
test('App returns ULID for /ulid endpoint', function (): void {
$app = new App('https://example.com/');
$request = Request::create('/ulid');
$response = $app->handle($request);
expect($response->getStatusCode())->toBe(200);
expect($response->headers->get('Content-Type'))->toBe('text/plain');
$ulid = $response->getContent();
expect(Ulid::isValid($ulid))->toBeTrue();
});
test('App returns help text for default endpoint', function (): void {
$base = 'https://example.com/';
$app = new App($base);
$request = Request::create('/');
$response = $app->handle($request);
expect($response->getStatusCode())->toBe(200);
expect($response->headers->get('Content-Type'))->toBe('text/plain');
$content = $response->getContent();
expect($content)->toContain(
'UUID/ULID Webservice',
'Available Endpoints:',
'v1',
'v4',
'v6',
'v7',
'ulid',
$base,
);
});
test('App run method processes the request with provided request', function (): void {
$app = new App('https://example.com/');
$request = Request::create('/v4');
// Use output buffering to capture the output
ob_start();
$app->run($request);
$output = ob_get_clean();
// Verify the output is a valid UUID v4
expect(Uuid::isValid($output))->toBeTrue();
expect(Uuid::fromString($output))->toBeInstanceOf(UuidV4::class);
});
test('App run method processes the request with createFromGlobals when no request provided', function (): void {
// Save current server state
$originalServer = $_SERVER;
// Set up $_SERVER to simulate a request to /v4
$_SERVER['REQUEST_URI'] = '/v4';
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['HTTP_HOST'] = 'example.com';
$app = new App('https://example.com/');
// Use output buffering to capture the output
ob_start();
$app->run();
$output = ob_get_clean();
// Restore original server state
$_SERVER = $originalServer;
// Verify the output is a valid UUID v4
expect(Uuid::isValid($output))->toBeTrue();
expect(Uuid::fromString($output))->toBeInstanceOf(UuidV4::class);
});
test('App run method sends the response for help page', function (): void {
$app = new App('https://example.com/');
$request = Request::create('/');
// Use output buffering to capture the output
ob_start();
$app->run($request);
$output = ob_get_clean();
// Verify the output contains the help text
expect($output)->toContain('UUID/ULID Webservice');
expect($output)->toContain('Available Endpoints:');
expect($output)->toContain('/v1');
expect($output)->toContain('/v4');
expect($output)->toContain('/v6');
expect($output)->toContain('/v7');
expect($output)->toContain('/ulid');
expect($output)->toContain('https://example.com/');
});

31
tests/Pest.php Normal file
View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
use Tests\TestCase;
/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "pest()" function to bind a different classes or traits.
|
*/
pest()->extend(TestCase::class)->in('Feature', 'Unit');
/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation API at any time.
|
*/
expect()->extend('toBeOne', fn() => $this->toBe(1));

12
tests/TestCase.php Normal file
View file

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Tests;
use PHPUnit\Framework\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
}

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
use App\PlaintextResponse;
covers(PlaintextResponse::class);
test('it sets content-type to text/plain by default', function (): void {
$response = new PlaintextResponse('Hello, World!');
expect($response->headers->get('Content-Type'))->toBe('text/plain');
});
test('it allows custom content-type to be set', function (): void {
$response = new PlaintextResponse('Hello, World!', 200, [
'Content-Type' => 'application/json',
]);
expect($response->headers->get('Content-Type'))->toBe('application/json');
});
test('it sets content correctly', function (): void {
$content = 'Hello, World!';
$response = new PlaintextResponse($content);
expect($response->getContent())->toBe($content);
});
test('it sets status code correctly', function (): void {
$status = 201;
$response = new PlaintextResponse('Hello, World!', $status);
expect($response->getStatusCode())->toBe($status);
});
test('it sets headers correctly', function (): void {
$headers = [
'X-Custom-Header' => 'Custom Value',
'X-Another-Header' => 'Another Value',
];
$response = new PlaintextResponse('Hello, World!', 200, $headers);
foreach ($headers as $key => $value) {
expect($response->headers->get($key))->toBe($value);
}
});
test('it handles null content', function (): void {
$response = new PlaintextResponse(null);
expect($response->getContent())->toBe('');
});
test('it has correct defaultstatuscode', function (): void {
$response = new PlaintextResponse('Hello, World!');
expect($response->getStatusCode())->toBe(200);
});