From d66f28e3f1fb815f132fcb695c97b1f0853ed6a4 Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Thu, 25 Jan 2024 15:09:02 +0200 Subject: [PATCH 1/8] Add TestScope attribute --- composer.json | 33 +++++++++++++++++---------------- src/Attribute/TestScope.php | 17 +++++++++++++++++ src/TestCase.php | 27 +++++++++++++++++++++++---- 3 files changed, 57 insertions(+), 20 deletions(-) create mode 100644 src/Attribute/TestScope.php diff --git a/composer.json b/composer.json index 2bf858a..076b128 100644 --- a/composer.json +++ b/composer.json @@ -44,21 +44,22 @@ "nyholm/psr7": "^1.5", "mockery/mockery": "^1.5", "phpunit/phpunit": "^9.6 || ^10.0", - "spiral/auth": "^3.8.4", - "spiral/auth-http": "^3.8.4", - "spiral/boot": "^3.8.4", - "spiral/events": "^3.8.4", - "spiral/console": "^3.8.4", - "spiral/http": "^3.8.4", - "spiral/mailer": "^3.8.4", - "spiral/queue": "^3.8.4", - "spiral/session": "^3.8.4", - "spiral/security": "^3.8.4", - "spiral/tokenizer": "^3.8.4", - "spiral/storage": "^3.8.4", - "spiral/views": "^3.8.4", - "spiral/translator": "^3.8.4", - "spiral/scaffolder": "^3.8.4", + "spiral/auth": "^3.12", + "spiral/auth-http": "^3.12", + "spiral/boot": "^3.12", + "spiral/events": "^3.12", + "spiral/console": "^3.12", + "spiral/core": "^3.12", + "spiral/http": "^3.12", + "spiral/mailer": "^3.12", + "spiral/queue": "^3.12", + "spiral/session": "^3.12", + "spiral/security": "^3.12", + "spiral/tokenizer": "^3.12", + "spiral/storage": "^3.12", + "spiral/views": "^3.12", + "spiral/translator": "^3.12", + "spiral/scaffolder": "^3.12", "symfony/mime": "^6.0 || ^7.0" }, "suggest": { @@ -66,7 +67,7 @@ "ext-gd": "Required to use generate fake image files" }, "require-dev": { - "spiral/framework": "^3.11", + "spiral/framework": "^3.12", "spiral/roadrunner-bridge": "^2.2 || ^3.0", "spiral-packages/league-event": "^1.0.1", "spiral/nyholm-bridge": "^1.2", diff --git a/src/Attribute/TestScope.php b/src/Attribute/TestScope.php new file mode 100644 index 0000000..dfbd7b1 --- /dev/null +++ b/src/Attribute/TestScope.php @@ -0,0 +1,17 @@ + $env * @return AbstractKernel|TestableKernelInterface */ - public function makeApp(array $env = []): AbstractKernel + public function makeApp(array $env = [], Container $container = new Container()): AbstractKernel { $environment = new Environment($env); - $app = $this->createAppInstance(); + $app = $this->createAppInstance($container); $app->getContainer()->removeBinding(EnvironmentInterface::class); $app->getContainer()->bindSingleton(EnvironmentInterface::class, $environment); @@ -158,9 +160,9 @@ public function makeApp(array $env = []): AbstractKernel return $app; } - public function initApp(array $env = []): void + public function initApp(array $env = [], Container $container = new Container()): void { - $this->app = $this->makeApp($env); + $this->app = $this->makeApp($env, $container); $this->suppressExceptionHandlingIfAttributeDefined(); (new \ReflectionClass(ContainerScope::class)) @@ -224,6 +226,23 @@ protected function tearDownTraits(): void $this->runTraitSetUpOrTearDown('tearDown'); } + protected function runTest(): mixed + { + $attribute = $this->getTestAttributes(TestScope::class)[0] ?? null; + if ($attribute === null) { + return parent::runTest(); + } + + return $this->app->getContainer()->runScope( + new Scope($attribute->scope, $attribute->bindings), + function (Container $container): mixed { + $this->initApp([...static::ENV, ...$this->getEnvVariablesFromConfig()], $container); + + return parent::runTest(); + }, + ); + } + private function runTraitSetUpOrTearDown(string $method): void { $ref = new \ReflectionClass(static::class); From 7b01577a04ceb2471b5840e84bc16741321adbbf Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Thu, 25 Jan 2024 15:14:13 +0200 Subject: [PATCH 2/8] Add branch-alias --- composer.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/composer.json b/composer.json index 076b128..f281007 100644 --- a/composer.json +++ b/composer.json @@ -90,5 +90,10 @@ "allow-plugins": { "spiral/composer-publish-plugin": false } + }, + "extra": { + "branch-alias": { + "dev-feature/scopes": "2.8.x-dev" + } } } From 2bedf48cf6f5ea313e467352f8ae410e943f34cb Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Thu, 25 Jan 2024 15:18:08 +0200 Subject: [PATCH 3/8] Fix branch-alias --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f281007..cfa469e 100644 --- a/composer.json +++ b/composer.json @@ -93,7 +93,7 @@ }, "extra": { "branch-alias": { - "dev-feature/scopes": "2.8.x-dev" + "dev-feature/scopes": "2.x-dev" } } } From 500759f99c455788931d51ff386014bf50e58e68 Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Thu, 25 Jan 2024 17:54:43 +0200 Subject: [PATCH 4/8] Run fake http requests in scope --- src/Attribute/TestScope.php | 2 +- src/Http/FakeHttp.php | 22 +++++++++++++++++----- src/TestCase.php | 32 ++++++++++++++++++++++++++++---- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/Attribute/TestScope.php b/src/Attribute/TestScope.php index dfbd7b1..95d7e1a 100644 --- a/src/Attribute/TestScope.php +++ b/src/Attribute/TestScope.php @@ -6,7 +6,7 @@ use Attribute; -#[Attribute(flags: Attribute::TARGET_METHOD)] +#[Attribute(flags: Attribute::TARGET_METHOD|Attribute::TARGET_CLASS)] final class TestScope { public function __construct( diff --git a/src/Http/FakeHttp.php b/src/Http/FakeHttp.php index e4884b9..ef9c513 100644 --- a/src/Http/FakeHttp.php +++ b/src/Http/FakeHttp.php @@ -6,6 +6,7 @@ use Nyholm\Psr7\ServerRequest; use Nyholm\Psr7\Stream; +use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; @@ -16,7 +17,9 @@ use Spiral\Auth\TokenStorageInterface; use Spiral\Auth\Transport\HeaderTransport; use Spiral\Auth\TransportRegistry; -use Spiral\Core\Container; +use Spiral\Core\Attribute\Proxy; +use Spiral\Core\BinderInterface; +use Spiral\Core\InvokerInterface; use Spiral\Http\Http; use Spiral\Session\SessionInterface; use Spiral\Testing\Auth\FakeActorProvider; @@ -32,12 +35,21 @@ class FakeHttp private ?object $actor = null; private ?SessionInterface $session = null; + private BinderInterface $binder; + private ContainerInterface $container; public function __construct( - private readonly Container $container, + ContainerInterface $container, private readonly FileFactory $fileFactory, private readonly \Closure $scope, ) { + $this->binder = $container + ->get(InvokerInterface::class) + ->invoke(static fn (#[Proxy] BinderInterface $binder): BinderInterface => $binder); + + $this->container = $container + ->get(InvokerInterface::class) + ->invoke(static fn (#[Proxy] ContainerInterface $container): ContainerInterface => $container); } public function withActor(object $actor): self @@ -129,7 +141,7 @@ public function flushSession(): self public function withMiddleware(string ...$middleware): self { foreach ($middleware as $name) { - $this->container->removeBinding($name); + $this->binder->removeBinding($name); } return $this; @@ -138,8 +150,8 @@ public function withMiddleware(string ...$middleware): self public function withoutMiddleware(string ...$middleware): self { foreach ($middleware as $name) { - $this->container->removeBinding($name); - $this->container->bindSingleton( + $this->binder->removeBinding($name); + $this->binder->bindSingleton( $name, new class implements MiddlewareInterface { public function process( diff --git a/src/TestCase.php b/src/TestCase.php index 9e1817e..de3e2d0 100644 --- a/src/TestCase.php +++ b/src/TestCase.php @@ -228,19 +228,25 @@ protected function tearDownTraits(): void protected function runTest(): mixed { - $attribute = $this->getTestAttributes(TestScope::class)[0] ?? null; - if ($attribute === null) { + $scope = $this->getTestScope(); + if ($scope === null) { return parent::runTest(); } - return $this->app->getContainer()->runScope( - new Scope($attribute->scope, $attribute->bindings), + $previousApp = $this->getApp(); + + $result = $this->getApp()->getContainer()->runScope( + new Scope($scope->scope, $scope->bindings), function (Container $container): mixed { $this->initApp([...static::ENV, ...$this->getEnvVariablesFromConfig()], $container); return parent::runTest(); }, ); + + $this->app = $previousApp; + + return $result; } private function runTraitSetUpOrTearDown(string $method): void @@ -263,4 +269,22 @@ private function runTraitSetUpOrTearDown(string $method): void $ref = $parent; } } + + private function getTestScope(): ?TestScope + { + $attribute = $this->getTestAttributes(TestScope::class)[0] ?? null; + if ($attribute !== null) { + return $attribute; + } + + try { + foreach ((new \ReflectionClass($this))->getAttributes(TestScope::class) as $attr) { + return $attr->newInstance(); + } + } catch (\Throwable) { + return null; + } + + return null; + } } From fdf99043fb3709bfbe3de5e256b4b373a4ab7d66 Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Sat, 17 Feb 2024 22:39:24 +0200 Subject: [PATCH 5/8] Add the ability to use array of scopes --- src/Attribute/TestScope.php | 2 +- src/TestCase.php | 28 +++++++++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Attribute/TestScope.php b/src/Attribute/TestScope.php index 95d7e1a..1c63a50 100644 --- a/src/Attribute/TestScope.php +++ b/src/Attribute/TestScope.php @@ -10,7 +10,7 @@ final class TestScope { public function __construct( - public readonly string|\BackedEnum $scope, + public readonly string|\BackedEnum|array $scope, public readonly array $bindings = [], ) { } diff --git a/src/TestCase.php b/src/TestCase.php index de3e2d0..5a6dcb2 100644 --- a/src/TestCase.php +++ b/src/TestCase.php @@ -235,14 +235,12 @@ protected function runTest(): mixed $previousApp = $this->getApp(); - $result = $this->getApp()->getContainer()->runScope( - new Scope($scope->scope, $scope->bindings), - function (Container $container): mixed { - $this->initApp([...static::ENV, ...$this->getEnvVariablesFromConfig()], $container); + $scopes = \is_array($scope->scope) ? $scope->scope : [$scope->scope]; + $result = $this->runScopes($scopes, function (Container $container): mixed { + $this->initApp([...static::ENV, ...$this->getEnvVariablesFromConfig()], $container); - return parent::runTest(); - }, - ); + return parent::runTest(); + }, $this->getContainer(), $scope->bindings); $this->app = $previousApp; @@ -287,4 +285,20 @@ private function getTestScope(): ?TestScope return null; } + + private function runScopes(array $scopes, Closure $callback, Container $container, array $bindings): mixed + { + if ($scopes === []) { + return $container->runScope($bindings, $callback); + } + + $scope = \array_shift($scopes); + + return $container->runScope( + new Scope($scope, []), + function (Container $container) use ($scopes, $callback, $bindings): mixed { + return $this->runScopes($scopes, $callback, $container, $bindings); + }, + ); + } } From da485d494885bce1396236794773e3a8ace3023d Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Wed, 1 May 2024 15:32:40 +0300 Subject: [PATCH 6/8] Remove branch alias --- composer.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/composer.json b/composer.json index cfa469e..076b128 100644 --- a/composer.json +++ b/composer.json @@ -90,10 +90,5 @@ "allow-plugins": { "spiral/composer-publish-plugin": false } - }, - "extra": { - "branch-alias": { - "dev-feature/scopes": "2.x-dev" - } } } From 6e2724662e9444b145bb50118c71faf5eb2170d1 Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Thu, 2 May 2024 13:18:50 +0300 Subject: [PATCH 7/8] Simplify code, add unit tests --- src/Http/FakeHttp.php | 7 +----- src/TestCase.php | 8 +----- src/Traits/InteractsWithHttp.php | 10 ++++---- src/Traits/TestableKernel.php | 10 ++++++++ tests/src/Attribute/ConfigTest.php | 20 +++++++++++++++ tests/src/Attribute/EnvTest.php | 20 +++++++++++++++ tests/src/Attribute/TestScopeTest.php | 36 +++++++++++++++++++++++++++ tests/src/Http/FakeHttpTest.php | 18 ++++++++++++++ 8 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 tests/src/Attribute/TestScopeTest.php diff --git a/src/Http/FakeHttp.php b/src/Http/FakeHttp.php index ef9c513..a8b32bd 100644 --- a/src/Http/FakeHttp.php +++ b/src/Http/FakeHttp.php @@ -36,20 +36,15 @@ class FakeHttp private ?object $actor = null; private ?SessionInterface $session = null; private BinderInterface $binder; - private ContainerInterface $container; public function __construct( - ContainerInterface $container, + #[Proxy] private readonly ContainerInterface $container, private readonly FileFactory $fileFactory, private readonly \Closure $scope, ) { $this->binder = $container ->get(InvokerInterface::class) ->invoke(static fn (#[Proxy] BinderInterface $binder): BinderInterface => $binder); - - $this->container = $container - ->get(InvokerInterface::class) - ->invoke(static fn (#[Proxy] ContainerInterface $container): ContainerInterface => $container); } public function withActor(object $actor): self diff --git a/src/TestCase.php b/src/TestCase.php index 5a6dcb2..41dd871 100644 --- a/src/TestCase.php +++ b/src/TestCase.php @@ -233,17 +233,11 @@ protected function runTest(): mixed return parent::runTest(); } - $previousApp = $this->getApp(); - $scopes = \is_array($scope->scope) ? $scope->scope : [$scope->scope]; - $result = $this->runScopes($scopes, function (Container $container): mixed { - $this->initApp([...static::ENV, ...$this->getEnvVariablesFromConfig()], $container); - + $result = $this->runScopes($scopes, function (): mixed { return parent::runTest(); }, $this->getContainer(), $scope->bindings); - $this->app = $previousApp; - return $result; } diff --git a/src/Traits/InteractsWithHttp.php b/src/Traits/InteractsWithHttp.php index 69da741..d0a02e0 100644 --- a/src/Traits/InteractsWithHttp.php +++ b/src/Traits/InteractsWithHttp.php @@ -4,6 +4,7 @@ namespace Spiral\Testing\Traits; +use Spiral\Core\FactoryInterface; use Spiral\Testing\Http\FakeHttp; use Spiral\Testing\Http\FileFactory; @@ -16,12 +17,11 @@ final public function getFileFactory(): FileFactory final public function fakeHttp(): FakeHttp { - return new FakeHttp( - $this->getContainer(), - $this->getFileFactory(), - function (\Closure $closure, array $bindings = []) { + return $this->getContainer()->get(FactoryInterface::class)->make(FakeHttp::class, [ + 'fileFactory' => $this->getFileFactory(), + 'scope' => function (\Closure $closure, array $bindings = []) { return $this->runScoped($closure, $bindings); } - ); + ]); } } diff --git a/src/Traits/TestableKernel.php b/src/Traits/TestableKernel.php index 0ec52f7..aadacc0 100644 --- a/src/Traits/TestableKernel.php +++ b/src/Traits/TestableKernel.php @@ -6,12 +6,22 @@ use Spiral\Boot\DispatcherInterface; use Spiral\Core\Container; +use Spiral\Core\ContainerScope; +use Spiral\Core\Internal\Introspector; trait TestableKernel { /** @inheritDoc */ public function getContainer(): Container { + $scopedContainer = ContainerScope::getContainer(); + if ( + $scopedContainer instanceof Container && + Introspector::scopeName($scopedContainer) !== Container::DEFAULT_ROOT_SCOPE_NAME + ) { + return $scopedContainer; + } + return $this->container; } diff --git a/tests/src/Attribute/ConfigTest.php b/tests/src/Attribute/ConfigTest.php index 693ee06..ef430ae 100644 --- a/tests/src/Attribute/ConfigTest.php +++ b/tests/src/Attribute/ConfigTest.php @@ -4,8 +4,10 @@ namespace Spiral\Testing\Tests\Attribute; +use Spiral\Core\Internal\Introspector; use Spiral\Storage\Config\StorageConfig; use Spiral\Testing\Attribute\Config; +use Spiral\Testing\Attribute\TestScope; use Spiral\Testing\Tests\TestCase; final class ConfigTest extends TestCase @@ -31,4 +33,22 @@ public function testMultipleAttributes(): void $this->assertSame('replaced', $config['default']); $this->assertSame('test', $config['servers']['static']['directory']); } + + #[TestScope('foo')] + #[Config('storage.default', 'replaced')] + public function testReplaceUsingAttributeInScope(): void + { + $config = $this->getConfig(StorageConfig::CONFIG); + $this->assertSame('replaced', $config['default']); + $this->assertSame(['foo', 'root'], Introspector::scopeNames($this->getContainer())); + } + + #[TestScope(['foo', 'bar'])] + #[Config('storage.default', 'replaced')] + public function testReplaceUsingAttributeInNestedScope(): void + { + $config = $this->getConfig(StorageConfig::CONFIG); + $this->assertSame('replaced', $config['default']); + $this->assertSame(['bar', 'foo', 'root'], Introspector::scopeNames($this->getContainer())); + } } diff --git a/tests/src/Attribute/EnvTest.php b/tests/src/Attribute/EnvTest.php index 122649b..d8184b5 100644 --- a/tests/src/Attribute/EnvTest.php +++ b/tests/src/Attribute/EnvTest.php @@ -4,7 +4,9 @@ namespace Spiral\Testing\Tests\Attribute; +use Spiral\Core\Internal\Introspector; use Spiral\Testing\Attribute\Env; +use Spiral\Testing\Attribute\TestScope; use Spiral\Testing\Tests\TestCase; final class EnvTest extends TestCase @@ -34,4 +36,22 @@ public function testMultipleAttributes(): void $this->assertEnvironmentValueSame('FOO', 'BAZ'); $this->assertEnvironmentValueSame('BAZ', 'BAZ'); } + + #[TestScope('foo')] + #[Env('FOO', 'BAZ')] + public function testEnvFromAttributeInScope(): void + { + $this->assertEnvironmentValueSame('FOO', 'BAZ'); + $this->assertEnvironmentValueSame('BAZ', 'QUX'); + $this->assertSame(['foo', 'root'], Introspector::scopeNames($this->getContainer())); + } + + #[TestScope(['foo', 'bar'])] + #[Env('FOO', 'BAZ')] + public function testEnvFromAttributeInNestedScope(): void + { + $this->assertEnvironmentValueSame('FOO', 'BAZ'); + $this->assertEnvironmentValueSame('BAZ', 'QUX'); + $this->assertSame(['bar', 'foo', 'root'], Introspector::scopeNames($this->getContainer())); + } } diff --git a/tests/src/Attribute/TestScopeTest.php b/tests/src/Attribute/TestScopeTest.php new file mode 100644 index 0000000..6733e1b --- /dev/null +++ b/tests/src/Attribute/TestScopeTest.php @@ -0,0 +1,36 @@ +assertSame(['root'], Introspector::scopeNames($this->getContainer())); + } + + #[TestScope('foo')] + public function testScopeFromAttribute(): void + { + $this->assertSame(['foo', 'root'], Introspector::scopeNames($this->getContainer())); + } + + #[TestScope(['foo', 'bar'])] + public function testNestedScopes(): void + { + $this->assertSame(['bar', 'foo', 'root'], Introspector::scopeNames($this->getContainer())); + } + + #[TestScope('foo', ['test' => \stdClass::class])] + public function testScopeWithBindings(): void + { + $this->assertSame(['foo', 'root'], Introspector::scopeNames($this->getContainer())); + $this->assertInstanceOf(\stdClass::class, $this->getContainer()->get('test')); + } +} diff --git a/tests/src/Http/FakeHttpTest.php b/tests/src/Http/FakeHttpTest.php index 31032d4..255ff1a 100644 --- a/tests/src/Http/FakeHttpTest.php +++ b/tests/src/Http/FakeHttpTest.php @@ -5,6 +5,8 @@ namespace Spiral\Testing\Tests\Http; use PHPUnit\Framework\ExpectationFailedException; +use Spiral\Core\Internal\Introspector; +use Spiral\Testing\Attribute\TestScope; use Spiral\Testing\Tests\TestCase; final class FakeHttpTest extends TestCase @@ -59,4 +61,20 @@ public function testGetJsonParsedBody(): void $response->getJsonParsedBody() ); } + + #[TestScope('foo')] + public function testGetWithQueryParamsInScope(): void + { + $response = $this->fakeHttp()->get('/get/query-params', ['foo' => 'bar', 'baz' => ['foo1' => 'bar1']]); + $response->assertBodySame('{"foo":"bar","baz":{"foo1":"bar1"}}'); + $this->assertSame(['foo', 'root'], Introspector::scopeNames($this->getContainer())); + } + + #[TestScope(['foo', 'bar'])] + public function testGetWithQueryParamsInNestedScope(): void + { + $response = $this->fakeHttp()->get('/get/query-params', ['foo' => 'bar', 'baz' => ['foo1' => 'bar1']]); + $response->assertBodySame('{"foo":"bar","baz":{"foo1":"bar1"}}'); + $this->assertSame(['bar', 'foo', 'root'], Introspector::scopeNames($this->getContainer())); + } } From dc6ca6ad410c1884b67dd8d2534b8497bafb1b81 Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Thu, 2 May 2024 13:20:41 +0300 Subject: [PATCH 8/8] Remove php versions from GitHub action --- .github/workflows/phpunit.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index cb6df2c..55040ad 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -12,7 +12,5 @@ jobs: with: os: >- ['ubuntu-latest'] - php: >- - ['8.1', '8.2'] stability: >- ['prefer-lowest', 'prefer-stable']