-
Notifications
You must be signed in to change notification settings - Fork 53
[Plugin] Add a plugin to record and replay responses #172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
GaryPEGEOT
wants to merge
2
commits into
php-http:master
from
GaryPEGEOT:feature/record-replay-plugin
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Http\Client\Common\Plugin; | ||
|
||
use Psr\Http\Message\RequestInterface; | ||
|
||
/** | ||
* Provides a unique name to identify a request. | ||
* | ||
* @author Gary PEGEOT <garypegeot@gmail.com> | ||
*/ | ||
interface NamingStrategyInterface | ||
{ | ||
public function name(RequestInterface $request): string; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Http\Client\Common\Plugin; | ||
|
||
use GuzzleHttp\Psr7; | ||
use Http\Client\Common\Plugin; | ||
use Http\Promise\FulfilledPromise; | ||
use Http\Promise\Promise; | ||
use Psr\Http\Message\RequestInterface; | ||
use Psr\Http\Message\ResponseInterface; | ||
use Symfony\Component\Filesystem\Filesystem; | ||
|
||
/** | ||
* Record successful responses into the filesystem and replay the response when a similar request is performed (VCR-like). | ||
* | ||
* @author Gary PEGEOT <garypegeot@gmail.com> | ||
*/ | ||
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; | ||
}); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace tests\Http\Client\Common\Plugin; | ||
|
||
use GuzzleHttp\Psr7\Response; | ||
use function GuzzleHttp\Psr7\str; | ||
use Http\Client\Common\Plugin\NamingStrategyInterface; | ||
use Http\Client\Common\Plugin\RecordAndReplayPlugin; | ||
use Http\Promise\FulfilledPromise; | ||
use Http\Promise\Promise; | ||
use PHPUnit\Framework\MockObject\MockObject; | ||
use PHPUnit\Framework\TestCase; | ||
use Psr\Http\Message\RequestInterface; | ||
use spec\Http\Client\Common\Plugin\PluginStub; | ||
use Symfony\Component\Filesystem\Filesystem; | ||
|
||
/** | ||
* @author Gary PEGEOT <garypegeot@gmail.com> | ||
* | ||
* @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())); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we read and write requests without a dependency on guzzle? and does that thing handle any response or only guzzle responses?
afaik there is no PSR for (de)serializing responses. if we need to depend on guzzle for it, we should create a separate repository for this plugin that depends on guzzle psr7.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly it only "deserialize" Guzzle Response, but can serialize any PSR 7 compatible Message. Adding a "mode" (record, replay or both) is a good idea, I will add it. Adding a new repo would justify the dependency on Guzzzle PSR7 and Symfony's filesystem as well (And PHPUnit in dev), but how can we proceed to create it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i can create an empty repository, then you can move the code to a pull request to that repository. like e.g. https://github.com/php-http/logger-plugin
i suggest the name php-http/vcr-plugin. record-and-replay-plugin is too long, and recorder-plugin is incomplete. when we agree on the name, i can create the repo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
vcr-plugin is fine to me :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
turns out the https://github.com/php-http/vcr-plugin repository already exists and jerome started something but abandoned it. @sagikazarmark says he talked with jerome and he does not intend to continue.
please do a pull request on it where you completely replace whats in there. jerome started to implement the whole recording and all, but i find your approach much better as there is less code to maintain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect! I'll start working on this then
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in php-http/vcr-plugin#3