├── .phiremock.dist ├── tests ├── acceptance │ ├── _output │ │ └── .gitignore │ ├── _support │ │ ├── _generated │ │ │ └── .gitignore │ │ ├── CommonTester.php │ │ ├── UnitTester.php │ │ ├── AcceptanceTester.php │ │ └── Helper │ │ │ ├── Common.php │ │ │ ├── Unit.php │ │ │ ├── Functional.php │ │ │ └── AcceptanceV1.php │ ├── _data │ │ ├── dump.sql │ │ ├── fixtures │ │ │ └── silhouette-1444982_640.png │ │ └── expectations │ │ │ ├── hello.json │ │ │ └── other_expectations │ │ │ └── world.json │ ├── _bootstrap.php │ ├── _envs │ │ └── scrutinizer.yml │ ├── common.suite.yml │ ├── v1.suite.yml │ ├── v2.suite.yml │ ├── v2 │ │ ├── ResetCest.php │ │ ├── BodyJsonCest.php │ │ ├── SameJsonCest.php │ │ ├── ReplacementCest.php │ │ ├── UrlConditionCest.php │ │ ├── BinaryContentCest.php │ │ ├── BodyConditionCest.php │ │ ├── ExpectationListCest.php │ │ ├── MethodConditionCest.php │ │ ├── SetScenarioStateCest.php │ │ ├── BodySpecificationCest.php │ │ ├── HeadersConditionsCest.php │ │ ├── DelaySpecificationCest.php │ │ ├── ExpectationCreationCest.php │ │ ├── HeadersSpecificationCest.php │ │ ├── StatusCodeSpecificationCest.php │ │ └── ProxyCest.php │ ├── common │ │ └── RecursiveDirectoryCest.php │ └── v1 │ │ ├── BodyJsonCest.php │ │ ├── ExpectationListCest.php │ │ ├── FormDataCest.php │ │ ├── RequestCountCest.php │ │ ├── BinaryContentCest.php │ │ ├── BodySpecificationCest.php │ │ ├── SetScenarioStateCest.php │ │ ├── ProxyCest.php │ │ ├── RequestListCest.php │ │ ├── ResetCest.php │ │ ├── StatusCodeSpecificationCest.php │ │ └── DelaySpecificationCest.php ├── bootstrap.php ├── .phpunit.result.cache ├── phpunit.xml ├── support │ └── PhiremockTest.php ├── unit │ └── FeatureReactLoopTest.php └── codeception │ └── extensions │ └── ServerControl.php ├── bin ├── phiremock └── phiremock.php ├── phiremock.phar ├── .gitignore ├── codeception.yml ├── box.json ├── .scrutinizer.yml ├── src ├── Http │ ├── RequestHandlerInterface.php │ ├── ServerInterface.php │ └── Implementation │ │ ├── FastRouterHandler.php │ │ └── ReactPhpServer.php ├── Utils │ ├── Loggable.php │ ├── DataStructures │ │ ├── Map.php │ │ └── StringObjectArrayMap.php │ ├── Strategies │ │ ├── ResponseStrategyInterface.php │ │ ├── HttpResponseStrategy.php │ │ ├── ProxyResponseStrategy.php │ │ ├── RegexProxyResponseStrategy.php │ │ ├── AbstractResponse.php │ │ ├── Utils │ │ │ └── RegexReplacer.php │ │ └── RegexResponseStrategy.php │ ├── GuzzlePsr18Client.php │ ├── HomePathService.php │ ├── Config │ │ ├── Directory.php │ │ ├── Config.php │ │ └── ConfigBuilder.php │ ├── ResponseStrategyLocator.php │ ├── ArraysHelper.php │ ├── RequestToExpectationMapper.php │ ├── Traits │ │ └── ExpectationValidator.php │ └── FileExpectationsLoader.php ├── Actions │ ├── ActionInterface.php │ ├── ClearScenariosAction.php │ ├── ResetRequestsCountAction.php │ ├── ClearExpectationsAction.php │ ├── ListExpectationsAction.php │ ├── ResetAction.php │ ├── ReloadPreconfiguredExpectationsAction.php │ ├── ActionLocator.php │ ├── CountRequestsAction.php │ ├── AddExpectationAction.php │ ├── ListRequestsAction.php │ ├── SetScenarioStateAction.php │ └── SearchRequestAction.php ├── Model │ ├── RequestStorage.php │ ├── ExpectationStorage.php │ ├── ScenarioStorage.php │ └── Implementation │ │ ├── RequestAutoStorage.php │ │ ├── ExpectationAutoStorage.php │ │ └── ScenarioAutoStorage.php └── Cli │ └── Options │ ├── HostInterface.php │ ├── Passphrase.php │ ├── Port.php │ ├── CertificateKeyPath.php │ ├── ExpectationsDirectory.php │ ├── CertificatePath.php │ ├── SecureOptions.php │ └── PhpFactoryFqcn.php ├── .php_cs.dist ├── composer.phar.json └── composer.json /.phiremock.dist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | ./unit 7 | 8 | 9 | 10 | ../src 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/acceptance/_support/CommonTester.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Tests\Support; 21 | 22 | class PhiremockTest 23 | { 24 | } 25 | -------------------------------------------------------------------------------- /tests/acceptance/_support/Helper/Common.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Helper; 21 | 22 | class Common extends \Codeception\Module 23 | { 24 | } 25 | -------------------------------------------------------------------------------- /tests/acceptance/v2/ResetCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\ResetCest as ResetCestV1; 23 | 24 | class ResetCest extends ResetCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /src/Http/RequestHandlerInterface.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Http; 20 | 21 | use Psr\Http\Message\ResponseInterface; 22 | use Psr\Http\Message\ServerRequestInterface; 23 | 24 | interface RequestHandlerInterface 25 | { 26 | public function dispatch(ServerRequestInterface $request): ResponseInterface; 27 | } 28 | -------------------------------------------------------------------------------- /tests/acceptance/v2/BodyJsonCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\BodyJsonCest as BodyJsonCestV1; 23 | 24 | class BodyJsonCest extends BodyJsonCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /tests/acceptance/v2/SameJsonCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\SameJsonCest as SameJsonCestV1; 23 | 24 | class SameJsonCest extends SameJsonCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /src/Utils/Loggable.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils; 20 | 21 | use Psr\Log\LoggerInterface; 22 | 23 | trait Loggable 24 | { 25 | /** @var LoggerInterface */ 26 | private $logger; 27 | 28 | public function setLogger(LoggerInterface $logger) 29 | { 30 | $this->logger = $logger; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/acceptance/v2/ReplacementCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\ReplacementCest as ReplacementCestV1; 23 | 24 | class ReplacementCest extends ReplacementCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /tests/acceptance/_support/Helper/Unit.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Helper; 21 | 22 | // here you can define custom actions 23 | // all public methods declared in helper class will be available in $I 24 | 25 | class Unit extends \Codeception\Module 26 | { 27 | } 28 | -------------------------------------------------------------------------------- /tests/acceptance/v2/UrlConditionCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\UrlConditionCest as UrlConditionCestV1; 23 | 24 | class UrlConditionCest extends UrlConditionCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /tests/acceptance/v2/BinaryContentCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\BinaryContentCest as BinaryContentCestV1; 23 | 24 | class BinaryContentCest extends BinaryContentCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /tests/acceptance/v2/BodyConditionCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\BodyConditionCest as BodyConditionCestV1; 23 | 24 | class BodyConditionCest extends BodyConditionCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /tests/acceptance/_support/Helper/Functional.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Helper; 21 | 22 | // here you can define custom actions 23 | // all public methods declared in helper class will be available in $I 24 | 25 | class Functional extends \Codeception\Module 26 | { 27 | } 28 | -------------------------------------------------------------------------------- /tests/acceptance/v2/ExpectationListCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\ExpectationListCest as ExpectationListCestV1; 23 | 24 | class ExpectationListCest extends ExpectationListCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /tests/acceptance/v2/MethodConditionCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\MethodConditionCest as MethodConditionCestV1; 23 | 24 | class MethodConditionCest extends MethodConditionCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /tests/acceptance/v2/SetScenarioStateCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\SetScenarioStateCest as SetScenarioStateCestV1; 23 | 24 | class SetScenarioStateCest extends SetScenarioStateCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /tests/acceptance/v2/BodySpecificationCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\BodySpecificationCest as BodySpecificationCestV1; 23 | 24 | class BodySpecificationCest extends BodySpecificationCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /tests/acceptance/v2/HeadersConditionsCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\HeadersConditionsCest as HeadersConditionsCestV1; 23 | 24 | class HeadersConditionsCest extends HeadersConditionsCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /tests/acceptance/v2/DelaySpecificationCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\DelaySpecificationCest as DelaySpecificationCestV1; 23 | 24 | class DelaySpecificationCest extends DelaySpecificationCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /tests/acceptance/v2/ExpectationCreationCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\ExpectationCreationCest as ExpectationCreationCestV1; 23 | 24 | class ExpectationCreationCest extends ExpectationCreationCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /src/Actions/ActionInterface.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Actions; 20 | 21 | use Psr\Http\Message\ResponseInterface; 22 | use Psr\Http\Message\ServerRequestInterface; 23 | 24 | interface ActionInterface 25 | { 26 | /** @return ResponseInterface */ 27 | public function execute(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface; 28 | } 29 | -------------------------------------------------------------------------------- /tests/acceptance/v2/HeadersSpecificationCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\HeadersSpecificationCest as HeadersSpecificationCestV1; 23 | 24 | class HeadersSpecificationCest extends HeadersSpecificationCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /tests/acceptance/v2/StatusCodeSpecificationCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use Mcustiel\Phiremock\Server\Tests\V1\StatusCodeSpecificationCest as StatusCodeSpecificationCestV1; 23 | 24 | class StatusCodeSpecificationCest extends StatusCodeSpecificationCestV1 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /src/Model/RequestStorage.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Model; 20 | 21 | use Psr\Http\Message\ServerRequestInterface; 22 | 23 | interface RequestStorage 24 | { 25 | public function addRequest(ServerRequestInterface $request): void; 26 | 27 | /** @return ServerRequestInterface[] */ 28 | public function listRequests(): array; 29 | 30 | public function clearRequests(): void; 31 | } 32 | -------------------------------------------------------------------------------- /src/Cli/Options/HostInterface.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Cli\Options; 20 | 21 | class HostInterface 22 | { 23 | /** @var string */ 24 | private $interface; 25 | 26 | public function __construct(string $interface) 27 | { 28 | $this->interface = $interface; 29 | } 30 | 31 | public function asString(): string 32 | { 33 | return $this->interface; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Model/ExpectationStorage.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Model; 20 | 21 | use Mcustiel\Phiremock\Domain\Expectation; 22 | 23 | interface ExpectationStorage 24 | { 25 | public function addExpectation(Expectation $expectation): void; 26 | 27 | public function clearExpectations(): void; 28 | 29 | /** @return Expectation[] */ 30 | public function listExpectations(): array; 31 | } 32 | -------------------------------------------------------------------------------- /src/Utils/DataStructures/Map.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils\DataStructures; 20 | 21 | use IteratorAggregate; 22 | 23 | interface Map extends IteratorAggregate 24 | { 25 | public function set($key, $value); 26 | 27 | public function get($key); 28 | 29 | public function clean(); 30 | 31 | public function has($key); 32 | 33 | public function delete($key); 34 | 35 | public function getIterator(); 36 | } 37 | -------------------------------------------------------------------------------- /src/Http/ServerInterface.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Http; 20 | 21 | use Mcustiel\Phiremock\Server\Cli\Options\HostInterface; 22 | use Mcustiel\Phiremock\Server\Cli\Options\Port; 23 | use Mcustiel\Phiremock\Server\Cli\Options\SecureOptions; 24 | 25 | interface ServerInterface 26 | { 27 | public function listen(HostInterface $interface, Port $port, ?SecureOptions $secureOptions): void; 28 | 29 | public function shutdown(): void; 30 | } 31 | -------------------------------------------------------------------------------- /src/Cli/Options/Passphrase.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Cli\Options; 21 | 22 | class Passphrase 23 | { 24 | /** @var string */ 25 | private $pass; 26 | 27 | public function __construct(string $pass) 28 | { 29 | $this->pass = $pass; 30 | } 31 | 32 | public function asString(): string 33 | { 34 | return $this->pass; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Utils/Strategies/ResponseStrategyInterface.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils\Strategies; 20 | 21 | use Mcustiel\Phiremock\Domain\Expectation; 22 | use Psr\Http\Message\ResponseInterface; 23 | use Psr\Http\Message\ServerRequestInterface; 24 | 25 | interface ResponseStrategyInterface 26 | { 27 | public function createResponse( 28 | Expectation $expectation, 29 | ResponseInterface $transactionData, 30 | ServerRequestInterface $request 31 | ): ResponseInterface; 32 | } 33 | -------------------------------------------------------------------------------- /src/Model/ScenarioStorage.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Model; 20 | 21 | use Mcustiel\Phiremock\Domain\Options\ScenarioName; 22 | use Mcustiel\Phiremock\Domain\Options\ScenarioState; 23 | use Mcustiel\Phiremock\Domain\ScenarioStateInfo; 24 | 25 | interface ScenarioStorage 26 | { 27 | const INITIAL_SCENARIO = 'Scenario.START'; 28 | 29 | public function setScenarioState(ScenarioStateInfo $scenarioState): void; 30 | 31 | public function getScenarioState(ScenarioName $name): ScenarioState; 32 | 33 | public function clearScenarios(): void; 34 | } 35 | -------------------------------------------------------------------------------- /tests/unit/FeatureReactLoopTest.php: -------------------------------------------------------------------------------- 1 | loop = EventLoop::create(); 15 | $this->loop->run(); 16 | } 17 | 18 | protected function tearDown(): void 19 | { 20 | $this->loop->stop(); 21 | } 22 | 23 | public function testParallelExecutions(): void 24 | { 25 | $function = function () { 26 | $deferred = new \React\Promise\Deferred(); 27 | 28 | $this->loop->addTimer(0, function () use ($deferred) { 29 | $seconds = rand(3, 8); 30 | echo sprintf('Sleeping for %d seconds', $seconds); 31 | sleep($seconds); 32 | $deferred->resolve($seconds); 33 | }); 34 | 35 | return $deferred->promise(); 36 | }; 37 | for ($i = 0; $i < 10; ++$i) { 38 | $promise = new \React\Promise\LazyPromise($function); 39 | $promise->then(function ($seconds) { 40 | echo sprintf('Slept for %d seconds', $seconds); 41 | }); 42 | } 43 | 44 | sleep(10); 45 | 46 | $this->assertTrue(true); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Cli/Options/Port.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Cli\Options; 20 | 21 | use InvalidArgumentException; 22 | 23 | class Port 24 | { 25 | /** @var int */ 26 | private $port; 27 | 28 | public function __construct(int $port) 29 | { 30 | $this->ensureIsValidPort($port); 31 | $this->port = $port; 32 | } 33 | 34 | public function asInt(): int 35 | { 36 | return $this->port; 37 | } 38 | 39 | /** @throws InvalidArgumentException */ 40 | private function ensureIsValidPort(int $port): void 41 | { 42 | if ($port < 1 || $port > 65535) { 43 | throw new InvalidArgumentException(sprintf('Invalid port number: %d', $port)); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/acceptance/common/RecursiveDirectoryCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\Common; 21 | 22 | use CommonTester; 23 | 24 | class RecursiveDirectoryCest 25 | { 26 | public function _before(CommonTester $I) 27 | { 28 | $I->sendPOST('/__phiremock/reset'); 29 | } 30 | 31 | public function detectFilesRecursively(CommonTester $I) 32 | { 33 | $I->sendGET('/hello'); 34 | $I->seeResponseCodeIs('200'); 35 | $I->seeResponseEquals('Hello!'); 36 | 37 | $I->sendGET('/world'); 38 | $I->seeResponseCodeIs('200'); 39 | $I->seeResponseEquals('World!'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Actions/ClearScenariosAction.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Actions; 20 | 21 | use Mcustiel\Phiremock\Server\Model\ScenarioStorage; 22 | use Psr\Http\Message\ResponseInterface; 23 | use Psr\Http\Message\ServerRequestInterface; 24 | 25 | class ClearScenariosAction implements ActionInterface 26 | { 27 | /** 28 | * @var ScenarioStorage 29 | */ 30 | private $storage; 31 | 32 | public function __construct(ScenarioStorage $storage) 33 | { 34 | $this->storage = $storage; 35 | } 36 | 37 | public function execute(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 38 | { 39 | $this->storage->clearScenarios(); 40 | 41 | return $response->withStatus(200); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Actions/ResetRequestsCountAction.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Actions; 20 | 21 | use Mcustiel\Phiremock\Server\Model\RequestStorage; 22 | use Psr\Http\Message\ResponseInterface; 23 | use Psr\Http\Message\ServerRequestInterface; 24 | 25 | class ResetRequestsCountAction implements ActionInterface 26 | { 27 | /** 28 | * @var RequestStorage 29 | */ 30 | private $storage; 31 | 32 | public function __construct(RequestStorage $storage) 33 | { 34 | $this->storage = $storage; 35 | } 36 | 37 | public function execute(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 38 | { 39 | $this->storage->clearRequests(); 40 | 41 | return $response->withStatus(200); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Actions/ClearExpectationsAction.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Actions; 20 | 21 | use Mcustiel\Phiremock\Server\Model\ExpectationStorage; 22 | use Psr\Http\Message\ResponseInterface; 23 | use Psr\Http\Message\ServerRequestInterface; 24 | 25 | class ClearExpectationsAction implements ActionInterface 26 | { 27 | /** 28 | * @var ExpectationStorage 29 | */ 30 | private $storage; 31 | 32 | public function __construct(ExpectationStorage $storage) 33 | { 34 | $this->storage = $storage; 35 | } 36 | 37 | public function execute(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 38 | { 39 | $this->storage->clearExpectations(); 40 | 41 | return $response->withStatus(200); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /bin/phiremock.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | use Mcustiel\Phiremock\Server\Cli\Commands\PhiremockServerCommand; 20 | use Symfony\Component\Console\Application; 21 | 22 | const IS_SINGLE_COMMAND = true; 23 | 24 | if (\PHP_SAPI !== 'cli') { 25 | throw new RuntimeException('This is a standalone CLI application'); 26 | } 27 | 28 | if (file_exists(__DIR__ . '/../vendor/autoload.php')) { 29 | $loader = require __DIR__ . '/../vendor/autoload.php'; 30 | } else { 31 | $loader = require __DIR__ . '/../../../autoload.php'; 32 | } 33 | 34 | $application = new Application('Phiremock', ''); 35 | $phiremockServerCommand = new PhiremockServerCommand(); 36 | $application->add($phiremockServerCommand); 37 | $application->setDefaultCommand($phiremockServerCommand->getName(), IS_SINGLE_COMMAND); 38 | $application->run(); 39 | -------------------------------------------------------------------------------- /src/Utils/GuzzlePsr18Client.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Utils; 21 | 22 | use GuzzleHttp\Client as GuzzleClient; 23 | use Psr\Http\Client\ClientInterface; 24 | use Psr\Http\Message\RequestInterface; 25 | use Psr\Http\Message\ResponseInterface; 26 | 27 | class GuzzlePsr18Client implements ClientInterface 28 | { 29 | /** @var GuzzleClient */ 30 | private $client; 31 | 32 | public function __construct(GuzzleClient $client = null) 33 | { 34 | $this->client = $client ?? new GuzzleClient(['allow_redirects' => false]); 35 | } 36 | 37 | public function sendRequest(RequestInterface $request): ResponseInterface 38 | { 39 | return $this->client->send($request); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Cli/Options/CertificateKeyPath.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Cli\Options; 21 | 22 | use Exception; 23 | 24 | class CertificateKeyPath 25 | { 26 | /** @var string */ 27 | private $path; 28 | 29 | public function __construct(string $path) 30 | { 31 | $this->ensureCanReadFile($path); 32 | $this->path = $path; 33 | } 34 | 35 | public function asString(): string 36 | { 37 | return $this->path; 38 | } 39 | 40 | private function ensureCanReadFile(string $path) 41 | { 42 | if (!file_exists($path) || !is_readable($path)) { 43 | throw new Exception(sprintf('File %s does not exist or is not readable', $path)); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Cli/Options/ExpectationsDirectory.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Cli\Options; 20 | 21 | class ExpectationsDirectory 22 | { 23 | /** @var string */ 24 | private $expectationsDir; 25 | 26 | /** @param string $expectationsDir */ 27 | public function __construct(string $expectationsDir) 28 | { 29 | $this->expectationsDir = $expectationsDir; 30 | } 31 | 32 | public function exists(): bool 33 | { 34 | return file_exists($this->expectationsDir); 35 | } 36 | 37 | public function isDirectory(): bool 38 | { 39 | return is_dir($this->expectationsDir); 40 | } 41 | 42 | public function create(): void 43 | { 44 | mkdir($this->expectationsDir, 0755, true); 45 | } 46 | 47 | public function asString(): string 48 | { 49 | return $this->expectationsDir; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Cli/Options/CertificatePath.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Cli\Options; 21 | 22 | use Exception; 23 | 24 | class CertificatePath 25 | { 26 | /** @var string */ 27 | private $path; 28 | 29 | /** @throws Exception */ 30 | public function __construct(string $path) 31 | { 32 | $this->ensureCanReadFile($path); 33 | $this->path = $path; 34 | } 35 | 36 | public function asString(): string 37 | { 38 | return $this->path; 39 | } 40 | 41 | /** @throws Exception */ 42 | private function ensureCanReadFile(string $path): void 43 | { 44 | if (!file_exists($path) || !is_readable($path)) { 45 | throw new Exception(sprintf('File %s does not exist or is not readable', $path)); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Utils/HomePathService.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils; 20 | 21 | use Exception; 22 | use Mcustiel\Phiremock\Server\Utils\Config\Directory; 23 | 24 | class HomePathService 25 | { 26 | /** @throws Exception */ 27 | public static function getHomePath(): Directory 28 | { 29 | $unixHome = getenv('HOME'); 30 | 31 | if (!empty($unixHome)) { 32 | return new Directory($unixHome); 33 | } 34 | 35 | $windowsHome = getenv('USERPROFILE'); 36 | if (!empty($windowsHome)) { 37 | return new Directory($windowsHome); 38 | } 39 | 40 | $windowsHome = getenv('HOMEPATH'); 41 | if (!empty($windowsHome)) { 42 | return new Directory(getenv('HOMEDRIVE') . getenv('HOMEPATH')); 43 | } 44 | 45 | throw new Exception('Could not get the users\'s home path'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 5 | ->setRules([ 6 | '@Symfony' => true, 7 | '@Symfony:risky' => true, 8 | 'array_syntax' => ['syntax' => 'short'], 9 | 'combine_consecutive_unsets' => true, 10 | // one should use PHPUnit methods to set up expected exception instead of annotations 11 | 'general_phpdoc_annotation_remove' => ['expectedException', 'expectedExceptionMessage', 'expectedExceptionMessageRegExp'], 12 | 'heredoc_to_nowdoc' => true, 13 | 'no_extra_consecutive_blank_lines' => ['break', 'continue', 'extra', 'return', 'throw', 'use', 'parenthesis_brace_block', 'square_brace_block', 'curly_brace_block'], 14 | 'no_unreachable_default_argument_value' => true, 15 | 'no_unused_imports' => true, 16 | 'no_useless_else' => true, 17 | 'no_useless_return' => true, 18 | 'ordered_class_elements' => true, 19 | 'ordered_imports' => true, 20 | 'php_unit_strict' => true, 21 | 'phpdoc_add_missing_param_annotation' => true, 22 | 'phpdoc_order' => true, 23 | 'psr4' => true, 24 | 'strict_comparison' => true, 25 | 'strict_param' => true, 26 | 'concat_space' => ['spacing' => 'one'], 27 | 'binary_operator_spaces' => ['align_double_arrow' => true], 28 | 'yoda_style' => false, 29 | ]) 30 | ->setFinder( 31 | PhpCsFixer\Finder::create() 32 | ->exclude('tests/Fixtures') 33 | ->in(__DIR__ . '/bin') 34 | ->in(__DIR__ . '/src') 35 | ->in(__DIR__ . '/tests') 36 | ) 37 | ; 38 | -------------------------------------------------------------------------------- /src/Model/Implementation/RequestAutoStorage.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Model\Implementation; 20 | 21 | use Mcustiel\Phiremock\Server\Model\RequestStorage; 22 | use Psr\Http\Message\ServerRequestInterface; 23 | 24 | class RequestAutoStorage implements RequestStorage 25 | { 26 | /** @var ServerRequestInterface[] */ 27 | private $requests; 28 | 29 | public function __construct() 30 | { 31 | $this->clearRequests(); 32 | } 33 | 34 | public function addRequest(ServerRequestInterface $request): void 35 | { 36 | $this->requests[] = $request; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | * 42 | * @see \Mcustiel\Phiremock\Server\Model\RequestStorage::listRequests() 43 | */ 44 | public function listRequests(): array 45 | { 46 | return $this->requests; 47 | } 48 | 49 | public function clearRequests(): void 50 | { 51 | $this->requests = []; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/acceptance/_support/Helper/AcceptanceV1.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Helper; 21 | 22 | class AcceptanceV1 extends \Codeception\Module 23 | { 24 | public function writeDebugMessage(string $message): void 25 | { 26 | $this->debug($message); 27 | } 28 | 29 | public function getPhiremockRequest(array $request): array 30 | { 31 | unset($request['request']['jsonPath']); 32 | return $request; 33 | } 34 | 35 | public function getPhiremockResponse(string $jsonResponse): string 36 | { 37 | $parsedExpectations = json_decode($jsonResponse, true); 38 | if (json_last_error() !== \JSON_ERROR_NONE) { 39 | return $jsonResponse; 40 | } 41 | 42 | foreach ($parsedExpectations as &$parsedExpectation) { 43 | unset($parsedExpectation['request']['jsonPath']); 44 | } 45 | 46 | return json_encode($parsedExpectations); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Model/Implementation/ExpectationAutoStorage.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Model\Implementation; 20 | 21 | use Mcustiel\Phiremock\Domain\Expectation; 22 | use Mcustiel\Phiremock\Server\Model\ExpectationStorage; 23 | 24 | class ExpectationAutoStorage implements ExpectationStorage 25 | { 26 | /** 27 | * @var Expectation[] 28 | */ 29 | private $expectations; 30 | 31 | public function __construct() 32 | { 33 | $this->clearExpectations(); 34 | } 35 | 36 | public function addExpectation(Expectation $expectation): void 37 | { 38 | $this->expectations[] = $expectation; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | * 44 | * @see \Mcustiel\Phiremock\Server\Model\ExpectationStorage::listExpectations() 45 | */ 46 | public function listExpectations(): array 47 | { 48 | return $this->expectations; 49 | } 50 | 51 | public function clearExpectations(): void 52 | { 53 | $this->expectations = []; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Utils/Config/Directory.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Utils\Config; 21 | 22 | use InvalidArgumentException; 23 | 24 | class Directory 25 | { 26 | /** @var string */ 27 | private $directory; 28 | 29 | public function __construct(string $directory) 30 | { 31 | $this->ensureIsDirectory($directory); 32 | $this->directory = rtrim($directory, \DIRECTORY_SEPARATOR); 33 | } 34 | 35 | public function asString(): string 36 | { 37 | return $this->directory; 38 | } 39 | 40 | public function getFullSubpathAsString(string $subPath): string 41 | { 42 | return $this->directory . \DIRECTORY_SEPARATOR . $subPath; 43 | } 44 | 45 | private function ensureIsDirectory(string $directory): void 46 | { 47 | if (!is_dir($directory)) { 48 | throw new InvalidArgumentException(sprintf('"%s" is not a directory or is not accessible.', $directory)); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Cli/Options/SecureOptions.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Cli\Options; 21 | 22 | class SecureOptions 23 | { 24 | /** @var CertificatePath */ 25 | private $certificate; 26 | /** @var CertificateKeyPath */ 27 | private $certificateKey; 28 | /** @var Passphrase */ 29 | private $passphrase; 30 | 31 | public function __construct(CertificatePath $cert, CertificateKeyPath $certKey, ?Passphrase $pass) 32 | { 33 | $this->certificate = $cert; 34 | $this->passphrase = $pass; 35 | $this->certificateKey = $certKey; 36 | } 37 | 38 | public function getCertificate(): CertificatePath 39 | { 40 | return $this->certificate; 41 | } 42 | 43 | public function getCertificateKey(): CertificateKeyPath 44 | { 45 | return $this->certificateKey; 46 | } 47 | 48 | public function hasPassphrase(): bool 49 | { 50 | return $this->passphrase !== null; 51 | } 52 | 53 | public function getPassphrase(): Passphrase 54 | { 55 | return $this->passphrase; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Model/Implementation/ScenarioAutoStorage.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Model\Implementation; 20 | 21 | use Mcustiel\Phiremock\Domain\Options\ScenarioName; 22 | use Mcustiel\Phiremock\Domain\Options\ScenarioState; 23 | use Mcustiel\Phiremock\Domain\ScenarioStateInfo; 24 | use Mcustiel\Phiremock\Server\Model\ScenarioStorage; 25 | 26 | class ScenarioAutoStorage implements ScenarioStorage 27 | { 28 | /** @var array */ 29 | private $scenarios; 30 | 31 | public function __construct() 32 | { 33 | $this->scenarios = []; 34 | } 35 | 36 | public function setScenarioState(ScenarioStateInfo $scenarioState): void 37 | { 38 | $this->scenarios[$scenarioState->getScenarioName()->asString()] = $scenarioState->getScenarioState(); 39 | } 40 | 41 | public function getScenarioState(ScenarioName $name): ScenarioState 42 | { 43 | $nameString = $name->asString(); 44 | if (!isset($this->scenarios[$nameString])) { 45 | $this->scenarios[$nameString] = new ScenarioState(self::INITIAL_SCENARIO); 46 | } 47 | 48 | return $this->scenarios[$nameString]; 49 | } 50 | 51 | public function clearScenarios(): void 52 | { 53 | $this->scenarios = []; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /composer.phar.json: -------------------------------------------------------------------------------- 1 | { 2 | "keywords": [ 3 | "http", 4 | "mock", 5 | "server", 6 | "external", 7 | "acceptance", 8 | "tests" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Mariano Custiel", 13 | "email": "jmcustiel@gmail.com", 14 | "homepage": "https://github.com/mcustiel", 15 | "role": "Maintainer" 16 | } 17 | ], 18 | "name": "mcustiel/phiremock-server", 19 | "type": "project", 20 | "description": "A mocker for HTTP and REST services", 21 | "license": "GPL-3.0-or-later", 22 | "require": { 23 | "php": "^7.2|^8.0", 24 | "mcustiel/phiremock-common": "^1.0", 25 | "react/http": "^1.0", 26 | "monolog/monolog": "^2.0|^3.0", 27 | "symfony/console": ">=4.0 <7.0", 28 | "nikic/fast-route": "^1.3.0", 29 | "psr/http-client": "^1.0", 30 | "guzzlehttp/guzzle" : "^6.0" 31 | }, 32 | "require-dev": { 33 | "phpunit/phpunit": "^8.0|^9.0", 34 | "codeception/codeception": "^4.0", 35 | "codeception/module-asserts": "^1.0", 36 | "codeception/module-rest": "^1.0", 37 | "codeception/module-phpbrowser": "^1.0", 38 | "symfony/process": ">=3.0 <7.0" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Mcustiel\\Phiremock\\Server\\": "src" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Mcustiel\\Phiremock\\Server\\Tests\\V1\\": "tests/acceptance/v1", 48 | "Mcustiel\\Phiremock\\Server\\Tests\\V2\\": "tests/acceptance/v2", 49 | "Mcustiel\\Phiremock\\Server\\Tests\\Common\\": "tests/acceptance/common", 50 | "Mcustiel\\Codeception\\Extensions\\": "tests/codeception/extensions", 51 | "Mcustiel\\Phiremock\\Server\\Tests\\Support\\": "tests/support" 52 | } 53 | }, 54 | "suggest": { 55 | "ext-pcntl": "Allows phiremock to handle system signals", 56 | "guzzlehttp/guzzle": "Provides default client for proxying http requests." 57 | }, 58 | "bin": [ 59 | "bin/phiremock" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "keywords": [ 3 | "http", 4 | "mock", 5 | "server", 6 | "external", 7 | "acceptance", 8 | "tests" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Mariano Custiel", 13 | "email": "jmcustiel@gmail.com", 14 | "homepage": "https://github.com/mcustiel", 15 | "role": "Maintainer" 16 | } 17 | ], 18 | "name": "mcustiel/phiremock-server", 19 | "type": "project", 20 | "description": "A mocker for HTTP and REST services", 21 | "license": "GPL-3.0-or-later", 22 | "require": { 23 | "php": "^7.2|^8.0", 24 | "ext-json": "*", 25 | "mcustiel/phiremock-common": "^1.0", 26 | "react/http": "^1.0", 27 | "monolog/monolog": ">=1.0 <4.0", 28 | "symfony/console": ">=3.0 <8.0", 29 | "nikic/fast-route": "^1.3.0", 30 | "psr/http-client": "^1.0" 31 | }, 32 | "require-dev": { 33 | "phpunit/phpunit": "^8.0|^9.0", 34 | "codeception/codeception": "^4.0", 35 | "codeception/module-asserts": "^1.0", 36 | "codeception/module-rest": "^1.0", 37 | "codeception/module-phpbrowser": "^1.0", 38 | "symfony/process": ">=3.0 <8.0", 39 | "guzzlehttp/guzzle" : "^6.0" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "Mcustiel\\Phiremock\\Server\\": "src" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "Mcustiel\\Phiremock\\Server\\Tests\\V1\\": "tests/acceptance/v1", 49 | "Mcustiel\\Phiremock\\Server\\Tests\\V2\\": "tests/acceptance/v2", 50 | "Mcustiel\\Phiremock\\Server\\Tests\\Common\\": "tests/acceptance/common", 51 | "Mcustiel\\Codeception\\Extensions\\": "tests/codeception/extensions", 52 | "Mcustiel\\Phiremock\\Server\\Tests\\Support\\": "tests/support" 53 | } 54 | }, 55 | "suggest": { 56 | "ext-pcntl": "Allows phiremock to handle system signals", 57 | "guzzlehttp/guzzle": "Provides default client for proxying http requests." 58 | }, 59 | "bin": [ 60 | "bin/phiremock" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /tests/acceptance/v2/ProxyCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V2; 21 | 22 | use AcceptanceTester; 23 | use GuzzleHttp\Client as HttpClient; 24 | use Mcustiel\Phiremock\Server\Tests\V1\ProxyCest as ProxyCestV1; 25 | 26 | class ProxyCest extends ProxyCestV1 27 | { 28 | public function proxyToGivenUriUsingDataFromRequestTest(AcceptanceTester $I): void 29 | { 30 | $realUrl = 'http://info.cern.ch/hypertext/WWW/TheProject.html'; 31 | 32 | $I->haveHttpHeader('Content-Type', 'application/json'); 33 | 34 | $I->sendPOST( 35 | '/__phiremock/expectations', 36 | $I->getPhiremockRequest([ 37 | 'version' => '2', 38 | 'request' => [ 39 | 'url' => ['matches' => '~^/path/([a-z]+)~i'], 40 | 'body' => ['matches' => '~"file"\s*:\s*\"([a-z]+)"~i'], 41 | ], 42 | 'proxyTo' => 'http://info.cern.ch/hypertext/${url.1}/${body.1}.html', 43 | ]) 44 | ); 45 | 46 | $guzzle = new HttpClient(); 47 | $originalBody = $guzzle->get($realUrl)->getBody()->__toString(); 48 | 49 | $I->sendPost('/path/WWW', ['file' => 'TheProject']); 50 | $I->seeResponseEquals($originalBody); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Actions/ListExpectationsAction.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Actions; 20 | 21 | use Exception; 22 | use Mcustiel\Phiremock\Common\StringStream; 23 | use Mcustiel\Phiremock\Common\Utils\ExpectationToArrayConverterLocator; 24 | use Mcustiel\Phiremock\Server\Model\ExpectationStorage; 25 | use Psr\Http\Message\ResponseInterface; 26 | use Psr\Http\Message\ServerRequestInterface; 27 | 28 | class ListExpectationsAction implements ActionInterface 29 | { 30 | /** @var ExpectationStorage */ 31 | private $storage; 32 | /** @var ExpectationToArrayConverterLocator */ 33 | private $converterLocator; 34 | 35 | public function __construct( 36 | ExpectationStorage $storage, 37 | ExpectationToArrayConverterLocator $converterLocator 38 | ) { 39 | $this->storage = $storage; 40 | $this->converterLocator = $converterLocator; 41 | } 42 | 43 | /** @throws Exception */ 44 | public function execute(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 45 | { 46 | $list = []; 47 | foreach ($this->storage->listExpectations() as $expectation) { 48 | $list[] = $this->converterLocator->locate($expectation)->convert($expectation); 49 | } 50 | $jsonList = json_encode($list); 51 | 52 | return $response->withBody(new StringStream($jsonList)) 53 | ->withHeader('Content-type', 'application/json'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Actions/ResetAction.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Actions; 20 | 21 | use Psr\Http\Message\ResponseInterface; 22 | use Psr\Http\Message\ServerRequestInterface; 23 | use Psr\Log\LoggerInterface; 24 | 25 | class ResetAction implements ActionInterface 26 | { 27 | /** @var ClearScenariosAction */ 28 | private $scenariosCleaner; 29 | /** @var ResetRequestsCountAction */ 30 | private $requestCounterCleaner; 31 | /** @var ReloadPreconfiguredExpectationsAction */ 32 | private $expectationsReloader; 33 | /** @var LoggerInterface */ 34 | private $logger; 35 | 36 | public function __construct( 37 | ClearScenariosAction $scenariosCleaner, 38 | ResetRequestsCountAction $requestCounterCleaner, 39 | ReloadPreconfiguredExpectationsAction $expectationsReloader, 40 | LoggerInterface $logger 41 | ) { 42 | $this->scenariosCleaner = $scenariosCleaner; 43 | $this->requestCounterCleaner = $requestCounterCleaner; 44 | $this->expectationsReloader = $expectationsReloader; 45 | $this->logger = $logger; 46 | } 47 | 48 | public function execute(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 49 | { 50 | $this->logger->debug('Executing reset'); 51 | $response = $this->scenariosCleaner->execute($request, $response); 52 | $response = $this->requestCounterCleaner->execute($request, $response); 53 | 54 | return $this->expectationsReloader->execute($request, $response); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Actions/ReloadPreconfiguredExpectationsAction.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Actions; 20 | 21 | use Mcustiel\Phiremock\Server\Model\ExpectationStorage; 22 | use Psr\Http\Message\ResponseInterface; 23 | use Psr\Http\Message\ServerRequestInterface; 24 | use Psr\Log\LoggerInterface; 25 | 26 | class ReloadPreconfiguredExpectationsAction implements ActionInterface 27 | { 28 | /** 29 | * @var ExpectationStorage 30 | */ 31 | private $expectationStorage; 32 | /** 33 | * @var ExpectationStorage 34 | */ 35 | private $expectationBackup; 36 | /** 37 | * @var LoggerInterface 38 | */ 39 | private $logger; 40 | 41 | public function __construct( 42 | ExpectationStorage $expectationStorage, 43 | ExpectationStorage $expectationBackup, 44 | LoggerInterface $logger 45 | ) { 46 | $this->expectationStorage = $expectationStorage; 47 | $this->expectationBackup = $expectationBackup; 48 | $this->logger = $logger; 49 | } 50 | 51 | public function execute(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 52 | { 53 | $this->expectationStorage->clearExpectations(); 54 | foreach ($this->expectationBackup->listExpectations() as $expectation) { 55 | $this->expectationStorage->addExpectation($expectation); 56 | } 57 | $this->logger->debug('Pre-defined expectations are restored, scenarios and requests history are cleared.'); 58 | 59 | return $response->withStatus(200); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Utils/Strategies/HttpResponseStrategy.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils\Strategies; 20 | 21 | use Mcustiel\Phiremock\Domain\Expectation; 22 | use Mcustiel\Phiremock\Domain\HttpResponse; 23 | use Psr\Http\Message\ResponseInterface; 24 | use Psr\Http\Message\ServerRequestInterface; 25 | 26 | class HttpResponseStrategy extends AbstractResponse implements ResponseStrategyInterface 27 | { 28 | /** 29 | * {@inheritdoc} 30 | * 31 | * @see \Mcustiel\Phiremock\Server\Utils\Strategies\ResponseStrategyInterface::createResponse() 32 | */ 33 | public function createResponse( 34 | Expectation $expectation, 35 | ResponseInterface $httpResponse, 36 | ServerRequestInterface $request 37 | ): ResponseInterface { 38 | /** @var HttpResponse $responseConfig */ 39 | $responseConfig = $expectation->getResponse(); 40 | 41 | $httpResponse = $this->getResponseWithBody($responseConfig, $httpResponse); 42 | $httpResponse = $this->getResponseWithStatusCode($responseConfig, $httpResponse); 43 | $httpResponse = $this->getResponseWithHeaders($responseConfig, $httpResponse); 44 | $this->processScenario($expectation); 45 | $this->processDelay($responseConfig); 46 | 47 | return $httpResponse; 48 | } 49 | 50 | private function getResponseWithBody(HttpResponse $responseConfig, ResponseInterface $httpResponse): ResponseInterface 51 | { 52 | if ($responseConfig->getBody()) { 53 | $httpResponse = $httpResponse->withBody($responseConfig->getBody()->asStream()); 54 | } 55 | 56 | return $httpResponse; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Cli/Options/PhpFactoryFqcn.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Cli\Options; 21 | 22 | use InvalidArgumentException; 23 | use Mcustiel\Phiremock\Server\Factory\Factory; 24 | use Mcustiel\Phiremock\Server\Utils\Config\Config; 25 | 26 | class PhpFactoryFqcn 27 | { 28 | private const CLASSNAME_REGEX = '~^(?:\\\\[a-z0-9_]+|[a-z0-9_]+)(?:\\\\[a-z0-9_]+)*$~i'; 29 | 30 | /** @var string */ 31 | private $className; 32 | 33 | public function __construct(string $className) 34 | { 35 | $this->ensureIsClassName($className); 36 | $this->ensureExtendsFactory($className); 37 | $this->className = $className; 38 | } 39 | 40 | public function asString(): string 41 | { 42 | return $this->className; 43 | } 44 | 45 | public function asInstance(Config $config): object 46 | { 47 | /** @var class-string $className */ 48 | $className = $this->className; 49 | 50 | return $className::createDefault($config); 51 | } 52 | 53 | private function ensureExtendsFactory(string $className): void 54 | { 55 | if (!is_a($className, Factory::class, true)) { 56 | throw new InvalidArgumentException(sprintf('Class %s does not extend %s', $className, Factory::class)); 57 | } 58 | } 59 | 60 | private function ensureIsClassName(string $className): void 61 | { 62 | if (preg_match(self::CLASSNAME_REGEX, $className) !== 1) { 63 | throw new InvalidArgumentException('Invalid class name: ' . $className); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/acceptance/v1/BodyJsonCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V1; 21 | 22 | use AcceptanceTester; 23 | 24 | class BodyJsonCest 25 | { 26 | public function _before(AcceptanceTester $I) 27 | { 28 | $I->sendDELETE('/__phiremock/expectations'); 29 | } 30 | 31 | public function createExpectationWithBodyJsonArrayTest(AcceptanceTester $I) 32 | { 33 | $I->wantTo('create an expectation with a JSON body defined as array'); 34 | $I->haveHttpHeader('Content-Type', 'application/json'); 35 | $I->sendPOST('/__phiremock/expectations', 36 | $I->getPhiremockRequest([ 37 | 'request' => [ 38 | 'url' => ['isEqualTo' => '/the/request/url'], 39 | ], 40 | 'response' => [ 41 | 'body' => ['foo' => 'bar'], 42 | ], 43 | ]) 44 | ); 45 | $I->seeResponseCodeIs('201'); 46 | 47 | $I->sendGET('/__phiremock/expectations'); 48 | $I->seeResponseCodeIs('200'); 49 | $I->seeResponseIsJson(); 50 | $I->seeResponseEquals($I->getPhiremockResponse( 51 | '[{"scenarioName":null,"scenarioStateIs":null,"newScenarioState":null,' 52 | . '"request":{"method":null,"url":{"isEqualTo":"\/the\/request\/url"},"body":null,"headers":null,"formData":null,"jsonPath":null},' 53 | . '"response":{"statusCode":200,"body":"{\"foo\":\"bar\"}","headers":null,"delayMillis":null},' 54 | . '"proxyTo":null,"priority":0}]' 55 | )); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Utils/ResponseStrategyLocator.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils; 20 | 21 | use Exception; 22 | use Mcustiel\Phiremock\Domain\Condition\MatchersEnum; 23 | use Mcustiel\Phiremock\Domain\Expectation; 24 | use Mcustiel\Phiremock\Server\Factory\Factory; 25 | use Mcustiel\Phiremock\Server\Utils\Strategies\ResponseStrategyInterface; 26 | 27 | class ResponseStrategyLocator 28 | { 29 | /** @var Factory */ 30 | private $factory; 31 | 32 | public function __construct(Factory $dependencyService) 33 | { 34 | $this->factory = $dependencyService; 35 | } 36 | 37 | /** @throws Exception */ 38 | public function getStrategyForExpectation(Expectation $expectation): ResponseStrategyInterface 39 | { 40 | if ($expectation->getResponse()->isProxyResponse()) { 41 | if ($this->requestBodyOrUrlAreRegexp($expectation)) { 42 | return $this->factory->createRegexProxyResponseStrategy(); 43 | } 44 | 45 | return $this->factory->createProxyResponseStrategy(); 46 | } 47 | 48 | if ($this->requestBodyOrUrlAreRegexp($expectation)) { 49 | return $this->factory->createRegexResponseStrategy(); 50 | } 51 | 52 | return $this->factory->createHttpResponseStrategy(); 53 | } 54 | 55 | private function requestBodyOrUrlAreRegexp(Expectation $expectation): bool 56 | { 57 | return $expectation->getRequest()->getBody() 58 | && MatchersEnum::MATCHES === $expectation->getRequest()->getBody()->getMatcher()->getName() 59 | || $expectation->getRequest()->getUrl() 60 | && MatchersEnum::MATCHES === $expectation->getRequest()->getUrl()->getMatcher()->getName(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Utils/Strategies/ProxyResponseStrategy.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils\Strategies; 20 | 21 | use Laminas\Diactoros\Uri; 22 | use Mcustiel\Phiremock\Domain\Expectation; 23 | use Mcustiel\Phiremock\Domain\ProxyResponse; 24 | use Mcustiel\Phiremock\Server\Model\ScenarioStorage; 25 | use Psr\Http\Client\ClientInterface; 26 | use Psr\Http\Message\ResponseInterface; 27 | use Psr\Http\Message\ServerRequestInterface; 28 | use Psr\Log\LoggerInterface; 29 | 30 | class ProxyResponseStrategy extends AbstractResponse implements ResponseStrategyInterface 31 | { 32 | /** @var ClientInterface */ 33 | private $httpService; 34 | 35 | public function __construct( 36 | ScenarioStorage $scenarioStorage, 37 | LoggerInterface $logger, 38 | ClientInterface $httpService 39 | ) { 40 | parent::__construct($scenarioStorage, $logger); 41 | $this->httpService = $httpService; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | * 47 | * @see \Mcustiel\Phiremock\Server\Utils\Strategies\ResponseStrategyInterface::createResponse() 48 | */ 49 | public function createResponse( 50 | Expectation $expectation, 51 | ResponseInterface $transactionData, 52 | ServerRequestInterface $request 53 | ): ResponseInterface { 54 | /** @var ProxyResponse $response */ 55 | $response = $expectation->getResponse(); 56 | $url = $response->getUri()->asString(); 57 | $this->logger->debug('Proxying request to : ' . $url); 58 | $this->processScenario($expectation); 59 | $this->processDelay($response); 60 | 61 | return $this->httpService->sendRequest( 62 | $request->withUri(new Uri($url)) 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/acceptance/v1/ExpectationListCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V1; 21 | 22 | use AcceptanceTester; 23 | 24 | class ExpectationListCest 25 | { 26 | public function _before(AcceptanceTester $I) 27 | { 28 | $I->sendDELETE('/__phiremock/expectations'); 29 | } 30 | 31 | public function returnEmptyListTest(AcceptanceTester $I) 32 | { 33 | $I->sendGET('/__phiremock/expectations'); 34 | $I->seeResponseCodeIs('200'); 35 | $I->seeResponseEquals('[]'); 36 | } 37 | 38 | public function returnCreatedExpectationTest(AcceptanceTester $I) 39 | { 40 | $I->haveHttpHeader('Content-Type', 'application/json'); 41 | $I->sendPOST( 42 | '/__phiremock/expectations', 43 | $I->getPhiremockRequest([ 44 | 'request' => [ 45 | 'url' => ['isEqualTo' => '/the/request/url'], 46 | ], 47 | 'response' => [ 48 | 'statusCode' => 201, 49 | ], 50 | ]) 51 | ); 52 | 53 | $I->sendGET('/__phiremock/expectations'); 54 | $I->seeResponseCodeIs('200'); 55 | $I->seeResponseIsJson(); 56 | $I->seeResponseEquals($I->getPhiremockResponse( 57 | '[{"scenarioName":null,"scenarioStateIs":null,"newScenarioState":null,' 58 | . '"request":{"method":null,"url":{"isEqualTo":"\/the\/request\/url"},"body":null,"headers":null,"formData":null,"jsonPath":null},' 59 | . '"response":{"statusCode":201,"body":null,"headers":null,"delayMillis":null},' 60 | . '"proxyTo":null,"priority":0}]' 61 | )); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Utils/ArraysHelper.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils; 20 | 21 | class ArraysHelper 22 | { 23 | public static function isAssociative(array $array): bool 24 | { 25 | if (empty($array)) { 26 | return false; 27 | } 28 | 29 | return array_keys($array) !== range(0, \count($array) - 1); 30 | } 31 | 32 | public static function areRecursivelyEquals(array $array1, array $array2): bool 33 | { 34 | if (\count($array1) !== \count($array2)) { 35 | return false; 36 | } 37 | 38 | return self::arrayIsContained($array1, $array2); 39 | } 40 | 41 | public static function arrayIsContained(array $array1, array $array2): bool 42 | { 43 | foreach ($array1 as $key => $value1) { 44 | if (!\array_key_exists($key, $array2)) { 45 | return false; 46 | } 47 | if (!self::haveTheSameTypeAndValue($value1, $array2[$key])) { 48 | return false; 49 | } 50 | } 51 | 52 | return true; 53 | } 54 | 55 | public static function haveTheSameTypeAndValue($value1, $value2): bool 56 | { 57 | if (\gettype($value1) !== \gettype($value2)) { 58 | return false; 59 | } 60 | 61 | return self::haveTheSameValue($value1, $value2); 62 | } 63 | 64 | public static function haveTheSameValue($value1, $value2): bool 65 | { 66 | if (\is_array($value1)) { 67 | if (!self::areRecursivelyEquals($value1, $value2)) { 68 | return false; 69 | } 70 | } else { 71 | if ($value1 !== $value2) { 72 | return false; 73 | } 74 | } 75 | 76 | return true; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Utils/DataStructures/StringObjectArrayMap.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils\DataStructures; 20 | 21 | use ArrayIterator; 22 | use BadMethodCallException; 23 | use InvalidArgumentException; 24 | 25 | class StringObjectArrayMap implements Map 26 | { 27 | private $mapData; 28 | 29 | public function __construct() 30 | { 31 | $this->clean(); 32 | } 33 | 34 | public function getIterator() 35 | { 36 | return new ArrayIterator($this->mapData); 37 | } 38 | 39 | public function set($key, $value) 40 | { 41 | if (!\is_string($key)) { 42 | throw new InvalidArgumentException('Expected key to be string. Got: ' . \gettype($key)); 43 | } 44 | 45 | if (!\is_object($value)) { 46 | throw new InvalidArgumentException('Expected value to be object. Got: ' . \gettype($key)); 47 | } 48 | $this->mapData[$key] = $value; 49 | } 50 | 51 | public function get($key) 52 | { 53 | if (!$this->has($key)) { 54 | throw new BadMethodCallException('Calling get for an absent key: ' . $key); 55 | } 56 | 57 | return $this->mapData[$key]; 58 | } 59 | 60 | public function has($key) 61 | { 62 | if (!\is_string($key)) { 63 | throw new InvalidArgumentException('Expected key to be string. Got: ' . \gettype($key)); 64 | } 65 | 66 | return isset($this->mapData[$key]); 67 | } 68 | 69 | public function clean() 70 | { 71 | $this->mapData = []; 72 | } 73 | 74 | public function delete($key) 75 | { 76 | if (!$this->has($key)) { 77 | throw new BadMethodCallException('Calling delete for an absent key: ' . $key); 78 | } 79 | unset($this->mapData[$key]); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/codeception/extensions/ServerControl.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Codeception\Extensions; 21 | 22 | use Codeception\Event\SuiteEvent; 23 | use Codeception\Events; 24 | use Symfony\Component\Process\Process; 25 | 26 | class ServerControl extends \Codeception\Extension 27 | { 28 | private const EXPECTATIONS_DIR = __DIR__ . '/../../acceptance/_data/expectations'; 29 | 30 | public static $events = [ 31 | Events::SUITE_BEFORE => 'suiteBefore', 32 | Events::SUITE_AFTER => 'suiteAfter', 33 | ]; 34 | 35 | /** @var Process */ 36 | private $application; 37 | 38 | public function suiteBefore(SuiteEvent $event): void 39 | { 40 | $this->writeln('Starting Phiremock server'); 41 | 42 | $isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; 43 | 44 | $commandLine = [ 45 | $isWindows ? PHP_BINARY : 'exec', 46 | './bin/phiremock', 47 | '-d', 48 | '-e', 49 | self::EXPECTATIONS_DIR, 50 | '>', 51 | codecept_log_dir('phiremock.log'), 52 | '2>&1', 53 | ]; 54 | 55 | $this->application = Process::fromShellCommandline(implode(' ', $commandLine)); 56 | $this->writeln($this->application->getCommandLine()); 57 | $this->application->start(); 58 | sleep(1); 59 | } 60 | 61 | public function suiteAfter(): void 62 | { 63 | $this->writeln('Stopping Phiremock server'); 64 | if (!$this->application->isRunning()) { 65 | return; 66 | } 67 | 68 | $signal = defined('SIGTERM') ? \SIGTERM : 15; 69 | $this->application->stop(5, $signal); 70 | $this->writeln('Phiremock is stopped'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/acceptance/v1/FormDataCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V1; 21 | 22 | use AcceptanceTester; 23 | 24 | class FormDataCest 25 | { 26 | public function _before(AcceptanceTester $I) 27 | { 28 | $I->sendDELETE('/__phiremock/expectations'); 29 | } 30 | 31 | public function creationWithOneFormFieldUsingEqualToTest(AcceptanceTester $I) 32 | { 33 | $I->wantTo('create an expectation that checks one form field using isEqualTo'); 34 | $I->haveHttpHeader('Content-Type', 'application/json'); 35 | $I->sendPOST( 36 | '/__phiremock/expectations', 37 | $I->getPhiremockRequest([ 38 | 'request' => [ 39 | 'headers' => ['Content-Type' => ['isEqualTo' => 'application/x-www-form-urlencoded']], 40 | 'formData' => ['name' => ['isEqualTo' => 'potato']], 41 | ], 42 | 'response' => [ 43 | 'statusCode' => 418, 44 | ], 45 | ]) 46 | ); 47 | 48 | $I->sendGET('/__phiremock/expectations'); 49 | $I->seeResponseCodeIs('200'); 50 | $I->seeResponseIsJson(); 51 | $I->seeResponseEquals($I->getPhiremockResponse( 52 | '[{"scenarioName":null,"scenarioStateIs":null,"newScenarioState":null,' 53 | . '"request":{"method":null,"url":null,"body":null,"headers":{"Content-Type":{"isEqualTo":"application\/x-www-form-urlencoded"}},"formData":{"name":{"isEqualTo":"potato"}}},' 54 | . '"response":{"statusCode":418,"body":null,"headers":null,"delayMillis":null},' 55 | . '"proxyTo":null,"priority":0}]' 56 | )); 57 | 58 | $I->haveHttpHeader('Content-Type', 'application/x-www-form-urlencoded'); 59 | $I->sendPOST('/it/does/not/matter', ['name' => 'potato']); 60 | $I->seeResponseCodeIs(418); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Actions/ActionLocator.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Actions; 20 | 21 | use InvalidArgumentException; 22 | 23 | class ActionLocator 24 | { 25 | const LIST_EXPECTATIONS = 'listExpectations'; 26 | const ADD_EXPECTATION = 'addExpectation'; 27 | const CLEAR_EXPECTATIONS = 'clearExpectations'; 28 | const SET_SCENARIO_STATE = 'setScenarioState'; 29 | const CLEAR_SCENARIOS = 'clearScenarios'; 30 | const COUNT_REQUESTS = 'countRequests'; 31 | const LIST_REQUESTS = 'listRequests'; 32 | const RESET_REQUESTS_COUNT = 'resetRequestsCount'; 33 | const RESET = 'reset'; 34 | 35 | const MANAGE_REQUEST = 'manageRequest'; 36 | 37 | const ACTION_FACTORY_METHOD_MAP = [ 38 | self::LIST_EXPECTATIONS => 'createListExpectations', 39 | self::ADD_EXPECTATION => 'createAddExpectation', 40 | self::CLEAR_EXPECTATIONS => 'createClearExpectations', 41 | 42 | self::SET_SCENARIO_STATE => 'createSetScenarioState', 43 | self::CLEAR_SCENARIOS => 'createClearScenarios', 44 | 45 | self::COUNT_REQUESTS => 'createCountRequests', 46 | self::LIST_REQUESTS => 'createListRequests', 47 | self::RESET_REQUESTS_COUNT => 'createResetRequestsCount', 48 | 49 | self::RESET => 'createReset', 50 | 51 | self::MANAGE_REQUEST => 'createSearchRequest', 52 | ]; 53 | 54 | /** @var ActionsFactory */ 55 | private $factory; 56 | 57 | public function __construct(ActionsFactory $factory) 58 | { 59 | $this->factory = $factory; 60 | } 61 | 62 | public function locate(string $actionIdentifier): ActionInterface 63 | { 64 | if (\array_key_exists($actionIdentifier, self::ACTION_FACTORY_METHOD_MAP)) { 65 | return $this->factory->{self::ACTION_FACTORY_METHOD_MAP[$actionIdentifier]}(); 66 | } 67 | throw new InvalidArgumentException(sprintf('Trying to get action using %s. Which is not a valid action name.', var_export($actionIdentifier, true))); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Utils/Strategies/RegexProxyResponseStrategy.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils\Strategies; 20 | 21 | use Laminas\Diactoros\Uri; 22 | use Mcustiel\Phiremock\Domain\Expectation; 23 | use Mcustiel\Phiremock\Domain\ProxyResponse; 24 | use Mcustiel\Phiremock\Server\Model\ScenarioStorage; 25 | use Mcustiel\Phiremock\Server\Utils\Strategies\Utils\RegexReplacer; 26 | use Psr\Http\Client\ClientInterface; 27 | use Psr\Http\Message\ResponseInterface; 28 | use Psr\Http\Message\ServerRequestInterface; 29 | use Psr\Log\LoggerInterface; 30 | 31 | class RegexProxyResponseStrategy extends AbstractResponse implements ResponseStrategyInterface 32 | { 33 | /** @var ClientInterface */ 34 | private $httpService; 35 | /** @var RegexReplacer */ 36 | private $regexReplacer; 37 | 38 | public function __construct( 39 | ScenarioStorage $scenarioStorage, 40 | LoggerInterface $logger, 41 | ClientInterface $httpService, 42 | RegexReplacer $regexReplacer 43 | ) { 44 | parent::__construct($scenarioStorage, $logger); 45 | $this->httpService = $httpService; 46 | $this->regexReplacer = $regexReplacer; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | * 52 | * @see \Mcustiel\Phiremock\Server\Utils\Strategies\ResponseStrategyInterface::createResponse() 53 | */ 54 | public function createResponse( 55 | Expectation $expectation, 56 | ResponseInterface $transactionData, 57 | ServerRequestInterface $request 58 | ): ResponseInterface { 59 | /** @var ProxyResponse $response */ 60 | $response = $expectation->getResponse(); 61 | $url = $response->getUri()->asString(); 62 | $url = $this->regexReplacer->fillWithUrlMatches($expectation, $request, $url); 63 | $url = $this->regexReplacer->fillWithBodyMatches($expectation, $request, $url); 64 | $this->logger->debug('Proxying request to : ' . $url); 65 | $this->processScenario($expectation); 66 | $this->processDelay($response); 67 | 68 | return $this->httpService->sendRequest( 69 | $request->withUri(new Uri($url)) 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Utils/RequestToExpectationMapper.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils; 20 | 21 | use Exception; 22 | use Mcustiel\Phiremock\Common\Utils\ArrayToExpectationConverterLocator; 23 | use Mcustiel\Phiremock\Domain\Expectation; 24 | use Psr\Http\Message\ServerRequestInterface; 25 | use Psr\Log\LoggerInterface; 26 | 27 | class RequestToExpectationMapper 28 | { 29 | const CONTENT_ENCODING_HEADER = 'Content-Encoding'; 30 | 31 | /** @var ArrayToExpectationConverterLocator */ 32 | private $converterLocator; 33 | 34 | /** @var LoggerInterface */ 35 | private $logger; 36 | 37 | public function __construct( 38 | ArrayToExpectationConverterLocator $converterLocator, 39 | LoggerInterface $logger 40 | ) { 41 | $this->converterLocator = $converterLocator; 42 | $this->logger = $logger; 43 | } 44 | 45 | /** @throws Exception */ 46 | public function map(ServerRequestInterface $request): Expectation 47 | { 48 | $parsedJson = $this->parseJsonBody($request); 49 | $object = $this->converterLocator->locate($parsedJson)->convert($parsedJson); 50 | $this->logger->debug('Parsed expectation: ' . var_export($object, true)); 51 | 52 | return $object; 53 | } 54 | 55 | /** @throws Exception */ 56 | private function parseJsonBody(ServerRequestInterface $request): array 57 | { 58 | $this->logger->debug('Adding Expectation->parseJsonBody'); 59 | $body = $request->getBody()->__toString(); 60 | $this->logger->debug($body); 61 | if ($this->hasBinaryBody($request)) { 62 | $body = base64_decode($body, true); 63 | } 64 | 65 | $bodyJson = @json_decode($body, true); 66 | if (\JSON_ERROR_NONE !== json_last_error()) { 67 | throw new Exception(json_last_error_msg()); 68 | } 69 | $this->logger->debug('BODY JSON: ' . var_export($bodyJson, true)); 70 | 71 | return $bodyJson; 72 | } 73 | 74 | private function hasBinaryBody(ServerRequestInterface $request): bool 75 | { 76 | return $request->hasHeader(self::CONTENT_ENCODING_HEADER) 77 | && 'base64' === $request->getHeader(self::CONTENT_ENCODING_HEADER); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Utils/Traits/ExpectationValidator.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils\Traits; 20 | 21 | use Mcustiel\Phiremock\Domain\Expectation; 22 | use Mcustiel\Phiremock\Domain\HttpResponse; 23 | use Psr\Log\LoggerInterface; 24 | use RuntimeException; 25 | 26 | trait ExpectationValidator 27 | { 28 | protected function validateExpectationOrThrowException(Expectation $expectation, LoggerInterface $logger): void 29 | { 30 | $this->validateResponseOrThrowException($expectation, $logger); 31 | $this->validateScenarioNameOrThrowException($expectation, $logger); 32 | $this->validateScenarioStateOrThrowException($expectation, $logger); 33 | } 34 | 35 | protected function validateResponseOrThrowException(Expectation $expectation, LoggerInterface $logger): void 36 | { 37 | $this->logger->debug('Validating response'); 38 | if ($this->responseIsInvalid($expectation)) { 39 | $logger->error('Invalid response specified in expectation'); 40 | throw new RuntimeException('Invalid response specified in expectation'); 41 | } 42 | } 43 | 44 | protected function responseIsInvalid(Expectation $expectation): bool 45 | { 46 | /** @var HttpResponse $response */ 47 | $response = $expectation->getResponse(); 48 | 49 | return $response->isHttpResponse() && empty($response->getStatusCode()); 50 | } 51 | 52 | protected function validateScenarioStateOrThrowException( 53 | Expectation $expectation, 54 | LoggerInterface $logger 55 | ): void { 56 | if ($expectation->getResponse()->hasNewScenarioState() && !$expectation->getRequest()->hasScenarioState()) { 57 | $logger->error('Scenario states misconfiguration'); 58 | throw new RuntimeException('Trying to set scenario state without specifying scenario previous state'); 59 | } 60 | } 61 | 62 | protected function validateScenarioNameOrThrowException( 63 | Expectation $expectation, 64 | LoggerInterface $logger 65 | ): void { 66 | if (!$expectation->hasScenarioName() 67 | && ($expectation->getRequest()->hasScenarioState() || $expectation->getResponse()->hasNewScenarioState()) 68 | ) { 69 | $logger->error('Scenario name related misconfiguration'); 70 | throw new RuntimeException('Expecting or trying to set scenario state without specifying scenario name'); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/acceptance/v1/RequestCountCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V1; 21 | 22 | use AcceptanceTester; 23 | 24 | class RequestCountCest 25 | { 26 | public function _before(AcceptanceTester $I) 27 | { 28 | $I->sendDELETE('/__phiremock/expectations'); 29 | $I->sendDELETE('/__phiremock/executions'); 30 | } 31 | 32 | public function returnEmptyList(AcceptanceTester $I) 33 | { 34 | $I->sendPOST('/__phiremock/executions'); 35 | $I->seeResponseCodeIs('200'); 36 | $I->seeResponseEquals('{"count":0}'); 37 | } 38 | 39 | public function returnAllExecutedRequest(AcceptanceTester $I) 40 | { 41 | $I->haveHttpHeader('Content-Type', 'application/json'); 42 | $I->sendPOST( 43 | '/__phiremock/expectations', 44 | $I->getPhiremockRequest([ 45 | 'request' => [ 46 | 'url' => ['isEqualTo' => '/the/request/url'], 47 | ], 48 | 'response' => [ 49 | 'statusCode' => 201, 50 | ], 51 | ]) 52 | ); 53 | 54 | $I->sendGET('/the/request/url'); 55 | $I->seeResponseCodeIs('201'); 56 | 57 | $I->sendPOST('/__phiremock/executions', ''); 58 | $I->seeResponseCodeIs('200'); 59 | $I->seeResponseEquals('{"count":1}'); 60 | } 61 | 62 | public function returnExecutedRequestMatchingExpectation(AcceptanceTester $I) 63 | { 64 | $I->haveHttpHeader('Content-Type', 'application/json'); 65 | $I->sendPOST( 66 | '/__phiremock/expectations', 67 | $I->getPhiremockRequest([ 68 | 'request' => [ 69 | 'url' => ['isEqualTo' => '/the/request/url'], 70 | ], 71 | 'response' => [ 72 | 'statusCode' => 201, 73 | ], 74 | ]) 75 | ); 76 | 77 | $I->sendGET('/the/request/url'); 78 | $I->seeResponseCodeIs('201'); 79 | 80 | $I->sendPOST('/__phiremock/executions', $I->getPhiremockRequest([ 81 | 'request' => [ 82 | 'url' => ['isEqualTo' => '/the/request/url'], 83 | ], 84 | 'response' => [ 85 | 'statusCode' => 201, 86 | ], 87 | ])); 88 | $I->seeResponseCodeIs('200'); 89 | $I->seeResponseEquals('{"count":1}'); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/acceptance/v1/BinaryContentCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V1; 21 | 22 | use AcceptanceTester; 23 | use Codeception\Configuration; 24 | 25 | class BinaryContentCest 26 | { 27 | public function _before(AcceptanceTester $I) 28 | { 29 | $I->sendDELETE('/__phiremock/expectations'); 30 | } 31 | 32 | public function shouldCreateAnExpectationWithBinaryResponse(AcceptanceTester $I) 33 | { 34 | $responseContents = file_get_contents(Configuration::dataDir() . 'fixtures/silhouette-1444982_640.png'); 35 | 36 | $I->haveHttpHeader('Content-Type', 'application/json'); 37 | $I->sendPOST( 38 | '/__phiremock/expectations', 39 | $I->getPhiremockRequest([ 40 | 'request' => [ 41 | 'url' => ['isEqualTo' => '/show-me-the-image-now'], 42 | ], 43 | 'response' => [ 44 | 'headers' => [ 45 | 'Content-Type' => 'image/jpeg', 46 | ], 47 | 'body' => 'phiremock.base64:' . base64_encode($responseContents), 48 | ], 49 | ]) 50 | ); 51 | 52 | $I->sendGET('/show-me-the-image-now'); 53 | $I->seeResponseCodeIs(200); 54 | $I->seeHttpHeader('Content-Type', 'image/jpeg'); 55 | $responseBody = $I->grabResponse(); 56 | $I->assertEquals($responseContents, $responseBody); 57 | } 58 | 59 | public function shouldCreateAnExpectationWithBinaryResponseAndRegexUrl(AcceptanceTester $I) 60 | { 61 | $responseContents = file_get_contents(Configuration::dataDir() . 'fixtures/silhouette-1444982_640.png'); 62 | 63 | $I->haveHttpHeader('Content-Type', 'application/json'); 64 | $I->sendPOST( 65 | '/__phiremock/expectations', 66 | $I->getPhiremockRequest([ 67 | 'request' => [ 68 | 'url' => ['matches' => '/\/show-me-the-image-\d+/'], 69 | ], 70 | 'response' => [ 71 | 'headers' => [ 72 | 'Content-Type' => 'image/jpeg', 73 | ], 74 | 'body' => 'phiremock.base64:' . base64_encode($responseContents), 75 | ], 76 | ]) 77 | ); 78 | 79 | $I->sendGET('/show-me-the-image-123'); 80 | $I->seeResponseCodeIs(200); 81 | $I->seeHttpHeader('Content-Type', 'image/jpeg'); 82 | $responseBody = $I->grabResponse(); 83 | $I->assertEquals($responseContents, $responseBody); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/acceptance/v1/BodySpecificationCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V1; 21 | 22 | use AcceptanceTester; 23 | 24 | class BodySpecificationCest 25 | { 26 | public function _before(AcceptanceTester $I) 27 | { 28 | $I->sendDELETE('/__phiremock/expectations'); 29 | } 30 | 31 | public function createExpectationWithBodyResponseTest(AcceptanceTester $I) 32 | { 33 | $I->wantTo('create an expectation with a valid body'); 34 | $I->haveHttpHeader('Content-Type', 'application/json'); 35 | $I->sendPOST( 36 | '/__phiremock/expectations', 37 | $I->getPhiremockRequest([ 38 | 'request' => [ 39 | 'url' => ['isEqualTo' => '/the/request/url'], 40 | ], 41 | 'response' => [ 42 | 'body' => 'This is the body', 43 | ], 44 | ]) 45 | ); 46 | 47 | $I->sendGET('/__phiremock/expectations'); 48 | $I->seeResponseCodeIs('200'); 49 | $I->seeResponseIsJson(); 50 | $I->seeResponseEquals($I->getPhiremockResponse( 51 | '[{"scenarioName":null,"scenarioStateIs":null,"newScenarioState":null,' 52 | . '"request":{"method":null,"url":{"isEqualTo":"\/the\/request\/url"},"body":null,"headers":null,"formData":null,"jsonPath":null},' 53 | . '"response":{"statusCode":200,"body":"This is the body","headers":null,"delayMillis":null},' 54 | . '"proxyTo":null,"priority":0}]' 55 | )); 56 | } 57 | 58 | public function createWithEmptyBodyTest(AcceptanceTester $I) 59 | { 60 | $I->wantTo('create an expectation with an empty body'); 61 | $I->haveHttpHeader('Content-Type', 'application/json'); 62 | $I->sendPOST('/__phiremock/expectations', 63 | $I->getPhiremockRequest([ 64 | 'request' => [ 65 | 'url' => ['isEqualTo' => '/the/request/url'], 66 | ], 67 | 'response' => [ 68 | 'body' => null, 69 | ], 70 | ]) 71 | ); 72 | 73 | $I->sendGET('/__phiremock/expectations'); 74 | $I->seeResponseCodeIs('200'); 75 | $I->seeResponseIsJson(); 76 | $I->seeResponseEquals($I->getPhiremockResponse( 77 | '[{"scenarioName":null,"scenarioStateIs":null,"newScenarioState":null,' 78 | . '"request":{"method":null,"url":{"isEqualTo":"\/the\/request\/url"},"body":null,"headers":null,"formData":null,"jsonPath":null},' 79 | . '"response":{"statusCode":200,"body":null,"headers":null,"delayMillis":null},' 80 | . '"proxyTo":null,"priority":0}]' 81 | )); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Actions/CountRequestsAction.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Actions; 20 | 21 | use Mcustiel\Phiremock\Common\StringStream; 22 | use Mcustiel\Phiremock\Domain\Expectation; 23 | use Mcustiel\Phiremock\Server\Model\RequestStorage; 24 | use Mcustiel\Phiremock\Server\Utils\RequestExpectationComparator; 25 | use Mcustiel\Phiremock\Server\Utils\RequestToExpectationMapper; 26 | use Mcustiel\Phiremock\Server\Utils\Traits\ExpectationValidator; 27 | use Psr\Http\Message\ResponseInterface; 28 | use Psr\Http\Message\ServerRequestInterface; 29 | use Psr\Log\LoggerInterface; 30 | 31 | class CountRequestsAction implements ActionInterface 32 | { 33 | use ExpectationValidator; 34 | 35 | /** @var RequestStorage */ 36 | private $requestsStorage; 37 | /** @var RequestExpectationComparator */ 38 | private $comparator; 39 | /** @var RequestToExpectationMapper */ 40 | private $converter; 41 | /** @var LoggerInterface */ 42 | private $logger; 43 | 44 | public function __construct( 45 | RequestToExpectationMapper $converter, 46 | RequestStorage $storage, 47 | RequestExpectationComparator $comparator, 48 | LoggerInterface $logger 49 | ) { 50 | $this->requestsStorage = $storage; 51 | $this->comparator = $comparator; 52 | $this->converter = $converter; 53 | $this->logger = $logger; 54 | } 55 | 56 | public function execute(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 57 | { 58 | if ($request->getBody()->__toString() === '') { 59 | $this->logger->info('Received empty body. Creating default'); 60 | $request = $request->withBody(new StringStream('{"request": {"url": {"matches": "/.+/"}}, "response": {"statusCode": 200}}')); 61 | } 62 | $this->logger->debug('Adding Expectation->createObjectFromRequestAndProcess'); 63 | $object = $this->converter->map($request); 64 | 65 | return $this->process($response, $object); 66 | } 67 | 68 | private function process(ResponseInterface $response, Expectation $expectation): ResponseInterface 69 | { 70 | $count = $this->searchForExecutionsCount($expectation); 71 | $this->logger->debug('Found ' . $count . ' request matching the expectation'); 72 | 73 | return $response 74 | ->withStatus(200) 75 | ->withHeader('Content-Type', 'application/json') 76 | ->withBody(new StringStream(json_encode(['count' => $count]))); 77 | } 78 | 79 | private function searchForExecutionsCount(Expectation $expectation): int 80 | { 81 | $count = 0; 82 | foreach ($this->requestsStorage->listRequests() as $request) { 83 | if ($this->comparator->equals($request, $expectation)) { 84 | ++$count; 85 | } 86 | } 87 | 88 | return $count; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Actions/AddExpectationAction.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Actions; 20 | 21 | use Exception; 22 | use Mcustiel\Phiremock\Common\StringStream; 23 | use Mcustiel\Phiremock\Domain\Expectation; 24 | use Mcustiel\Phiremock\Server\Model\ExpectationStorage; 25 | use Mcustiel\Phiremock\Server\Utils\RequestToExpectationMapper; 26 | use Mcustiel\Phiremock\Server\Utils\Traits\ExpectationValidator; 27 | use Psr\Http\Message\ResponseInterface; 28 | use Psr\Http\Message\ServerRequestInterface; 29 | use Psr\Log\LoggerInterface; 30 | 31 | class AddExpectationAction implements ActionInterface 32 | { 33 | use ExpectationValidator; 34 | 35 | /** @var ExpectationStorage */ 36 | private $storage; 37 | /** @var RequestToExpectationMapper */ 38 | private $converter; 39 | /** @var LoggerInterface */ 40 | private $logger; 41 | 42 | public function __construct( 43 | RequestToExpectationMapper $converter, 44 | ExpectationStorage $storage, 45 | LoggerInterface $logger 46 | ) { 47 | $this->converter = $converter; 48 | $this->logger = $logger; 49 | $this->storage = $storage; 50 | } 51 | 52 | public function execute(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 53 | { 54 | try { 55 | $object = $this->converter->map($request); 56 | 57 | return $this->process($response, $object); 58 | } catch (Exception $e) { 59 | $this->logger->error('An unexpected exception occurred: ' . $e->getMessage()); 60 | $this->logger->debug($e->__toString()); 61 | 62 | return $this->constructErrorResponse([$e->getMessage()], $response); 63 | } 64 | } 65 | 66 | private function process(ResponseInterface $response, Expectation $expectation): ResponseInterface 67 | { 68 | $this->logger->debug('process'); 69 | $this->validateExpectationOrThrowException($expectation, $this->logger); 70 | $this->storage->addExpectation($expectation); 71 | 72 | return $this->constructResponse([], $response); 73 | } 74 | 75 | private function constructResponse(array $listOfErrors, ResponseInterface $response): ResponseInterface 76 | { 77 | if (empty($listOfErrors)) { 78 | return $response->withStatus(201)->withBody(new StringStream('{"result" : "OK"}')); 79 | } 80 | 81 | return $this->constructErrorResponse($listOfErrors, $response); 82 | } 83 | 84 | private function constructErrorResponse(array $listOfErrors, ResponseInterface $response): ResponseInterface 85 | { 86 | return $response->withStatus(500) 87 | ->withBody( 88 | new StringStream( 89 | '{"result" : "ERROR", "details" : ' 90 | . json_encode($listOfErrors) 91 | . '}' 92 | ) 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/acceptance/v1/SetScenarioStateCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V1; 21 | 22 | use AcceptanceTester; 23 | 24 | class SetScenarioStateCest 25 | { 26 | public function _before(AcceptanceTester $I) 27 | { 28 | $I->sendDELETE('/__phiremock/expectations'); 29 | } 30 | 31 | public function setScenarioState(AcceptanceTester $I) 32 | { 33 | $I->haveHttpHeader('Content-Type', 'application/json'); 34 | $I->sendPOST( 35 | '/__phiremock/expectations', 36 | $I->getPhiremockRequest([ 37 | 'request' => [ 38 | 'method' => 'get', 39 | 'url' => ['isEqualTo' => '/test'], 40 | ], 41 | 'response' => [ 42 | 'body' => 'start', 43 | ], 44 | 'scenarioName' => 'test-scenario', 45 | 'scenarioStateIs' => 'Scenario.START', 46 | ]) 47 | ); 48 | 49 | $I->haveHttpHeader('Content-Type', 'application/json'); 50 | $I->sendPOST( 51 | '/__phiremock/expectations', 52 | $I->getPhiremockRequest([ 53 | 'request' => [ 54 | 'method' => 'get', 55 | 'url' => ['isEqualTo' => '/test'], 56 | ], 57 | 'response' => [ 58 | 'body' => 'potato', 59 | ], 60 | 'scenarioName' => 'test-scenario', 61 | 'scenarioStateIs' => 'Scenario.POTATO', 62 | ]) 63 | ); 64 | 65 | $I->sendGET('/test'); 66 | $I->seeResponseCodeIs('200'); 67 | $I->seeResponseEquals('start'); 68 | 69 | $I->sendPUT( 70 | '/__phiremock/scenarios', 71 | [ 72 | 'scenarioName' => 'test-scenario', 73 | 'scenarioState' => 'Scenario.POTATO', 74 | ] 75 | ); 76 | $I->sendGET('/test'); 77 | $I->seeResponseCodeIs('200'); 78 | $I->seeResponseEquals('potato'); 79 | 80 | $I->sendPUT( 81 | '/__phiremock/scenarios', 82 | [ 83 | 'scenarioName' => 'test-scenario', 84 | 'scenarioState' => 'Scenario.START', 85 | ] 86 | ); 87 | $I->sendGET('/test'); 88 | $I->seeResponseCodeIs('200'); 89 | $I->seeResponseEquals('start'); 90 | } 91 | 92 | public function checkScenarioStateValidation(AcceptanceTester $I) 93 | { 94 | $I->haveHttpHeader('Content-Type', 'application/json'); 95 | $I->sendPUT('/__phiremock/scenarios', []); 96 | $I->seeResponseCodeIs(500); 97 | $I->seeResponseEquals('{"result":"ERROR","details":"Scenario name not set"}'); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Utils/FileExpectationsLoader.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils; 20 | 21 | use Exception; 22 | use Mcustiel\Phiremock\Common\Utils\ArrayToExpectationConverterLocator; 23 | use Mcustiel\Phiremock\Server\Model\ExpectationStorage; 24 | use Mcustiel\Phiremock\Server\Utils\Traits\ExpectationValidator; 25 | use Psr\Log\LoggerInterface; 26 | 27 | class FileExpectationsLoader 28 | { 29 | use ExpectationValidator; 30 | 31 | /** @var ArrayToExpectationConverterLocator */ 32 | private $converterLocator; 33 | /** @var ExpectationStorage */ 34 | private $storage; 35 | /** @var ExpectationStorage */ 36 | private $backup; 37 | /** @var \Psr\Log\LoggerInterface */ 38 | private $logger; 39 | 40 | public function __construct( 41 | ArrayToExpectationConverterLocator $converterLocator, 42 | ExpectationStorage $storage, 43 | ExpectationStorage $backup, 44 | LoggerInterface $logger 45 | ) { 46 | $this->converterLocator = $converterLocator; 47 | $this->storage = $storage; 48 | $this->backup = $backup; 49 | $this->logger = $logger; 50 | } 51 | 52 | /** @throws Exception */ 53 | public function loadExpectationFromFile(string $fileName): void 54 | { 55 | $this->logger->debug("Loading expectation file $fileName"); 56 | $content = file_get_contents($fileName); 57 | $data = @json_decode($content, true); 58 | if (\JSON_ERROR_NONE !== json_last_error()) { 59 | throw new Exception(json_last_error_msg()); 60 | } 61 | $expectation = $this->converterLocator->locate($data)->convert($data); 62 | $this->validateExpectationOrThrowException($expectation, $this->logger); 63 | 64 | $this->logger->debug('Parsed expectation: ' . var_export($expectation, true)); 65 | $this->storage->addExpectation($expectation); 66 | // As we have no API to modify expectation, parsed the same object could be used for backup. 67 | // On futher changes when $expectation modifications are possible something like deep-copy 68 | // should be used to clone expectation. 69 | $this->backup->addExpectation($expectation); 70 | } 71 | 72 | /** @throws Exception */ 73 | public function loadExpectationsFromDirectory(string $directory): void 74 | { 75 | $this->logger->info("Loading expectations from directory $directory"); 76 | $iterator = new \RecursiveDirectoryIterator( 77 | $directory, 78 | \RecursiveDirectoryIterator::FOLLOW_SYMLINKS 79 | ); 80 | 81 | $iterator = new \RecursiveIteratorIterator($iterator); 82 | foreach ($iterator as $fileInfo) { 83 | if ($fileInfo->isFile()) { 84 | $filePath = $fileInfo->getRealPath(); 85 | if (preg_match('/\.json$/i', $filePath)) { 86 | $this->loadExpectationFromFile($filePath); 87 | } 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/acceptance/v1/ProxyCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V1; 21 | 22 | use AcceptanceTester; 23 | use GuzzleHttp\Client as HttpClient; 24 | 25 | class ProxyCest 26 | { 27 | public function _before(AcceptanceTester $I) 28 | { 29 | $I->sendDELETE('/__phiremock/expectations'); 30 | } 31 | 32 | public function createAnExpectationWithProxyToTest(AcceptanceTester $I) 33 | { 34 | $I->wantTo('create a specification that checks url using matches'); 35 | $I->haveHttpHeader('Content-Type', 'application/json'); 36 | 37 | $I->sendPOST( 38 | '/__phiremock/expectations', 39 | $I->getPhiremockRequest([ 40 | 'scenarioName' => 'PotatoScenario', 41 | 'scenarioStateIs' => 'Scenario.START', 42 | 'request' => [ 43 | 'method' => 'post', 44 | 'url' => ['isEqualTo' => '/potato'], 45 | 'body' => ['isEqualTo' => '{"key": "This is the body"}'], 46 | 'headers' => ['X-Potato' => ['isSameString' => 'bAnaNa']], 47 | ], 48 | 'proxyTo' => 'https://www.w3schools.com/html/', 49 | ]) 50 | ); 51 | 52 | $I->sendGET('/__phiremock/expectations'); 53 | $I->seeResponseCodeIs('200'); 54 | $I->seeResponseIsJson(); 55 | $I->seeResponseEquals($I->getPhiremockResponse( 56 | '[{"scenarioName":"PotatoScenario","scenarioStateIs":"Scenario.START",' 57 | . '"newScenarioState":null,"request":{"method":"post","url":{"isEqualTo":"\/potato"},' 58 | . '"body":{"isEqualTo":"{\"key\": \"This is the body\"}"},"headers":{"X-Potato":' 59 | . '{"isSameString":"bAnaNa"}},"formData":null,"jsonPath":null},"response":null,' 60 | . '"proxyTo":"https:\/\/www.w3schools.com\/html\/","priority":0}]' 61 | )); 62 | } 63 | 64 | public function proxyToGivenUriTest(AcceptanceTester $I) 65 | { 66 | $realUrl = 'http://info.cern.ch/'; 67 | 68 | $I->haveHttpHeader('Content-Type', 'application/json'); 69 | 70 | $I->sendPOST( 71 | '/__phiremock/expectations', 72 | $I->getPhiremockRequest([ 73 | 'scenarioName' => 'PotatoScenario', 74 | 'scenarioStateIs' => 'Scenario.START', 75 | 'request' => [ 76 | 'url' => ['isEqualTo' => '/potato'], 77 | 'headers' => ['X-Potato' => ['isSameString' => 'bAnaNa']], 78 | ], 79 | 'proxyTo' => $realUrl, 80 | ]) 81 | ); 82 | 83 | $guzzle = new HttpClient(); 84 | $originalBody = $guzzle->get($realUrl)->getBody()->__toString(); 85 | 86 | $I->haveHttpHeader('X-Potato', 'banana'); 87 | $I->sendGet('/potato'); 88 | $I->seeResponseEquals($originalBody); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Utils/Strategies/AbstractResponse.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils\Strategies; 20 | 21 | use Mcustiel\Phiremock\Domain\Expectation; 22 | use Mcustiel\Phiremock\Domain\HttpResponse; 23 | use Mcustiel\Phiremock\Domain\Response; 24 | use Mcustiel\Phiremock\Domain\ScenarioStateInfo; 25 | use Mcustiel\Phiremock\Server\Model\ScenarioStorage; 26 | use Psr\Http\Message\ResponseInterface; 27 | use Psr\Log\LoggerInterface; 28 | use RuntimeException; 29 | 30 | class AbstractResponse 31 | { 32 | /** @var LoggerInterface */ 33 | protected $logger; 34 | /** @var ScenarioStorage */ 35 | private $scenariosStorage; 36 | 37 | public function __construct(ScenarioStorage $scenarioStorage, LoggerInterface $logger) 38 | { 39 | $this->scenariosStorage = $scenarioStorage; 40 | $this->logger = $logger; 41 | } 42 | 43 | protected function processDelay(Response $responseConfig): void 44 | { 45 | if ($responseConfig->getDelayMillis()) { 46 | $this->logger->debug( 47 | 'Delaying the response for ' . $responseConfig->getDelayMillis()->asInt() . ' milliseconds' 48 | ); 49 | usleep($responseConfig->getDelayMillis()->asInt() * 1000); 50 | } 51 | } 52 | 53 | protected function processScenario(Expectation $foundExpectation): void 54 | { 55 | if ($foundExpectation->getResponse()->hasNewScenarioState()) { 56 | if (!$foundExpectation->hasScenarioName()) { 57 | throw new RuntimeException('Expecting scenario state without specifying scenario name'); 58 | } 59 | $this->logger->debug( 60 | sprintf( 61 | 'Setting scenario %s to %s', 62 | $foundExpectation->getScenarioName()->asString(), 63 | $foundExpectation->getResponse()->getNewScenarioState()->asString() 64 | ) 65 | ); 66 | $this->scenariosStorage->setScenarioState( 67 | new ScenarioStateInfo( 68 | $foundExpectation->getScenarioName(), 69 | $foundExpectation->getResponse()->getNewScenarioState() 70 | ) 71 | ); 72 | } 73 | } 74 | 75 | protected function getResponseWithHeaders(HttpResponse $responseConfig, ResponseInterface $httpResponse): ResponseInterface 76 | { 77 | if ($responseConfig->getHeaders()) { 78 | foreach ($responseConfig->getHeaders() as $header) { 79 | $httpResponse = $httpResponse->withHeader( 80 | $header->getName()->asString(), 81 | $header->getValue()->asString() 82 | ); 83 | } 84 | } 85 | 86 | return $httpResponse; 87 | } 88 | 89 | protected function getResponseWithStatusCode(HttpResponse $responseConfig, ResponseInterface $httpResponse): ResponseInterface 90 | { 91 | if ($responseConfig->getStatusCode()) { 92 | $httpResponse = $httpResponse->withStatus($responseConfig->getStatusCode()->asInt()); 93 | } 94 | 95 | return $httpResponse; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Actions/ListRequestsAction.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Actions; 20 | 21 | use Mcustiel\Phiremock\Common\StringStream; 22 | use Mcustiel\Phiremock\Domain\Expectation; 23 | use Mcustiel\Phiremock\Server\Model\RequestStorage; 24 | use Mcustiel\Phiremock\Server\Utils\RequestExpectationComparator; 25 | use Mcustiel\Phiremock\Server\Utils\RequestToExpectationMapper; 26 | use Mcustiel\Phiremock\Server\Utils\Traits\ExpectationValidator; 27 | use Psr\Http\Message\ResponseInterface; 28 | use Psr\Http\Message\ServerRequestInterface; 29 | use Psr\Log\LoggerInterface; 30 | 31 | class ListRequestsAction implements ActionInterface 32 | { 33 | use ExpectationValidator; 34 | 35 | /** @var RequestStorage */ 36 | private $requestsStorage; 37 | /** @var RequestExpectationComparator */ 38 | private $comparator; 39 | /** @var RequestToExpectationMapper */ 40 | private $converter; 41 | /** @var LoggerInterface */ 42 | private $logger; 43 | 44 | public function __construct( 45 | RequestToExpectationMapper $converter, 46 | RequestStorage $storage, 47 | RequestExpectationComparator $comparator, 48 | LoggerInterface $logger 49 | ) { 50 | $this->requestsStorage = $storage; 51 | $this->comparator = $comparator; 52 | $this->converter = $converter; 53 | $this->logger = $logger; 54 | } 55 | 56 | public function execute(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 57 | { 58 | if ($request->getBody()->__toString() === '') { 59 | $this->logger->info('Received empty body. Creating default'); 60 | $request = $request->withBody(new StringStream('{"request": {"url": {"matches": "/.+/"}}, "response": {"statusCode": 200}}')); 61 | } 62 | $object = $this->converter->map($request); 63 | 64 | return $this->process($response, $object); 65 | } 66 | 67 | public function process(ResponseInterface $response, Expectation $expectation): ResponseInterface 68 | { 69 | $executions = $this->searchForExecutionsCount($expectation); 70 | $this->logger->debug('Listed ' . \count($executions) . ' request matching the expectation'); 71 | 72 | return $response 73 | ->withStatus(200) 74 | ->withHeader('Content-Type', 'application/json') 75 | ->withBody(new StringStream(json_encode($executions))); 76 | } 77 | 78 | /** @return array[] */ 79 | private function searchForExecutionsCount(Expectation $expectation): array 80 | { 81 | $executions = []; 82 | foreach ($this->requestsStorage->listRequests() as $request) { 83 | if ($this->comparator->equals($request, $expectation)) { 84 | $executions[] = [ 85 | 'method' => $request->getMethod(), 86 | 'url' => (string) $request->getUri(), 87 | 'headers' => $request->getHeaders(), 88 | 'cookies' => $request->getCookieParams(), 89 | 'body' => (string) $request->getBody(), 90 | ]; 91 | } 92 | } 93 | 94 | return $executions; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Utils/Config/Config.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Utils\Config; 21 | 22 | use Exception; 23 | use Mcustiel\Phiremock\Server\Cli\Options\CertificateKeyPath; 24 | use Mcustiel\Phiremock\Server\Cli\Options\CertificatePath; 25 | use Mcustiel\Phiremock\Server\Cli\Options\ExpectationsDirectory; 26 | use Mcustiel\Phiremock\Server\Cli\Options\HostInterface; 27 | use Mcustiel\Phiremock\Server\Cli\Options\Passphrase; 28 | use Mcustiel\Phiremock\Server\Cli\Options\PhpFactoryFqcn; 29 | use Mcustiel\Phiremock\Server\Cli\Options\Port; 30 | use Mcustiel\Phiremock\Server\Cli\Options\SecureOptions; 31 | 32 | class Config 33 | { 34 | public const IP = 'ip'; 35 | public const PORT = 'port'; 36 | public const DEBUG = 'debug'; 37 | public const EXPECTATIONS_DIR = 'expectations-dir'; 38 | public const FACTORY_CLASS = 'factory-class'; 39 | public const CERTIFICATE = 'certificate'; 40 | public const CERTIFICATE_KEY = 'certificate-key'; 41 | public const CERT_PASSPHRASE = 'cert-passphrase'; 42 | 43 | public const CONFIG_OPTIONS = [ 44 | self::IP, 45 | self::PORT, 46 | self::DEBUG, 47 | self::EXPECTATIONS_DIR, 48 | self::FACTORY_CLASS, 49 | self::CERTIFICATE, 50 | self::CERTIFICATE_KEY, 51 | self::CERT_PASSPHRASE, 52 | ]; 53 | 54 | /** @var array */ 55 | private $configurationArray; 56 | 57 | public function __construct(array $configurationArray) 58 | { 59 | $this->configurationArray = $configurationArray; 60 | } 61 | 62 | public function getInterfaceIp(): HostInterface 63 | { 64 | return new HostInterface($this->configurationArray[self::IP]); 65 | } 66 | 67 | public function getPort(): Port 68 | { 69 | return new Port((int) $this->configurationArray[self::PORT]); 70 | } 71 | 72 | public function isDebugMode(): bool 73 | { 74 | return $this->configurationArray[self::DEBUG]; 75 | } 76 | 77 | public function getExpectationsPath(): ExpectationsDirectory 78 | { 79 | return new ExpectationsDirectory($this->configurationArray[self::EXPECTATIONS_DIR]); 80 | } 81 | 82 | public function getFactoryClassName(): PhpFactoryFqcn 83 | { 84 | return new PhpFactoryFqcn($this->configurationArray[self::FACTORY_CLASS]); 85 | } 86 | 87 | public function isSecure(): bool 88 | { 89 | return isset($this->configurationArray[self::CERTIFICATE]) 90 | && isset($this->configurationArray[self::CERTIFICATE_KEY]); 91 | } 92 | 93 | /** @throws Exception */ 94 | public function getSecureOptions(): ?SecureOptions 95 | { 96 | if (!$this->isSecure()) { 97 | return null; 98 | } 99 | 100 | return new SecureOptions( 101 | new CertificatePath($this->configurationArray[self::CERTIFICATE]), 102 | new CertificateKeyPath($this->configurationArray[self::CERTIFICATE_KEY]), 103 | isset($this->configurationArray[self::CERT_PASSPHRASE]) 104 | ? new Passphrase($this->configurationArray[self::CERT_PASSPHRASE]) 105 | : null 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/acceptance/v1/RequestListCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V1; 21 | 22 | use AcceptanceTester; 23 | 24 | class RequestListCest 25 | { 26 | public function _before(AcceptanceTester $I) 27 | { 28 | $I->sendDELETE('/__phiremock/expectations'); 29 | $I->sendDELETE('/__phiremock/executions'); 30 | } 31 | 32 | public function returnEmptyList(AcceptanceTester $I) 33 | { 34 | $I->sendPUT('/__phiremock/executions'); 35 | $I->seeResponseCodeIs('200'); 36 | $I->seeResponseEquals('[]'); 37 | } 38 | 39 | public function returnAllExecutedRequest(AcceptanceTester $I) 40 | { 41 | $I->haveHttpHeader('Content-Type', 'application/json'); 42 | $I->sendPOST( 43 | '/__phiremock/expectations', 44 | $I->getPhiremockRequest([ 45 | 'request' => [ 46 | 'url' => ['isEqualTo' => '/the/request/url'], 47 | ], 48 | 'response' => [ 49 | 'statusCode' => 201, 50 | ], 51 | ]) 52 | ); 53 | 54 | $I->sendGET('/the/request/url'); 55 | $I->seeResponseCodeIs('201'); 56 | 57 | $I->sendPUT('/__phiremock/executions', ''); 58 | $I->seeResponseCodeIs('200'); 59 | $I->seeResponseContainsJson( 60 | json_decode( 61 | '[{"method":"GET","url":"http:\/\/localhost:8086\/the\/request\/url","headers":{"Host":["localhost:8086"],"User-Agent":["Symfony BrowserKit"],"Content-Type":["application\/json"],"Referer":["http:\/\/localhost:8086\/__phiremock\/expectations"]},"cookies":[],"body":""}]', 62 | true 63 | ) 64 | ); 65 | } 66 | 67 | public function returnExecutedRequestMatchingExpectation(AcceptanceTester $I) 68 | { 69 | $I->haveHttpHeader('Content-Type', 'application/json'); 70 | $I->sendPOST( 71 | '/__phiremock/expectations', 72 | $I->getPhiremockRequest([ 73 | 'request' => [ 74 | 'url' => ['isEqualTo' => '/the/request/url'], 75 | ], 76 | 'response' => [ 77 | 'statusCode' => 201, 78 | ], 79 | ]) 80 | ); 81 | 82 | $I->sendGET('/the/request/url'); 83 | $I->seeResponseCodeIs('201'); 84 | 85 | $I->sendPUT('/__phiremock/executions', $I->getPhiremockRequest([ 86 | 'request' => [ 87 | 'url' => ['isEqualTo' => '/the/request/url'], 88 | ], 89 | 'response' => [ 90 | 'statusCode' => 201, 91 | ], 92 | ])); 93 | $I->seeResponseCodeIs('200'); 94 | $I->seeResponseIsJson('200'); 95 | $I->seeResponseContainsJson( 96 | json_decode( 97 | '[{"method":"GET","url":"http:\/\/localhost:8086\/the\/request\/url","headers":{"Host":["localhost:8086"],"User-Agent":["Symfony BrowserKit"],"Content-Type":["application\/json"],"Referer":["http:\/\/localhost:8086\/__phiremock\/expectations"]},"cookies":[],"body":""}]', 98 | true 99 | ) 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Actions/SetScenarioStateAction.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Actions; 20 | 21 | use Mcustiel\Phiremock\Common\StringStream; 22 | use Mcustiel\Phiremock\Common\Utils\ArrayToScenarioStateInfoConverter; 23 | use Mcustiel\Phiremock\Domain\ScenarioStateInfo; 24 | use Mcustiel\Phiremock\Server\Model\ScenarioStorage; 25 | use Psr\Http\Message\ResponseInterface; 26 | use Psr\Http\Message\ServerRequestInterface; 27 | use Psr\Log\LoggerInterface; 28 | 29 | class SetScenarioStateAction implements ActionInterface 30 | { 31 | /** @var ScenarioStorage */ 32 | private $storage; 33 | 34 | /** @var ArrayToScenarioStateInfoConverter */ 35 | private $converter; 36 | 37 | /** @var LoggerInterface */ 38 | private $logger; 39 | 40 | public function __construct( 41 | ArrayToScenarioStateInfoConverter $requestBuilder, 42 | ScenarioStorage $storage, 43 | LoggerInterface $logger 44 | ) { 45 | $this->converter = $requestBuilder; 46 | $this->storage = $storage; 47 | $this->logger = $logger; 48 | } 49 | 50 | /** @throws \Exception */ 51 | public function execute(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 52 | { 53 | $state = $this->parseRequestObject($request); 54 | if ($state->getScenarioName() === null || $state->getScenarioState() === null) { 55 | return $response 56 | ->withStatus(400) 57 | ->withHeader('Content-Type', 'application/json') 58 | ->withBody( 59 | new StringStream( 60 | json_encode(['error' => 'Scenario name or state is not set']) 61 | ) 62 | ); 63 | } 64 | 65 | $this->storage->setScenarioState($state); 66 | $this->logger->debug( 67 | sprintf( 68 | 'Scenario %s state is set to %s', 69 | $state->getScenarioName()->asString(), 70 | $state->getScenarioState()->asString() 71 | ) 72 | ); 73 | 74 | return $response 75 | ->withStatus(200) 76 | ->withHeader('Content-Type', 'application/json') 77 | ->withBody($request->getBody()); 78 | } 79 | 80 | /** @throws \Exception */ 81 | private function parseRequestObject(ServerRequestInterface $request): ScenarioStateInfo 82 | { 83 | $object = $this->converter->convert( 84 | $this->parseJsonBody($request) 85 | ); 86 | $this->logger->debug('Parsed scenario state: ' . var_export($object, true)); 87 | 88 | return $object; 89 | } 90 | 91 | /** @throws \Exception */ 92 | private function parseJsonBody(ServerRequestInterface $request): array 93 | { 94 | $body = $request->getBody()->__toString(); 95 | $this->logger->debug($body); 96 | if ($request->hasHeader('Content-Encoding') && 'base64' === implode(',', $request->getHeader('Content-Encoding'))) { 97 | $body = base64_decode($body, true); 98 | } 99 | 100 | $bodyJson = @json_decode($body, true); 101 | if (\JSON_ERROR_NONE !== json_last_error()) { 102 | throw new \Exception(json_last_error_msg()); 103 | } 104 | 105 | return $bodyJson; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/acceptance/v1/ResetCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V1; 21 | 22 | use AcceptanceTester; 23 | 24 | class ResetCest 25 | { 26 | public function _before(AcceptanceTester $I) 27 | { 28 | $I->sendDELETE('/__phiremock/expectations'); 29 | $I->sendDELETE('/__phiremock/executions'); 30 | } 31 | 32 | public function restoreExpectationAfterDelete(AcceptanceTester $I) 33 | { 34 | $I->sendPOST('/__phiremock/reset'); 35 | 36 | $I->sendGET('/hello'); 37 | $I->seeResponseCodeIs('200'); 38 | $I->seeResponseEquals('Hello!'); 39 | } 40 | 41 | public function restoreExpectationAfterRewrite(AcceptanceTester $I) 42 | { 43 | $I->sendPOST('/__phiremock/reset'); 44 | 45 | $I->haveHttpHeader('Content-Type', 'application/json'); 46 | $I->sendPOST( 47 | '/__phiremock/expectations', 48 | $I->getPhiremockRequest([ 49 | 'request' => [ 50 | 'method' => 'get', 51 | 'url' => ['isEqualTo' => '/hello'], 52 | ], 53 | 'response' => [ 54 | 'statusCode' => 200, 55 | 'body' => 'Bye!', 56 | ], 57 | 'priority' => 1, 58 | ]) 59 | ); 60 | 61 | $I->sendGET('/hello'); 62 | $I->seeResponseCodeIs('200'); 63 | $I->seeResponseEquals('Bye!'); 64 | 65 | $I->sendPOST('/__phiremock/reset'); 66 | 67 | $I->sendGET('/hello'); 68 | $I->seeResponseCodeIs('200'); 69 | $I->seeResponseEquals('Hello!'); 70 | } 71 | 72 | public function resetRequestsCount(AcceptanceTester $I) 73 | { 74 | $I->sendPOST('/__phiremock/executions', ''); 75 | $I->seeResponseCodeIs('200'); 76 | $I->seeResponseEquals('{"count":0}'); 77 | 78 | $I->sendGET('/the/request/url'); 79 | 80 | $I->sendPOST('/__phiremock/executions', ''); 81 | $I->seeResponseCodeIs('200'); 82 | $I->seeResponseEquals('{"count":1}'); 83 | 84 | $I->sendPOST('/__phiremock/reset'); 85 | 86 | $I->sendPOST('/__phiremock/executions', ''); 87 | $I->seeResponseCodeIs('200'); 88 | $I->seeResponseEquals('{"count":0}'); 89 | } 90 | 91 | public function clearRequestsList(AcceptanceTester $I) 92 | { 93 | $I->sendPUT('/__phiremock/executions', ''); 94 | $I->seeResponseCodeIs('200'); 95 | $I->seeResponseEquals('[]'); 96 | 97 | $I->sendGET('/the/request/url'); 98 | 99 | $I->sendPUT('/__phiremock/executions', ''); 100 | $I->seeResponseCodeIs('200'); 101 | $I->seeResponseContainsJson( 102 | json_decode( 103 | '[{"method":"GET","url":"http:\/\/localhost:8086\/the\/request\/url","headers":{"Host":["localhost:8086"],"User-Agent":["Symfony BrowserKit"],"Referer":["http:\/\/localhost:8086\/__phiremock\/executions"]},"cookies":[],"body":""}]', 104 | true 105 | ) 106 | ); 107 | 108 | $I->sendPOST('/__phiremock/reset'); 109 | 110 | $I->sendPUT('/__phiremock/executions', ''); 111 | $I->seeResponseCodeIs('200'); 112 | $I->seeResponseEquals('[]'); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Utils/Config/ConfigBuilder.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Utils\Config; 21 | 22 | use DomainException; 23 | use Exception; 24 | use Mcustiel\Phiremock\Server\Cli\Options\ExpectationsDirectory; 25 | use Mcustiel\Phiremock\Server\Factory\Factory; 26 | use Mcustiel\Phiremock\Server\Utils\HomePathService; 27 | 28 | class ConfigBuilder 29 | { 30 | private const DEFAULT_IP = '0.0.0.0'; 31 | private const DEFAULT_PORT = 8086; 32 | 33 | /** @var array */ 34 | private static $defaultConfig; 35 | 36 | /** @var Directory|null */ 37 | private $configPath; 38 | 39 | /** @throws Exception */ 40 | public function __construct(?Directory $configPath) 41 | { 42 | if (self::$defaultConfig === null) { 43 | self::$defaultConfig = [ 44 | Config::PORT => self::DEFAULT_PORT, 45 | Config::IP => self::DEFAULT_IP, 46 | Config::EXPECTATIONS_DIR => self::getDefaultExpectationsDir()->asString(), 47 | Config::DEBUG => false, 48 | Config::FACTORY_CLASS => Factory::class, 49 | ]; 50 | } 51 | $this->configPath = $configPath; 52 | } 53 | 54 | /** @throws Exception */ 55 | public function build(array $cliConfig): Config 56 | { 57 | $config = self::$defaultConfig; 58 | 59 | $fileConfiguration = $this->getConfigurationFromConfigFile(); 60 | $extraKeys = array_diff_key($fileConfiguration, self::$defaultConfig); 61 | if (!empty($extraKeys)) { 62 | throw new DomainException('Extra keys in configuration file: ' . implode(',', $extraKeys)); 63 | } 64 | 65 | return new Config(array_replace($config, $fileConfiguration, $cliConfig)); 66 | } 67 | 68 | /** @throws Exception */ 69 | public static function getDefaultExpectationsDir(): ExpectationsDirectory 70 | { 71 | return new ExpectationsDirectory( 72 | HomePathService::getHomePath()->getFullSubpathAsString( 73 | '.phiremock' . \DIRECTORY_SEPARATOR . 'expectations' 74 | ) 75 | ); 76 | } 77 | 78 | /** @throws Exception */ 79 | protected function getConfigurationFromConfigFile(): array 80 | { 81 | if ($this->configPath) { 82 | $configFiles = ['.phiremock', '.phiremock.dist']; 83 | foreach ($configFiles as $configFileName) { 84 | $configFilePath = $this->configPath->getFullSubpathAsString($configFileName); 85 | if (is_file($configFilePath)) { 86 | return require $configFilePath; 87 | } 88 | } 89 | throw new Exception('No config file found in: ' . $this->configPath->asString()); 90 | } 91 | 92 | return $this->searchFileAndGetConfig(); 93 | } 94 | 95 | protected function searchFileAndGetConfig(): array 96 | { 97 | $configFiles = [ 98 | __DIR__ . '/../../../../../../.phiremock', 99 | __DIR__ . '/../../../../../../.phiremock.dist', 100 | __DIR__ . '/../../../.phiremock', 101 | __DIR__ . '/../../../.phiremock.dist', 102 | getcwd() . '/.phiremock', 103 | getcwd() . '/.phiremock.dist', 104 | HomePathService::getHomePath()->getFullSubpathAsString( 105 | '.phiremock' . \DIRECTORY_SEPARATOR . 'config' 106 | ), 107 | '.phiremock', 108 | '.phiremock.dist', 109 | ]; 110 | foreach ($configFiles as $configFilePath) { 111 | if (is_file($configFilePath)) { 112 | return require $configFilePath; 113 | } 114 | } 115 | 116 | return []; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Utils/Strategies/Utils/RegexReplacer.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils\Strategies\Utils; 20 | 21 | use Mcustiel\Phiremock\Domain\Condition\MatchersEnum; 22 | use Mcustiel\Phiremock\Domain\Expectation; 23 | use Psr\Http\Message\ServerRequestInterface; 24 | 25 | class RegexReplacer 26 | { 27 | const PLACEHOLDER_BODY = 'body'; 28 | const PLACEHOLDER_URL = 'url'; 29 | 30 | public function fillWithBodyMatches( 31 | Expectation $expectation, 32 | ServerRequestInterface $httpRequest, 33 | string $text 34 | ): string { 35 | if ($this->bodyConditionIsRegex($expectation)) { 36 | $text = $this->replaceMatches( 37 | self::PLACEHOLDER_BODY, 38 | $expectation->getRequest()->getBody()->getValue()->asString(), 39 | $httpRequest->getBody()->__toString(), 40 | $text 41 | ); 42 | } 43 | 44 | return $text; 45 | } 46 | 47 | public function fillWithUrlMatches( 48 | Expectation $expectation, 49 | ServerRequestInterface $httpRequest, 50 | string $text 51 | ): string { 52 | if ($this->urlConditionIsRegex($expectation)) { 53 | return $this->replaceMatches( 54 | self::PLACEHOLDER_URL, 55 | $expectation->getRequest()->getUrl()->getValue()->asString(), 56 | $this->getUri($httpRequest), 57 | $text 58 | ); 59 | } 60 | 61 | return $text; 62 | } 63 | 64 | private function getUri(ServerRequestInterface $httpRequest): string 65 | { 66 | $path = ltrim($httpRequest->getUri()->getPath(), '/'); 67 | $query = $httpRequest->getUri()->getQuery(); 68 | $return = '/' . $path; 69 | if ($query) { 70 | $return .= '?' . $httpRequest->getUri()->getQuery(); 71 | } 72 | 73 | return $return; 74 | } 75 | 76 | private function urlConditionIsRegex(Expectation $expectation): bool 77 | { 78 | return $expectation->getRequest()->getUrl() 79 | && MatchersEnum::MATCHES === $expectation->getRequest()->getUrl()->getMatcher()->getName(); 80 | } 81 | 82 | private function bodyConditionIsRegex(Expectation $expectation): bool 83 | { 84 | return $expectation->getRequest()->getBody() 85 | && MatchersEnum::MATCHES === $expectation->getRequest()->getBody()->getMatcher()->getName(); 86 | } 87 | 88 | private function replaceMatches( 89 | string $type, string $pattern, string $subject, string $destination): string 90 | { 91 | $matches = []; 92 | 93 | $matchCount = preg_match_all( 94 | $pattern, 95 | $subject, 96 | $matches 97 | ); 98 | if ($matchCount > 0) { 99 | // we don't need full matches 100 | unset($matches[0]); 101 | $destination = $this->replaceMatchesInBody($matches, $type, $destination); 102 | } 103 | 104 | return $destination; 105 | } 106 | 107 | private function replaceMatchesInBody(array $matches, string $type, string $responseBody): string 108 | { 109 | $search = []; 110 | $replace = []; 111 | 112 | foreach ($matches as $matchGroupId => $matchGroup) { 113 | // add first element as replacement for $(type.index) 114 | $search[] = "\${{$type}.{$matchGroupId}}"; 115 | $replace[] = reset($matchGroup); 116 | foreach ($matchGroup as $matchId => $match) { 117 | // fix index to start with 1 instead of 0 118 | ++$matchId; 119 | $search[] = "\${{$type}.{$matchGroupId}.{$matchId}}"; 120 | $replace[] = $match; 121 | } 122 | } 123 | 124 | return str_replace($search, $replace, $responseBody); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Utils/Strategies/RegexResponseStrategy.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Utils\Strategies; 20 | 21 | use Mcustiel\Phiremock\Common\StringStream; 22 | use Mcustiel\Phiremock\Domain\Expectation; 23 | use Mcustiel\Phiremock\Domain\Http\BinaryBody; 24 | use Mcustiel\Phiremock\Domain\HttpResponse; 25 | use Mcustiel\Phiremock\Server\Model\ScenarioStorage; 26 | use Mcustiel\Phiremock\Server\Utils\Strategies\Utils\RegexReplacer; 27 | use Psr\Http\Message\ResponseInterface; 28 | use Psr\Http\Message\ServerRequestInterface; 29 | use Psr\Log\LoggerInterface; 30 | 31 | class RegexResponseStrategy extends AbstractResponse implements ResponseStrategyInterface 32 | { 33 | /** @var RegexReplacer */ 34 | private $regexReplacer; 35 | 36 | public function __construct( 37 | ScenarioStorage $scenarioStorage, 38 | LoggerInterface $logger, 39 | RegexReplacer $regexReplacer 40 | ) { 41 | parent::__construct($scenarioStorage, $logger); 42 | 43 | $this->regexReplacer = $regexReplacer; 44 | } 45 | 46 | public function createResponse(Expectation $expectation, ResponseInterface $httpResponse, ServerRequestInterface $request): ResponseInterface 47 | { 48 | $httpResponse = $this->getResponseWithReplacedBody( 49 | $expectation, 50 | $httpResponse, 51 | $request 52 | ); 53 | $httpResponse = $this->getResponseWithReplacedHeaders( 54 | $expectation, 55 | $httpResponse, 56 | $request 57 | ); 58 | /** @var HttpResponse $responseConfig */ 59 | $responseConfig = $expectation->getResponse(); 60 | $httpResponse = $this->getResponseWithStatusCode($responseConfig, $httpResponse); 61 | $this->processScenario($expectation); 62 | $this->processDelay($responseConfig); 63 | 64 | return $httpResponse; 65 | } 66 | 67 | private function getResponseWithReplacedBody( 68 | Expectation $expectation, 69 | ResponseInterface $httpResponse, 70 | ServerRequestInterface $httpRequest 71 | ): ResponseInterface { 72 | /** @var HttpResponse $responseConfig */ 73 | $responseConfig = $expectation->getResponse(); 74 | 75 | if ($responseConfig->hasBody()) { 76 | if ($responseConfig->getBody() instanceof BinaryBody) { 77 | $httpResponse = $httpResponse->withBody($responseConfig->getBody()->asStream()); 78 | } else { 79 | $bodyString = $responseConfig->getBody()->asString(); 80 | $bodyString = $this->regexReplacer->fillWithUrlMatches($expectation, $httpRequest, $bodyString); 81 | $bodyString = $this->regexReplacer->fillWithBodyMatches($expectation, $httpRequest, $bodyString); 82 | $httpResponse = $httpResponse->withBody(new StringStream($bodyString)); 83 | } 84 | } 85 | 86 | return $httpResponse; 87 | } 88 | 89 | private function getResponseWithReplacedHeaders( 90 | Expectation $expectation, 91 | ResponseInterface $httpResponse, 92 | ServerRequestInterface $httpRequest 93 | ): ResponseInterface { 94 | /** @var HttpResponse $responseConfig */ 95 | $responseConfig = $expectation->getResponse(); 96 | $headers = $responseConfig->getHeaders(); 97 | 98 | if ($headers === null || $headers->isEmpty()) { 99 | return $httpResponse; 100 | } 101 | 102 | foreach ($headers as $header) { 103 | $headerValue = $header->getValue()->asString(); 104 | $headerValue = $this->regexReplacer->fillWithUrlMatches($expectation, $httpRequest, $headerValue); 105 | $headerValue = $this->regexReplacer->fillWithBodyMatches($expectation, $httpRequest, $headerValue); 106 | $httpResponse = $httpResponse->withHeader($header->getName()->asString(), $headerValue); 107 | } 108 | 109 | return $httpResponse; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/acceptance/v1/StatusCodeSpecificationCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V1; 21 | 22 | use AcceptanceTester; 23 | 24 | class StatusCodeSpecificationCest 25 | { 26 | public function _before(AcceptanceTester $I) 27 | { 28 | $I->sendDELETE('/__phiremock/expectations'); 29 | } 30 | 31 | public function createExpectationWithStatusCodeTest(AcceptanceTester $I) 32 | { 33 | $I->wantTo('create a specification with a valid status code'); 34 | 35 | $I->haveHttpHeader('Content-Type', 'application/json'); 36 | $I->sendPOST( 37 | '/__phiremock/expectations', 38 | $I->getPhiremockRequest([ 39 | 'request' => [ 40 | 'url' => ['isEqualTo' => '/the/request/url'], 41 | ], 42 | 'response' => [ 43 | 'statusCode' => 401, 44 | ], 45 | ]) 46 | ); 47 | 48 | $I->sendGET('/__phiremock/expectations'); 49 | $I->seeResponseCodeIs(200); 50 | $I->seeResponseIsJson(); 51 | $I->seeResponseEquals($I->getPhiremockResponse( 52 | '[{"scenarioName":null,"scenarioStateIs":null,"newScenarioState":null,' 53 | . '"request":{"method":null,"url":{"isEqualTo":"\/the\/request\/url"},"body":null,"headers":null,"formData":null,"jsonPath":null},' 54 | . '"response":{"statusCode":401,"body":null,"headers":null,"delayMillis":null},' 55 | . '"proxyTo":null,"priority":0}]' 56 | )); 57 | } 58 | 59 | public function createExpectationWithDefaultStatusCodeTest(AcceptanceTester $I) 60 | { 61 | $I->wantTo('create a specification with a default status code'); 62 | $I->haveHttpHeader('Content-Type', 'application/json'); 63 | $I->sendPOST( 64 | '/__phiremock/expectations', 65 | $I->getPhiremockRequest([ 66 | 'request' => [ 67 | 'url' => ['isEqualTo' => '/the/request/url'], 68 | ], 69 | 'response' => [], 70 | ]) 71 | ); 72 | 73 | $I->sendGET('/__phiremock/expectations'); 74 | $I->seeResponseCodeIs(200); 75 | $I->seeResponseIsJson(); 76 | $I->seeResponseEquals($I->getPhiremockResponse( 77 | '[{"scenarioName":null,"scenarioStateIs":null,"newScenarioState":null,' 78 | . '"request":{"method":null,"url":{"isEqualTo":"\/the\/request\/url"},"body":null,"headers":null,"formData":null,"jsonPath":null},' 79 | . '"response":{"statusCode":200,"body":null,"headers":null,"delayMillis":null},' 80 | . '"proxyTo":null,"priority":0}]' 81 | )); 82 | } 83 | 84 | public function useDefaultWhenNoStatusCodeIsSetTest(AcceptanceTester $I) 85 | { 86 | $I->wantTo('fail when the status code is not set'); 87 | $I->haveHttpHeader('Content-Type', 'application/json'); 88 | $I->sendPOST( 89 | '/__phiremock/expectations', 90 | $I->getPhiremockRequest([ 91 | 'request' => [ 92 | 'url' => ['isEqualTo' => '/the/request/url'], 93 | ], 94 | 'response' => [ 95 | 'statusCode' => null, 96 | ], 97 | ]) 98 | ); 99 | 100 | $I->seeResponseCodeIs(201); 101 | 102 | $I->sendGET('/__phiremock/expectations'); 103 | $I->seeResponseCodeIs(200); 104 | $I->seeResponseIsJson(); 105 | $I->seeResponseEquals($I->getPhiremockResponse( 106 | '[{"scenarioName":null,"scenarioStateIs":null,"newScenarioState":null,' 107 | . '"request":{"method":null,"url":{"isEqualTo":"\/the\/request\/url"},"body":null,"headers":null,"formData":null,"jsonPath":null},' 108 | . '"response":{"statusCode":200,"body":null,"headers":null,"delayMillis":null},' 109 | . '"proxyTo":null,"priority":0}]' 110 | )); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Actions/SearchRequestAction.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Actions; 20 | 21 | use Mcustiel\Phiremock\Domain\Expectation; 22 | use Mcustiel\Phiremock\Server\Model\ExpectationStorage; 23 | use Mcustiel\Phiremock\Server\Model\RequestStorage; 24 | use Mcustiel\Phiremock\Server\Utils\RequestExpectationComparator; 25 | use Mcustiel\Phiremock\Server\Utils\ResponseStrategyLocator; 26 | use Psr\Http\Message\ResponseInterface; 27 | use Psr\Http\Message\ServerRequestInterface; 28 | use Psr\Log\LoggerInterface; 29 | 30 | class SearchRequestAction implements ActionInterface 31 | { 32 | /** @var ExpectationStorage */ 33 | private $expectationsStorage; 34 | /** @var RequestExpectationComparator */ 35 | private $comparator; 36 | /** @var LoggerInterface */ 37 | private $logger; 38 | /** @var ResponseStrategyLocator */ 39 | private $responseStrategyFactory; 40 | /** @var RequestStorage */ 41 | private $requestsStorage; 42 | 43 | public function __construct( 44 | ExpectationStorage $expectationsStorage, 45 | RequestExpectationComparator $comparator, 46 | ResponseStrategyLocator $responseStrategyLocator, 47 | RequestStorage $requestsStorage, 48 | LoggerInterface $logger 49 | ) { 50 | $this->expectationsStorage = $expectationsStorage; 51 | $this->comparator = $comparator; 52 | $this->logger = $logger; 53 | $this->requestsStorage = $requestsStorage; 54 | $this->responseStrategyFactory = $responseStrategyLocator; 55 | } 56 | 57 | public function execute(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 58 | { 59 | $this->logger->info('Request received: ' . $this->getLoggableRequest($request)); 60 | $this->requestsStorage->addRequest($request); 61 | $foundExpectation = $this->searchForMatchingExpectation($request); 62 | if (null === $foundExpectation) { 63 | return $response->withStatus(404, 'Not Found'); 64 | } 65 | $response = $this->responseStrategyFactory 66 | ->getStrategyForExpectation($foundExpectation) 67 | ->createResponse($foundExpectation, $response, $request); 68 | $this->logger->debug('Responding: ' . $this->getLoggableResponse($response)); 69 | 70 | return $response; 71 | } 72 | 73 | private function searchForMatchingExpectation(ServerRequestInterface $request): ?Expectation 74 | { 75 | $lastFound = null; 76 | foreach ($this->expectationsStorage->listExpectations() as $expectation) { 77 | $lastFound = $this->getNextMatchingExpectation($lastFound, $request, $expectation); 78 | } 79 | 80 | return $lastFound; 81 | } 82 | 83 | private function getNextMatchingExpectation(?Expectation $lastFound, ServerRequestInterface $request, Expectation $expectation): ?Expectation 84 | { 85 | if (null === $lastFound || $expectation->getPriority() > $lastFound->getPriority()) { 86 | if ($this->comparator->equals($request, $expectation)) { 87 | $lastFound = $expectation; 88 | } 89 | } 90 | 91 | return $lastFound; 92 | } 93 | 94 | private function getLoggableRequest(ServerRequestInterface $request): string 95 | { 96 | $body = $request->getBody()->__toString(); 97 | $longBody = '--VERY LONG CONTENTS--'; 98 | $body = isset($body[2000]) ? $longBody : preg_replace('|\s+|', ' ', $body); 99 | 100 | return sprintf( 101 | '%s: %s || %s', 102 | $request->getMethod(), 103 | $request->getUri()->__toString(), 104 | $body 105 | ); 106 | } 107 | 108 | private function getLoggableResponse(ResponseInterface $response): string 109 | { 110 | $body = $response->getBody()->__toString(); 111 | 112 | return sprintf( 113 | '%d / %s', 114 | $response->getStatusCode(), 115 | isset($body[2000]) ? '--VERY LONG CONTENTS--' : preg_replace('|\s+|', ' ', $body) 116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/acceptance/v1/DelaySpecificationCest.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Mcustiel\Phiremock\Server\Tests\V1; 21 | 22 | use AcceptanceTester; 23 | 24 | class DelaySpecificationCest 25 | { 26 | public function _before(AcceptanceTester $I) 27 | { 28 | $I->sendDELETE('/__phiremock/expectations'); 29 | } 30 | 31 | // tests 32 | public function createExpectationWhithValidDelayTest(AcceptanceTester $I) 33 | { 34 | $I->wantTo('create an expectation with a valid delay specification'); 35 | $I->haveHttpHeader('Content-Type', 'application/json'); 36 | $I->sendPOST( 37 | '/__phiremock/expectations', 38 | $I->getPhiremockRequest([ 39 | 'request' => [ 40 | 'url' => ['isEqualTo' => '/the/request/url'], 41 | ], 42 | 'response' => [ 43 | 'delayMillis' => 5000, 44 | ], 45 | ]) 46 | ); 47 | 48 | $I->sendGET('/__phiremock/expectations'); 49 | $I->seeResponseCodeIs('200'); 50 | $I->seeResponseIsJson(); 51 | $I->seeResponseEquals($I->getPhiremockResponse( 52 | '[{"scenarioName":null,"scenarioStateIs":null,"newScenarioState":null,' 53 | . '"request":{"method":null,"url":{"isEqualTo":"\/the\/request\/url"},"body":null,"headers":null,"formData":null,"jsonPath":null},' 54 | . '"response":{"statusCode":200,"body":null,"headers":null,"delayMillis":5000},' 55 | . '"proxyTo":null,"priority":0}]' 56 | )); 57 | } 58 | 59 | public function failWhithNegativedDelayTest(AcceptanceTester $I) 60 | { 61 | $I->wantTo('create an expectation with a negative delay specification'); 62 | $I->haveHttpHeader('Content-Type', 'application/json'); 63 | $I->sendPOST( 64 | '/__phiremock/expectations', 65 | $I->getPhiremockRequest([ 66 | 'request' => [ 67 | 'url' => ['isEqualTo' => '/the/request/url'], 68 | ], 69 | 'response' => [ 70 | 'delayMillis' => -5000, 71 | ], 72 | ]) 73 | ); 74 | 75 | $I->seeResponseCodeIs('500'); 76 | $I->seeResponseIsJson(); 77 | $I->seeResponseEquals( 78 | '{"result" : "ERROR", "details" : ["Delay must be greater or equal to 0. Got: -5000"]}' 79 | ); 80 | } 81 | 82 | public function failWhithInvalidDelayTest(AcceptanceTester $I) 83 | { 84 | $I->wantTo('create an expectation with an invalid delay specification'); 85 | $I->haveHttpHeader('Content-Type', 'application/json'); 86 | $I->sendPOST( 87 | '/__phiremock/expectations', 88 | $I->getPhiremockRequest([ 89 | 'request' => [ 90 | 'url' => ['isEqualTo' => '/the/request/url'], 91 | ], 92 | 'response' => [ 93 | 'delayMillis' => 'potato', 94 | ], 95 | ]) 96 | ); 97 | 98 | $I->seeResponseCodeIs('500'); 99 | $I->seeResponseIsJson(); 100 | $I->seeResponseEquals( 101 | '{"result" : "ERROR", "details" : ["Delay must be an integer. Got: string"]}' 102 | ); 103 | } 104 | 105 | // tests 106 | public function mockRequestWithDelayTest(AcceptanceTester $I) 107 | { 108 | $I->wantTo('mock a request with delay'); 109 | $I->haveHttpHeader('Content-Type', 'application/json'); 110 | $I->sendPOST( 111 | '/__phiremock/expectations', 112 | $I->getPhiremockRequest([ 113 | 'request' => [ 114 | 'url' => ['isEqualTo' => '/the/request/url'], 115 | ], 116 | 'response' => [ 117 | 'delayMillis' => 2000, 118 | ], 119 | ]) 120 | ); 121 | 122 | $I->seeResponseCodeIs(201); 123 | 124 | $start = microtime(true); 125 | $I->sendGET('/the/request/url'); 126 | $I->seeResponseCodeIs(200); 127 | $I->assertGreaterThan(2000, (microtime(true) - $start) * 1000); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Http/Implementation/FastRouterHandler.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Http\Implementation; 20 | 21 | use FastRoute\Dispatcher; 22 | use FastRoute\RouteCollector; 23 | use function FastRoute\simpleDispatcher; 24 | use Laminas\Diactoros\Response; 25 | use Mcustiel\Phiremock\Common\StringStream; 26 | use Mcustiel\Phiremock\Server\Actions\ActionLocator; 27 | use Mcustiel\Phiremock\Server\Http\RequestHandlerInterface; 28 | use Mcustiel\Phiremock\Server\Utils\Config\Config; 29 | use Psr\Http\Message\ResponseInterface; 30 | use Psr\Http\Message\ServerRequestInterface; 31 | use Psr\Log\LoggerInterface; 32 | use Throwable; 33 | 34 | class FastRouterHandler implements RequestHandlerInterface 35 | { 36 | /** @var Dispatcher */ 37 | private $dispatcher; 38 | /** @var ActionLocator */ 39 | private $actionsLocator; 40 | /** @var LoggerInterface */ 41 | private $logger; 42 | 43 | public function __construct(ActionLocator $locator, Config $config, LoggerInterface $logger) 44 | { 45 | $this->dispatcher = simpleDispatcher( 46 | $this->createDispatcherCallable(), 47 | [ 48 | 'cacheFile' => __DIR__ . '/route.cache', 49 | 'cacheDisabled' => $config->isDebugMode(), 50 | ] 51 | ); 52 | $this->actionsLocator = $locator; 53 | $this->logger = $logger; 54 | } 55 | 56 | public function dispatch(ServerRequestInterface $request): ResponseInterface 57 | { 58 | $uri = $request->getUri()->getPath(); 59 | $routeInfo = $this->dispatcher->dispatch($request->getMethod(), $uri); 60 | try { 61 | switch ($routeInfo[0]) { 62 | case Dispatcher::NOT_FOUND: 63 | return $this->actionsLocator 64 | ->locate(ActionLocator::MANAGE_REQUEST) 65 | ->execute($request, new Response()); 66 | case Dispatcher::METHOD_NOT_ALLOWED: 67 | return new Response( 68 | sprintf( 69 | 'Method not allowed. Allowed methods for %s: %s', 70 | $uri, 71 | implode(', ', $routeInfo[1]) 72 | ), 73 | 405 74 | ); 75 | case Dispatcher::FOUND: 76 | return $this->actionsLocator 77 | ->locate($routeInfo[1]) 78 | ->execute($request, new Response()); 79 | } 80 | 81 | return new Response( 82 | new StringStream( 83 | json_encode(['result' => 'ERROR', 'details' => 'Unexpected error: Router returned unexpected info']) 84 | ), 85 | 500, 86 | ['Content-Type' => 'application/json'] 87 | ); 88 | } catch (Throwable $e) { 89 | $this->logger->error($e->getMessage()); 90 | 91 | return new Response( 92 | new StringStream( 93 | json_encode(['result' => 'ERROR', 'details' => $e->getMessage()]) 94 | ), 95 | 500, 96 | ['Content-Type' => 'application/json'] 97 | ); 98 | } 99 | } 100 | 101 | private function createDispatcherCallable(): callable 102 | { 103 | return function (RouteCollector $r) { 104 | $r->addRoute('GET', '/__phiremock/expectations', ActionLocator::LIST_EXPECTATIONS); 105 | $r->addRoute('POST', '/__phiremock/expectations', ActionLocator::ADD_EXPECTATION); 106 | $r->addRoute('DELETE', '/__phiremock/expectations', ActionLocator::CLEAR_EXPECTATIONS); 107 | 108 | $r->addRoute('PUT', '/__phiremock/scenarios', ActionLocator::SET_SCENARIO_STATE); 109 | $r->addRoute('DELETE', '/__phiremock/scenarios', ActionLocator::CLEAR_SCENARIOS); 110 | 111 | $r->addRoute('POST', '/__phiremock/executions', ActionLocator::COUNT_REQUESTS); 112 | $r->addRoute('PUT', '/__phiremock/executions', ActionLocator::LIST_REQUESTS); 113 | $r->addRoute('DELETE', '/__phiremock/executions', ActionLocator::RESET_REQUESTS_COUNT); 114 | 115 | $r->addRoute('POST', '/__phiremock/reset', ActionLocator::RESET); 116 | }; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Http/Implementation/ReactPhpServer.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace Mcustiel\Phiremock\Server\Http\Implementation; 20 | 21 | use Mcustiel\Phiremock\Common\StringStream; 22 | use Mcustiel\Phiremock\Server\Cli\Options\HostInterface; 23 | use Mcustiel\Phiremock\Server\Cli\Options\Port; 24 | use Mcustiel\Phiremock\Server\Cli\Options\SecureOptions; 25 | use Mcustiel\Phiremock\Server\Http\RequestHandlerInterface; 26 | use Mcustiel\Phiremock\Server\Http\ServerInterface; 27 | use Psr\Http\Message\ResponseInterface; 28 | use Psr\Http\Message\ServerRequestInterface; 29 | use Psr\Log\LoggerInterface; 30 | use React\EventLoop\Factory as EventLoop; 31 | use React\EventLoop\LoopInterface; 32 | use React\Http\Middleware\RequestBodyBufferMiddleware; 33 | use React\Http\Middleware\RequestBodyParserMiddleware; 34 | use React\Http\Middleware\StreamingRequestMiddleware; 35 | use React\Http\Server; 36 | use React\Socket\Server as ReactSocket; 37 | 38 | class ReactPhpServer implements ServerInterface 39 | { 40 | /** @var RequestHandlerInterface */ 41 | private $requestHandler; 42 | 43 | /** @var LoopInterface */ 44 | private $loop; 45 | 46 | /** @var ReactSocket */ 47 | private $socket; 48 | 49 | /** @var Server */ 50 | private $http; 51 | 52 | /** @var LoggerInterface */ 53 | private $logger; 54 | 55 | public function __construct(RequestHandlerInterface $requestHandler, LoggerInterface $logger) 56 | { 57 | $this->loop = EventLoop::create(); 58 | $this->logger = $logger; 59 | $this->requestHandler = $requestHandler; 60 | } 61 | 62 | public function listen(HostInterface $host, Port $port, ?SecureOptions $secureOptions): void 63 | { 64 | $this->http = new Server( 65 | $this->loop, 66 | new StreamingRequestMiddleware(), 67 | new RequestBodyBufferMiddleware(), 68 | new RequestBodyParserMiddleware(), 69 | function (ServerRequestInterface $request) { 70 | return $this->onRequest($request); 71 | } 72 | ); 73 | 74 | $listenConfig = "{$host->asString()}:{$port->asInt()}"; 75 | $this->initSocket($listenConfig, $secureOptions); 76 | $this->http->listen($this->socket); 77 | 78 | // Dispatch pending signals periodically 79 | if (\function_exists('pcntl_signal_dispatch')) { 80 | $this->loop->addPeriodicTimer(0.5, function () { 81 | pcntl_signal_dispatch(); 82 | }); 83 | } 84 | $this->loop->run(); 85 | } 86 | 87 | public function shutdown(): void 88 | { 89 | $this->http->removeAllListeners(); 90 | $this->socket->close(); 91 | $this->loop->stop(); 92 | } 93 | 94 | private function onRequest(ServerRequestInterface $request): ResponseInterface 95 | { 96 | $start = microtime(true); 97 | 98 | // TODO: Remove this patch if ReactPHP is fixed 99 | if ($request->getParsedBody() !== null) { 100 | $request = $request->withBody(new StringStream(http_build_query($request->getParsedBody()))); 101 | } 102 | 103 | $psrResponse = $this->requestHandler->dispatch(new ServerRequestWithCachedBody($request)); 104 | $this->logger->debug('Processing took ' . number_format((microtime(true) - $start) * 1000, 3) . ' milliseconds'); 105 | 106 | return $psrResponse; 107 | } 108 | 109 | private function initSocket(string $listenConfig, ?SecureOptions $secureOptions): void 110 | { 111 | $this->logger->info( 112 | sprintf( 113 | 'Phiremock http server listening on %s over %s', 114 | $listenConfig, 115 | null === $secureOptions ? 'http' : 'https' 116 | ) 117 | ); 118 | $context = []; 119 | if ($secureOptions !== null) { 120 | $tlsContext = []; 121 | $listenConfig = sprintf('tls://%s', $listenConfig); 122 | $tlsContext['local_cert'] = $secureOptions->getCertificate()->asString(); 123 | $tlsContext['local_pk'] = $secureOptions->getCertificateKey()->asString(); 124 | if ($secureOptions->hasPassphrase()) { 125 | $tlsContext['passphrase'] = $secureOptions->getPassphrase()->asString(); 126 | } 127 | $context['tls'] = $tlsContext; 128 | } 129 | $this->socket = new ReactSocket($listenConfig, $this->loop, $context); 130 | } 131 | } 132 | --------------------------------------------------------------------------------