diff --git a/composer.json b/composer.json index 9d65c4a..251e45e 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,9 @@ "guzzlehttp/psr7": "^1.4", "phpspec/phpspec": "^5.1", "phpspec/prophecy": "^1.8", - "sebastian/comparator": "^3.0" + "sebastian/comparator": "^3.0", + "symfony/filesystem": " ^3.4.20 || ^4.0.15 || ^4.1.9 || ^4.2.1", + "symfony/phpunit-bridge": "*" }, "suggest": { "ext-json": "To detect JSON responses with the ContentTypePlugin", @@ -38,7 +40,8 @@ }, "autoload-dev": { "psr-4": { - "spec\\Http\\Client\\Common\\": "spec/" + "spec\\Http\\Client\\Common\\": "spec/", + "tests\\Http\\Client\\Common\\": "tests/" } }, "scripts": { diff --git a/src/Plugin/NamingStrategyInterface.php b/src/Plugin/NamingStrategyInterface.php new file mode 100644 index 0000000..2492efd --- /dev/null +++ b/src/Plugin/NamingStrategyInterface.php @@ -0,0 +1,17 @@ + + */ +interface NamingStrategyInterface +{ + public function name(RequestInterface $request): string; +} diff --git a/src/Plugin/RecordAndReplayPlugin.php b/src/Plugin/RecordAndReplayPlugin.php new file mode 100644 index 0000000..dceb65b --- /dev/null +++ b/src/Plugin/RecordAndReplayPlugin.php @@ -0,0 +1,73 @@ + + */ +final class RecordAndReplayPlugin implements Plugin +{ + /** + * Return a unique name to identify a given request. + * + * @var NamingStrategyInterface + */ + private $namingStrategy; + + /** + * The directory containing your fixtures (Must be writable). + * + * @var string + */ + private $directory; + + /** + * @var Filesystem + */ + private $fs; + + public function __construct(NamingStrategyInterface $namingStrategy, string $directory, ?Filesystem $fs = null) + { + $this->namingStrategy = $namingStrategy; + $this->directory = $directory; + $this->fs = $fs ?? new Filesystem(); + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + if (!$this->fs->exists($this->directory)) { + $this->fs->mkdir($this->directory); + } + + $directory = realpath($this->directory); + $name = $this->namingStrategy->name($request); + $filename = "$directory/$name.txt"; + + if ($this->fs->exists($filename)) { + return new FulfilledPromise(Psr7\parse_response(file_get_contents($filename))); + } + + return $next($request)->then(function (ResponseInterface $response) use ($filename) { + if ($response->getStatusCode() < 300) { + $this->fs->dumpFile($filename, Psr7\str($response)); + } + + return $response; + }); + } +} diff --git a/tests/Plugin/RecordAndReplayPluginTest.php b/tests/Plugin/RecordAndReplayPluginTest.php new file mode 100644 index 0000000..d29051a --- /dev/null +++ b/tests/Plugin/RecordAndReplayPluginTest.php @@ -0,0 +1,83 @@ + + * + * @internal + */ +final class RecordAndReplayPluginTest extends TestCase +{ + /** + * @var NamingStrategyInterface|MockObject + */ + private $strategy; + + private $directory; + + /** + * @var Filesystem + */ + private $fs; + + /** + * @var RecordAndReplayPlugin + */ + private $plugin; + + protected function setUp(): void + { + $this->strategy = $this->createMock(NamingStrategyInterface::class); + $this->directory = sys_get_temp_dir().\DIRECTORY_SEPARATOR.md5(random_bytes(10)); + $this->fs = new Filesystem(); + $this->plugin = new RecordAndReplayPlugin($this->strategy, $this->directory, $this->fs); + } + + protected function tearDown(): void + { + if ($this->fs->exists($this->directory)) { + $this->fs->remove($this->directory); + } + } + + public function testHandleRequest(): void + { + /** @var RequestInterface $request */ + $request = $this->createMock(RequestInterface::class); + $next = function (): Promise { + return new FulfilledPromise(new Response(200, ['X-Foo' => 'Bar'], '{"baz": true}')); + }; + $filename = "$this->directory/foo.txt"; + $first = PluginStub::first(); + + $this->assertFileNotExists($filename, 'File should not exists yet'); + + $this->strategy->expects($this->any())->method('name')->with($request)->willReturn('foo'); + + $response = $this->plugin->handleRequest($request, $next, $first)->wait(); + + $this->assertInstanceOf(Response::class, $response); + $this->assertFileExists($filename, 'File should be created'); + $this->assertStringEqualsFile($filename, str($response)); + + $next = function (): void { + $this->fail('Next should not be called when the fixture file exists'); + }; + $this->assertSame(str($response), str($this->plugin->handleRequest($request, $next, $first)->wait())); + } +}