diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9b8a8c5..4106962 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: test: - name: PHP ${{ matrix.php-version }} (${{ matrix.experimental && 'experimental' || 'full support' }}) + name: PHP ${{ matrix.php-version }} (${{ matrix.experimental && 'experimental' || 'full support' }}, ${{ matrix.dependencies }} dependencies) runs-on: ubuntu-18.04 @@ -12,11 +12,17 @@ jobs: fail-fast: false matrix: php-version: - - 7.1 - - 7.2 - - 7.3 - 7.4 + - 8.0 experimental: [false] + dependencies: [lowest, stable] + include: + - php-version: 8.1 + experimental: true + dependencies: lowest + - php-version: 8.1 + experimental: true + dependencies: stable continue-on-error: ${{ matrix.experimental }} steps: @@ -34,6 +40,7 @@ jobs: uses: ramsey/composer-install@v1 with: composer-options: --prefer-dist + dependency-versions: ${{ matrix.dependencies }} continue-on-error: ${{ matrix.experimental }} - name: Setup PCOV @@ -45,3 +52,11 @@ jobs: - name: Run Tests run: composer tests continue-on-error: ${{ matrix.experimental }} + + - name: Check coding style + run: composer coding-style + continue-on-error: ${{ matrix.experimental }} + + - name: Check dependencies + run: composer dependencies + continue-on-error: ${{ matrix.experimental }} diff --git a/.gitignore b/.gitignore index 7278a54..78040de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ vendor/ composer.lock .idea/ -state/*-* +var/ build/* phpunit.xml +.phpunit.result.cache +.deptrac.cache diff --git a/composer.json b/composer.json index 6a612fc..6612cea 100644 --- a/composer.json +++ b/composer.json @@ -13,17 +13,26 @@ } ], "require": { - "php": "~7.1", - "silex/silex": "~2.0", - "guzzle/guzzle": ">=3.8", - "symfony/process": "~3|~4|~5", - "jeremeamia/superclosure": "~2", - "lstrojny/hmmmath": ">=0.5.0" - }, + "php": "~7.4|~8.0", + "symfony/process": "~4.4|~5", + "lstrojny/hmmmath": ">=0.5.0", + "slevomat/coding-standard": "^7.0", + "opis/closure": "^3.6", + "guzzlehttp/guzzle": "^7.3", + "psr/http-factory": "^1.0", + "guzzlehttp/psr7": "^1.8", + "http-interop/http-factory-guzzle": "^1.0", + "php-http/discovery": "^1.13", + "symfony/config": "~4.4|~5", + "symfony/http-kernel": "~4.4|~5", + "symfony/http-foundation": "~4.4|~5", + "symfony/routing": "~4.4|~5", + "symfony/dependency-injection": "~4.4|~5", + "symfony/framework-bundle": "~4.4|~5" + }, "require-dev": { - "internations/kodierungsregelwerksammlung": "~0.23.0", - "internations/testing-component": "1.0.1", - "phpunit/phpunit": "^7" + "phpunit/phpunit": "^9", + "qossmic/deptrac-shim": "^0.13.0" }, "autoload": { "psr-4": {"InterNations\\Component\\HttpMock\\": "src/"} @@ -32,6 +41,13 @@ "psr-4": {"InterNations\\Component\\HttpMock\\Tests\\": "tests/"} }, "scripts": { - "tests": "phpunit" + "tests": "phpunit", + "coding-style": "phpcs", + "dependencies": "deptrac", + "checkup": [ + "@composer tests", + "@composer coding-style", + "@composer dependencies" + ] } } diff --git a/depfile.yaml b/depfile.yaml new file mode 100644 index 0000000..43446a1 --- /dev/null +++ b/depfile.yaml @@ -0,0 +1,126 @@ +paths: + - ./src + - ./tests + - ./vendor/psr + - ./vendor/guzzlehttp + - ./vendor/phpunit + - ./vendor/opis +exclude_files: + - '#vendor/.*test.*#' +layers: + - name: "HTTP Mock Client" + collectors: + - type: className + regex: ^InterNations\\Component\\HttpMock\\(?!Tests\\|Server\\|Util|Http|ServerProcess).* + - name: "HTTP Mock PSR Middleware" + collectors: + - type: className + regex: ^InterNations\\Component\\HttpMock\\Http\\.* + - name: "HTTP Mock Server Process" + collectors: + - type: className + regex: ^InterNations\\Component\\HttpMock\\ServerProcess$ + - name: "HTTP Mock Server" + collectors: + - type: className + regex: ^InterNations\\Component\\HttpMock\\Server\\.+ + - name: "HTTP Mock Util" + collectors: + - type: className + regex: ^InterNations\\Component\\HttpMock\\Util$ + - name: "HTTP Mock Tests" + collectors: + - type: className + regex: ^InterNations\\Component\\HttpMock\\Tests\\.+ + - name: "PSR HTTP" + collectors: + - type: className + regex: ^Psr\\Http\\.* + - name: "PSR Logger" + collectors: + - type: className + regex: ^Psr\\Log\\.* + - name: "PSR HTTP Discovery" + collectors: + - type: className + regex: ^Http\\Discovery\\* + - name: Guzzle + collectors: + - type: className + regex: ^GuzzleHttp\\.* + - name: PHPUnit + collectors: + - type: className + regex: ^(PHPUnit|SebastianBergmann|DeepCopy|Prophecy|TheSeer|PharIo|Doctrine|Webmozart|PhpParser|phpDocumentor|Composer|Symfony\\Polyfill)\\.* + - name: "Symfony HttpFoundation Component" + collectors: + - type: className + regex: ^Symfony\\Component\\HttpFoundation\\.* + - name: "Symfony Framework Bundle" + collectors: + - type: className + regex: ^Symfony\\Bundle\\FrameworkBundle\\.* + - name: "Symfony HttpKernel Component" + collectors: + - type: className + regex: ^Symfony\\Component\\HttpKernel\\.* + - name: "Symfony DependencyInjection Component" + collectors: + - type: className + regex: ^Symfony\\Component\\DependencyInjection\\.* + - name: "Symfony Routing Component" + collectors: + - type: className + regex: ^Symfony\\Component\\Routing\\.* + - name: "Symfony Process Component" + collectors: + - type: className + regex: ^Symfony\\Component\\Process\\.* + - name: "Opis Serializable Closure" + collectors: + - type: className + regex: ^(Opis\\Closure|SuperClosure) + - name: "hmmmath Fibonacci" + collectors: + - type: className + regex: ^hmmmath\\Fibonacci\\.* +ruleset: + "HTTP Mock Client": + - "Opis Serializable Closure" + - "HTTP Mock Util" + - "HTTP Mock Server Process" + - "PSR HTTP" + - "PSR HTTP Discovery" + - "Symfony HttpFoundation Component" + "HTTP Mock Server Process": + - "hmmmath Fibonacci" + - "Symfony Process Component" + - "PSR HTTP" + - "PSR HTTP Discovery" + - "HTTP Mock PSR Middleware" + - "HTTP Mock Client" + "HTTP Mock Server": + - "Symfony HttpFoundation Component" + - "Symfony HttpKernel Component" + - "Symfony DependencyInjection Component" + - "Symfony Routing Component" + - "Symfony Framework Bundle" + - "HTTP Mock Util" + "HTTP Mock Tests": + - "HTTP Mock Client" + - "HTTP Mock Server Process" + - "Symfony HttpFoundation Component" + - "Opis Serializable Closure" + - "HTTP Mock Util" + - "PSR HTTP" + - "PSR HTTP Discovery" + - PHPUnit + "HTTP Mock Util": ~ + "HTTP Mock PSR Middleware": + - "PSR HTTP" + Guzzle: + - "PSR HTTP" + - "PSR Logger" + PHPUnit: ~ + "PSR Logger": + - PHPUnit diff --git a/doc/start.md b/doc/start.md index b972367..39b34d3 100644 --- a/doc/start.md +++ b/doc/start.md @@ -7,33 +7,34 @@ server in `setUpBeforeClass()` and `tearDownAfterClass()` respectively. ```php namespace Acme\Tests; -use InterNations\Component\HttpMock\PHPUnit\HttpMockTrait; +use PHPUnit\Framework\TestCase; +use InterNations\Component\HttpMock\PHPUnit\HttpMock; -class ExampleTest extends PHPUnit_Framework_TestCase +class ExampleTest extends TestCase { - use HttpMockTrait; + use HttpMock; - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { static::setUpHttpMockBeforeClass('8082', 'localhost'); } - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { static::tearDownHttpMockAfterClass(); } - public function setUp() + protected function setUp(): void { $this->setUpHttpMock(); } - public function tearDown() + protected function tearDown(): void { $this->tearDownHttpMock(); } - public function testSimpleRequest() + public function testSimpleRequest(): void { $this->http->mock ->when() @@ -44,10 +45,10 @@ class ExampleTest extends PHPUnit_Framework_TestCase ->end(); $this->http->setUp(); - $this->assertSame('mocked body', file_get_contents('http://localhost:8082/foo')); + self::assertSame('mocked body', file_get_contents('http://localhost:8082/foo')); } - public function testAccessingRecordedRequests() + public function testAccessingRecordedRequests(): void { $this->http->mock ->when() @@ -58,10 +59,10 @@ class ExampleTest extends PHPUnit_Framework_TestCase ->end(); $this->http->setUp(); - $this->assertSame('mocked body', $this->http->client->post('http://localhost:8082/foo')->send()->getBody(true)); + self::assertSame('mocked body', $this->http->client->post('http://localhost:8082/foo')->send()->getBody(true)); - $this->assertSame('POST', $this->http->requests->latest()->getMethod()); - $this->assertSame('/foo', $this->http->requests->latest()->getPath()); + self::assertSame('POST', $this->http->requests->latest()->getMethod()); + self::assertSame('/foo', $this->http->requests->latest()->getPath()); } } ``` diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..78a1d66 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,76 @@ + + + + ./src + ./tests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 51c72a8..07a6982 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,6 @@ + + + + + + ./tests/ @@ -17,8 +23,7 @@ - - + @@ -26,5 +31,6 @@ + diff --git a/public/index.php b/public/index.php index d9d901e..cc80fac 100644 --- a/public/index.php +++ b/public/index.php @@ -1,8 +1,47 @@ run(); +$autoloadFiles = [ + __DIR__ . '/../vendor/autoload.php', + __DIR__ . '/../../../autoload.php', +]; + +$autoloaderFound = false; + +foreach ($autoloadFiles as $autoloadFile) { + if (file_exists($autoloadFile)) { + require_once $autoloadFile; + $autoloaderFound = true; + break; + } +} + +if (!$autoloaderFound) { + throw new RuntimeException( + sprintf('Could not locate autoloader file. Tried "%s"', implode('", "', $autoloadFiles)) + ); +} + +Request::setFactory(static fn (...$args) => new SerializableRequest(...$args)); +$app = new ServerApplication('prod', getenv('HTTP_MOCK_TESTSUITE') === 'true'); +$request = Request::createFromGlobals(); +$response = $app->handle($request); +$response->send(); +$app->terminate($request, $response); diff --git a/src/Expectation.php b/src/Expectation.php index 1429b22..c3661c3 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -3,29 +3,21 @@ use InterNations\Component\HttpMock\Matcher\ExtractorFactory; use InterNations\Component\HttpMock\Matcher\MatcherFactory; -use InterNations\Component\HttpMock\Matcher\MatcherInterface; -use SuperClosure\SerializableClosure; +use InterNations\Component\HttpMock\Matcher\Matcher; use Closure; +use InterNations\Component\HttpMock\Response\ResponseBuilder; +use Opis\Closure\SerializableClosure; +use Symfony\Component\HttpFoundation\Response; class Expectation { - /** @var MatcherInterface[] */ - private $matcher = []; - - /** @var MatcherFactory */ - private $matcherFactory; - - /** @var ResponseBuilder */ - private $responseBuilder; - - /** @var Closure */ - private $limiter; - - /** @var ExtractorFactory */ - private $extractorFactory; - - /** @var int */ - private $priority; + /** @var array */ + private array $matcher = []; + private MatcherFactory $matcherFactory; + private ResponseBuilder $responseBuilder; + private Closure $limiter; + private ExtractorFactory $extractorFactory; + private int $priority; public function __construct( MockBuilder $mockBuilder, @@ -42,42 +34,46 @@ public function __construct( $this->priority = $priority; } - public function pathIs($matcher) + /** @param string|Matcher $matcher */ + public function pathIs($matcher): self { $this->appendMatcher($matcher, $this->extractorFactory->createPathExtractor()); return $this; } - public function methodIs($matcher) + /** @param string|Matcher $matcher */ + public function methodIs($matcher): self { $this->appendMatcher($matcher, $this->extractorFactory->createMethodExtractor()); return $this; } - public function queryParamIs($param, $matcher) + /** @param string|Matcher $matcher */ + public function queryParamIs(string $param, $matcher): self { $this->appendMatcher($matcher, $this->extractorFactory->createParamExtractor($param)); return $this; } - public function queryParamExists($param) + public function queryParamExists(string $param): self { $this->appendMatcher(true, $this->extractorFactory->createParamExistsExtractor($param)); return $this; } - public function queryParamNotExists($param) + public function queryParamNotExists(string $param): self { $this->appendMatcher(false, $this->extractorFactory->createParamExistsExtractor($param)); return $this; } - public function queryParamsAre(array $paramMap) + /** @param array $paramMap */ + public function queryParamsAre(array $paramMap): self { foreach ($paramMap as $param => $value) { $this->queryParamIs($param, $value); @@ -86,7 +82,8 @@ public function queryParamsAre(array $paramMap) return $this; } - public function queryParamsExist(array $params) + /** @param array $params */ + public function queryParamsExist(array $params): self { foreach ($params as $param) { $this->queryParamExists($param); @@ -95,7 +92,8 @@ public function queryParamsExist(array $params) return $this; } - public function queryParamsNotExist(array $params) + /** @param array $params */ + public function queryParamsNotExist(array $params): self { foreach ($params as $param) { $this->queryParamNotExists($param); @@ -104,29 +102,30 @@ public function queryParamsNotExist(array $params) return $this; } - public function headerIs($name, $value) + /** @param string|Matcher $value */ + public function headerIs(string $name, $value): self { $this->appendMatcher($value, $this->extractorFactory->createHeaderExtractor($name)); return $this; } - public function headerExists($name) + public function headerExists(string $name): self { $this->appendMatcher(true, $this->extractorFactory->createHeaderExistsExtractor($name)); return $this; } - public function callback(Closure $callback) + public function callback(Closure $callback): self { $this->appendMatcher($this->matcherFactory->closure($callback)); return $this; } - /** @return SerializableClosure[] */ - public function getMatcherClosures() + /** @return array */ + public function getMatcherClosures(): array { $closures = []; @@ -137,27 +136,28 @@ public function getMatcherClosures() return $closures; } - public function getPriority() + public function getPriority(): int { return $this->priority; } - public function then() + public function then(): ResponseBuilder { return $this->responseBuilder; } - public function getResponse() + public function getResponse(): Response { return $this->responseBuilder->getResponse(); } - public function getLimiter() + public function getLimiter(): SerializableClosure { return new SerializableClosure($this->limiter); } - private function appendMatcher($matcher, Closure $extractor = null) + /** @param string|Matcher $matcher */ + private function appendMatcher($matcher, Closure $extractor = null): void { $matcher = $this->createMatcher($matcher); @@ -168,8 +168,9 @@ private function appendMatcher($matcher, Closure $extractor = null) $this->matcher[] = $matcher; } - private function createMatcher($matcher) + /** @param string|Matcher $matcher */ + private function createMatcher($matcher): Matcher { - return $matcher instanceof MatcherInterface ? $matcher : $this->matcherFactory->str($matcher); + return $matcher instanceof Matcher ? $matcher : $this->matcherFactory->str($matcher); } } diff --git a/src/Http/BaseUriMiddleware.php b/src/Http/BaseUriMiddleware.php new file mode 100644 index 0000000..c9515e0 --- /dev/null +++ b/src/Http/BaseUriMiddleware.php @@ -0,0 +1,31 @@ +baseUri = $baseUri; + } + + public function process(RequestInterface $request, ClientInterface $client, callable $next): ResponseInterface + { + $originalUri = $request->getUri(); + + $uri = $originalUri + ->withScheme($this->baseUri->getScheme() ?: $originalUri->getScheme()) + ->withHost($this->baseUri->getHost() ?: $originalUri->getHost()) + ->withPort($this->baseUri->getPort() ?: $originalUri->getPort()) + ->withPath(sprintf('%s/%s', rtrim($this->baseUri->getPath(), '/'), ltrim($originalUri->getPath(), '/'))); + + + return $next($request->withUri($uri), $client); + } +} diff --git a/src/Http/ClientMiddleware.php b/src/Http/ClientMiddleware.php new file mode 100644 index 0000000..8acc474 --- /dev/null +++ b/src/Http/ClientMiddleware.php @@ -0,0 +1,11 @@ + */ + private SplStack $middlewareStack; + + public function __construct(ClientInterface $client, ClientMiddleware ...$middlewareStack) + { + $this->client = $client; + $this->middlewareStack = array_reduce( + $middlewareStack, + static function (SplStack $stack, ClientMiddleware $middleware) { + $stack->push($middleware); + + return $stack; + }, + new SplStack() + ); + } + + public function sendRequest(RequestInterface $request): ResponseInterface + { + return self::createNext(clone $this->middlewareStack)($request, $this->client); + } + + /** @param SplStack|iterable $pendingMiddlewareStack */ + private static function createNext(SplStack $pendingMiddlewareStack): callable + { + return static fn (RequestInterface $request, ClientInterface $client) => + count($pendingMiddlewareStack) > 0 + ? $pendingMiddlewareStack->pop()->process($request, $client, self::createNext($pendingMiddlewareStack)) + : $client->sendRequest($request); + } +} diff --git a/src/Matcher/ClosureMatcher.php b/src/Matcher/ClosureMatcher.php index c528e67..1362c34 100644 --- a/src/Matcher/ClosureMatcher.php +++ b/src/Matcher/ClosureMatcher.php @@ -3,16 +3,16 @@ use Closure; -class ClosureMatcher extends AbstractMatcher +class ClosureMatcher extends ExtractorBasedMatcher { - private $closure; + private Closure $closure; public function __construct(Closure $closure) { $this->closure = $closure; } - protected function createMatcher() + protected function createMatcher(): Closure { return $this->closure; } diff --git a/src/Matcher/AbstractMatcher.php b/src/Matcher/ExtractorBasedMatcher.php similarity index 66% rename from src/Matcher/AbstractMatcher.php rename to src/Matcher/ExtractorBasedMatcher.php index 429d60b..4aa16cc 100644 --- a/src/Matcher/AbstractMatcher.php +++ b/src/Matcher/ExtractorBasedMatcher.php @@ -2,19 +2,19 @@ namespace InterNations\Component\HttpMock\Matcher; use Closure; -use SuperClosure\SerializableClosure; +use Opis\Closure\SerializableClosure; use Symfony\Component\HttpFoundation\Request; -abstract class AbstractMatcher implements MatcherInterface +abstract class ExtractorBasedMatcher implements Matcher { - protected $extractor; + protected ?Closure $extractor = null; - public function setExtractor(Closure $extractor) + public function setExtractor(Closure $extractor): void { $this->extractor = $extractor; } - protected function createExtractor() + protected function createExtractor(): Closure { if (!$this->extractor) { return static function (Request $request) { @@ -25,9 +25,9 @@ protected function createExtractor() return $this->extractor; } - abstract protected function createMatcher(); + abstract protected function createMatcher(): Closure; - public function getMatcher() + public function getMatcher(): SerializableClosure { $matcher = new SerializableClosure($this->createMatcher()); $extractor = new SerializableClosure($this->createExtractor()); diff --git a/src/Matcher/ExtractorFactory.php b/src/Matcher/ExtractorFactory.php index 0faecb9..1f38850 100644 --- a/src/Matcher/ExtractorFactory.php +++ b/src/Matcher/ExtractorFactory.php @@ -3,16 +3,16 @@ use Symfony\Component\HttpFoundation\Request; -class ExtractorFactory +final class ExtractorFactory { - private $basePath; + private string $basePath; - public function __construct($basePath = '') + public function __construct(?string $basePath = null) { $this->basePath = rtrim($basePath, '/'); } - public function createPathExtractor() + public function createPathExtractor(): callable { $basePath = $this->basePath; @@ -21,35 +21,35 @@ public function createPathExtractor() }; } - public function createMethodExtractor() + public function createMethodExtractor(): callable { return static function (Request $request) { return $request->getMethod(); }; } - public function createParamExtractor($param) + public function createParamExtractor(string $param): callable { return static function (Request $request) use ($param) { return $request->query->get($param); }; } - public function createParamExistsExtractor($param) + public function createParamExistsExtractor(string $param): callable { return static function (Request $request) use ($param) { return $request->query->has($param); }; } - public function createHeaderExtractor($header) + public function createHeaderExtractor(string $header): callable { return static function (Request $request) use ($header) { return $request->headers->get($header); }; } - public function createHeaderExistsExtractor($header) + public function createHeaderExistsExtractor(string $header): callable { return static function (Request $request) use ($header) { return $request->headers->has($header); diff --git a/src/Matcher/Matcher.php b/src/Matcher/Matcher.php new file mode 100644 index 0000000..ae9c8cf --- /dev/null +++ b/src/Matcher/Matcher.php @@ -0,0 +1,11 @@ +regex = $regex; } - protected function createMatcher() + protected function createMatcher(): Closure { $regex = $this->regex; diff --git a/src/Matcher/StringMatcher.php b/src/Matcher/StringMatcher.php index 43a112a..c863b6d 100644 --- a/src/Matcher/StringMatcher.php +++ b/src/Matcher/StringMatcher.php @@ -1,16 +1,18 @@ string = $string; } - protected function createMatcher() + protected function createMatcher(): Closure { $string = $this->string; diff --git a/src/MockBuilder.php b/src/MockBuilder.php index c51bc70..32cbc80 100644 --- a/src/MockBuilder.php +++ b/src/MockBuilder.php @@ -11,20 +11,16 @@ class MockBuilder private const PRIORITY_EXACTLY = 10; private const PRIORITY_NTH = 100; - /** @var Expectation[] */ - private $expectations = []; + /** @var array */ + private array $expectations = []; - /** @var MatcherFactory */ - private $matcherFactory; + private MatcherFactory $matcherFactory; - /** @var Closure */ - private $limiter; + private Closure $limiter; - /** @var ExtractorFactory */ - private $extractorFactory; + private ExtractorFactory $extractorFactory; - /** @var int */ - private $priority; + private int $priority; public function __construct(MatcherFactory $matcherFactory, ExtractorFactory $extractorFactory) { @@ -33,22 +29,22 @@ public function __construct(MatcherFactory $matcherFactory, ExtractorFactory $ex $this->any(); } - public function once() + public function once(): self { return $this->exactly(1); } - public function twice() + public function twice(): self { return $this->exactly(2); } - public function thrice() + public function thrice(): self { return $this->exactly(3); } - public function exactly($times) + public function exactly(int $times): self { $this->limiter = static function ($runs) use ($times) { return $runs < $times; @@ -58,32 +54,32 @@ public function exactly($times) return $this; } - public function first() + public function first(): self { return $this->nth(1); } - public function second() + public function second(): self { return $this->nth(2); } - public function third() + public function third(): self { return $this->nth(3); } - public function nth($position) + public function nth(int $position): self { $this->limiter = static function ($runs) use ($position) { - return $runs === ($position - 1); + return $runs === $position - 1; }; $this->priority = $position * self::PRIORITY_NTH; return $this; } - public function any() + public function any(): self { $this->limiter = static function () { return true; @@ -93,8 +89,8 @@ public function any() return $this; } - /** @return Expectation */ - public function when() + /***/ + public function when(): Expectation { $this->expectations[] = new Expectation( $this, @@ -109,7 +105,8 @@ public function when() return end($this->expectations); } - public function flushExpectations() + /** @return list */ + public function flushExpectations(): array { $expectations = $this->expectations; $this->expectations = []; diff --git a/src/PHPUnit/HttpMock.php b/src/PHPUnit/HttpMock.php new file mode 100644 index 0000000..3ed8992 --- /dev/null +++ b/src/PHPUnit/HttpMock.php @@ -0,0 +1,95 @@ + $facade] + static::$staticHttp->all()); + } else { + static::$staticHttp = new HttpMockFacadeMap([$name => $facade]); + } + + ServerManager::getInstance()->add($facade->server); + } + + protected function setUpHttpMock(): void + { + static::assertHttpMockSetup(); + + $this->http = clone static::$staticHttp; + } + + protected static function assertHttpMockSetup(): void + { + if (static::$staticHttp) { + return; + } + + static::fail( + sprintf( + 'Static HTTP mock facade not present. Did you forget to invoke static::setUpHttpMockBeforeClass()' + . ' in %s::setUpBeforeClass()?', + static::class + ) + ); + } + + protected function tearDownHttpMock(): void + { + if (!$this->http) { + return; + } + + $http = $this->http; + $this->http = null; + $http->each( + function (HttpMockFacade $facade): void { + $this->assertSame( + '', + (string) $facade->server->getIncrementalErrorOutput(), + 'HTTP mock server standard error output should be empty' + ); + } + ); + } + + protected static function tearDownHttpMockAfterClass(): void + { + static::$staticHttp->each( + static function (HttpMockFacade $facade): void { + $facade->server->stop(); + ServerManager::getInstance()->remove($facade->server); + } + ); + } +} diff --git a/src/PHPUnit/HttpMockFacade.php b/src/PHPUnit/HttpMockFacade.php index 9b0dbbf..347bc86 100644 --- a/src/PHPUnit/HttpMockFacade.php +++ b/src/PHPUnit/HttpMockFacade.php @@ -1,77 +1,70 @@ */ + private array $services = []; - private $basePath; + private ?string $basePath; - public function __construct($port, $host, $basePath) + public function __construct(int $port, string $host, ?string $basePath = null) { - $server = new Server($port, $host); + $server = new ServerProcess($port, $host); $server->start(); $this->services['server'] = $server; $this->basePath = $basePath; } - public static function getProperties() + /** @return list */ + public static function getProperties(): array { return ['server', 'matches', 'mock', 'requests', 'client']; } - public function setUp() + public function setUp(): void { $this->server->setUp($this->mock->flushExpectations()); } - public function __get($property) + public function __get(string $property): object { - if (isset($this->services[$property])) { - return $this->services[$property]; - } - - return $this->services[$property] = $this->createService($property); + return $this->services[$property] ?? ($this->services[$property] = $this->createService($property)); } - private function createService($property) + private function createService(string $property): object { switch ($property) { case 'matches': return new MatcherFactory(); - break; case 'mock': return new MockBuilder($this->matches, new ExtractorFactory($this->basePath)); - break; case 'client': return $this->server->getClient(); - break; case 'requests': - return new RequestCollectionFacade($this->client); - break; + return new RequestCollectionFacade($this->client, Psr17FactoryDiscovery::findRequestFactory()); default: throw new RuntimeException(sprintf('Invalid property "%s" read', $property)); - break; } } @@ -80,7 +73,7 @@ public function __clone() $this->server->clean(); } - public function each(callable $callback) + public function each(callable $callback): void { $callback($this); } diff --git a/src/PHPUnit/HttpMockFacadeMap.php b/src/PHPUnit/HttpMockFacadeMap.php index 90aadab..602ac4d 100644 --- a/src/PHPUnit/HttpMockFacadeMap.php +++ b/src/PHPUnit/HttpMockFacadeMap.php @@ -8,14 +8,19 @@ /** @property-read HttpMockFacade */ class HttpMockFacadeMap implements ArrayAccess { - /** @var HttpMockFacade[] */ - private $facadeMap; + /** @var array */ + private array $facadeMap; + /** @param array $facadeMap */ public function __construct(array $facadeMap) { $this->facadeMap = $facadeMap; } + /** + * @param int|string $offset + * @return mixed + */ public function offsetGet($offset) { if (!$this->offsetExists($offset)) { @@ -25,17 +30,23 @@ public function offsetGet($offset) return $this->facadeMap[$offset]; } - public function offsetExists($offset) + /** @param int|string $offset */ + public function offsetExists($offset): bool { return isset($this->facadeMap[$offset]); } - public function offsetSet($offset, $value) + /** + * @param int|string $offset + * @param mixed $value + */ + public function offsetSet($offset, $value): void // @codingStandardsIgnoreLine { throw new BadMethodCallException(__METHOD__); } - public function offsetUnset($offset) + /** @param int|string $offset */ + public function offsetUnset($offset): void // @codingStandardsIgnoreLine { throw new BadMethodCallException(__METHOD__); } @@ -50,12 +61,12 @@ static function (HttpMockFacade $facade) { ); } - public function each(callable $callback) + public function each(callable $callback): void { array_map($callback, $this->facadeMap); } - public function __get($property) + public function __get(string $property): void { if (in_array($property, HttpMockFacade::getProperties(), true)) { throw new OutOfBoundsException( @@ -77,7 +88,8 @@ public function __get($property) ); } - public function all() + /** @return array */ + public function all(): array { return $this->facadeMap; } diff --git a/src/PHPUnit/HttpMockTrait.php b/src/PHPUnit/HttpMockTrait.php index 51afa3d..d4bd3b4 100644 --- a/src/PHPUnit/HttpMockTrait.php +++ b/src/PHPUnit/HttpMockTrait.php @@ -1,88 +1,12 @@ $facade] + static::$staticHttp->all()); - } else { - static::$staticHttp = new HttpMockFacadeMap([$name => $facade]); - } +use function trigger_error; +use const E_USER_DEPRECATED; - ServerManager::getInstance()->add($facade->server); - } +trigger_error(E_USER_DEPRECATED, sprintf('%s is deprecated. Use %s instead', HttpMockTrait::class, HttpMock::class)); - protected function setUpHttpMock() - { - static::assertHttpMockSetup(); - - $this->http = clone static::$staticHttp; - } - - protected static function assertHttpMockSetup() - { - if (!static::$staticHttp) { - static::fail( - sprintf( - 'Static HTTP mock facade not present. Did you forget to invoke static::setUpHttpMockBeforeClass()' - . ' in %s::setUpBeforeClass()?', - get_called_class() - ) - ); - } - } - - protected function tearDownHttpMock() - { - if (!$this->http) { - return; - } - - $http = $this->http; - $this->http = null; - $http->each( - function (HttpMockFacade $facade) { - $this->assertSame( - '', - (string) $facade->server->getIncrementalErrorOutput(), - 'HTTP mock server standard error output should be empty' - ); - } - ); - } - - protected static function tearDownHttpMockAfterClass() - { - static::$staticHttp->each( - static function (HttpMockFacade $facade) { - $facade->server->stop(); - ServerManager::getInstance()->remove($facade->server); - } - ); - } +trait HttpMockTrait // @codingStandardsIgnoreLine +{ + use HttpMock; } diff --git a/src/PHPUnit/ServerManager.php b/src/PHPUnit/ServerManager.php index 7f24e90..e608822 100644 --- a/src/PHPUnit/ServerManager.php +++ b/src/PHPUnit/ServerManager.php @@ -1,41 +1,34 @@ */ + private SplObjectStorage $servers; - private static $instance; + private static ?self $instance = null; - /** - * @return self - */ - public static function getInstance() + public static function getInstance(): self { - if (!static::$instance) { - static::$instance = new static(); - } - - return static::$instance; + return self::$instance ?: (self::$instance = new self()); } - public function add(Server $server) + public function add(ServerProcess $server): void { $this->servers->attach($server); } - public function remove(Server $server) + public function remove(ServerProcess $server): void { $this->servers->detach($server); } - public function cleanup() + public function cleanup(): void { foreach ($this->servers as $server) { $server->stop(); diff --git a/src/Request/SerializableRequest.php b/src/Request/SerializableRequest.php new file mode 100644 index 0000000..42c0f96 --- /dev/null +++ b/src/Request/SerializableRequest.php @@ -0,0 +1,64 @@ +getContent(); + return serialize([ + 'attributes' => $this->attributes, + 'request' => $this->request, + 'query' => $this->query, + 'server' => $this->server, + 'files' => $this->files, + 'cookies' => $this->cookies, + 'headers' => $this->headers, + 'content' => $this->content, + 'languages' => $this->languages, + 'charsets' => $this->charsets, + 'encodings' => $this->encodings, + 'acceptableContentTypes' => $this->acceptableContentTypes, + 'pathInfo' => $this->pathInfo, + 'requestUri' => $this->requestUri, + 'baseUrl' => $this->baseUrl, + 'basePath' => $this->basePath, + 'method' => $this->method, + 'format' => $this->format, + 'session' => $this->session, + 'locale' => $this->locale, + 'defaultLocale' => $this->defaultLocale, + ]); + } + + /** @param array $data */ + public function unserialize($data): void // @codingStandardsIgnoreLine + { + $attributes = unserialize($data); + + $this->attributes = $attributes['attributes']; + $this->request = $attributes['request']; + $this->query = $attributes['query']; + $this->server = $attributes['server']; + $this->files = $attributes['files']; + $this->cookies = $attributes['cookies']; + $this->headers = $attributes['headers']; + $this->content = $attributes['content']; + $this->languages = $attributes['languages']; + $this->charsets = $attributes['charsets']; + $this->encodings = $attributes['encodings']; + $this->acceptableContentTypes = $attributes['acceptableContentTypes']; + $this->pathInfo = $attributes['pathInfo']; + $this->requestUri = $attributes['requestUri']; + $this->baseUrl = $attributes['baseUrl']; + $this->basePath = $attributes['basePath']; + $this->method = $attributes['method']; + $this->format = $attributes['format']; + $this->session = $attributes['session']; + $this->locale = $attributes['locale']; + $this->defaultLocale = $attributes['defaultLocale']; + } +} diff --git a/src/Request/UnifiedRequest.php b/src/Request/UnifiedRequest.php deleted file mode 100644 index 45dfed3..0000000 --- a/src/Request/UnifiedRequest.php +++ /dev/null @@ -1,312 +0,0 @@ -wrapped = $wrapped; - $this->init($params); - } - - /** - * Get the user agent of the request - * - * @return string - */ - public function getUserAgent() - { - return $this->userAgent; - } - - /** - * Get the body of the request if set - * - * @return EntityBodyInterface|null - */ - public function getBody() - { - return $this->invokeWrappedIfEntityEnclosed(__FUNCTION__, func_get_args()); - } - - /** - * Get a POST field from the request - * - * @param string $field Field to retrieve - * - * @return mixed|null - */ - public function getPostField($field) - { - return $this->invokeWrappedIfEntityEnclosed(__FUNCTION__, func_get_args()); - } - - /** - * Get the post fields that will be used in the request - * - * @return QueryString - */ - public function getPostFields() - { - return $this->invokeWrappedIfEntityEnclosed(__FUNCTION__, func_get_args()); - } - - /** - * Returns an associative array of POST field names to PostFileInterface objects - * - * @return array - */ - public function getPostFiles() - { - return $this->invokeWrappedIfEntityEnclosed(__FUNCTION__, func_get_args()); - } - - /** - * Get a POST file from the request - * - * @param string $fieldName POST fields to retrieve - * - * @return array|null Returns an array wrapping an array of PostFileInterface objects - */ - public function getPostFile($fieldName) - { - return $this->invokeWrappedIfEntityEnclosed(__FUNCTION__, func_get_args()); - } - - /** - * Get application and plugin specific parameters set on the message. - * - * @return Collection - */ - public function getParams() - { - return $this->wrapped->getParams(); - } - - /** - * Retrieve an HTTP header by name. Performs a case-insensitive search of all headers. - * - * @param string $header Header to retrieve. - * - * @return Header|null Returns NULL if no matching header is found. - * Returns a Header object if found. - */ - public function getHeader($header) - { - return $this->wrapped->getHeader($header); - } - - /** - * Get all headers as a collection - * - * @return HeaderCollection - */ - public function getHeaders() - { - return $this->wrapped->getHeaders(); - } - - /** - * Get an array of message header lines - * - * @return array - */ - public function getHeaderLines() - { - return $this->wrapped->getHeaderLines(); - } - - /** - * Check if the specified header is present. - * - * @param string $header The header to check. - * - * @return bool Returns TRUE or FALSE if the header is present - */ - public function hasHeader($header) - { - return $this->wrapped->hasHeader($header); - } - - /** - * Get the raw message headers as a string - * - * @return string - */ - public function getRawHeaders() - { - return $this->wrapped->getRawHeaders(); - } - - /** - * Get the collection of key value pairs that will be used as the query - * string in the request - * - * @return QueryString - */ - public function getQuery() - { - return $this->wrapped->getQuery(); - } - - /** - * Get the HTTP method of the request - * - * @return string - */ - public function getMethod() - { - return $this->wrapped->getMethod(); - } - - /** - * Get the URI scheme of the request (http, https, ftp, etc) - * - * @return string - */ - public function getScheme() - { - return $this->wrapped->getScheme(); - } - - /** - * Get the host of the request - * - * @return string - */ - public function getHost() - { - return $this->wrapped->getHost(); - } - - /** - * Get the HTTP protocol version of the request - * - * @return string - */ - public function getProtocolVersion() - { - return $this->wrapped->getProtocolVersion(); - } - - /** - * Get the path of the request (e.g. '/', '/index.html') - * - * @return string - */ - public function getPath() - { - return $this->wrapped->getPath(); - } - - /** - * Get the port that the request will be sent on if it has been set - * - * @return int|null - */ - public function getPort() - { - return $this->wrapped->getPort(); - } - - /** - * Get the username to pass in the URL if set - * - * @return string|null - */ - public function getUsername() - { - return $this->wrapped->getUsername(); - } - - /** - * Get the password to pass in the URL if set - * - * @return string|null - */ - public function getPassword() - { - return $this->wrapped->getPassword(); - } - - /** - * Get the full URL of the request (e.g. 'http://www.guzzle-project.com/') - * scheme://username:password@domain:port/path?query_string#fragment - * - * @param bool $asObject Set to TRUE to retrieve the URL as a clone of the URL object owned by the request. - * - * @return string|Url - */ - public function getUrl($asObject = false) - { - return $this->wrapped->getUrl($asObject); - } - - /** - * Get an array of Cookies - * - * @return array - */ - public function getCookies() - { - return $this->wrapped->getCookies(); - } - - /** - * Get a cookie value by name - * - * @param string $name Cookie to retrieve - * - * @return null|string - */ - public function getCookie($name) - { - return $this->wrapped->getCookie($name); - } - - protected function invokeWrappedIfEntityEnclosed($method, array $params = []) - { - if (!$this->wrapped instanceof EntityEnclosingRequestInterface) { - throw new BadMethodCallException( - sprintf( - 'Cannot call method "%s" on a request that does not enclose an entity.' - . ' Did you expect a POST/PUT request instead of %s %s?', - $method, - $this->wrapped->getMethod(), - $this->wrapped->getPath() - ) - ); - } - - return call_user_func_array([$this->wrapped, $method], $params); - } - - private function init(array $params) - { - foreach ($params as $property => $value) { - if (property_exists($this, $property)) { - $this->{$property} = $value; - } - } - } -} diff --git a/src/RequestCollectionFacade.php b/src/RequestCollectionFacade.php index 850723e..d8df46d 100644 --- a/src/RequestCollectionFacade.php +++ b/src/RequestCollectionFacade.php @@ -2,91 +2,67 @@ namespace InterNations\Component\HttpMock; use Countable; -use Guzzle\Http\ClientInterface; -use Guzzle\Http\Message\EntityEnclosingRequestInterface; -use Guzzle\Http\Message\RequestFactory; -use Guzzle\Http\Message\RequestInterface; -use Guzzle\Http\Message\Response; -use InterNations\Component\HttpMock\Request\UnifiedRequest; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Symfony\Component\HttpFoundation\Request; use UnexpectedValueException; class RequestCollectionFacade implements Countable { - private $client; + private ClientInterface $client; + private RequestFactoryInterface $requestFactory; - public function __construct(ClientInterface $client) + public function __construct(ClientInterface $client, RequestFactoryInterface $requestFactory) { $this->client = $client; + $this->requestFactory = $requestFactory; } - /** - * @return UnifiedRequest - */ - public function latest() + public function latest(): Request { return $this->getRecordedRequest('/_request/last'); } - /** - * @return UnifiedRequest - */ - public function last() + public function last(): Request { return $this->getRecordedRequest('/_request/last'); } - /** - * @return UnifiedRequest - */ - public function first() + public function first(): Request { return $this->getRecordedRequest('/_request/first'); } - /** - * @param int $position - * @return UnifiedRequest - */ - public function at($position) + public function at(int $position): Request { return $this->getRecordedRequest('/_request/' . $position); } - /** - * @return UnifiedRequest - */ - public function pop() + public function pop(): Request { return $this->deleteRecordedRequest('/_request/last'); } - /** - * @return UnifiedRequest - */ - public function shift() + public function shift(): Request { return $this->deleteRecordedRequest('/_request/first'); } - public function count() + public function count(): int { - $response = $this->client - ->get('/_request/count') - ->send(); - - return (int) $response->getBody(true); + return (int) (string) $this->client->sendRequest( + $this->requestFactory->createRequest('GET', '/_request/count') + )->getBody(); } /** - * @param Response $response - * @param string $path * @throws UnexpectedValueException - * @return UnifiedRequest */ - private function parseRequestFromResponse(Response $response, $path) + private function parseRequestFromResponse(ResponseInterface $response, string $path): Request { try { - $requestInfo = Util::deserialize($response->getBody()); + return Util::deserialize($response->getBody()); } catch (UnexpectedValueException $e) { throw new UnexpectedValueException( sprintf('Cannot deserialize response from "%s": "%s"', $path, $response->getBody()), @@ -94,63 +70,25 @@ private function parseRequestFromResponse(Response $response, $path) $e ); } - - $request = RequestFactory::getInstance()->fromMessage($requestInfo['request']); - $params = $this->configureRequest( - $request, - $requestInfo['server'], - isset($requestInfo['enclosure']) ? $requestInfo['enclosure'] : [] - ); - - return new UnifiedRequest($request, $params); - } - - private function configureRequest(RequestInterface $request, array $server, array $enclosure) - { - if (isset($server['HTTP_HOST'])) { - $request->setHost($server['HTTP_HOST']); - } - - if (isset($server['HTTP_PORT'])) { - $request->setPort($server['HTTP_PORT']); - } - - if (isset($server['PHP_AUTH_USER'])) { - $request->setAuth($server['PHP_AUTH_USER'], isset($server['PHP_AUTH_PW']) ? $server['PHP_AUTH_PW'] : null); - } - - $params = []; - - if (isset($server['HTTP_USER_AGENT'])) { - $params['userAgent'] = $server['HTTP_USER_AGENT']; - } - - if ($request instanceof EntityEnclosingRequestInterface) { - $request->addPostFields($enclosure); - } - - return $params; } - private function getRecordedRequest($path) + private function getRecordedRequest(string $path): Request { - $response = $this->client - ->get($path) - ->send(); - - return $this->parseResponse($response, $path); + return $this->parseResponse( + $this->client->sendRequest($this->requestFactory->createRequest('GET', $path)), + $path + ); } - private function deleteRecordedRequest($path) + private function deleteRecordedRequest(string $path): Request { - $response = $this->client - ->delete($path) - ->send(); - - return $this->parseResponse($response, $path); + return $this->parseResponse( + $this->client->sendRequest($this->requestFactory->createRequest('DELETE', $path)), + $path + ); } - private function parseResponse(Response $response, $path) + private function parseResponse(ResponseInterface $response, string $path): Request { $statusCode = $response->getStatusCode(); @@ -160,11 +98,9 @@ private function parseResponse(Response $response, $path) ); } - $contentType = $response->hasHeader('content-type') - ? $response->getContentType() - : ''; + $contentType = $response->getHeaderLine('content-type'); - if (substr($contentType, 0, 10) !== 'text/plain') { + if (strpos($contentType, 'text/plain') !== 0) { throw new UnexpectedValueException( sprintf('Expected content type "text/plain" from "%s", got "%s"', $path, $contentType) ); diff --git a/src/RequestStorage.php b/src/RequestStorage.php deleted file mode 100644 index f2838f8..0000000 --- a/src/RequestStorage.php +++ /dev/null @@ -1,61 +0,0 @@ -pid = $pid; - $this->directory = $directory; - } - - public function store(Request $request, $name, $data) - { - file_put_contents($this->getFileName($request, $name), serialize($data)); - } - - public function read(Request $request, $name) - { - $fileName = $this->getFileName($request, $name); - - if (!file_exists($fileName)) { - return []; - } - - return Util::deserialize(file_get_contents($fileName)); - } - - public function append(Request $request, $name, $data) - { - $list = $this->read($request, $name); - $list[] = $data; - $this->store($request, $name, $list); - } - - public function prepend(Request $request, $name, $data) - { - $list = $this->read($request, $name); - array_unshift($list, $data); - $this->store($request, $name, $list); - } - - private function getFileName(Request $request, $name) - { - return $this->directory . $this->pid . '-' . $name . '-' . $request->server->get('SERVER_PORT'); - } - - public function clear(Request $request, $name) - { - $fileName = $this->getFileName($request, $name); - - if (file_exists($fileName)) { - unlink($fileName); - } - } -} diff --git a/src/Response/CallbackResponse.php b/src/Response/CallbackResponse.php index 025979c..c49d71e 100644 --- a/src/Response/CallbackResponse.php +++ b/src/Response/CallbackResponse.php @@ -5,22 +5,25 @@ class CallbackResponse extends Response { + /** @var callable */ private $callback; - public function setCallback(callable $callback) + public function setCallback(callable $callback): void { $this->callback = $callback; } - public function sendCallback() + public function sendCallback(): void { - if ($this->callback) { - $callback = $this->callback; - $callback($this); + if (!$this->callback) { + return; } + + $callback = $this->callback; + $callback($this); } - public function send() + public function send(): void { $this->sendCallback(); parent::send(); diff --git a/src/ResponseBuilder.php b/src/Response/ResponseBuilder.php similarity index 52% rename from src/ResponseBuilder.php rename to src/Response/ResponseBuilder.php index 62890ba..07ea51b 100644 --- a/src/ResponseBuilder.php +++ b/src/Response/ResponseBuilder.php @@ -1,17 +1,15 @@ response = new CallbackResponse(); } - public function statusCode($statusCode) + public function statusCode(int $statusCode): self { $this->response->setStatusCode($statusCode); return $this; } - public function body($body) + public function body(string $body): self { $this->response->setContent($body); return $this; } - public function callback(Closure $callback) + public function callback(Closure $callback): self { $this->response->setCallback(new SerializableClosure($callback)); return $this; } - public function header($header, $value) + public function header(string $header, string $value): self { $this->response->headers->set($header, $value); return $this; } - public function end() + public function end(): MockBuilder { return $this->mockBuilder; } - public function getResponse() + public function getResponse(): Response { return $this->response; } diff --git a/src/Server.php b/src/Server.php deleted file mode 100644 index b1b7a8c..0000000 --- a/src/Server.php +++ /dev/null @@ -1,164 +0,0 @@ -port = $port; - $this->host = $host; - $packageRoot = __DIR__ . '/../'; - $command = [ - 'php', - '-dalways_populate_raw_post_data=-1', - '-derror_log=', - '-S=' . $this->getConnectionString(), - '-t=public/', - $packageRoot . 'public/index.php', - ]; - - parent::__construct($command, $packageRoot); - $this->setTimeout(null); - } - - public function start(callable $callback = null, array $env = []) - { - parent::start($callback, $env); - - $this->pollWait(); - } - - public function stop($timeout = 10, $signal = null) - { - return parent::stop($timeout, $signal); - } - - public function getClient() - { - return $this->client ?: $this->client = $this->createClient(); - } - - private function createClient() - { - $client = new Client($this->getBaseUrl()); - $client->getEventDispatcher()->addListener( - 'request.error', - static function (Event $event) { - $event->stopPropagation(); - } - ); - - return $client; - } - - public function getBaseUrl() - { - return sprintf('http://%s', $this->getConnectionString()); - } - - public function getConnectionString() - { - return sprintf('%s:%d', $this->host, $this->port); - } - - /** - * @param Expectation[] $expectations - * @throws RuntimeException - */ - public function setUp(array $expectations) - { - /** @var Expectation $expectation */ - foreach ($expectations as $expectation) { - $response = $this->getClient()->post( - '/_expectation', - null, - [ - 'matcher' => serialize($expectation->getMatcherClosures()), - 'limiter' => serialize($expectation->getLimiter()), - 'response' => serialize($expectation->getResponse()), - ] - )->send(); - - if ($response->getStatusCode() !== 201) { - throw new RuntimeException('Could not set up expectations'); - } - } - } - - public function clean() - { - if (!$this->isRunning()) { - $this->start(); - } - - $this->getClient()->delete('/_all')->send(); - } - - private function pollWait() - { - foreach (FibonacciFactory::sequence(50000, 10000) as $sleepTime) { - try { - usleep($sleepTime); - $this->getClient()->head('/_me')->send(); - break; - } catch (CurlException $e) { - continue; - } - } - } - - public function getIncrementalErrorOutput() - { - return self::cleanErrorOutput(parent::getIncrementalErrorOutput()); - } - - public function getErrorOutput() - { - return self::cleanErrorOutput(parent::getErrorOutput()); - } - - private static function cleanErrorOutput($output) - { - if (!trim($output)) { - return ''; - } - - $errorLines = []; - - foreach (explode(PHP_EOL, $output) as $line) { - if (!$line) { - continue; - } - - if (!self::stringEndsWithAny($line, ['Accepted', 'Closing', ' started'])) { - $errorLines[] = $line; - } - } - - return $errorLines ? implode(PHP_EOL, $errorLines) : ''; - } - - private static function stringEndsWithAny($haystack, array $needles) - { - foreach ($needles as $needle) { - if (substr($haystack, (-1 * strlen($needle))) === $needle) { - return true; - } - } - - return false; - } -} diff --git a/src/Server/FileBasedStorage.php b/src/Server/FileBasedStorage.php new file mode 100644 index 0000000..8ac450f --- /dev/null +++ b/src/Server/FileBasedStorage.php @@ -0,0 +1,99 @@ +pid = $pid; + $this->directory = $directory; + $this->requestStack = $requestStack; + } + + /** @param list $requests */ + public function storeRequests(array $requests): void + { + file_put_contents($this->getFileName(self::NAMESPACE_REQUESTS), serialize($requests)); + } + + public function appendRequest(Request $request): void + { + $list = $this->read(self::NAMESPACE_REQUESTS); + $list[] = $request; + $this->store(self::NAMESPACE_REQUESTS, $list); + } + + /** @return list */ + public function readExpectations(): array + { + return $this->read(self::NAMESPACE_EXPECTATIONS); + } + + /** @return list */ + public function readRequests(): array + { + return $this->read(self::NAMESPACE_REQUESTS); + } + + /** @param list $expectations */ + public function storeExpectations(array $expectations): void + { + $this->store(self::NAMESPACE_EXPECTATIONS, $expectations); + } + + public function prependExpectation(ServerExpectation $expectation): void + { + $list = $this->read(self::NAMESPACE_EXPECTATIONS); + array_unshift($list, $expectation); + $this->store(self::NAMESPACE_EXPECTATIONS, $list); + } + + /** @param array|array $data */ + private function store(string $name, array $data): void + { + file_put_contents($this->getFileName($name), serialize($data)); + } + + /** @return array|array */ + private function read(string $name): array + { + $fileName = $this->getFileName($name); + + if (!file_exists($fileName)) { + return []; + } + + return Util::deserialize(file_get_contents($fileName)); + } + + private function getFileName(string $namespace): string + { + return sprintf( + '%s/%d-%s-%d.data', + $this->directory, + $this->pid, + $namespace, + $this->requestStack->getMasterRequest()->server->get('SERVER_PORT') + ); + } +} diff --git a/src/Server/RequestStorage.php b/src/Server/RequestStorage.php new file mode 100644 index 0000000..cca6d03 --- /dev/null +++ b/src/Server/RequestStorage.php @@ -0,0 +1,19 @@ + $expectations */ + public function storeExpectations(array $expectations): void; + /** @return array */ + public function readExpectations(): array; + public function prependExpectation(ServerExpectation $expectation): void; + + /** @param list $requests */ + public function storeRequests(array $requests): void; + /** @return array */ + public function readRequests(): array; + public function appendRequest(Request $request): void; +} diff --git a/src/Server/ServerApplication.php b/src/Server/ServerApplication.php new file mode 100644 index 0000000..47714dc --- /dev/null +++ b/src/Server/ServerApplication.php @@ -0,0 +1,244 @@ + */ + public function registerBundles(): array + { + return [new FrameworkBundle()]; + } + + public function configureContainer(ContainerConfigurator $container): void + { + $container->extension('framework', ['secret' => base64_encode(random_bytes(8))]); + + $container + ->services() + ->set(RequestStorage::class, FileBasedStorage::class) + ->autowire() + ->args([getmypid(), $this->getCacheDir() . '/state/']); + } + + public function configureRoutes(RoutingConfigurator $routes): void + { + $routes + ->add('me', '/_me') + ->controller([$this, 'meAction']) + ->methods(['GET']) + + ->add('reset_all', '/_all') + ->controller([$this, 'deleteAll']) + ->methods(['DELETE']) + + ->add('request_delete_all', '/_request') + ->controller([$this, 'deleteRequests']) + ->methods(['DELETE']) + + ->add('request_by_position_get', '/_request/{position}') + ->controller([$this, 'getRequestByPosition']) + ->methods(['GET']) + ->requirements(['position' => 'first|last'])# + + ->add('request_by_position_delete', '/_request/{position}') + ->controller([$this, 'deleteRequestByPosition']) + ->methods(['DELETE']) + ->requirements(['position' => 'first|last']) + + ->add('request_by_index_get', '/_request/{index}') + ->controller([$this, 'getRequestByIndex']) + ->methods(['GET']) + ->requirements(['index' => '\d+']) + + ->add('request_count', '/_request/count') + ->controller([$this, 'countRequests']) + ->methods(['GET']) + + ->add('expectations_delete_all', '/_expectation') + ->controller([$this, 'deleteExpectations']) + ->methods(['DELETE']) + + ->add('expectations_post', '/_expectation') + ->controller([$this, 'postExpectations']) + ->methods(['POST']) + + ->add('record', '{path}') + ->controller([$this, 'record']) + ->requirements(['path' => '.*']) + ; + } + + public function meAction(): Response + { + return self::createResponse(Response::HTTP_I_AM_A_TEAPOT, 'O RLY?'); + } + + public function deleteAll(RequestStorage $requestStorage): Response + { + $requestStorage->storeRequests([]); + $requestStorage->storeExpectations([]); + + return self::createResponse(Response::HTTP_OK); + } + + public function deleteRequests(RequestStorage $requestStorage): Response + { + $requestStorage->storeRequests([]); + + return self::createResponse(Response::HTTP_OK); + } + + public function getRequestByIndex(int $index, RequestStorage $requestStorage): Response + { + $requests = $requestStorage->readRequests(); + + if (!isset($requests[$index])) { + return self::createResponse(Response::HTTP_NOT_FOUND, 'Index ' . $index . ' not found'); + } + + return self::createResponse(Response::HTTP_OK, serialize($requests[$index])); + } + + public function getRequestByPosition(string $position, RequestStorage $requestStorage): Response + { + $requestData = $requestStorage->readRequests(); + $fn = 'array_' . ($position === 'last' ? 'pop': 'shift'); + $request = $fn($requestData); + + if (!$request) { + return self::createResponse(Response::HTTP_NOT_FOUND, $position . ' not available'); + } + + return self::createResponse(Response::HTTP_OK, serialize($request)); + } + + public function deleteRequestByPosition(string $position, RequestStorage $requestStorage): Response + { + $requests = $requestStorage->readRequests(); + $fn = 'array_' . ($position === 'last' ? 'pop' : 'shift'); + $request = $fn($requests); + $requestStorage->storeRequests($requests); + + if (!$request) { + return self::createResponse(Response::HTTP_NOT_FOUND, $position . ' not possible'); + } + + return self::createResponse(Response::HTTP_OK, serialize($request)); + } + + public function countRequests(RequestStorage $requestStorage): Response + { + return self::createResponse(Response::HTTP_OK, count($requestStorage->readRequests())); + } + + public function postExpectations(Request $currentRequest, RequestStorage $requestStorage): Response + { + $matcher = []; + + if ($currentRequest->request->has('matcher')) { + $matcherParameter = $currentRequest->request->get('matcher'); + if (!is_string($matcherParameter)) { + return self::createResponse( + Response::HTTP_EXPECTATION_FAILED, + 'POST data key "matcher" must be a serialized list of closures' + ); + } + + $matcher = Util::silentDeserialize($matcherParameter); + $validator = static function ($closure) { + return is_callable($closure); + }; + + if (!is_array($matcher) || count(array_filter($matcher, $validator)) !== count($matcher)) { + return self::createResponse( + Response::HTTP_EXPECTATION_FAILED, + 'POST data key "matcher" must be a serialized list of closures' + ); + } + } + + if (!$currentRequest->request->has('response')) { + return new Response('POST data key "response" not found in POST data', Response::HTTP_EXPECTATION_FAILED); + } + + $response = Util::silentDeserialize($currentRequest->request->get('response')); + + if (!$response instanceof Response) { + return new Response( + 'POST data key "response" must be a serialized Symfony response', + Response::HTTP_EXPECTATION_FAILED + ); + } + + $limiter = null; + + if ($currentRequest->request->has('limiter')) { + $limiter = Util::silentDeserialize($currentRequest->request->get('limiter')); + + if (!is_callable($limiter)) { + return new Response( + 'POST data key "limiter" must be a serialized closure', + Response::HTTP_EXPECTATION_FAILED + ); + } + } + + $requestStorage->prependExpectation(new ServerExpectation($matcher, $response, $limiter, 0)); + + return self::createResponse(Response::HTTP_CREATED); + } + + public function record(Request $currentRequest, RequestStorage $requestStorage): Response + { + $requestStorage->appendRequest($currentRequest); + $expectations = $requestStorage->readExpectations(); + + try { + foreach ($expectations as $expectation) { + $response = $expectation->matchRequest($currentRequest); + + if (!$response) { + continue; + } + + return $response; + } + + return self::createResponse(Response::HTTP_NOT_FOUND, 'No matching expectation found'); + } finally { + $requestStorage->storeExpectations($expectations); + } + } + + public function deleteExpectations(RequestStorage $requestStorage): Response + { + $requestStorage->storeExpectations([]); + + return self::createResponse(Response::HTTP_OK); + } + + private static function createResponse(int $statusCode, string $body = ''): Response + { + return new Response($body, $statusCode, ['Content-Type' => 'text/plain']); + } +} diff --git a/src/Server/ServerExpectation.php b/src/Server/ServerExpectation.php new file mode 100644 index 0000000..95fcd67 --- /dev/null +++ b/src/Server/ServerExpectation.php @@ -0,0 +1,54 @@ + */ + private array $matchers; + private Response $response; + /** @var callable|null */ + private $limiter; + private int $runs; + + /** @param list $matchers */ + public function __construct(array $matchers, Response $response, ?callable $limiter, int $runs) + { + $this->matchers = $matchers; + $this->response = $response; + $this->limiter = $limiter; + $this->runs = $runs; + } + + public function matchRequest(Request $currentRequest): ?Response + { + if (!$this->matches($currentRequest)) { + return null; + } + + try { + return $this->isApplicable() ? $this->response : null; + } finally { + $this->runs++; + } + } + + private function matches(Request $currentRequest): bool + { + foreach ($this->matchers as $matcher) { + if (!$matcher($currentRequest)) { + return false; + } + } + + return true; + } + + private function isApplicable(): bool + { + $limiter = $this->limiter; + return $limiter === null || $limiter($this->runs); + } +} diff --git a/src/Server/UpwardsCompatibleMicroKernelTrait.php b/src/Server/UpwardsCompatibleMicroKernelTrait.php new file mode 100644 index 0000000..631fddd --- /dev/null +++ b/src/Server/UpwardsCompatibleMicroKernelTrait.php @@ -0,0 +1,216 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace InterNations\Component\HttpMock\Server; + +use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\Configurator\AbstractConfigurator; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader as ContainerPhpFileLoader; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; +use Symfony\Component\Routing\Loader\PhpFileLoader as RoutingPhpFileLoader; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\RouteCollectionBuilder; +use function array_keys; +use function class_alias; +use function is_a; +use function sprintf; +use function strpos; +use function trigger_deprecation; + +/** + * A Kernel that provides configuration hooks. + * + * @author Ryan Weaver + * @author Fabien Potencier + * + * @method void configureRoutes(RoutingConfigurator $routes) + * @method void configureContainer(ContainerConfigurator $container) + */ +trait Symfony4MicroKernelTrait +{ + /** + * Adds or imports routes into your application. + * + * $routes->import($this->getProjectDir().'/config/*.{yaml,php}'); + * $routes + * ->add('admin_dashboard', '/admin') + * ->controller('App\Controller\AdminController::dashboard') + * ; + */ + //abstract protected function configureRoutes(RoutingConfigurator $routes): void; + + /** + * Configures the container. + * + * You can register extensions: + * + * $c->extension('framework', [ + * 'secret' => '%secret%' + * ]); + * + * Or services: + * + * $c->services()->set('halloween', 'FooBundle\HalloweenProvider'); + * + * Or parameters: + * + * $c->parameters()->set('halloween', 'lot of fun'); + */ + //abstract protected function configureContainer(ContainerConfigurator $container): void; + + /** + * {@inheritdoc} + */ + public function getCacheDir(): string + { + if (isset($_SERVER['APP_CACHE_DIR'])) { + return $_SERVER['APP_CACHE_DIR'].'/'.$this->environment; + } + + return parent::getCacheDir(); + } + + /** + * {@inheritdoc} + */ + public function getLogDir(): string + { + return $_SERVER['APP_LOG_DIR'] ?? parent::getLogDir(); + } + + /** + * {@inheritdoc} + */ + public function registerBundles(): iterable + { + $contents = require $this->getProjectDir().'/config/bundles.php'; + foreach ($contents as $class => $envs) { + if ($envs[$this->environment] ?? $envs['all'] ?? false) { + yield new $class(); + } + } + } + + /** + * {@inheritdoc} + */ + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(function (ContainerBuilder $container) use ($loader) { + $container->loadFromExtension('framework', [ + 'router' => [ + 'resource' => 'kernel::loadRoutes', + 'type' => 'service', + ], + ]); + + $kernelClass = false !== strpos(static::class, "@anonymous\0") ? parent::class : static::class; + + if (!$container->hasDefinition('kernel')) { + $container->register('kernel', $kernelClass) + ->addTag('controller.service_arguments') + ->setAutoconfigured(true) + ->setSynthetic(true) + ->setPublic(true) + ; + } + + $kernelDefinition = $container->getDefinition('kernel'); + $kernelDefinition->addTag('routing.route_loader'); + + $container->addObjectResource($this); + $container->fileExists($this->getProjectDir().'/config/bundles.php'); + + try { + $configureContainer = new \ReflectionMethod($this, 'configureContainer'); + } catch (\ReflectionException $e) { + throw new \LogicException(sprintf('"%s" uses "%s", but does not implement the required method "protected function configureContainer(ContainerConfigurator $container): void".', get_debug_type($this), MicroKernelTrait::class), 0, $e); + } + + $configuratorClass = $configureContainer->getNumberOfParameters() > 0 && ($type = $configureContainer->getParameters()[0]->getType()) instanceof \ReflectionNamedType && !$type->isBuiltin() ? $type->getName() : null; + + if ($configuratorClass && !is_a(ContainerConfigurator::class, $configuratorClass, true)) { + $this->configureContainer($container, $loader); + + return; + } + + // the user has opted into using the ContainerConfigurator + /* @var ContainerPhpFileLoader $kernelLoader */ + $kernelLoader = $loader->getResolver()->resolve($file = $configureContainer->getFileName()); + $kernelLoader->setCurrentDir(\dirname($file)); + $instanceof = &\Closure::bind(function &() { return $this->instanceof; }, $kernelLoader, $kernelLoader)(); + + + try { + $this->configureContainer(new ContainerConfigurator($container, $kernelLoader, $instanceof, $file, $file), $loader); + } finally { + $instanceof = []; + $kernelLoader->registerAliasesForSinglyImplementedInterfaces(); + } + + $container->setAlias($kernelClass, 'kernel')->setPublic(true); + }); + } + + /** + * @internal + * + * @return RouteCollection + */ + public function loadRoutes(LoaderInterface $loader) + { + $file = (new \ReflectionObject($this))->getFileName(); + /* @var RoutingPhpFileLoader $kernelLoader */ + $kernelLoader = $loader->getResolver()->resolve($file, 'php'); + $kernelLoader->setCurrentDir(\dirname($file)); + $collection = new RouteCollection(); + + try { + $configureRoutes = new \ReflectionMethod($this, 'configureRoutes'); + } catch (\ReflectionException $e) { + throw new \LogicException(sprintf('"%s" uses "%s", but does not implement the required method "protected function configureRoutes(RoutingConfigurator $routes): void".', get_debug_type($this), MicroKernelTrait::class), 0, $e); + } + + $configuratorClass = $configureRoutes->getNumberOfParameters() > 0 && ($type = $configureRoutes->getParameters()[0]->getType()) && !$type->isBuiltin() ? $type->getName() : null; + + if ($configuratorClass && !is_a(RoutingConfigurator::class, $configuratorClass, true)) { + trigger_deprecation('symfony/framework-bundle', '5.1', 'Using type "%s" for argument 1 of method "%s:configureRoutes()" is deprecated, use "%s" instead.', RouteCollectionBuilder::class, self::class, RoutingConfigurator::class); + + $routes = new RouteCollectionBuilder($loader); + $this->configureRoutes($routes); + + return $routes->build(); + } + + $this->configureRoutes(new RoutingConfigurator($collection, $kernelLoader, $file, $file)); + + foreach ($collection as $route) { + $controller = $route->getDefault('_controller'); + + if (\is_array($controller) && [0, 1] === array_keys($controller) && $this === $controller[0]) { + $route->setDefault('_controller', ['kernel', $controller[1]]); + } + } + + return $collection; + } +} + +if (version_compare(Kernel::VERSION, '5.0.0', '<')) { + class_alias(Symfony4MicroKernelTrait::class, UpwardsCompatibleMicroKernelTrait::class); +} else { + class_alias(MicroKernelTrait::class, UpwardsCompatibleMicroKernelTrait::class); +} diff --git a/src/ServerProcess.php b/src/ServerProcess.php new file mode 100644 index 0000000..c41a74b --- /dev/null +++ b/src/ServerProcess.php @@ -0,0 +1,206 @@ +port = $port; + $this->host = $host; + $packageRoot = __DIR__ . '/../'; + $command = [ + 'php', + '-dalways_populate_raw_post_data=-1', + '-derror_log=', + '-S=' . $this->getConnectionString(), + '-t=public/', + $packageRoot . 'public/index.php', + ]; + + parent::__construct($command, $packageRoot, ['HTTP_MOCK_TESTSUITE' => getenv('HTTP_MOCK_TESTSUITE')]); + $this->setTimeout(null); + } + + /** @param array $env */ + public function start(callable $callback = null, array $env = []): void + { + parent::start($callback, $env); + + $this->pollWait(); + } + + /** + * @param int|float $timeout + * @param int $signal + */ + public function stop($timeout = 10, $signal = null): ?int // @codingStandardsIgnoreLine + { + return parent::stop($timeout, $signal); + } + + public function getClient(): ClientInterface + { + return $this->client ?: $this->client = $this->createClient(); + } + + private function createClient(): ClientInterface + { + return new MiddlewareSupportingClient( + Psr18ClientDiscovery::find(), + new BaseUriMiddleware($this->getBaseUrl()) + ); + } + + public function getBaseUrl(): UriInterface + { + return $this->getUriFactory()->createUri(sprintf('http://%s', $this->getConnectionString())); + } + + private function getUriFactory(): UriFactoryInterface + { + return Psr17FactoryDiscovery::findUriFactory(); + } + + private function getRequestFactory(): RequestFactoryInterface + { + return Psr17FactoryDiscovery::findRequestFactory(); + } + + private function getStreamFactory(): StreamFactoryInterface + { + return Psr17FactoryDiscovery::findStreamFactory(); + } + + public function getConnectionString(): string + { + return sprintf('%s:%d', $this->host, $this->port); + } + + /** + * @param array $expectations + * @throws RuntimeException + */ + public function setUp(array $expectations): void + { + foreach ($expectations as $expectation) { + $response = $this->getClient()->sendRequest( + $this->getRequestFactory() + ->createRequest('POST', '/_expectation') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody( + $this->getStreamFactory()->createStream( + http_build_query( + [ + 'matcher' => serialize($expectation->getMatcherClosures()), + 'limiter' => serialize($expectation->getLimiter()), + 'response' => serialize($expectation->getResponse()), + ] + ) + ) + ) + ); + + if ($response->getStatusCode() !== 201) { + throw new RuntimeException('Could not set up expectations'); + } + } + } + + public function clean(): void + { + if (!$this->isRunning()) { + $this->start(); + } + + $this->getClient()->sendRequest($this->getRequestFactory()->createRequest('DELETE', '/_all')); + } + + private function pollWait(): void + { + foreach (FibonacciFactory::sequence(50000, 10000) as $sleepTime) { + try { + usleep($sleepTime); + $this->getClient()->sendRequest($this->getRequestFactory()->createRequest('HEAD', '/_me')); + break; + } catch (ClientExceptionInterface $e) { + continue; + } + } + } + + public function getIncrementalErrorOutput(): string + { + return self::cleanErrorOutput(parent::getIncrementalErrorOutput()); + } + + public function getErrorOutput(): string + { + return self::cleanErrorOutput(parent::getErrorOutput()); + } + + private static function cleanErrorOutput(string $output): string + { + if (!trim($output)) { + return ''; + } + + $errorLines = []; + + foreach (explode(PHP_EOL, $output) as $line) { + if (!$line) { + continue; + } + + if (self::stringContainsAny( + $line, + [ + 'Accepted', + 'Closing', + 'Development Server', + 'JIT is incompatible with third party extensions', + ' [info] ', + ' [debug] ', + ] + )) { + continue; + } + + $errorLines[] = $line; + } + + return $errorLines ? implode(PHP_EOL, $errorLines) : ''; + } + + /** @param list $needles */ + private static function stringContainsAny(string $haystack, array $needles): bool + { + foreach ($needles as $needle) { + if (strpos($haystack, $needle) !== false) { + return true; + } + } + + return false; + } +} diff --git a/src/Util.php b/src/Util.php index 6437a41..998db3d 100644 --- a/src/Util.php +++ b/src/Util.php @@ -5,9 +5,10 @@ final class Util { - public static function deserialize($string) + /** @return mixed */ + public static function deserialize(string $string) { - $result = static::silentDeserialize($string); + $result = self::silentDeserialize($string); if ($result === false) { throw new UnexpectedValueException('Cannot deserialize string'); @@ -16,7 +17,8 @@ public static function deserialize($string) return $result; } - public static function silentDeserialize($string) + /** @return mixed */ + public static function silentDeserialize(string $string) { // @codingStandardsIgnoreStart return @unserialize($string); diff --git a/src/app.php b/src/app.php deleted file mode 100644 index 0dd46b3..0000000 --- a/src/app.php +++ /dev/null @@ -1,232 +0,0 @@ -delete( - '/_expectation', - static function (Request $request) use ($app) { - $app['storage']->clear($request, 'expectations'); - - return new Response('', Response::HTTP_OK); - } -); - -$app->post( - '/_expectation', - static function (Request $request) use ($app) { - - $matcher = []; - - if ($request->request->has('matcher')) { - $matcher = Util::silentDeserialize($request->request->get('matcher')); - $validator = static function ($closure) { - return is_callable($closure); - }; - - if (!is_array($matcher) || count(array_filter($matcher, $validator)) !== count($matcher)) { - return new Response( - 'POST data key "matcher" must be a serialized list of closures', - Response::HTTP_EXPECTATION_FAILED - ); - } - } - - if (!$request->request->has('response')) { - return new Response('POST data key "response" not found in POST data', Response::HTTP_EXPECTATION_FAILED); - } - - $response = Util::silentDeserialize($request->request->get('response')); - - if (!$response instanceof Response) { - return new Response( - 'POST data key "response" must be a serialized Symfony response', - Response::HTTP_EXPECTATION_FAILED - ); - } - - $limiter = null; - - if ($request->request->has('limiter')) { - $limiter = Util::silentDeserialize($request->request->get('limiter')); - - if (!is_callable($limiter)) { - return new Response( - 'POST data key "limiter" must be a serialized closure', - Response::HTTP_EXPECTATION_FAILED - ); - } - } - - // Fix issue with silex default error handling - $response->headers->set('X-Status-Code', $response->getStatusCode()); - - $app['storage']->prepend( - $request, - 'expectations', - ['matcher' => $matcher, 'response' => $response, 'limiter' => $limiter, 'runs' => 0] - ); - - return new Response('', Response::HTTP_CREATED); - } -); - -$app->error( - static function (Exception $e, Request $request, $code, GetResponseForExceptionEvent $event = null) use ($app) { - if ($e instanceof NotFoundHttpException) { - if (method_exists($event, 'allowCustomResponseCode')) { - $event->allowCustomResponseCode(); - } - - $app['storage']->append( - $request, - 'requests', - serialize( - [ - 'server' => $request->server->all(), - 'request' => (string) $request, - 'enclosure' => $request->request->all(), - ] - ) - ); - - $notFoundResponse = new Response('No matching expectation found', Response::HTTP_NOT_FOUND); - - $expectations = $app['storage']->read($request, 'expectations'); - - foreach ($expectations as $pos => $expectation) { - foreach ($expectation['matcher'] as $matcher) { - if (!$matcher($request)) { - continue 2; - } - } - - $applicable = !isset($expectation['limiter']) || $expectation['limiter']($expectation['runs']); - - ++$expectations[$pos]['runs']; - $app['storage']->store($request, 'expectations', $expectations); - - if (!$applicable) { - $notFoundResponse = new Response('Expectation not met', Response::HTTP_GONE); - continue; - } - - return $expectation['response']; - } - - return $notFoundResponse; - } - - return new Response('Server error: ' . $e->getMessage(), $code); - } -); - -$app->get( - '/_request/count', - static function (Request $request) use ($app) { - return count($app['storage']->read($request, 'requests')); - } -); - -$app->get( - '/_request/{index}', - static function (Request $request, $index) use ($app) { - $requestData = $app['storage']->read($request, 'requests'); - - if (!isset($requestData[$index])) { - return new Response('Index ' . $index . ' not found', Response::HTTP_NOT_FOUND); - } - - return new Response($requestData[$index], Response::HTTP_OK, ['Content-Type' => 'text/plain']); - } -)->assert('index', '\d+'); - -$app->delete( - '/_request/{action}', - static function (Request $request, $action) use ($app) { - $requestData = $app['storage']->read($request, 'requests'); - $fn = 'array_' . ($action === 'last' ? 'pop' : 'shift'); - $requestString = $fn($requestData); - $app['storage']->store($request, 'requests', $requestData); - - if (!$requestString) { - return new Response($action . ' not possible', Response::HTTP_NOT_FOUND); - } - - return new Response($requestString, Response::HTTP_OK, ['Content-Type' => 'text/plain']); - } -)->assert('index', '(last|first)'); - -$app->get( - '/_request/{action}', - static function (Request $request, $action) use ($app) { - $requestData = $app['storage']->read($request, 'requests'); - $fn = 'array_' . ($action === 'last' ? 'pop' : 'shift'); - $requestString = $fn($requestData); - - if (!$requestString) { - return new Response($action . ' not available', Response::HTTP_NOT_FOUND); - } - - return new Response($requestString, Response::HTTP_OK, ['Content-Type' => 'text/plain']); - } -)->assert('index', '(last|first)'); - -$app->delete( - '/_request', - static function (Request $request) use ($app) { - $app['storage']->store($request, 'requests', []); - - return new Response('', Response::HTTP_OK); - } -); - -$app->delete( - '/_all', - static function (Request $request) use ($app) { - $app['storage']->store($request, 'requests', []); - $app['storage']->store($request, 'expectations', []); - - return new Response('', Response::HTTP_OK); - } -); - -$app->get( - '/_me', - static function () { - return new Response('O RLY?', Response::HTTP_I_AM_A_TEAPOT, ['Content-Type' => 'text/plain']); - } -); - -return $app; diff --git a/state/.gitkeep b/state/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/AppIntegrationTest.php b/tests/AppIntegrationTest.php index c8d8ebc..8fa0e0a 100644 --- a/tests/AppIntegrationTest.php +++ b/tests/AppIntegrationTest.php @@ -1,215 +1,251 @@ start(); } - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { static::assertSame('', (string) static::$server1->getOutput(), (string) static::$server1->getOutput()); - static::assertSame('', (string) static::$server1->getErrorOutput(), (string) static::$server1->getErrorOutput()); + static::assertSame( + '', + (string) static::$server1->getErrorOutput(), + (string) static::$server1->getErrorOutput() + ); static::$server1->stop(); } - public function setUp() + public function setUp(): void { static::$server1->clean(); $this->client = static::$server1->getClient(); } - public function testSimpleUseCase() + public function testSimpleUseCase(): void { - $response = $this->client->post( - '/_expectation', - null, - $this->createExpectationParams( - [ - static function ($request) { - return $request instanceof Request; - } - ], - new Response('fake body', 200) - ) - )->send(); - $this->assertSame('', (string) $response->getBody()); - $this->assertSame(201, $response->getStatusCode()); - - $response = $this->client->post('/foobar', ['X-Special' => 1], ['post' => 'data'])->send(); - - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('fake body', (string) $response->getBody()); - - $response = $this->client->get('/_request/latest')->send(); - - /** @var EntityEnclosingRequest $request */ + $response = $this->client->sendRequest( + $this->getRequestFactory() + ->createRequest('POST', '/_expectation') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody( + $this->getStreamFactory()->createStream( + http_build_query( + $this->createExpectationParams( + [ + static function ($request) { + return $request instanceof Request; + }, + ], + new Response('fake body', 200) + ) + ) + ) + ) + ); + self::assertSame('', (string) $response->getBody()); + self::assertSame(201, $response->getStatusCode()); + + $response = $this->client->sendRequest( + $this->getRequestFactory() + ->createRequest('POST', '/foobar') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withHeader('X-Special', 1) + ->withBody($this->getStreamFactory()->createStream(http_build_query(['post' => 'data']))) + ); + + self::assertSame(200, $response->getStatusCode()); + self::assertSame('fake body', (string) $response->getBody()); + + $response = $this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/_request/last')); + $request = $this->parseRequestFromResponse($response); - $this->assertSame('1', (string) $request->getHeader('X-Special')); - $this->assertSame('post=data', (string) $request->getBody()); + self::assertSame('1', $request->headers->get('X-Special')); + self::assertSame('post=data', $request->getContent()); } - public function testRecording() + public function testRecording(): void { - $this->client->delete('/_all')->send(); + $this->client->sendRequest($this->getRequestFactory()->createRequest('DELETE', '/_all')); - $this->assertSame(404, $this->client->get('/_request/latest')->send()->getStatusCode()); - $this->assertSame(404, $this->client->get('/_request/0')->send()->getStatusCode()); - $this->assertSame(404, $this->client->get('/_request/first')->send()->getStatusCode()); - $this->assertSame(404, $this->client->get('/_request/last')->send()->getStatusCode()); + self::assertSame(404, $this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/_request/0'))->getStatusCode()); + self::assertSame(404, $this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/_request/first'))->getStatusCode()); + self::assertSame(404, $this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/_request/last'))->getStatusCode()); - $this->client->get('/req/0')->send(); - $this->client->get('/req/1')->send(); - $this->client->get('/req/2')->send(); - $this->client->get('/req/3')->send(); + $this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/req/0')); + $this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/req/1')); + $this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/req/2')); + $this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/req/3')); - $this->assertSame( + self::assertSame( '/req/3', - $this->parseRequestFromResponse($this->client->get('/_request/last')->send())->getPath() + $this->parseRequestFromResponse($this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/_request/last')))->getRequestUri() ); - $this->assertSame( + self::assertSame( '/req/0', - $this->parseRequestFromResponse($this->client->get('/_request/0')->send())->getPath() + $this->parseRequestFromResponse($this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/_request/0')))->getRequestUri() ); - $this->assertSame( + self::assertSame( '/req/1', - $this->parseRequestFromResponse($this->client->get('/_request/1')->send())->getPath() + $this->parseRequestFromResponse($this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/_request/1')))->getRequestUri() ); - $this->assertSame( + self::assertSame( '/req/2', - $this->parseRequestFromResponse($this->client->get('/_request/2')->send())->getPath() + $this->parseRequestFromResponse($this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/_request/2')))->getRequestUri() ); - $this->assertSame( + self::assertSame( '/req/3', - $this->parseRequestFromResponse($this->client->get('/_request/3')->send())->getPath() + $this->parseRequestFromResponse($this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/_request/3')))->getRequestUri() ); - $this->assertSame(404, $this->client->get('/_request/4')->send()->getStatusCode()); + self::assertSame(404, $this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/_request/4'))->getStatusCode()); - $this->assertSame( + self::assertSame( '/req/3', - $this->parseRequestFromResponse($this->client->delete('/_request/last')->send())->getPath() + $this->parseRequestFromResponse($this->client->sendRequest($this->getRequestFactory()->createRequest('DELETE', '/_request/last')))->getRequestUri() ); - $this->assertSame( + self::assertSame( '/req/0', - $this->parseRequestFromResponse($this->client->delete('/_request/first')->send())->getPath() + $this->parseRequestFromResponse($this->client->sendRequest($this->getRequestFactory()->createRequest('DELETE', '/_request/first')))->getRequestUri() ); - $this->assertSame( + self::assertSame( '/req/1', - $this->parseRequestFromResponse($this->client->get('/_request/0')->send())->getPath() + $this->parseRequestFromResponse($this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/_request/0')))->getRequestUri() ); - $this->assertSame( + self::assertSame( '/req/2', - $this->parseRequestFromResponse($this->client->get('/_request/1')->send())->getPath() + $this->parseRequestFromResponse($this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/_request/1')))->getRequestUri() ); - $this->assertSame(404, $this->client->get('/_request/2')->send()->getStatusCode()); + self::assertSame(404, $this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/_request/2'))->getStatusCode()); } - public function testErrorHandling() + public function testErrorHandling(): void { - $this->client->delete('/_all')->send(); + $this->client->sendRequest($this->getRequestFactory()->createRequest('DELETE', '/_all')); - $response = $this->client->post('/_expectation', null, ['matcher' => ''])->send(); - $this->assertSame(417, $response->getStatusCode()); - $this->assertSame('POST data key "matcher" must be a serialized list of closures', (string) $response->getBody()); - - $response = $this->client->post('/_expectation', null, ['matcher' => ['foo']])->send(); - $this->assertSame(417, $response->getStatusCode()); - $this->assertSame('POST data key "matcher" must be a serialized list of closures', (string) $response->getBody()); + $response = $this->client->sendRequest( + $this->getRequestFactory() + ->createRequest('POST', '/_expectation') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody($this->getStreamFactory()->createStream(http_build_query(['matcher' => '']))) + ); + self::assertSame(417, $response->getStatusCode()); + self::assertSame( + 'POST data key "matcher" must be a serialized list of closures', + (string) $response->getBody() + ); - $response = $this->client->post('/_expectation', null, [])->send(); - $this->assertSame(417, $response->getStatusCode()); - $this->assertSame('POST data key "response" not found in POST data', (string) $response->getBody()); + $response = $this->client->sendRequest( + $this->getRequestFactory() + ->createRequest('POST', '/_expectation') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody( + $this->getStreamFactory()->createStream( + http_build_query(['matcher' => ['foo']]) + ) + ) + ); + self::assertSame(417, $response->getStatusCode()); + self::assertSame( + 'POST data key "matcher" must be a serialized list of closures', + (string) $response->getBody() + ); - $response = $this->client->post('/_expectation', null, ['response' => ''])->send(); - $this->assertSame(417, $response->getStatusCode()); - $this->assertSame('POST data key "response" must be a serialized Symfony response', (string) $response->getBody()); + $response = $this->client->sendRequest($this->getRequestFactory()->createRequest('POST', '/_expectation')); + self::assertSame(417, $response->getStatusCode()); + self::assertSame('POST data key "response" not found in POST data', (string) $response->getBody()); - $response = $this->client->post('/_expectation', null, ['response' => serialize(new Response()), 'limiter' => 'foo'])->send(); - $this->assertSame(417, $response->getStatusCode()); - $this->assertSame('POST data key "limiter" must be a serialized closure', (string) $response->getBody()); - } + $response = $this->client->sendRequest( + $this->getRequestFactory() + ->createRequest('POST', '/_expectation') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody($this->getStreamFactory()->createStream(http_build_query(['response' => '']))) + ); + self::assertSame(417, $response->getStatusCode()); + self::assertSame( + 'POST data key "response" must be a serialized Symfony response', + (string) $response->getBody() + ); - public function testServerParamsAreRecorded() - { - $this->client - ->setUserAgent('CUSTOM UA') - ->get('/foo') - ->setAuth('username', 'password') - ->setProtocolVersion('1.0') - ->send(); - - $latestRequest = unserialize($this->client->get('/_request/latest')->send()->getBody()); - - $this->assertSame(HTTP_MOCK_HOST, $latestRequest['server']['SERVER_NAME']); - $this->assertSame(HTTP_MOCK_PORT, $latestRequest['server']['SERVER_PORT']); - $this->assertSame('username', $latestRequest['server']['PHP_AUTH_USER']); - $this->assertSame('password', $latestRequest['server']['PHP_AUTH_PW']); - $this->assertSame('HTTP/1.0', $latestRequest['server']['SERVER_PROTOCOL']); - $this->assertSame('CUSTOM UA', $latestRequest['server']['HTTP_USER_AGENT']); + $response = $this->client->sendRequest( + $this->getRequestFactory() + ->createRequest('POST', '/_expectation') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody( + $this->getStreamFactory()->createStream( + http_build_query(['response' => serialize(new Response()), 'limiter' => 'foo']) + ) + ) + ); + self::assertSame(417, $response->getStatusCode()); + self::assertSame('POST data key "limiter" must be a serialized closure', (string) $response->getBody()); } - public function testNewestExpectationsAreFirstEvaluated() + public function testNewestExpectationsAreFirstEvaluated(): void { - $this->client->post( - '/_expectation', - null, - $this->createExpectationParams( - [ - static function ($request) { - return $request instanceof Request; - } - ], - new Response('first', 200) - ) - )->send(); - $this->assertSame('first', $this->client->get('/')->send()->getBody(true)); - - $this->client->post( - '/_expectation', - null, - $this->createExpectationParams( - [ - static function ($request) { - return $request instanceof Request; - } - ], - new Response('second', 200) - ) - )->send(); - $this->assertSame('second', $this->client->get('/')->send()->getBody(true)); + $this->client->sendRequest( + $this->getRequestFactory() + ->createRequest('POST', '/_expectation') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody( + $this->getStreamFactory()->createStream( + http_build_query( + $this->createExpectationParams( + [ + static function ($request) { + return $request instanceof Request; + }, + ], + new Response('first', 200) + ) + ) + ) + ) + ); + self::assertSame('first', (string) $this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/'))->getBody()); + + $this->client->sendRequest( + $this->getRequestFactory() + ->createRequest('POST', '/_expectation') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody( + $this->getStreamFactory()->createStream( + http_build_query( + $this->createExpectationParams( + [static function ($request) { return $request instanceof Request; }], + new Response('second', 200) + ) + ) + ) + ) + ); + self::assertSame('second', (string) $this->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/'))->getBody()); } - public function testServerLogsAreNotInErrorOutput() + public function testServerLogsAreNotInErrorOutput(): void { - $this->client->delete('/_all'); + $this->client->sendRequest($this->getRequestFactory()->createRequest('DELETE', '/_all')); $expectedServerErrorOutput = '[404]: (null) / - No such file or directory'; @@ -220,26 +256,28 @@ public function testServerLogsAreNotInErrorOutput() $actualServerErrorOutput = self::$server1->getErrorOutput(); - $this->assertEquals($expectedServerErrorOutput, $actualServerErrorOutput); + self::assertEquals($expectedServerErrorOutput, $actualServerErrorOutput); self::$server1->clearErrorOutput(); } - private function parseRequestFromResponse(GuzzleResponse $response) + private function parseRequestFromResponse(ResponseInterface $response): Request { - $body = unserialize($response->getBody()); - - return RequestFactory::getInstance()->fromMessage($body['request']); + return Util::deserialize($response->getBody()); } - private function createExpectationParams(array $closures, Response $response) + /** + * @param list $closures + * @return array{matcher: string, response: string} + */ + private function createExpectationParams(array $closures, Response $response): array { foreach ($closures as $index => $closure) { $closures[$index] = new SerializableClosure($closure); } return [ - 'matcher' => serialize($closures), + 'matcher' => serialize($closures), 'response' => serialize($response), ]; } diff --git a/tests/Fixtures/Request.php b/tests/Fixtures/Request.php index b6d746e..0356251 100644 --- a/tests/Fixtures/Request.php +++ b/tests/Fixtures/Request.php @@ -1,16 +1,16 @@ requestUri = $requestUri; } - public function setContent($content) + public function setContent(string $content): void { $this->content = $content; } diff --git a/tests/Matcher/ExtractorFactoryTest.php b/tests/Matcher/ExtractorFactoryTest.php index 3f5f8f7..8932953 100644 --- a/tests/Matcher/ExtractorFactoryTest.php +++ b/tests/Matcher/ExtractorFactoryTest.php @@ -2,86 +2,85 @@ namespace InterNations\Component\HttpMock\Tests\Matcher; use InterNations\Component\HttpMock\Matcher\ExtractorFactory; -use InterNations\Component\Testing\AbstractTestCase; -use Symfony\Component\HttpFoundation\Request; +use InterNations\Component\HttpMock\Tests\TestCase; use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\HttpFoundation\Request; -class ExtractorFactoryTest extends AbstractTestCase +class ExtractorFactoryTest extends TestCase { - /** @var ExtractorFactory */ - private $extractorFactory; + private ExtractorFactory $extractorFactory; /** @var Request|MockObject */ private $request; - public function setUp() + public function setUp(): void { $this->extractorFactory = new ExtractorFactory(); - $this->request = $this->createMock('Symfony\Component\HttpFoundation\Request'); + $this->request = $this->createMock(Request::class); } - public function testGetMethod() + public function testGetMethod(): void { $this->request - ->expects($this->once()) + ->expects(self::once()) ->method('getMethod') - ->will($this->returnValue('POST')); + ->willReturn('POST'); $extractor = $this->extractorFactory->createMethodExtractor(); - $this->assertSame('POST', $extractor($this->request)); + self::assertSame('POST', $extractor($this->request)); } - public function testGetPath() + public function testGetPath(): void { $this->request - ->expects($this->once()) + ->expects(self::once()) ->method('getPathInfo') - ->will($this->returnValue('/foo/bar')); + ->willReturn('/foo/bar'); $extractor = $this->extractorFactory->createPathExtractor(); - $this->assertSame('/foo/bar', $extractor($this->request)); + self::assertSame('/foo/bar', $extractor($this->request)); } - public function testGetPathWithBasePath() + public function testGetPathWithBasePath(): void { $this->request - ->expects($this->once()) + ->expects(self::once()) ->method('getPathInfo') - ->will($this->returnValue('/foo/bar')); + ->willReturn('/foo/bar'); $extractorFactory = new ExtractorFactory('/foo'); $extractor = $extractorFactory->createPathExtractor(); - $this->assertSame('/bar', $extractor($this->request)); + self::assertSame('/bar', $extractor($this->request)); } - public function testGetPathWithBasePathTrailingSlash() + public function testGetPathWithBasePathTrailingSlash(): void { $this->request - ->expects($this->once()) + ->expects(self::once()) ->method('getPathInfo') - ->will($this->returnValue('/foo/bar')); + ->willReturn('/foo/bar'); $extractorFactory = new ExtractorFactory('/foo/'); $extractor = $extractorFactory->createPathExtractor(); - $this->assertSame('/bar', $extractor($this->request)); + self::assertSame('/bar', $extractor($this->request)); } - public function testGetPathWithBasePathThatDoesNotMatch() + public function testGetPathWithBasePathThatDoesNotMatch(): void { $this->request - ->expects($this->once()) + ->expects(self::once()) ->method('getPathInfo') - ->will($this->returnValue('/bar')); + ->willReturn('/bar'); $extractorFactory = new ExtractorFactory('/foo'); $extractor = $extractorFactory->createPathExtractor(); - $this->assertSame('', $extractor($this->request)); + self::assertSame('', $extractor($this->request)); } - public function testGetHeaderWithExistingHeader() + public function testGetHeaderWithExistingHeader(): void { $request = new Request( [], @@ -95,10 +94,10 @@ public function testGetHeaderWithExistingHeader() $extractorFactory = new ExtractorFactory('/foo'); $extractor = $extractorFactory->createHeaderExtractor('content-type'); - $this->assertSame('application/json', $extractor($request)); + self::assertSame('application/json', $extractor($request)); } - public function testGetHeaderWithNonExistingHeader() + public function testGetHeaderWithNonExistingHeader(): void { $request = new Request( [], @@ -112,10 +111,10 @@ public function testGetHeaderWithNonExistingHeader() $extractorFactory = new ExtractorFactory('/foo'); $extractor = $extractorFactory->createHeaderExtractor('content-type'); - $this->assertNull($extractor($request)); + self::assertNull($extractor($request)); } - public function testHeaderExistsWithExistingHeader() + public function testHeaderExistsWithExistingHeader(): void { $request = new Request( [], @@ -129,10 +128,10 @@ public function testHeaderExistsWithExistingHeader() $extractorFactory = new ExtractorFactory('/foo'); $extractor = $extractorFactory->createHeaderExistsExtractor('content-type'); - $this->assertTrue($extractor($request)); + self::assertTrue($extractor($request)); } - public function testHeaderExistsWithNonExistingHeader() + public function testHeaderExistsWithNonExistingHeader(): void { $request = new Request( [], @@ -146,6 +145,6 @@ public function testHeaderExistsWithNonExistingHeader() $extractorFactory = new ExtractorFactory('/foo'); $extractor = $extractorFactory->createHeaderExistsExtractor('content-type'); - $this->assertFalse($extractor($request)); + self::assertFalse($extractor($request)); } } diff --git a/tests/Matcher/StringMatcherTest.php b/tests/Matcher/StringMatcherTest.php index 32df60c..29c937b 100644 --- a/tests/Matcher/StringMatcherTest.php +++ b/tests/Matcher/StringMatcherTest.php @@ -2,17 +2,15 @@ namespace InterNations\Component\HttpMock\Tests\Matcher; use InterNations\Component\HttpMock\Matcher\StringMatcher; -use InterNations\Component\Testing\AbstractTestCase; +use InterNations\Component\HttpMock\Tests\TestCase; use Symfony\Component\HttpFoundation\Request; -class StringMatcherTest extends AbstractTestCase +class StringMatcherTest extends TestCase { - public function testConversionToString() + public function testConversionToString(): void { $matcher = new StringMatcher('0'); - $matcher->setExtractor(static function() { - return 0; - }); + $matcher->setExtractor(static fn () => 0); self::assertTrue($matcher->getMatcher()(new Request())); } } diff --git a/tests/MockBuilderIntegrationTest.php b/tests/MockBuilderIntegrationTest.php index 5e06a54..5f81b48 100644 --- a/tests/MockBuilderIntegrationTest.php +++ b/tests/MockBuilderIntegrationTest.php @@ -1,12 +1,10 @@ matches = new MatcherFactory(); $this->builder = new MockBuilder($this->matches, new ExtractorFactory()); - $this->server = new Server(HTTP_MOCK_PORT, HTTP_MOCK_HOST); + $this->server = new ServerProcess(HTTP_MOCK_PORT, HTTP_MOCK_HOST); $this->server->start(); $this->server->clean(); } - public function tearDown() + public function tearDown(): void { $this->server->stop(); } - public function testCreateExpectation() + public function testCreateExpectation(): void { $builder = $this->builder ->when() @@ -53,15 +48,15 @@ public function testCreateExpectation() }) ->then() ->statusCode(401) - ->body('response body') ->header('X-Foo', 'Bar') + ->body('response body') ->end(); - $this->assertSame($this->builder, $builder); + self::assertSame($this->builder, $builder); $expectations = $this->builder->flushExpectations(); - $this->assertCount(1, $expectations); + self::assertCount(1, $expectations); /** @var Expectation $expectation */ $expectation = current($expectations); @@ -72,31 +67,34 @@ public function testCreateExpectation() $run = 0; $oldValue = ini_set('error_log', '/dev/null'); foreach ($expectation->getMatcherClosures() as $closure) { - $this->assertTrue($closure($request)); + self::assertTrue($closure($request)); $unserializedClosure = unserialize(serialize($closure)); - $this->assertTrue($unserializedClosure($request)); + self::assertTrue($unserializedClosure($request)); $run++; } ini_set('error_log', $oldValue); - $this->assertSame(3, $run); + self::assertSame(3, $run); $expectation->getResponse()->setDate(new DateTime('2012-11-10 09:08:07', new DateTimeZone('UTC'))); - $response = "HTTP/1.0 401 Unauthorized\r\nCache-Control: no-cache, private\r\nDate: Sat, 10 Nov 2012 09:08:07 GMT\r\nX-Foo: Bar\r\n\r\nresponse body"; - $this->assertSame($response, (string)$expectation->getResponse()); + $response = "HTTP/1.0 401 Unauthorized\r\nCache-Control: no-cach%s\r\nDate: Sat, 10 Nov 2012 09:08:07 GMT\r\nX-Foo: Bar\r\n\r\nresponse body"; + self::assertStringMatchesFormat($response, (string)$expectation->getResponse()); $this->server->setUp($expectations); $client = $this->server->getClient(); - $this->assertSame('response body', (string) $client->post('/foo')->send()->getBody()); + self::assertSame( + 'response body', + (string) $client->sendRequest($this->getRequestFactory()->createRequest('POST', '/foo'))->getBody() + ); - $this->assertContains('CLOSURE MATCHER: POST /foo', $this->server->getErrorOutput()); + self::assertNotFalse(strpos($this->server->getErrorOutput(), 'CLOSURE MATCHER: POST /foo')); } - public function testCreateTwoExpectationsAfterEachOther() + public function testCreateTwoExpectationsAfterEachOther(): void { $this->builder ->when() @@ -118,13 +116,33 @@ public function testCreateTwoExpectationsAfterEachOther() ->end(); $this->server->setUp($this->builder->flushExpectations()); - $this->assertSame('POST 1', (string) $this->server->getClient()->post('/post-resource-1')->send()->getBody()); - $this->assertSame('POST 2', (string) $this->server->getClient()->post('/post-resource-2')->send()->getBody()); - $this->assertSame('POST 1', (string) $this->server->getClient()->post('/post-resource-1')->send()->getBody()); - $this->assertSame('POST 2', (string) $this->server->getClient()->post('/post-resource-2')->send()->getBody()); + self::assertSame( + 'POST 1', + (string) $this->server->getClient()->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/post-resource-1') + )->getBody() + ); + self::assertSame( + 'POST 2', + (string) $this->server->getClient()->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/post-resource-2') + )->getBody() + ); + self::assertSame( + 'POST 1', + (string) $this->server->getClient()->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/post-resource-1') + )->getBody() + ); + self::assertSame( + 'POST 2', + (string) $this->server->getClient()->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/post-resource-2') + )->getBody() + ); } - public function testCreateSuccessiveExpectationsOnSameWhen() + public function testCreateSuccessiveExpectationsOnSameWhen(): void { $this->builder ->first() @@ -150,12 +168,27 @@ public function testCreateSuccessiveExpectationsOnSameWhen() $this->server->setUp($this->builder->flushExpectations()); - $this->assertSame('called once', (string) $this->server->getClient()->post('/resource')->send()->getBody()); - $this->assertSame('called twice', (string) $this->server->getClient()->post('/resource')->send()->getBody()); - $this->assertSame('called 3 times', (string) $this->server->getClient()->post('/resource')->send()->getBody()); + self::assertSame( + 'called once', + (string) $this->server->getClient()->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/resource') + )->getBody() + ); + self::assertSame( + 'called twice', + (string) $this->server->getClient()->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/resource') + )->getBody() + ); + self::assertSame( + 'called 3 times', + (string) $this->server->getClient()->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/resource') + )->getBody() + ); } - public function testCreateSuccessiveExpectationsWithAny() + public function testCreateSuccessiveExpectationsWithAny(): void { $this->builder ->first() @@ -181,12 +214,12 @@ public function testCreateSuccessiveExpectationsWithAny() $this->server->setUp($this->builder->flushExpectations()); - $this->assertSame('1', (string) $this->server->getClient()->post('/resource')->send()->getBody()); - $this->assertSame('2', (string) $this->server->getClient()->post('/resource')->send()->getBody()); - $this->assertSame('any', (string) $this->server->getClient()->post('/resource')->send()->getBody()); + self::assertSame('1', (string) $this->server->getClient()->sendRequest($this->getRequestFactory()->createRequest('POST', '/resource'))->getBody()); + self::assertSame('2', (string) $this->server->getClient()->sendRequest($this->getRequestFactory()->createRequest('POST', '/resource'))->getBody()); + self::assertSame('any', (string) $this->server->getClient()->sendRequest($this->getRequestFactory()->createRequest('POST', '/resource'))->getBody()); } - public function testCreateSuccessiveExpectationsInUnexpectedOrder() + public function testCreateSuccessiveExpectationsInUnexpectedOrder(): void { $this->builder ->second() @@ -205,11 +238,11 @@ public function testCreateSuccessiveExpectationsInUnexpectedOrder() $this->server->setUp($this->builder->flushExpectations()); - $this->assertSame('1', (string) $this->server->getClient()->post('/resource')->send()->getBody()); - $this->assertSame('2', (string) $this->server->getClient()->post('/resource')->send()->getBody()); + self::assertSame('1', (string) $this->server->getClient()->sendRequest($this->getRequestFactory()->createRequest('POST', '/resource'))->getBody()); + self::assertSame('2', (string) $this->server->getClient()->sendRequest($this->getRequestFactory()->createRequest('POST', '/resource'))->getBody()); } - public function testCreateSuccessiveExpectationsWithOnce() + public function testCreateSuccessiveExpectationsWithOnce(): void { $this->builder ->first() @@ -235,10 +268,35 @@ public function testCreateSuccessiveExpectationsWithOnce() $this->server->setUp($this->builder->flushExpectations()); - $this->assertSame('1', (string) $this->server->getClient()->post('/resource')->send()->getBody()); - $this->assertSame('2', (string) $this->server->getClient()->post('/resource')->send()->getBody()); - $this->assertSame('twice', (string) $this->server->getClient()->post('/resource')->send()->getBody()); - $this->assertSame('twice', (string) $this->server->getClient()->post('/resource')->send()->getBody()); - $this->assertSame('Expectation not met', (string) $this->server->getClient()->post('/resource')->send()->getBody()); + self::assertSame( + '1', + (string) $this->server->getClient()->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/resource') + )->getBody() + ); + self::assertSame( + '2', + (string) $this->server->getClient()->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/resource') + )->getBody() + ); + self::assertSame( + 'twice', + (string) $this->server->getClient()->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/resource') + )->getBody() + ); + self::assertSame( + 'twice', + (string) $this->server->getClient()->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/resource') + )->getBody() + ); + self::assertSame( + 'No matching expectation found', + (string) $this->server->getClient()->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/resource') + )->getBody() + ); } } diff --git a/tests/PHPUnit/HttpMockMultiPHPUnitIntegrationTest.php b/tests/PHPUnit/HttpMockMultiPHPUnitIntegrationTest.php index baad498..9df4c66 100644 --- a/tests/PHPUnit/HttpMockMultiPHPUnitIntegrationTest.php +++ b/tests/PHPUnit/HttpMockMultiPHPUnitIntegrationTest.php @@ -1,49 +1,48 @@ setUpHttpMock(); } - public function tearDown() + public function tearDown(): void { $this->tearDownHttpMock(); } - public static function getPaths() + /** @return array */ + public static function getPaths(): array { return [ - [ - '/foo', - '/bar', - ] + ['/foo'], + ['/bar'], ]; } /** @dataProvider getPaths */ - public function testSimpleRequest($path) + public function testSimpleRequest(string $path): void { $this->http['firstNamedServer']->mock ->when() @@ -53,33 +52,38 @@ public function testSimpleRequest($path) ->end(); $this->http['firstNamedServer']->setUp(); - $this->assertSame($path . ' body', (string) $this->http['firstNamedServer']->client->get($path)->send()->getBody()); + self::assertSame( + $path . ' body', + (string) $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory()->createRequest('GET', $path) + )->getBody() + ); $request = $this->http['firstNamedServer']->requests->latest(); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame($path, $request->getPath()); + self::assertSame('GET', $request->getMethod()); + self::assertSame($path, $request->getRequestUri()); $request = $this->http['firstNamedServer']->requests->last(); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame($path, $request->getPath()); + self::assertSame('GET', $request->getMethod()); + self::assertSame($path, $request->getRequestUri()); $request = $this->http['firstNamedServer']->requests->first(); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame($path, $request->getPath()); + self::assertSame('GET', $request->getMethod()); + self::assertSame($path, $request->getRequestUri()); $request = $this->http['firstNamedServer']->requests->at(0); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame($path, $request->getPath()); + self::assertSame('GET', $request->getMethod()); + self::assertSame($path, $request->getRequestUri()); $request = $this->http['firstNamedServer']->requests->pop(); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame($path, $request->getPath()); + self::assertSame('GET', $request->getMethod()); + self::assertSame($path, $request->getRequestUri()); - $this->assertSame($path . ' body', (string) $this->http['firstNamedServer']->client->get($path)->send()->getBody()); + self::assertSame($path . ' body', (string) $this->http['firstNamedServer']->client->sendRequest($this->getRequestFactory()->createRequest('GET', $path))->getBody()); $request = $this->http['firstNamedServer']->requests->shift(); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame($path, $request->getPath()); + self::assertSame('GET', $request->getMethod()); + self::assertSame($path, $request->getRequestUri()); $this->expectException('UnexpectedValueException'); @@ -87,46 +91,50 @@ public function testSimpleRequest($path) $this->http['firstNamedServer']->requests->pop(); } - public function testErrorLogOutput() + public function testErrorLogOutput(): void { $this->http['firstNamedServer']->mock ->when() - ->callback(static function () {error_log('error output');}) + ->callback(static function (): void {error_log('error output');}) ->then() ->end(); $this->http['firstNamedServer']->setUp(); - $this->http['firstNamedServer']->client->get('/foo')->send(); + $this->http['firstNamedServer']->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/foo')); // Should fail during tear down as we have an error_log() on the server side try { $this->tearDown(); - $this->fail('Exception expected'); + self::fail('Exception expected'); } catch (\Exception $e) { - $this->assertContains('HTTP mock server standard error output should be empty', $e->getMessage()); + self::assertNotFalse(strpos($e->getMessage(), 'HTTP mock server standard error output should be empty')); } } - public function testFailedRequest() + public function testFailedRequest(): void { - $response = $this->http['firstNamedServer']->client->get('/foo')->send(); - $this->assertSame(404, $response->getStatusCode()); - $this->assertSame('No matching expectation found', (string) $response->getBody()); + $response = $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory()->createRequest('GET', '/foo') + ); + self::assertSame(404, $response->getStatusCode()); + self::assertSame('No matching expectation found', (string) $response->getBody()); } - public function testStopServer() + public function testStopServer(): void { $this->http['firstNamedServer']->server->stop(); } /** @depends testStopServer */ - public function testHttpServerIsRestartedIfATestStopsIt() + public function testHttpServerIsRestartedIfATestStopsIt(): void { - $response = $this->http['firstNamedServer']->client->get('/')->send(); - $this->assertSame(404, $response->getStatusCode()); + $response = $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory()->createRequest('GET', '/') + ); + self::assertSame(404, $response->getStatusCode()); } - public function testLimitDurationOfAResponse() + public function testLimitDurationOfAResponse(): void { $this->http['firstNamedServer']->mock ->once() @@ -136,11 +144,15 @@ public function testLimitDurationOfAResponse() ->body('POST METHOD') ->end(); $this->http['firstNamedServer']->setUp(); - $firstResponse = $this->http['firstNamedServer']->client->post('/')->send(); - $this->assertSame(200, $firstResponse->getStatusCode()); - $secondResponse = $this->http['firstNamedServer']->client->post('/')->send(); - $this->assertSame(410, $secondResponse->getStatusCode()); - $this->assertSame('Expectation not met', $secondResponse->getBody(true)); + $firstResponse = $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/') + ); + self::assertSame(200, $firstResponse->getStatusCode()); + $secondResponse = $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/') + ); + self::assertSame(404, $secondResponse->getStatusCode()); + self::assertSame('No matching expectation found', (string) $secondResponse->getBody()); $this->http['firstNamedServer']->mock ->exactly(2) @@ -150,13 +162,19 @@ public function testLimitDurationOfAResponse() ->body('POST METHOD') ->end(); $this->http['firstNamedServer']->setUp(); - $firstResponse = $this->http['firstNamedServer']->client->post('/')->send(); - $this->assertSame(200, $firstResponse->getStatusCode()); - $secondResponse = $this->http['firstNamedServer']->client->post('/')->send(); - $this->assertSame(200, $secondResponse->getStatusCode()); - $thirdResponse = $this->http['firstNamedServer']->client->post('/')->send(); - $this->assertSame(410, $thirdResponse->getStatusCode()); - $this->assertSame('Expectation not met', $thirdResponse->getBody(true)); + $firstResponse = $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/') + ); + self::assertSame(200, $firstResponse->getStatusCode()); + $secondResponse = $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/') + ); + self::assertSame(200, $secondResponse->getStatusCode()); + $thirdResponse = $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/') + ); + self::assertSame(404, $thirdResponse->getStatusCode()); + self::assertSame('No matching expectation found', (string) $thirdResponse->getBody()); $this->http['firstNamedServer']->mock ->any() @@ -166,27 +184,38 @@ public function testLimitDurationOfAResponse() ->body('POST METHOD') ->end(); $this->http['firstNamedServer']->setUp(); - $firstResponse = $this->http['firstNamedServer']->client->post('/')->send(); - $this->assertSame(200, $firstResponse->getStatusCode()); - $secondResponse = $this->http['firstNamedServer']->client->post('/')->send(); - $this->assertSame(200, $secondResponse->getStatusCode()); - $thirdResponse = $this->http['firstNamedServer']->client->post('/')->send(); - $this->assertSame(200, $thirdResponse->getStatusCode()); + $firstResponse = $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/') + ); + self::assertSame(200, $firstResponse->getStatusCode()); + $secondResponse = $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/') + ); + self::assertSame(200, $secondResponse->getStatusCode()); + $thirdResponse = $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/') + ); + self::assertSame(200, $thirdResponse->getStatusCode()); } - public function testCallbackOnResponse() + public function testCallbackOnResponse(): void { $this->http['firstNamedServer']->mock ->when() ->methodIs('POST') ->then() - ->callback(static function(Response $response) {$response->setContent('CALLBACK');}) + ->callback(static function(Response $response): void {$response->setContent('CALLBACK');}) ->end(); $this->http['firstNamedServer']->setUp(); - $this->assertSame('CALLBACK', $this->http['firstNamedServer']->client->post('/')->send()->getBody(true)); + self::assertSame( + 'CALLBACK', + (string) $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/') + )->getBody() + ); } - public function testComplexResponse() + public function testComplexResponse(): void { $this->http['firstNamedServer']->mock ->when() @@ -197,15 +226,22 @@ public function testComplexResponse() ->header('X-Foo', 'Bar') ->end(); $this->http['firstNamedServer']->setUp(); - $response = $this->http['firstNamedServer']->client - ->post('/', ['x-client-header' => 'header-value'], ['post-key' => 'post-value'])->send(); - $this->assertSame('BODY', $response->getBody(true)); - $this->assertSame(201, $response->getStatusCode()); - $this->assertSame('Bar', (string) $response->getHeader('X-Foo')); - $this->assertSame('post-value', $this->http['firstNamedServer']->requests->latest()->getPostField('post-key')); + $response = $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory() + ->createRequest('POST', '/') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withHeader('x-client-header', 'header-value') + ->withBody( + $this->getStreamFactory()->createStream(http_build_query(['post-key' => 'post-value'])) + ) + ); + self::assertSame('BODY', (string) $response->getBody()); + self::assertSame(201, $response->getStatusCode()); + self::assertSame('Bar', $response->getHeaderLine('X-Foo')); + self::assertSame('post-value', $this->http['firstNamedServer']->requests->latest()->request->get('post-key')); } - public function testPutRequest() + public function testPutRequest(): void { $this->http['firstNamedServer']->mock ->when() @@ -216,15 +252,20 @@ public function testPutRequest() ->header('X-Foo', 'Bar') ->end(); $this->http['firstNamedServer']->setUp(); - $response = $this->http['firstNamedServer']->client - ->put('/', ['x-client-header' => 'header-value'], ['put-key' => 'put-value'])->send(); - $this->assertSame('BODY', $response->getBody(true)); - $this->assertSame(201, $response->getStatusCode()); - $this->assertSame('Bar', (string) $response->getHeader('X-Foo')); - $this->assertSame('put-value', $this->http['firstNamedServer']->requests->latest()->getPostField('put-key')); + $response = $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory() + ->createRequest('PUT', '/') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withHeader('x-client-header', 'header-value') + ->withBody($this->getStreamFactory()->createStream(http_build_query(['put-key' => 'put-value']))) + ); + self::assertSame('BODY', (string) $response->getBody()); + self::assertSame(201, $response->getStatusCode()); + self::assertSame('Bar', (string) $response->getHeaderLine('X-Foo')); + self::assertSame('put-value', $this->http['firstNamedServer']->requests->latest()->request->get('put-key')); } - public function testPostRequest() + public function testPostRequest(): void { $this->http['firstNamedServer']->mock ->when() @@ -235,18 +276,23 @@ public function testPostRequest() ->header('X-Foo', 'Bar') ->end(); $this->http['firstNamedServer']->setUp(); - $response = $this->http['firstNamedServer']->client - ->post('/', ['x-client-header' => 'header-value'], ['post-key' => 'post-value'])->send(); - $this->assertSame('BODY', $response->getBody(true)); - $this->assertSame(201, $response->getStatusCode()); - $this->assertSame('Bar', (string) $response->getHeader('X-Foo')); - $this->assertSame('post-value', $this->http['firstNamedServer']->requests->latest()->getPostField('post-key')); + $response = $this->http['firstNamedServer']->client->sendRequest( + $this->getRequestFactory() + ->createRequest('POST', '/') + ->withHeader('x-client-header', 'header-value') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody($this->getStreamFactory()->createStream(http_build_query(['post-key' => 'post-value']))) + ); + self::assertSame('BODY', (string) $response->getBody()); + self::assertSame(201, $response->getStatusCode()); + self::assertSame('Bar', $response->getHeaderLine('X-Foo')); + self::assertSame('post-value', $this->http['firstNamedServer']->requests->latest()->request->get('post-key')); } - public function testFatalError() + public function testFatalError(): void { - if (version_compare(PHP_VERSION, '7.0', '<')) { - $this->markTestSkipped('Comment in to test if fatal errors are properly handled'); + if (PHP_VERSION_ID < 70000) { + self::markTestSkipped('Comment in to test if fatal errors are properly handled'); } $this->expectException('Error'); diff --git a/tests/PHPUnit/HttpMockPHPUnitIntegrationBasePathTest.php b/tests/PHPUnit/HttpMockPHPUnitIntegrationBasePathTest.php index 8cc8199..4504a24 100644 --- a/tests/PHPUnit/HttpMockPHPUnitIntegrationBasePathTest.php +++ b/tests/PHPUnit/HttpMockPHPUnitIntegrationBasePathTest.php @@ -1,35 +1,35 @@ setUpHttpMock(); } - public function tearDown() + public function tearDown(): void { $this->tearDownHttpMock(); } - public function testSimpleRequest() + public function testSimpleRequest(): void { $this->http->mock ->when() @@ -39,10 +39,15 @@ public function testSimpleRequest() ->end(); $this->http->setUp(); - $this->assertSame('/foo body', (string) $this->http->client->get('/custom-base-path/foo')->send()->getBody()); + self::assertSame( + '/foo body', + (string) $this->http->client->sendRequest( + $this->getRequestFactory()->createRequest('GET', '/custom-base-path/foo') + )->getBody() + ); $request = $this->http->requests->latest(); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame('/custom-base-path/foo', $request->getPath()); + self::assertSame('GET', $request->getMethod()); + self::assertSame('/custom-base-path/foo', $request->getRequestUri()); } } diff --git a/tests/PHPUnit/HttpMockPHPUnitIntegrationTest.php b/tests/PHPUnit/HttpMockPHPUnitIntegrationTest.php index 39a8518..f8b5db9 100644 --- a/tests/PHPUnit/HttpMockPHPUnitIntegrationTest.php +++ b/tests/PHPUnit/HttpMockPHPUnitIntegrationTest.php @@ -1,49 +1,48 @@ setUpHttpMock(); } - public function tearDown() + public function tearDown(): void { $this->tearDownHttpMock(); } - public static function getPaths() + /** @return array */ + public static function getPaths(): array { return [ - [ - '/foo', - '/bar', - ] + ['/foo'], + ['/bar'], ]; } /** @dataProvider getPaths */ - public function testSimpleRequest($path) + public function testSimpleRequest(string $path): void { $this->http->mock ->when() @@ -53,33 +52,43 @@ public function testSimpleRequest($path) ->end(); $this->http->setUp(); - $this->assertSame($path . ' body', (string) $this->http->client->get($path)->send()->getBody()); + self::assertSame( + $path . ' body', + (string) $this->http->client->sendRequest( + $this->getRequestFactory()->createRequest('GET', $path) + )->getBody() + ); $request = $this->http->requests->latest(); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame($path, $request->getPath()); + self::assertSame('GET', $request->getMethod()); + self::assertSame($path, $request->getRequestUri()); $request = $this->http->requests->last(); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame($path, $request->getPath()); + self::assertSame('GET', $request->getMethod()); + self::assertSame($path, $request->getRequestUri()); $request = $this->http->requests->first(); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame($path, $request->getPath()); + self::assertSame('GET', $request->getMethod()); + self::assertSame($path, $request->getRequestUri()); $request = $this->http->requests->at(0); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame($path, $request->getPath()); + self::assertSame('GET', $request->getMethod()); + self::assertSame($path, $request->getRequestUri()); $request = $this->http->requests->pop(); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame($path, $request->getPath()); - - $this->assertSame($path . ' body', (string) $this->http->client->get($path)->send()->getBody()); + self::assertSame('GET', $request->getMethod()); + self::assertSame($path, $request->getRequestUri()); + + self::assertSame( + $path . ' body', + (string) $this->http->client->sendRequest( + $this->getRequestFactory()->createRequest('GET', $path) + )->getBody() + ); $request = $this->http->requests->shift(); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame($path, $request->getPath()); + self::assertSame('GET', $request->getMethod()); + self::assertSame($path, $request->getRequestUri()); $this->expectException('UnexpectedValueException'); @@ -87,46 +96,46 @@ public function testSimpleRequest($path) $this->http->requests->pop(); } - public function testErrorLogOutput() + public function testErrorLogOutput(): void { $this->http->mock ->when() - ->callback(static function () {error_log('error output');}) + ->callback(static function (): void {error_log('error output');}) ->then() ->end(); $this->http->setUp(); - $this->http->client->get('/foo')->send(); + $this->http->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/foo')); // Should fail during tear down as we have an error_log() on the server side try { $this->tearDown(); - $this->fail('Exception expected'); + self::fail('Exception expected'); } catch (\Exception $e) { - $this->assertContains('HTTP mock server standard error output should be empty', $e->getMessage()); + self::assertNotFalse(strpos($e->getMessage(), 'HTTP mock server standard error output should be empty')); } } - public function testFailedRequest() + public function testFailedRequest(): void { - $response = $this->http->client->get('/foo')->send(); - $this->assertSame(404, $response->getStatusCode()); - $this->assertSame('No matching expectation found', (string) $response->getBody()); + $response = $this->http->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/foo')); + self::assertSame(404, $response->getStatusCode()); + self::assertSame('No matching expectation found', (string) $response->getBody()); } - public function testStopServer() + public function testStopServer(): void { $this->http->server->stop(); } /** @depends testStopServer */ - public function testHttpServerIsRestartedIfATestStopsIt() + public function testHttpServerIsRestartedIfATestStopsIt(): void { - $response = $this->http->client->get('/')->send(); - $this->assertSame(404, $response->getStatusCode()); + $response = $this->http->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/foo')); + self::assertSame(404, $response->getStatusCode()); } - public function testLimitDurationOfAResponse() + public function testLimitDurationOfAResponse(): void { $this->http->mock ->once() @@ -136,11 +145,11 @@ public function testLimitDurationOfAResponse() ->body('POST METHOD') ->end(); $this->http->setUp(); - $firstResponse = $this->http->client->post('/')->send(); - $this->assertSame(200, $firstResponse->getStatusCode()); - $secondResponse = $this->http->client->post('/')->send(); - $this->assertSame(410, $secondResponse->getStatusCode()); - $this->assertSame('Expectation not met', $secondResponse->getBody(true)); + $firstResponse = $this->http->client->sendRequest($this->getRequestFactory()->createRequest('POST', '/')); + self::assertSame(200, $firstResponse->getStatusCode()); + $secondResponse = $this->http->client->sendRequest($this->getRequestFactory()->createRequest('POST', '/')); + self::assertSame(404, $secondResponse->getStatusCode()); + self::assertSame('No matching expectation found', (string) $secondResponse->getBody()); $this->http->mock ->exactly(2) @@ -150,13 +159,13 @@ public function testLimitDurationOfAResponse() ->body('POST METHOD') ->end(); $this->http->setUp(); - $firstResponse = $this->http->client->post('/')->send(); - $this->assertSame(200, $firstResponse->getStatusCode()); - $secondResponse = $this->http->client->post('/')->send(); - $this->assertSame(200, $secondResponse->getStatusCode()); - $thirdResponse = $this->http->client->post('/')->send(); - $this->assertSame(410, $thirdResponse->getStatusCode()); - $this->assertSame('Expectation not met', $thirdResponse->getBody(true)); + $firstResponse = $this->http->client->sendRequest($this->getRequestFactory()->createRequest('POST', '/')); + self::assertSame(200, $firstResponse->getStatusCode()); + $secondResponse = $this->http->client->sendRequest($this->getRequestFactory()->createRequest('POST', '/')); + self::assertSame(200, $secondResponse->getStatusCode()); + $thirdResponse = $this->http->client->sendRequest($this->getRequestFactory()->createRequest('POST', '/')); + self::assertSame(404, $thirdResponse->getStatusCode()); + self::assertSame('No matching expectation found', (string) $thirdResponse->getBody()); $this->http->mock ->any() @@ -166,27 +175,32 @@ public function testLimitDurationOfAResponse() ->body('POST METHOD') ->end(); $this->http->setUp(); - $firstResponse = $this->http->client->post('/')->send(); - $this->assertSame(200, $firstResponse->getStatusCode()); - $secondResponse = $this->http->client->post('/')->send(); - $this->assertSame(200, $secondResponse->getStatusCode()); - $thirdResponse = $this->http->client->post('/')->send(); - $this->assertSame(200, $thirdResponse->getStatusCode()); + $firstResponse = $this->http->client->sendRequest($this->getRequestFactory()->createRequest('POST', '/')); + self::assertSame(200, $firstResponse->getStatusCode()); + $secondResponse = $this->http->client->sendRequest($this->getRequestFactory()->createRequest('POST', '/')); + self::assertSame(200, $secondResponse->getStatusCode()); + $thirdResponse = $this->http->client->sendRequest($this->getRequestFactory()->createRequest('POST', '/')); + self::assertSame(200, $thirdResponse->getStatusCode()); } - public function testCallbackOnResponse() + public function testCallbackOnResponse(): void { $this->http->mock ->when() ->methodIs('POST') ->then() - ->callback(static function(Response $response) {$response->setContent('CALLBACK');}) + ->callback(static function(Response $response): void {$response->setContent('CALLBACK');}) ->end(); $this->http->setUp(); - $this->assertSame('CALLBACK', $this->http->client->post('/')->send()->getBody(true)); + self::assertSame( + 'CALLBACK', + (string) $this->http->client->sendRequest( + $this->getRequestFactory()->createRequest('POST', '/') + )->getBody() + ); } - public function testComplexResponse() + public function testComplexResponse(): void { $this->http->mock ->when() @@ -197,15 +211,20 @@ public function testComplexResponse() ->body('BODY') ->end(); $this->http->setUp(); - $response = $this->http->client - ->post('/', ['x-client-header' => 'header-value'], ['post-key' => 'post-value'])->send(); - $this->assertSame('BODY', $response->getBody(true)); - $this->assertSame(201, $response->getStatusCode()); - $this->assertSame('Bar', (string) $response->getHeader('X-Foo')); - $this->assertSame('post-value', $this->http->requests->latest()->getPostField('post-key')); + $response = $this->http->client->sendRequest( + $this->getRequestFactory() + ->createRequest('POST', '/') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withHeader('x-client-header', 'header-value') + ->withBody($this->getStreamFactory()->createStream(http_build_query(['post-key' => 'post-value']))) + ); + self::assertSame('BODY', (string) $response->getBody()); + self::assertSame(201, $response->getStatusCode()); + self::assertSame('Bar', $response->getHeaderLine('X-Foo')); + self::assertSame('post-value', $this->http->requests->latest()->request->get('post-key')); } - public function testPutRequest() + public function testPutRequest(): void { $this->http->mock ->when() @@ -216,15 +235,20 @@ public function testPutRequest() ->header('X-Foo', 'Bar') ->end(); $this->http->setUp(); - $response = $this->http->client - ->put('/', ['x-client-header' => 'header-value'], ['put-key' => 'put-value'])->send(); - $this->assertSame('BODY', $response->getBody(true)); - $this->assertSame(201, $response->getStatusCode()); - $this->assertSame('Bar', (string) $response->getHeader('X-Foo')); - $this->assertSame('put-value', $this->http->requests->latest()->getPostField('put-key')); + $response = $this->http->client->sendRequest( + $this->getRequestFactory() + ->createRequest('PUT', '/') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withHeader('x-client-header', 'header-value') + ->withBody($this->getStreamFactory()->createStream(http_build_query(['put-key' => 'put-value']))) + ); + self::assertSame('BODY', (string) $response->getBody()); + self::assertSame(201, $response->getStatusCode()); + self::assertSame('Bar', $response->getHeaderLine('X-Foo')); + self::assertSame('put-value', $this->http->requests->latest()->request->get('put-key')); } - public function testPostRequest() + public function testPostRequest(): void { $this->http->mock ->when() @@ -235,15 +259,20 @@ public function testPostRequest() ->header('X-Foo', 'Bar') ->end(); $this->http->setUp(); - $response = $this->http->client - ->post('/', ['x-client-header' => 'header-value'], ['post-key' => 'post-value'])->send(); - $this->assertSame('BODY', $response->getBody(true)); - $this->assertSame(201, $response->getStatusCode()); - $this->assertSame('Bar', (string) $response->getHeader('X-Foo')); - $this->assertSame('post-value', $this->http->requests->latest()->getPostField('post-key')); + $response = $this->http->client->sendRequest( + $this->getRequestFactory() + ->createRequest('POST', '/') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withHeader('x-client-header', 'header-value') + ->withBody($this->getStreamFactory()->createStream(http_build_query(['post-key' => 'post-value']))) + ); + self::assertSame('BODY', (string) $response->getBody()); + self::assertSame(201, $response->getStatusCode()); + self::assertSame('Bar', $response->getHeaderLine('X-Foo')); + self::assertSame('post-value', $this->http->requests->latest()->request->get('post-key')); } - public function testCountRequests() + public function testCountRequests(): void { $this->http->mock ->when() @@ -253,17 +282,22 @@ public function testCountRequests() ->end(); $this->http->setUp(); - $this->assertCount(0, $this->http->requests); - $this->assertSame('resource body', (string) $this->http->client->get('/resource')->send()->getBody()); - $this->assertCount(1, $this->http->requests); + self::assertCount(0, $this->http->requests); + self::assertSame( + 'resource body', + (string) $this->http->client->sendRequest( + $this->getRequestFactory()->createRequest('GET', '/resource') + )->getBody() + ); + self::assertCount(1, $this->http->requests); } - public function testMatchQueryString() + public function testMatchQueryString(): void { $this->http->mock ->when() ->callback( - function (Request $request) { + static function (Request $request) { return $request->query->has('key1'); } ) @@ -273,13 +307,21 @@ function (Request $request) { ->end(); $this->http->setUp(); - $this->assertSame('query string', (string) $this->http->client->get('/?key1=')->send()->getBody()); - - $this->assertSame(Response::HTTP_NOT_FOUND, $this->http->client->get('/')->send()->getStatusCode()); - $this->assertSame(Response::HTTP_NOT_FOUND, $this->http->client->post('/')->send()->getStatusCode()); + self::assertSame( + 'query string', (string) $this->http->client->sendRequest( + $this->getRequestFactory()->createRequest('GET', '/?key1='))->getBody() + ); + self::assertSame( + Response::HTTP_NOT_FOUND, + $this->http->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/'))->getStatusCode() + ); + self::assertSame( + Response::HTTP_NOT_FOUND, + $this->http->client->sendRequest($this->getRequestFactory()->createRequest('POST', '/'))->getStatusCode() + ); } - public function testMatchRegex() + public function testMatchRegex(): void { $this->http->mock ->when() @@ -289,11 +331,17 @@ public function testMatchRegex() ->end(); $this->http->setUp(); - $this->assertSame('response', (string) $this->http->client->get('/')->send()->getBody()); - $this->assertSame('response', (string) $this->http->client->get('/')->send()->getBody()); + self::assertSame( + 'response', + (string) $this->http->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/'))->getBody() + ); + self::assertSame( + 'response', + (string) $this->http->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/'))->getBody() + ); } - public function testMatchQueryParams() + public function testMatchQueryParams(): void { $this->http->mock ->when() @@ -308,28 +356,31 @@ public function testMatchQueryParams() ->end(); $this->http->setUp(); - $this->assertSame( + self::assertSame( 'response', - (string) $this->http->client->get('/?p1=&p2=v2&p4=any&p5=v5&p6=v6')->send()->getBody() - ); - $this->assertSame( + (string) $this->http->client->sendRequest( + $this->getRequestFactory()->createRequest('GET', '/?p1=&p2=v2&p4=any&p5=v5&p6=v6'))->getBody() + ); + self::assertSame( Response::HTTP_NOT_FOUND, - $this->http->client->get('/?p1=&p2=v2&p3=foo')->send()->getStatusCode() + $this->http->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/?p1=&p2=v2&p3=foo')) + ->getStatusCode() ); - $this->assertSame( + self::assertSame( Response::HTTP_NOT_FOUND, - $this->http->client->get('/?p1=')->send()->getStatusCode() + $this->http->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/?p1='))->getStatusCode() ); - $this->assertSame( + self::assertSame( Response::HTTP_NOT_FOUND, - $this->http->client->get('/?p3=foo')->send()->getStatusCode() + $this->http->client->sendRequest($this->getRequestFactory()->createRequest('GET', '/?p3=foo')) + ->getStatusCode() ); } - public function testFatalError() + public function testFatalError(): void { - if (version_compare(PHP_VERSION, '7.0', '<')) { - $this->markTestSkipped('Comment in to test if fatal errors are properly handled'); + if (PHP_VERSION_ID < 70000) { + self::markTestSkipped('Comment in to test if fatal errors are properly handled'); } $this->expectException('Error'); diff --git a/tests/Request/UnifiedRequestTest.php b/tests/Request/UnifiedRequestTest.php deleted file mode 100644 index 479a384..0000000 --- a/tests/Request/UnifiedRequestTest.php +++ /dev/null @@ -1,134 +0,0 @@ -wrappedRequest = $this->createMock('Guzzle\Http\Message\RequestInterface'); - $this->wrappedEntityEnclosingRequest = $this->createMock('Guzzle\Http\Message\EntityEnclosingRequestInterface'); - $this->unifiedRequest = new UnifiedRequest($this->wrappedRequest); - $this->unifiedEnclosingEntityRequest = new UnifiedRequest($this->wrappedEntityEnclosingRequest); - } - - public static function provideMethods() - { - return [ - ['getParams'], - ['getHeaders'], - ['getHeaderLines'], - ['getRawHeaders'], - ['getQuery'], - ['getMethod'], - ['getScheme'], - ['getHost'], - ['getProtocolVersion'], - ['getPath'], - ['getPort'], - ['getUsername'], - ['getPassword'], - ['getUrl'], - ['getCookies'], - ['getHeader', ['header']], - ['hasHeader', ['header']], - ['getUrl', [false]], - ['getUrl', [true]], - ['getCookie', ['cookieName']], - ]; - } - - public static function provideEntityEnclosingInterfaceMethods() - { - return [ - ['getBody'], - ['getPostField', ['postField']], - ['getPostFields'], - ['getPostFiles'], - ['getPostFile', ['fileName']], - ]; - } - - /** @dataProvider provideMethods */ - public function testMethodsFromRequestInterface($method, array $params = []) - { - $this->wrappedRequest - ->expects($this->once()) - ->method($method) - ->will($this->returnValue('REQ')) - ->with(...$params); - $this->assertSame('REQ', call_user_func_array([$this->unifiedRequest, $method], $params)); - - - $this->wrappedEntityEnclosingRequest - ->expects($this->once()) - ->method($method) - ->will($this->returnValue('ENTITY_ENCL_REQ')) - ->with(...$params); - $this->assertSame( - 'ENTITY_ENCL_REQ', - call_user_func_array([$this->unifiedEnclosingEntityRequest, $method], $params) - ); - } - - /** @dataProvider provideEntityEnclosingInterfaceMethods */ - public function testEntityEnclosingInterfaceMethods($method, array $params = []) - { - $this->wrappedEntityEnclosingRequest - ->expects($this->once()) - ->method($method) - ->will($this->returnValue('ENTITY_ENCL_REQ')) - ->with(...$params); - - $this->assertSame( - 'ENTITY_ENCL_REQ', - call_user_func_array([$this->unifiedEnclosingEntityRequest, $method], $params) - ); - - $this->wrappedRequest - ->expects($this->any()) - ->method('getMethod') - ->will($this->returnValue('METHOD')); - $this->wrappedRequest - ->expects($this->any()) - ->method('getPath') - ->will($this->returnValue('/foo')); - - $this->expectException('BadMethodCallException'); - - $this->expectExceptionMessage( - - sprintf( - 'Cannot call method "%s" on a request that does not enclose an entity. Did you expect a POST/PUT request instead of METHOD /foo?', - $method - ) - - ); - call_user_func_array([$this->unifiedRequest, $method], $params); - } - - public function testUserAgent() - { - $this->assertNull($this->unifiedRequest->getUserAgent()); - - $unifiedRequest = new UnifiedRequest($this->wrappedRequest, ['userAgent' => 'UA']); - $this->assertSame('UA', $unifiedRequest->getUserAgent()); - } -} diff --git a/tests/RequestCollectionFacadeTest.php b/tests/RequestCollectionFacadeTest.php index e111263..118406b 100644 --- a/tests/RequestCollectionFacadeTest.php +++ b/tests/RequestCollectionFacadeTest.php @@ -1,34 +1,28 @@ client = $this->createMock('Guzzle\Http\ClientInterface'); - $this->facade = new RequestCollectionFacade($this->client); - $this->request = new Request('GET', '/_request/last'); - $this->request->setClient($this->client); + $this->client = $this->createMock(ClientInterface::class); + $this->facade = new RequestCollectionFacade($this->client, $this->getRequestFactory()); } - public static function provideMethodAndUrls() + /** @return array,3:string}> */ + public static function getMethodAndUrls(): array { return [ ['latest', '/_request/last'], @@ -40,158 +34,194 @@ public static function provideMethodAndUrls() ]; } - /** @dataProvider provideMethodAndUrls */ - public function testRequestingLatestRequest($method, $path, array $args = [], $httpMethod = 'get') + /** + * @dataProvider getMethodAndUrls + * @param array $args + */ + public function testRequestingLatestRequest( + string $method, + string $path, + array $args = [], + string $httpMethod = 'get' + ): void { $this->mockClient($path, $this->createSimpleResponse(), $httpMethod); - $request = call_user_func_array([$this->facade, $method], $args); + /** @var \Symfony\Component\HttpFoundation\Request $request */ + $request = $this->facade->{$method}(...$args); - $this->assertSame('POST', $request->getMethod()); - $this->assertSame('/foo', $request->getPath()); - $this->assertSame('RECORDED=1', (string) $request->getBody()); + self::assertSame('POST', $request->getMethod()); + self::assertSame('/foo', $request->getRequestUri()); + self::assertSame('RECORDED=1', $request->getContent()); } - /** @dataProvider provideMethodAndUrls */ - public function testRequestLatestResponseWithHttpAuth($method, $path, array $args = [], $httpMethod = 'get') + /** + * @dataProvider getMethodAndUrls + * @param array $args + */ + public function testRequestLatestResponseWithHttpAuth( + string $method, + string $path, + array $args = [], + string $httpMethod = 'get' + ): void { $this->mockClient($path, $this->createComplexResponse(), $httpMethod); - $request = call_user_func_array([$this->facade, $method], $args); - - $this->assertSame('POST', $request->getMethod()); - $this->assertSame('/foo', $request->getPath()); - $this->assertSame('RECORDED=1', (string) $request->getBody()); - $this->assertSame('host', $request->getHost()); - $this->assertSame(1234, $request->getPort()); - $this->assertSame('username', $request->getUsername()); - $this->assertSame('password', $request->getPassword()); - $this->assertSame('CUSTOM UA', $request->getUserAgent()); + /** @var \Symfony\Component\HttpFoundation\Request $request */ + $request = $this->facade->{$method}(...$args); + + self::assertSame('POST', $request->getMethod()); + self::assertSame('/foo', $request->getRequestUri()); + self::assertSame('RECORDED=1', $request->getContent()); + self::assertSame('host', $request->getHost()); + self::assertSame(1234, $request->getPort()); + self::assertSame('username', $request->getUser()); + self::assertSame('password', $request->getPassword()); + self::assertSame('CUSTOM UA', $request->headers->get('User-Agent')); } - /** @dataProvider provideMethodAndUrls */ - public function testRequestResponse_InvalidStatusCode($method, $path, array $args = [], $httpMethod = 'get') + /** + * @dataProvider getMethodAndUrls + * @param array $args + */ + public function testRequestResponseWithInvalidStatusCode( + string $method, + string $path, + array $args = [], + string $httpMethod = 'get' + ): void { $this->mockClient($path, $this->createResponseWithInvalidStatusCode(), $httpMethod); $this->expectException('UnexpectedValueException'); $this->expectExceptionMessage('Expected status code 200 from "' . $path . '", got 404'); - call_user_func_array([$this->facade, $method], $args); + $this->facade->{$method}(...$args); } - /** @dataProvider provideMethodAndUrls */ - public function testRequestResponse_EmptyContentType($method, $path, array $args = [], $httpMethod = 'get') + /** + * @dataProvider getMethodAndUrls + * @param array $args + */ + public function testRequestResponseWithEmptyContentType( + string $method, + string $path, + array $args = [], + string $httpMethod = 'get' + ): void { $this->mockClient($path, $this->createResponseWithEmptyContentType(), $httpMethod); $this->expectException('UnexpectedValueException'); $this->expectExceptionMessage('Expected content type "text/plain" from "' . $path . '", got ""'); - call_user_func_array([$this->facade, $method], $args); + $this->facade->{$method}(...$args); } - /** @dataProvider provideMethodAndUrls */ - public function testRequestResponse_InvalidContentType($method, $path, array $args = [], $httpMethod = 'get') + /** + * @dataProvider getMethodAndUrls + * @param array $args + */ + public function testRequestResponseWithInvalidContentType( + string $method, + string $path, + array $args = [], + string $httpMethod = 'get' + ): void { $this->mockClient($path, $this->createResponseWithInvalidContentType(), $httpMethod); $this->expectException('UnexpectedValueException'); $this->expectExceptionMessage('Expected content type "text/plain" from "' . $path . '", got "text/html"'); - call_user_func_array([$this->facade, $method], $args); + $this->facade->{$method}(...$args); } - /** @dataProvider provideMethodAndUrls */ - public function testRequestResponse_DeserializationError($method, $path, array $args = [], $httpMethod = 'get') + /** + * @dataProvider getMethodAndUrls + * @param array $args + */ + public function testRequestResponseWithDeserializationError( + string $method, + string $path, + array $args = [], + string $httpMethod = 'get' + ): void { $this->mockClient($path, $this->createResponseThatCannotBeDeserialized(), $httpMethod); $this->expectException('UnexpectedValueException'); $this->expectExceptionMessage('Cannot deserialize response from "' . $path . '": "invalid response"'); - call_user_func_array([$this->facade, $method], $args); + $this->facade->{$method}(...$args); } - private function mockClient($path, Response $response, $method) + private function mockClient(string $path, ResponseInterface $response, string $method): void { $this->client - ->expects($this->once()) - ->method($method) - ->with($path) - ->will($this->returnValue($this->request)); - - $this->client - ->expects($this->once()) - ->method('send') - ->with($this->request) - ->will($this->returnValue($response)); + ->expects(self::once()) + ->method('sendRequest') + ->with($this->getRequestFactory()->createRequest($method, $path)) + ->willReturn($response); } - private function createSimpleResponse() + private function createSimpleResponse(): ResponseInterface { $recordedRequest = new TestRequest(); $recordedRequest->setMethod('POST'); $recordedRequest->setRequestUri('/foo'); $recordedRequest->setContent('RECORDED=1'); - return new Response( - '200', - ['Content-Type' => 'text/plain'], - serialize( - [ - 'server' => [], - 'request' => (string) $recordedRequest, - ] - ) - ); + return $this->getResponseFactory() + ->createResponse() + ->withHeader('Content-Type', 'text/plain') + ->withBody($this->getStreamFactory()->createStream(serialize($recordedRequest))); } - private function createComplexResponse() + private function createComplexResponse(): ResponseInterface { $recordedRequest = new TestRequest(); $recordedRequest->setMethod('POST'); $recordedRequest->setRequestUri('/foo'); $recordedRequest->setContent('RECORDED=1'); - $recordedRequest->headers->set('Php-Auth-User', 'ignored'); - $recordedRequest->headers->set('Php-Auth-Pw', 'ignored'); - $recordedRequest->headers->set('User-Agent', 'ignored'); - - return new Response( - '200', - ['Content-Type' => 'text/plain; charset=UTF-8'], - serialize( - [ - 'server' => [ - 'HTTP_HOST' => 'host', - 'HTTP_PORT' => 1234, - 'PHP_AUTH_USER' => 'username', - 'PHP_AUTH_PW' => 'password', - 'HTTP_USER_AGENT' => 'CUSTOM UA', - ], - 'request' => (string) $recordedRequest, - ] - ) - ); + $recordedRequest->server->set('SERVER_NAME', 'host'); + $recordedRequest->server->set('SERVER_PORT', 1234); + $recordedRequest->headers->set('Php-Auth-User', 'username'); + $recordedRequest->headers->set('Php-Auth-Pw', 'password'); + $recordedRequest->headers->set('User-Agent', 'CUSTOM UA'); + + + return $this->getResponseFactory() + ->createResponse() + ->withHeader('Content-Type', 'text/plain; charset=UTF-8') + ->withBody($this->getStreamFactory()->createStream(serialize($recordedRequest))); } - private function createResponseWithInvalidStatusCode() + private function createResponseWithInvalidStatusCode(): ResponseInterface { - return new Response(404); + return $this->getResponseFactory() + ->createResponse(404); } - private function createResponseWithInvalidContentType() + private function createResponseWithInvalidContentType(): ResponseInterface { - return new Response(200, ['Content-Type' => 'text/html']); + return $this->getResponseFactory() + ->createResponse() + ->withHeader('Content-Type', 'text/html'); } - private function createResponseWithEmptyContentType() + private function createResponseWithEmptyContentType(): ResponseInterface { - return new Response(200, []); + return $this->getResponseFactory() + ->createResponse(); } - private function createResponseThatCannotBeDeserialized() + private function createResponseThatCannotBeDeserialized(): ResponseInterface { - return new Response(200, ['Content-Type' => 'text/plain'], 'invalid response'); + return $this->getResponseFactory() + ->createResponse() + ->withHeader('Content-Type', 'text/plain; charset=UTF-8') + ->withBody($this->getStreamFactory()->createStream('invalid response')); } } diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..91eec83 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,26 @@ +