├── .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 |
--------------------------------------------------------------------------------