Compare commits
3 commits
57dcabc44f
...
933e2a52df
| Author | SHA1 | Date | |
|---|---|---|---|
| 933e2a52df | |||
| 21110c6e70 | |||
| afb99a11ec |
13 changed files with 549 additions and 5 deletions
|
|
@ -27,6 +27,7 @@
|
|||
"symfony/security-bundle": "8.1.*",
|
||||
"symfony/serializer": "8.1.*",
|
||||
"symfony/twig-bundle": "8.1.*",
|
||||
"symfony/uid": "8.1.*",
|
||||
"symfony/validator": "8.1.*",
|
||||
"symfony/yaml": "8.1.*"
|
||||
},
|
||||
|
|
@ -85,6 +86,7 @@
|
|||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"dama/doctrine-test-bundle": "^8.6",
|
||||
"friendsofphp/php-cs-fixer": "^3.95",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "^2.2",
|
||||
|
|
@ -92,6 +94,7 @@
|
|||
"phpstan/phpstan-symfony": "^2.0",
|
||||
"phpunit/phpunit": "^13.2",
|
||||
"symfony/browser-kit": "8.1.*",
|
||||
"symfony/css-selector": "8.1.*"
|
||||
"symfony/css-selector": "8.1.*",
|
||||
"symfony/maker-bundle": "^1.67"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
170
composer.lock
generated
170
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "fb5afe48f605868daa9f1a0ec4868778",
|
||||
"content-hash": "8096a72d560f2d6e1c5aafb6175ba005",
|
||||
"packages": [
|
||||
{
|
||||
"name": "api-platform/doctrine-common",
|
||||
|
|
@ -7594,6 +7594,75 @@
|
|||
],
|
||||
"time": "2024-05-06T16:37:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dama/doctrine-test-bundle",
|
||||
"version": "v8.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dmaicher/doctrine-test-bundle.git",
|
||||
"reference": "f7e3487643e685432f7e27c50cac64e9f8c515a4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/f7e3487643e685432f7e27c50cac64e9f8c515a4",
|
||||
"reference": "f7e3487643e685432f7e27c50cac64e9f8c515a4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/dbal": "^3.3 || ^4.0",
|
||||
"doctrine/doctrine-bundle": "^2.11.0 || ^3.0",
|
||||
"php": ">= 8.2",
|
||||
"psr/cache": "^2.0 || ^3.0",
|
||||
"symfony/cache": "^6.4 || ^7.3 || ^8.0",
|
||||
"symfony/framework-bundle": "^6.4 || ^7.3 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpunit/phpunit": "<11.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"behat/behat": "^3.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.27",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"phpunit/phpunit": "^11.5.41|| ^12.3.14",
|
||||
"symfony/dotenv": "^6.4 || ^7.3 || ^8.0",
|
||||
"symfony/process": "^6.4 || ^7.3 || ^8.0"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "8.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"DAMA\\DoctrineTestBundle\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "David Maicher",
|
||||
"email": "mail@dmaicher.de"
|
||||
}
|
||||
],
|
||||
"description": "Symfony bundle to isolate doctrine database tests and improve test performance",
|
||||
"keywords": [
|
||||
"doctrine",
|
||||
"isolation",
|
||||
"performance",
|
||||
"symfony",
|
||||
"testing",
|
||||
"tests"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/dmaicher/doctrine-test-bundle/issues",
|
||||
"source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.6.0"
|
||||
},
|
||||
"time": "2026-01-21T07:39:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ergebnis/agent-detector",
|
||||
"version": "1.2.0",
|
||||
|
|
@ -10746,6 +10815,105 @@
|
|||
],
|
||||
"time": "2026-05-29T05:06:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/maker-bundle",
|
||||
"version": "v1.67.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/maker-bundle.git",
|
||||
"reference": "6ce8b313845f16bcf385ee3cb31d8b24e30d5516"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/maker-bundle/zipball/6ce8b313845f16bcf385ee3cb31d8b24e30d5516",
|
||||
"reference": "6ce8b313845f16bcf385ee3cb31d8b24e30d5516",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer-runtime-api": "^2.1",
|
||||
"doctrine/inflector": "^2.0",
|
||||
"nikic/php-parser": "^5.0",
|
||||
"php": ">=8.1",
|
||||
"symfony/config": "^6.4|^7.0|^8.0",
|
||||
"symfony/console": "^6.4|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/deprecation-contracts": "^2.2|^3",
|
||||
"symfony/filesystem": "^6.4|^7.0|^8.0",
|
||||
"symfony/finder": "^6.4|^7.0|^8.0",
|
||||
"symfony/framework-bundle": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||
"symfony/process": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/doctrine-bundle": "<2.10",
|
||||
"doctrine/orm": "<2.15"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/semver": "^3.0",
|
||||
"doctrine/doctrine-bundle": "^2.10|^3.0",
|
||||
"doctrine/orm": "^2.15|^3",
|
||||
"doctrine/persistence": "^3.1|^4.0",
|
||||
"symfony/http-client": "^6.4|^7.0|^8.0",
|
||||
"symfony/phpunit-bridge": "^6.4.1|^7.0|^8.0",
|
||||
"symfony/security-core": "^6.4|^7.0|^8.0",
|
||||
"symfony/security-http": "^6.4|^7.0|^8.0",
|
||||
"symfony/yaml": "^6.4|^7.0|^8.0",
|
||||
"twig/twig": "^3.0|^4.x-dev"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Bundle\\MakerBundle\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.",
|
||||
"homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html",
|
||||
"keywords": [
|
||||
"code generator",
|
||||
"dev",
|
||||
"generator",
|
||||
"scaffold",
|
||||
"scaffolding"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/symfony/maker-bundle/issues",
|
||||
"source": "https://github.com/symfony/maker-bundle/tree/v1.67.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-18T13:39:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
"version": "v8.1.0",
|
||||
|
|
|
|||
|
|
@ -8,4 +8,6 @@ return [
|
|||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||
];
|
||||
|
|
|
|||
5
config/packages/dama_doctrine_test_bundle.yaml
Normal file
5
config/packages/dama_doctrine_test_bundle.yaml
Normal 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
|
||||
|
|
@ -5,7 +5,11 @@ security:
|
|||
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
users_in_memory: { memory: null }
|
||||
# used to reload user from session & other features (e.g. switch_user)
|
||||
app_user_provider:
|
||||
entity:
|
||||
class: App\Entity\User
|
||||
property: email
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
|
|
@ -14,7 +18,7 @@ security:
|
|||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: users_in_memory
|
||||
provider: app_user_provider
|
||||
|
||||
# Activate different ways to authenticate:
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
|
|
|
|||
|
|
@ -1439,7 +1439,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||
* validation_error_resource_class?: scalar|Param|null, // The class used to represent validation errors in the OpenAPI documentation. // Default: null
|
||||
* },
|
||||
* maker?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* enabled?: bool|Param, // Default: true
|
||||
* namespace_prefix?: scalar|Param|null, // Add a prefix to all maker generated classes. e.g set it to "Api" to set the maker namespace to "App\Api\" (if the maker.root_namespace config is App). e.g. App\Api\State\MyStateProcessor // Default: ""
|
||||
* },
|
||||
* mcp?: bool|array{
|
||||
|
|
@ -1575,6 +1575,17 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||
* ...<string, mixed>
|
||||
* },
|
||||
* }
|
||||
* @psalm-type MakerConfig = array{
|
||||
* root_namespace?: scalar|Param|null, // Default: "App"
|
||||
* generate_final_classes?: bool|Param, // Default: true
|
||||
* generate_final_entities?: bool|Param, // Default: false
|
||||
* }
|
||||
* @psalm-type DamaDoctrineTestConfig = array{
|
||||
* enable_static_connection?: mixed, // Default: true
|
||||
* enable_static_meta_data_cache?: bool|Param, // Default: true
|
||||
* enable_static_query_cache?: bool|Param, // Default: true
|
||||
* connection_keys?: list<mixed>,
|
||||
* }
|
||||
* @psalm-type ConfigType = array{
|
||||
* imports?: ImportsConfig,
|
||||
* parameters?: ParametersConfig,
|
||||
|
|
@ -1597,6 +1608,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* api_platform?: ApiPlatformConfig,
|
||||
* maker?: MakerConfig,
|
||||
* },
|
||||
* "when@prod"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
|
|
@ -1621,6 +1633,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* api_platform?: ApiPlatformConfig,
|
||||
* dama_doctrine_test?: DamaDoctrineTestConfig,
|
||||
* },
|
||||
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
||||
* imports?: ImportsConfig,
|
||||
|
|
|
|||
30
migrations/Version20260617184045.php
Normal file
30
migrations/Version20260617184045.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260617184045 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE "user" (id UUID NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
|
||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP TABLE "user"');
|
||||
}
|
||||
}
|
||||
|
|
@ -40,5 +40,6 @@
|
|||
</source>
|
||||
|
||||
<extensions>
|
||||
<bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" />
|
||||
</extensions>
|
||||
</phpunit>
|
||||
|
|
|
|||
114
src/Entity/User.php
Normal file
114
src/Entity/User.php
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Uid\UuidV7;
|
||||
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
#[ORM\Table(name: '`user`')]
|
||||
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid', unique: true)]
|
||||
private UuidV7 $id;
|
||||
|
||||
#[ORM\Column(length: 180)]
|
||||
private string $email;
|
||||
|
||||
/**
|
||||
* @var list<string> The user roles
|
||||
*/
|
||||
#[ORM\Column]
|
||||
private array $roles = [];
|
||||
|
||||
/**
|
||||
* @var string The hashed password
|
||||
*/
|
||||
#[ORM\Column]
|
||||
private string $password;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = new UuidV7();
|
||||
}
|
||||
|
||||
public function getId(): ?UuidV7
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(string $email): static
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A visual identifier that represents this user.
|
||||
*
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return (string) $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = $this->roles;
|
||||
// guarantee every user at least has ROLE_USER
|
||||
$roles[] = 'ROLE_USER';
|
||||
|
||||
return array_unique($roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roles
|
||||
*/
|
||||
public function setRoles(array $roles): static
|
||||
{
|
||||
$this->roles = $roles;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PasswordAuthenticatedUserInterface
|
||||
*/
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): static
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the session doesn't contain actual password hashes by CRC32C-hashing them, as supported since Symfony 7.3.
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
$data = (array) $this;
|
||||
$data["\0".self::class."\0password"] = hash('crc32c', $this->password);
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
60
src/Repository/UserRepository.php
Normal file
60
src/Repository/UserRepository.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<User>
|
||||
*/
|
||||
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to upgrade (rehash) the user's password automatically over time.
|
||||
*/
|
||||
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
|
||||
{
|
||||
if (!$user instanceof User) {
|
||||
throw new UnsupportedUserException(\sprintf('Instances of "%s" are not supported.', $user::class));
|
||||
}
|
||||
|
||||
$user->setPassword($newHashedPassword);
|
||||
$this->getEntityManager()->persist($user);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return User[] Returns an array of User objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('u')
|
||||
// ->andWhere('u.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('u.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?User
|
||||
// {
|
||||
// return $this->createQueryBuilder('u')
|
||||
// ->andWhere('u.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
12
symfony.lock
12
symfony.lock
|
|
@ -13,6 +13,9 @@
|
|||
"src/ApiResource/.gitignore"
|
||||
]
|
||||
},
|
||||
"dama/doctrine-test-bundle": {
|
||||
"version": "v8.6.0"
|
||||
},
|
||||
"doctrine/deprecations": {
|
||||
"version": "1.1",
|
||||
"recipe": {
|
||||
|
|
@ -145,6 +148,15 @@
|
|||
".editorconfig"
|
||||
]
|
||||
},
|
||||
"symfony/maker-bundle": {
|
||||
"version": "1.67",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
||||
}
|
||||
},
|
||||
"symfony/property-info": {
|
||||
"version": "8.1",
|
||||
"recipe": {
|
||||
|
|
|
|||
84
tests/Entity/UserPersistenceTest.php
Normal file
84
tests/Entity/UserPersistenceTest.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Uid\UuidV7;
|
||||
|
||||
final class UserPersistenceTest extends KernelTestCase
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private UserPasswordHasherInterface $hasher;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->em = self::getContainer()->get(EntityManagerInterface::class);
|
||||
|
||||
// Build the hasher directly — avoids the chicken-and-egg where
|
||||
// UserPasswordHasherInterface gets compiled away before User exists.
|
||||
$factory = new PasswordHasherFactory([
|
||||
User::class => new NativePasswordHasher(cost: 4),
|
||||
]);
|
||||
$this->hasher = new UserPasswordHasher($factory);
|
||||
}
|
||||
|
||||
public function testItPersistsAUserWithUuidIdentifierAndHashedPassword(): void
|
||||
{
|
||||
$hasher = $this->hasher;
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail('alice@example.com');
|
||||
$user->setPassword($hasher->hashPassword($user, 'secret123'));
|
||||
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
|
||||
$id = $user->getId();
|
||||
self::assertInstanceOf(UuidV7::class, $id);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$reloaded = $this->em->find(User::class, $id);
|
||||
|
||||
self::assertNotNull($reloaded, 'User must be retrievable from the database after an identity-map clear.');
|
||||
self::assertInstanceOf(User::class, $reloaded);
|
||||
self::assertSame('alice@example.com', $reloaded->getEmail());
|
||||
self::assertTrue(
|
||||
$hasher->isPasswordValid($reloaded, 'secret123'),
|
||||
'Stored password hash must verify against the original plain-text password.',
|
||||
);
|
||||
self::assertStringNotContainsString(
|
||||
'secret123',
|
||||
$reloaded->getPassword(),
|
||||
'Plain-text password must never be stored.',
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmailMustBeUnique(): void
|
||||
{
|
||||
$hasher = $this->hasher;
|
||||
|
||||
$first = new User();
|
||||
$first->setEmail('bob@example.com');
|
||||
$first->setPassword($hasher->hashPassword($first, 'pass1'));
|
||||
$this->em->persist($first);
|
||||
$this->em->flush();
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$second = new User();
|
||||
$second->setEmail('bob@example.com');
|
||||
$second->setPassword($hasher->hashPassword($second, 'pass2'));
|
||||
$this->em->persist($second);
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
|
|
@ -11,3 +11,51 @@ if (method_exists(Dotenv::class, 'bootEnv')) {
|
|||
if ($_SERVER['APP_DEBUG']) {
|
||||
umask(0000);
|
||||
}
|
||||
|
||||
// Bootstrap the test database so that a fresh checkout / CI never hits
|
||||
// "database does not exist" or schema drift.
|
||||
//
|
||||
// Strategy: use the Symfony kernel (test env) to reach Doctrine, then:
|
||||
// 1. CREATE DATABASE IF NOT EXISTS — via the DBAL schema manager
|
||||
// 2. Run pending migrations — via the MigrationsBundle console command
|
||||
//
|
||||
// This runs once per phpunit invocation (not per test — DAMA wraps each test
|
||||
// in a transaction and rolls it back, so the schema itself must already exist).
|
||||
(static function (): void {
|
||||
$kernel = new App\Kernel('test', true);
|
||||
$kernel->boot();
|
||||
|
||||
$container = $kernel->getContainer();
|
||||
|
||||
/** @var Doctrine\DBAL\Connection $connection */
|
||||
$connection = $container->get('doctrine')->getConnection();
|
||||
|
||||
// 1. Ensure the database exists.
|
||||
$schemaManager = $connection->createSchemaManager();
|
||||
$dbName = $connection->getDatabase();
|
||||
|
||||
if (!in_array($dbName, $schemaManager->listDatabases(), true)) {
|
||||
$schemaManager->createDatabase($dbName);
|
||||
}
|
||||
|
||||
// 2. Run any pending migrations idempotently.
|
||||
// --no-interaction suppresses the "are you sure?" prompt.
|
||||
/** @var Symfony\Component\Console\Application $application */
|
||||
$application = new Symfony\Bundle\FrameworkBundle\Console\Application($kernel);
|
||||
$application->setAutoExit(false);
|
||||
|
||||
$exitCode = $application->run(
|
||||
new Symfony\Component\Console\Input\ArrayInput([
|
||||
'command' => 'doctrine:migrations:migrate',
|
||||
'--no-interaction' => true,
|
||||
]),
|
||||
new Symfony\Component\Console\Output\NullOutput(),
|
||||
);
|
||||
|
||||
if (0 !== $exitCode) {
|
||||
fwrite(\STDERR, "bootstrap.php: doctrine:migrations:migrate failed (exit {$exitCode})\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$kernel->shutdown();
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in a new issue