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'] 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..1c63a50 --- /dev/null +++ b/src/Attribute/TestScope.php @@ -0,0 +1,17 @@ +binder = $container + ->get(InvokerInterface::class) + ->invoke(static fn (#[Proxy] BinderInterface $binder): BinderInterface => $binder); } public function withActor(object $actor): self @@ -129,7 +136,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 +145,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 691ec95..41dd871 100644 --- a/src/TestCase.php +++ b/src/TestCase.php @@ -14,6 +14,8 @@ use Spiral\Core\ConfigsInterface; use Spiral\Core\Container; use Spiral\Core\ContainerScope; +use Spiral\Core\Scope; +use Spiral\Testing\Attribute\TestScope; abstract class TestCase extends BaseTestCase { @@ -127,11 +129,11 @@ public function createAppInstance(Container $container = new Container()): Testa * @param array $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,21 @@ protected function tearDownTraits(): void $this->runTraitSetUpOrTearDown('tearDown'); } + protected function runTest(): mixed + { + $scope = $this->getTestScope(); + if ($scope === null) { + return parent::runTest(); + } + + $scopes = \is_array($scope->scope) ? $scope->scope : [$scope->scope]; + $result = $this->runScopes($scopes, function (): mixed { + return parent::runTest(); + }, $this->getContainer(), $scope->bindings); + + return $result; + } + private function runTraitSetUpOrTearDown(string $method): void { $ref = new \ReflectionClass(static::class); @@ -244,4 +261,38 @@ 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; + } + + 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); + }, + ); + } } 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())); + } }