refs #377. Creacion de test funcionales para API User

pull/5/head
Manuel Aranda Rosales 2024-05-27 11:05:03 +02:00
parent 1350735e5c
commit c76d0e4c74
17 changed files with 2552 additions and 12 deletions

6
.env.test 100644
View File

@ -0,0 +1,6 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots

10
.gitignore vendored
View File

@ -15,3 +15,13 @@
###> lexik/jwt-authentication-bundle ###
/config/jwt/*.pem
###< lexik/jwt-authentication-bundle ###
###> symfony/phpunit-bridge ###
.phpunit.result.cache
/phpunit.xml
###< symfony/phpunit-bridge ###
###> phpunit/phpunit ###
/phpunit.xml
.phpunit.result.cache
###< phpunit/phpunit ###

23
bin/phpunit 100755
View File

@ -0,0 +1,23 @@
#!/usr/bin/env php
<?php
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
if (PHP_VERSION_ID >= 80000) {
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
} else {
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit\TextUI\Command::main();
}
} else {
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
exit(1);
}
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
}

View File

@ -83,8 +83,14 @@
}
},
"require-dev": {
"dama/doctrine-test-bundle": "^8.1",
"doctrine/doctrine-fixtures-bundle": "^3.6",
"phpunit/phpunit": "^9.5",
"symfony/browser-kit": "6.4.*",
"symfony/css-selector": "6.4.*",
"symfony/http-client": "6.4.*",
"symfony/maker-bundle": "^1.59",
"symfony/phpunit-bridge": "^7.0",
"symfony/web-profiler-bundle": "^6.4",
"zenstruck/foundry": "^1.37"
}

2200
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,9 @@ resources:
provider: App\State\Provider\UserProvider
ApiPlatform\Metadata\Patch:
provider: App\State\Provider\UserProvider
ApiPlatform\Metadata\Post: ~
ApiPlatform\Metadata\Post:
validation_context:
groups: [ 'default', 'user:post' ]
ApiPlatform\Metadata\Delete: ~
properties:

View File

@ -15,4 +15,5 @@ return [
Gesdinet\JWTRefreshTokenBundle\GesdinetJWTRefreshTokenBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
];

View File

@ -11,6 +11,13 @@ api_platform:
paths: ['%kernel.project_dir%/config/api_platform', '%kernel.project_dir%/src/Dto']
defaults:
pagination_client_items_per_page: true
cache_headers:
vary: [ 'Content-Type', 'Authorization', 'Origin' ]
extra_properties:
standard_put: true
rfc_7807_compliant_errors: true
event_listeners_backward_compatibility_layer: false
keep_legacy_inflector: false
docs_formats:
jsonld: ['application/ld+json']
jsonopenapi: ['application/vnd.openapi+json']

View File

@ -0,0 +1,5 @@
when@test:
dama_doctrine_test:
enable_static_connection: true
enable_static_meta_data_cache: true
enable_static_query_cache: true

39
phpunit.xml.dist 100644
View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/bootstrap.php"
convertDeprecationsToExceptions="false"
>
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
<server name="SYMFONY_PHPUNIT_VERSION" value="9.6" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>
<extensions>
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
</extensions>
</phpunit>

View File

@ -23,10 +23,10 @@ final class UserInput
#[Groups(['user:write'])]
public array $allowedOrganizationalUnits = [];
#[Assert\NotBlank]
#[Assert\NotBlank(groups: ['user:create'])]
#[Assert\Length(min: 8)]
#[Groups(['user:write'])]
public string $password;
public ?string $password = null;
#[Assert\NotNull]
#[Groups(['user:write'])]
@ -59,9 +59,13 @@ final class UserInput
$user->setUsername($this->username);
$user->setRoles($this->roles);
$user->setPassword($this->password);
$user->setEnabled($this->enabled);
$user->setUserGroups($this->userGroups);
if ($this->password !== null) {
$user->setPassword($this->password);
}
//$user->setAllowedOrganizationalUnits($this->allowedOrganizationalUnits);
return $user;

View File

@ -16,11 +16,6 @@ final class UserFactory extends ModelFactory
{
CONST PLAIN_PASSWORD = '12345678';
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
*
* @todo inject services if required
*/
public function __construct()
{
parent::__construct();

View File

@ -27,7 +27,7 @@ class UserProcessor implements ProcessorInterface
/**
* @throws \Exception
*/
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): UserOutput|null
{
switch ($operation){
case $operation instanceof Post:
@ -55,7 +55,9 @@ class UserProcessor implements ProcessorInterface
$user = $data->createOrUpdateEntity($entity);
$user->setPassword($this->userPasswordHasher->hashPassword($user, $data->password));
if ($data->password !== null){
$user->setPassword($this->userPasswordHasher->hashPassword($user, $data->password));
}
$this->validator->validate($user);
$this->userRepository->save($user);

View File

@ -13,6 +13,18 @@
"src/ApiResource/.gitignore"
]
},
"dama/doctrine-test-bundle": {
"version": "8.1",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "7.2",
"ref": "896306d79d4ee143af9eadf9b09fd34a8c391b70"
},
"files": [
"config/packages/dama_doctrine_test_bundle.yaml"
]
},
"doctrine/doctrine-bundle": {
"version": "2.12",
"recipe": {
@ -90,6 +102,20 @@
"config/packages/nelmio_cors.yaml"
]
},
"phpunit/phpunit": {
"version": "9.6",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "9.6",
"ref": "7364a21d87e658eb363c5020c072ecfdc12e2326"
},
"files": [
".env.test",
"phpunit.xml.dist",
"tests/bootstrap.php"
]
},
"ramsey/uuid-doctrine": {
"version": "2.0",
"recipe": {
@ -166,6 +192,21 @@
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
"symfony/phpunit-bridge": {
"version": "7.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.3",
"ref": "a411a0480041243d97382cac7984f7dce7813c08"
},
"files": [
".env.test",
"bin/phpunit",
"phpunit.xml.dist",
"tests/bootstrap.php"
]
},
"symfony/routing": {
"version": "6.4",
"recipe": {

View File

@ -0,0 +1,73 @@
<?php
namespace Functional;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Symfony\Bundle\Test\Client;
use App\Factory\UserFactory;
use Faker\Factory;
use Faker\Generator;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
abstract class AbstractTest extends ApiTestCase
{
use ResetDatabase, Factories;
protected static Client $client;
protected static Generator $faker;
private ?string $token = null;
public function setUp(): void
{
parent::setUp();
self::bootKernel();
self::$client = static::createClient();
self::$faker = Factory::create();
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
*/
protected function getToken($body = []): string
{
if ($this->token) {
return $this->token;
}
$response = static::createClient()->request('POST', '/auth/login', ['json' => $body ?: [
'username' => 'ogadmin',
'password' => '12345678',
]]);
$this->assertResponseIsSuccessful();
$data = $response->toArray();
$this->token = $data['token'];
return $data['token'];
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
*/
protected function createClientWithCredentials($token = null): Client
{
$token = $token ?: $this->getToken();
return static::createClient([], ['headers' => ['authorization' => 'Bearer '.$token]]);
}
}

View File

@ -0,0 +1,115 @@
<?php
namespace Functional;
use App\Entity\User;
use App\Factory\UserFactory;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
class UserTest extends AbstractTest
{
CONST USER_ADMIN = 'ogadmin';
CONST USER_CREATE = 'test-create';
CONST USER_UPDATE = 'test-update';
CONST USER_DELETE = 'test-delete';
/**
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
*/
public function testGetCollection(): void
{
UserFactory::createOne(['username' => self::USER_ADMIN]);
UserFactory::createMany(10);
$this->createClientWithCredentials()->request('GET', '/api/users');
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains([
'@context' => '/api/contexts/User',
'@id' => '/api/users',
'@type' => 'hydra:Collection',
'hydra:totalItems' => 11,
]);
}
/**
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
*/
public function testCreate(): void
{
UserFactory::createOne(['username' => self::USER_ADMIN]);
$this->createClientWithCredentials()->request('POST', '/api/users',['json' => [
'username' => self::USER_CREATE,
'password' => '12345678',
'enabled' => true,
]]);
$this->assertResponseStatusCodeSame(201);
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains([
'@context' => '/api/contexts/UserOutput',
'@type' => 'User',
'username' => self::USER_CREATE,
'enabled' => true,
]);
}
/**
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
*/
public function testUpdateBook(): void
{
UserFactory::createOne(['username' => self::USER_ADMIN]);
UserFactory::createOne(['username' => self::USER_UPDATE]);
$iri = $this->findIriBy(User::class, ['username' => self::USER_UPDATE]);
$this->createClientWithCredentials()->request('PATCH', $iri, ['json' => [
'username' => self::USER_UPDATE,
]]);
$this->assertResponseIsSuccessful();
$this->assertJsonContains([
'@id' => $iri,
'username' => self::USER_UPDATE,
]);
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
*/
public function testDeleteBook(): void
{
UserFactory::createOne(['username' => self::USER_ADMIN]);
UserFactory::createOne(['username' => self::USER_DELETE]);
$iri = $this->findIriBy(User::class, ['username' => self::USER_DELETE]);
$this->createClientWithCredentials()->request('DELETE', $iri);
$this->assertResponseStatusCodeSame(204);
$this->assertNull(
static::getContainer()->get('doctrine')->getRepository(User::class)->findOneBy(['username' => self::USER_DELETE])
);
}
}

View File

@ -0,0 +1,13 @@
<?php
use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__).'/vendor/autoload.php';
if (method_exists(Dotenv::class, 'bootEnv')) {
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
}
if ($_SERVER['APP_DEBUG']) {
umask(0000);
}