diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index 92c1939..44e5cc4 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -38,6 +38,7 @@ jobs:
- dependencies: "lowest"
stability: "stable"
php-version: "7.2"
+ symfony-deprecations-helper: "weak"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
diff --git a/README.md b/README.md
index 4db9af8..fb22f17 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@ VCRBundle
Integrates [php-vcr](https://github.com/php-vcr/php-vcr) into Symfony and its
web profiler.
+It also provides a VideoRecorderBrowser for testing purpose with extra helper methods handling php-vcr recordings.
@@ -20,41 +21,109 @@ composer require php-vcr/vcr-bundle
And declare the bundle in your `config/bundles.php` file:
```php
+ ['test' => true],
];
+```
+
+## Usage
+
+Enable the required library hooks for your purpose and write test cases.
+
+### VideoRecorderBrowser (without Trait)
+
+```php
+getContainer()->get('test.client.vcr');
+
+ $client->insertVideoRecorderCassette('my-test-cassette-name');
+
+ // this is an example, normally services inside you project do stuff like this and you trigger them by
+ // execute requests via the KernelBrowser client
+ file_get_contents('https://www.google.de');
+
+ // cassette.path is configured to '%kernel.project_dir%/tests/Fixtures'
+ // recordings are written to %kernel.project_dir%/tests/Fixtures/my-test-cassette-name
+ // cassette.path + cassetteName (done by inserting the cassette)
+ }
+}
+```
+
+### VideoRecorderBrowser (with Trait)
+
+```php
+setUpTestSuiteName();
+
+ return $this->testSuiteName;
+ }
+
+ /**
+ * @beforeClass
+ *
+ * @return void
+ */
+ protected function setUpTestSuiteName(): void
+ {
+ if (! $this->testSuiteName) {
+ $testSuiteName = (new \ReflectionClass($this))->getName();
+ if (
+ ! empty($this->ignoredTestSuiteNamespacePrefix) &&
+ str_starts_with($testSuiteName, $this->ignoredTestSuiteNamespacePrefix)
+ ) {
+ $testSuiteName = str_replace($this->ignoredTestSuiteNamespacePrefix, '', $testSuiteName);
+ }
+ $testSuiteNameParts = array_filter(explode('\\', $testSuiteName));
+ $this->testSuiteName = implode(DIRECTORY_SEPARATOR, $testSuiteNameParts);
+ }
+ }
+
+ /**
+ * Normalize a video recorder cassette name.
+ *
+ * @param string $name The name to normalize
+ *
+ * @return string The normalized cassette name
+ */
+ protected function normalizeVideoRecorderCassetteName(string $name): string
+ {
+ return preg_replace(['/\s+/', '/[^a-zA-Z0-9\-_]/'], ['-', ''], $name);
+ }
+
+ /**
+ * Get the video recorder cassette name from the current test.
+ *
+ * @param null|string $suffix
+ * @param bool $withDataSet With dataset
+ *
+ * @return string
+ */
+ protected function getVideoRecorderCassetteName(string $suffix = null, bool $withDataSet = true): string
+ {
+ $name = $this->getName($withDataSet);
+ if (! empty($suffix)) {
+ $name .= $suffix;
+ }
+
+ return $this->normalizeVideoRecorderCassetteName($name);
+ }
+
+ /**
+ * Enable VCR VideoRecorder.
+ *
+ * @param ContainerInterface $container
+ * @param string|null $suffix
+ *
+ * @param bool $withDataSet
+ *
+ * @return void
+ */
+ protected function enableVideoRecorder(
+ ContainerInterface $container,
+ string $suffix = null,
+ bool $withDataSet = true
+ ): void {
+ $videoRecorder = $this->getVideoRecorder($container);
+ $videoRecorder->turnOn();
+ $this->insertVideoRecorderCassette($container, $suffix, $withDataSet);
+ }
+
+ /**
+ * Insert cassette into VCR VideoRecorder.
+ *
+ * @param ContainerInterface $container
+ * @param null|string $suffix
+ *
+ * @param bool $withDataSet
+ *
+ * @return void
+ */
+ protected function insertVideoRecorderCassette(
+ ContainerInterface $container,
+ string $suffix = null,
+ bool $withDataSet = true
+ ): void {
+ $name = $this->getVideoRecorderCassetteName($suffix, $withDataSet);
+ $name = implode(
+ DIRECTORY_SEPARATOR,
+ [
+ $this->getTestSuiteName(),
+ $name,
+ ]
+ );
+
+ $videoRecorder = $this->getVideoRecorder($container);
+
+ $videoRecorder->insertCassette($name);
+ }
+
+ /**
+ * Insert default cassette into VCR VideoRecorder.
+ *
+ * @param ContainerInterface $container
+ *
+ * @return void
+ */
+ protected function insertDefaultVideoRecorderCassette(ContainerInterface $container): void
+ {
+ $defaultCassetteName = $container->getParameter('vcr.cassette.name');
+
+ $this->getVideoRecorder($container)->insertCassette($defaultCassetteName);
+ }
+
+ /**
+ * Disable VCR VideoRecorder.
+ *
+ * @param ContainerInterface $container
+ *
+ * @return void
+ */
+ protected function disableVideoRecorder(ContainerInterface $container): void
+ {
+ $this->getVideoRecorder($container)->turnOff();
+ }
+
+ /**
+ * @param ContainerInterface $container
+ *
+ * @return Videorecorder
+ */
+ protected function getVideoRecorder(ContainerInterface $container): Videorecorder
+ {
+ return $container->get('vcr.recorder');
+ }
+
+ /**
+ * Creates a Client supports the VideoRecorder framework.
+ *
+ * @param array $options An array of options to pass to the createKernel method
+ * @param array $server An array of server parameters
+ * @param null|string $suffix
+ * @param bool $withDataSet
+ *
+ * @return VideoRecorderBrowser A KernelBrowser instance
+ */
+ protected function createVideoRecorderClient(
+ array $options = [],
+ array $server = [],
+ string $suffix = null,
+ bool $withDataSet = true
+ ) {
+ if (! \method_exists(\get_called_class(), 'createClient')) {
+ throw new \LogicException(
+ 'Current test case has no static method to createClient(array $options, array $server).'
+ );
+ }
+
+ /** @var KernelBrowser $client */
+ $client = static::createClient($options, $server);
+ try {
+ /** @var VideoRecorderBrowser $client */
+ $client = $client->getKernel()->getContainer()->get('test.client.vcr');
+ } catch (ServiceNotFoundException $e) {
+ throw new \LogicException('VideoRecorderBrowser not loaded. Did you enable the this bundle?');
+ }
+
+ $client->setVideoRecorderCassetteBasePath($this->getTestSuiteName());
+ $client->enableVideoRecorder();
+ $client->insertVideoRecorderCassette($this->getVideoRecorderCassetteName($suffix, $withDataSet));
+
+ return $client;
+ }
+}
diff --git a/src/VCRBundle.php b/src/VCRBundle.php
index 76a174d..c2dc024 100644
--- a/src/VCRBundle.php
+++ b/src/VCRBundle.php
@@ -3,8 +3,10 @@
namespace VCR\VCRBundle;
+use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\Bundle\Bundle;
+use VCR\Videorecorder;
class VCRBundle extends Bundle
{
@@ -17,12 +19,33 @@ public function boot(): void
$fs->mkdir($cassettePath);
}
- if ($this->container->getParameter('vcr.enabled')) {
- $recorder = $this->container->get('vcr.recorder');
+ if ($this->isEnabled()) {
+ $recorder = $this->getVideoRecorder();
$cassetteName = $this->container->getParameter('vcr.cassette.name');
$recorder->turnOn();
$recorder->insertCassette($cassetteName);
}
}
+
+ public function shutdown(): void
+ {
+ if ($this->isEnabled()) {
+ $this->getVideoRecorder()->turnOff();
+ }
+ }
+
+ private function isEnabled(): bool
+ {
+ try {
+ return $this->container->getParameter('vcr.enabled');
+ } catch (InvalidArgumentException $e) {
+ return false;
+ }
+ }
+
+ private function getVideoRecorder(): Videorecorder
+ {
+ return $this->container->get('vcr.recorder');
+ }
}
diff --git a/src/VideoRecorderBrowser.php b/src/VideoRecorderBrowser.php
new file mode 100644
index 0000000..b02718e
--- /dev/null
+++ b/src/VideoRecorderBrowser.php
@@ -0,0 +1,274 @@
+boot();
+ if ($this->getContainer()->getParameter('vcr.enabled')) {
+ $this->vcrEnabled = true;
+ }
+ }
+
+ /**
+ * @return null|string
+ */
+ public function getVideoRecorderCassetteBasePath(): ?string
+ {
+ return $this->vcrCassetteBasePath;
+ }
+
+ /**
+ * @param null|string $vcrCassetteBasePath
+ *
+ * @return void
+ */
+ public function setVideoRecorderCassetteBasePath(?string $vcrCassetteBasePath)
+ {
+ if (! empty($vcrCassetteBasePath)) {
+ $vcrCassetteBasePath = trim($vcrCassetteBasePath, "\t\n\r\0\x0B" . DIRECTORY_SEPARATOR);
+ } else {
+ $vcrCassetteBasePath = null;
+ }
+
+ $this->vcrCassetteBasePath = $vcrCassetteBasePath;
+ }
+
+ /**
+ * Enable the video recorder for this client (& all its performed requests).
+ *
+ * @param null|string $cassetteName The cassette name to insert
+ * @param bool $insertDefaultCassette Insert the default cassette if non passed
+ *
+ * @return void
+ */
+ public function enableVideoRecorder(?string $cassetteName = null, bool $insertDefaultCassette = true): void
+ {
+ if ($this->isVideoRecorderEnabled()) {
+ return;
+ }
+
+ $this->getVideoRecorder()->turnOn();
+ $this->vcrEnabled = true;
+ if (! empty($cassetteName)) {
+ $this->insertVideoRecorderCassette($cassetteName);
+ } elseif ($insertDefaultCassette) {
+ $this->insertDefaultVideoRecorderCassette();
+ }
+ }
+
+ /**
+ * Disable the video recorder for this client (&all its performed requests).
+ *
+ * @return void
+ */
+ public function disableVideoRecorder(): void
+ {
+ $this->ejectVideoRecorderCassette(false);
+ $this->getVideoRecorder()->turnOff();
+ $this->vcrEnabled = false;
+ }
+
+ /**
+ * Is video recorder enabled?
+ *
+ * @return bool
+ */
+ public function isVideoRecorderEnabled(): bool
+ {
+ return $this->vcrEnabled;
+ }
+
+ /**
+ * Get the current video recorder.
+ *
+ * @return Videorecorder
+ */
+ public function getVideoRecorder(): Videorecorder
+ {
+ $container = $this->getContainer();
+ if (null === $container) {
+ throw new \LogicException('Kernel seems not to be booted, due container is not available.');
+ }
+
+ return $container->get('vcr.recorder');
+ }
+
+ /**
+ * @param string $cassetteName
+ * @param bool $force True if force
+ *
+ * @return void
+ */
+ public function insertVideoRecorderCassette(string $cassetteName, bool $force = false)
+ {
+ if (! $this->isVideoRecorderEnabled()) {
+ throw new \LogicException('VideoRecorder seems not be enabled. Enable it first, then insert a cassette.');
+ }
+
+ if ($this->vcrCassetteName !== $cassetteName || $force) {
+ $resolvedCassetteName = [$this->vcrCassetteBasePath, $cassetteName];
+ $resolvedCassetteName = \array_filter($resolvedCassetteName);
+ $resolvedCassetteName = \implode(DIRECTORY_SEPARATOR, $resolvedCassetteName);
+ $this->getVideoRecorder()->insertCassette($resolvedCassetteName);
+ $this->vcrCassetteName = $cassetteName;
+ }
+ }
+
+ /**
+ * Ejects the current inserted video recorder cassette.
+ *
+ * @param bool $reInsertDefaultCassette
+ *
+ * @return void
+ */
+ public function ejectVideoRecorderCassette(bool $reInsertDefaultCassette = true)
+ {
+ $this->getVideoRecorder()->eject();
+ $this->vcrCassetteName = null;
+ if ($reInsertDefaultCassette) {
+ $this->insertDefaultVideoRecorderCassette();
+ }
+ }
+
+ /**
+ * The default video recorder cassette name.
+ *
+ * @return string The default video recorder cassette name
+ */
+ protected function getDefaultVideoRecorderCassetteName(): string
+ {
+ $container = $this->getContainer();
+ if (null === $container) {
+ throw new \LogicException('Kernel seems not to be bootet, due container is not available.');
+ }
+
+ return $container->getParameter('vcr.cassette.name');
+ }
+
+ /**
+ * Insert the default video recorder cassette.
+ *
+ * @return void
+ */
+ protected function insertDefaultVideoRecorderCassette(): void
+ {
+ $cassetteName = $this->getDefaultVideoRecorderCassetteName();
+ $this->getVideoRecorder()->insertCassette($cassetteName);
+ }
+
+ /**
+ * Set a callable is called after container has been set up but before the request is performed to configure the
+ * video recorder used for this request.
+ *
+ * @param callable|null $callback
+ *
+ * @return void
+ */
+ public function setVideoRecorderConfigureCallback(callable $callback = null): void
+ {
+ $this->vcrConfigureCallback = $callback;
+ }
+
+ /**
+ * Remove existing video recorder configure callback.
+ *
+ * @return void
+ */
+ public function removeVideoRecorderConfigureCallback(): void
+ {
+ $this->vcrConfigureCallback = null;
+ }
+
+ /**
+ * @param Request $request
+ *
+ * @return Response
+ */
+ protected function doRequest($request): Response
+ {
+ // this is a little bit hacky because the we need the parent properties, but they are private :-(
+ $propertyAccessor = function ($property) {
+ $funcArgs = \func_get_args();
+ if (2 === count($funcArgs)) {
+ $this->$property = \func_get_arg(1);
+ } else {
+ return $this->$property;
+ }
+ };
+ $propertyAccessor = $propertyAccessor->bindTo($this, BaseBrowser::class);
+
+ $shouldReboot = $propertyAccessor('reboot');
+ $hasPerformedRequest = $propertyAccessor('hasPerformedRequest');
+ if ($this->isVideoRecorderEnabled() && $hasPerformedRequest && $shouldReboot) {
+ $this->disableReboot();
+
+ $kernel = $this->getKernel();
+ $kernel->shutdown();
+ $kernel->boot();
+
+ $this->enableVideoRecorder();
+ $this->insertVideoRecorderCassette($this->vcrCassetteName, true);
+ }
+
+ if ($this->vcrConfigureCallback instanceof \Closure) {
+ $vcrCallable = \Closure::bind($this->vcrConfigureCallback, $this);
+ \call_user_func($vcrCallable, $this->getVideoRecorder(), $this->getContainer());
+ }
+
+ $response = parent::doRequest($request);
+
+ if ($this->isVideoRecorderEnabled() && $hasPerformedRequest && $shouldReboot) {
+ $this->enableReboot();
+ }
+
+ return $response;
+ }
+}
diff --git a/tests/Functional/VCRTestCaseTraitTest.php b/tests/Functional/VCRTestCaseTraitTest.php
new file mode 100644
index 0000000..494f95d
--- /dev/null
+++ b/tests/Functional/VCRTestCaseTraitTest.php
@@ -0,0 +1,49 @@
+getContainer();
+
+ $basePath = $container->getParameter('vcr.cassette.path');
+ $testName = 'test';
+
+ $fixturePath = implode('/', [$basePath, $this->testSuitePath, $testName]);
+
+ $this->enableVideoRecorder($container);
+ $this->insertVideoRecorderCassette($container);
+
+ file_get_contents('https://www.google.de');
+
+ static::assertFileExists($fixturePath);
+ }
+
+ public function testVideoRecorderBrowser(): void
+ {
+ $client = static::createVideoRecorderClient();
+ static::assertInstanceOf(VideoRecorderBrowser::class, $client);
+
+ $container = $client->getKernel()->getContainer();
+ $basePath = $container->getParameter('vcr.cassette.path');
+ $testName = 'testVideoRecorderBrowser';
+ $fixturePath = implode('/', [$basePath, $this->testSuitePath, $testName]);
+
+ file_get_contents('https://www.google.de');
+
+ static::assertFileExists($fixturePath);
+ }
+}
diff --git a/tests/Functional/VideoRecorderBrowserTest.php b/tests/Functional/VideoRecorderBrowserTest.php
new file mode 100644
index 0000000..bda62d6
--- /dev/null
+++ b/tests/Functional/VideoRecorderBrowserTest.php
@@ -0,0 +1,34 @@
+getContainer();
+ $basePath = $container->getParameter('vcr.cassette.path');
+ $fixturesPath = $basePath . '/' . $cassetteName;
+
+ static::assertTrue(
+ $container->has($serviceId),
+ 'VideoRecorderBrowser (Client) service seems not to be registered.'
+ );
+
+ $client = $container->get($serviceId);
+ static::assertInstanceOf(VideoRecorderBrowser::class, $client);
+
+ $client->insertVideoRecorderCassette($cassetteName);
+ file_get_contents('https://www.google.de');
+
+ static::assertFileExists($fixturesPath);
+ }
+}
diff --git a/tests/Functional/app/config/Default/config.yml b/tests/Functional/app/config/Default/config.yml
index bd5053e..8153724 100644
--- a/tests/Functional/app/config/Default/config.yml
+++ b/tests/Functional/app/config/Default/config.yml
@@ -1,4 +1,3 @@
framework:
secret: ThisIsVerySecret!
test: ~
-
diff --git a/tests/Functional/app/config/VideoRecorderBrowser/config.yml b/tests/Functional/app/config/VideoRecorderBrowser/config.yml
new file mode 100644
index 0000000..7a8546c
--- /dev/null
+++ b/tests/Functional/app/config/VideoRecorderBrowser/config.yml
@@ -0,0 +1,8 @@
+imports:
+ - { resource: '../Default/config.yml' }
+
+vcr:
+ library_hooks:
+ stream_wrapper: true
+ cassette:
+ type: yaml