diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml new file mode 100644 index 0000000..d9acade --- /dev/null +++ b/.github/workflows/code_quality.yml @@ -0,0 +1,42 @@ +name: "Code Quality" + +on: + pull_request: + paths: + - "**.php" + - "phpcs.xml" + - ".github/workflows/code_quality.yml" + +jobs: + phpcs: + name: "PHP CodeSniffer" + runs-on: ubuntu-latest + steps: + - name: Check out code into the workspace + uses: actions/checkout@v2 + - name: Run PHPCS + uses: chekalsky/phpcs-action@v1 + phpmd: + name: "PHP MessDetector" + runs-on: ubuntu-latest + steps: + - name: Check out code into the workspace + uses: actions/checkout@v2 + - name: Run PHPMD + uses: GeneaLabs/action-reviewdog-phpmd@1.0.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + level: 'warning' + reporter: github-pr-check + standard: './phpmd.xml' + target_directory: 'src' + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - name: Check out code into the workspace + uses: actions/checkout@v2 + - name: Run PHPStan + uses: docker://oskarstark/phpstan-ga + with: + args: analyse src -c phpstan.neon --memory-limit=1G --no-progress diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e3afaf0 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,38 @@ +name: Tests + +on: + push: + branches: + - '**' + tags-ignore: + - '*.*' + pull_request: + +jobs: + test: + name: "PHPUnit" + runs-on: ubuntu-latest + strategy: + matrix: + php-version: ['7.1', '7.2', '7.3', '7.4', '8.0'] + steps: + - name: Check out code into the workspace + uses: actions/checkout@v2 + - name: Setup PHP ${{ matrix.php-version }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + coverage: pcov + - name: Composer cache + uses: actions/cache@v2 + with: + path: ${{ env.HOME }}/.composer/cache + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + - name: Install dependencies + run: composer install -o + - name: Configure matchers + uses: mheap/phpunit-matcher-action@v1 + - name: Run tests + run: composer run-script phpunit-ci + - name: Coverage + run: bash <(curl -s https://codecov.io/bash) diff --git a/.gitignore b/.gitignore index 6a78056..afbdeb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ composer.lock vendor/* .idea/* +.php_cs.cache +.phpunit.result.cache +test-report.xml diff --git a/composer.json b/composer.json index c62ca0d..db1c10a 100644 --- a/composer.json +++ b/composer.json @@ -14,15 +14,50 @@ "Pock\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Pock\\Tests\\": "tests/src/", + "Pock\\TestUtils\\": "tests/utils/" + } + }, "require": { "php": ">=7.1.0", "psr/http-client": "^1.0", "psr/http-message": "^1.0", "php-http/httplug": "^1.0 || ^2.0" }, + "require-dev": { + "squizlabs/php_codesniffer": "^3.6", + "phpmd/phpmd": "^2.10", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^0.12.87", + "jms/serializer": "^3.12", + "symfony/phpunit-bridge": "^5.2", + "symfony/var-dumper": "^5.2", + "symfony/serializer": "^5.2", + "nyholm/psr7": "^1.4" + }, "provide": { "psr/http-client-implementation": "1.0", "php-http/client-implementation": "1.0", "php-http/async-client-implementation": "1.0" + }, + "scripts": { + "phpunit": "./vendor/bin/simple-phpunit -c phpunit.xml.dist --coverage-text", + "phpunit-ci": "@php -dpcov.enabled=1 -dpcov.directory=. -dpcov.exclude=\"~vendor~\" ./vendor/bin/simple-phpunit --teamcity -c phpunit.xml.dist", + "phpmd": "./vendor/bin/phpmd src text ./phpmd.xml", + "phpcs": "./vendor/bin/phpcs -p src --runtime-set testVersion 7.1-8.0 && ./vendor/bin/phpcs -p tests --runtime-set testVersion 7.1-8.0 --warning-severity=0", + "phpstan": "./vendor/bin/phpstan analyse -c phpstan.neon src --memory-limit=-1", + "lint:fix": "./vendor/bin/phpcbf src", + "lint": [ + "@phpcs", + "@phpmd", + "@phpstan" + ], + "verify": [ + "@lint", + "@phpunit" + ] } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..d12440d --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,13 @@ + + + + + + + + + + src/ + tests/ + diff --git a/phpdoc.dist.xml b/phpdoc.dist.xml new file mode 100644 index 0000000..3743ea2 --- /dev/null +++ b/phpdoc.dist.xml @@ -0,0 +1,21 @@ + + + RetailCRM API Client + + docs/build/html + docs/build/cache + + + + public + + src + + + + diff --git a/phpmd.xml b/phpmd.xml new file mode 100644 index 0000000..550b68e --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,16 @@ + + + Ruleset + + + + + + + + tests/* + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..1494f51 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: max + paths: + - src + - tests diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d0314d3 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,39 @@ + + + + + + src + dev + + + + + + + + tests/src + + + + + + + + + diff --git a/src/Client.php b/src/Client.php index a14967e..1aea0e9 100644 --- a/src/Client.php +++ b/src/Client.php @@ -70,19 +70,19 @@ class Client implements ClientInterface, HttpClient, HttpAsyncClient public function sendAsyncRequest(RequestInterface $request): Promise { foreach ($this->mocks as $mock) { - if ($mock->isFired()) { + if (!$mock->available()) { continue; } if ($mock->getMatcher()->matches($request)) { if (null !== $mock->getResponse()) { - $mock->markAsFired(); + $mock->registerHit(); return new HttpFulfilledPromise($mock->getResponse()); } if (null !== $mock->getThrowable()) { - $mock->markAsFired(); + $mock->registerHit(); return new HttpRejectedPromise($mock->getThrowable()); } diff --git a/src/Creator/AbstractJmsSerializerCreator.php b/src/Creator/AbstractJmsSerializerCreator.php new file mode 100644 index 0000000..0e68cf3 --- /dev/null +++ b/src/Creator/AbstractJmsSerializerCreator.php @@ -0,0 +1,55 @@ +build(), static::getFormat()); // @phpstan-ignore-line + } + } catch (Throwable $throwable) { + return null; + } + } + + return null; + } + + /** + * Returns format for the serializer; + * + * @return string + */ + abstract protected static function getFormat(): string; +} diff --git a/src/Creator/JmsJsonSerializerCreator.php b/src/Creator/JmsJsonSerializerCreator.php new file mode 100644 index 0000000..62e1095 --- /dev/null +++ b/src/Creator/JmsJsonSerializerCreator.php @@ -0,0 +1,27 @@ +getUri()) === strtolower((string) $this->uri); + return ((string) $request->getUri()) === ((string) $this->uri); } } diff --git a/src/Mock.php b/src/Mock.php index d96ec64..101ead7 100644 --- a/src/Mock.php +++ b/src/Mock.php @@ -30,8 +30,11 @@ class Mock implements MockInterface /** @var \Throwable|null */ private $throwable; - /** @var bool */ - private $fired = false; + /** @var int */ + private $hits; + + /** @var int */ + private $maxHits; /** * Mock constructor. @@ -39,30 +42,40 @@ class Mock implements MockInterface * @param \Pock\Matchers\RequestMatcherInterface $matcher * @param \Psr\Http\Message\ResponseInterface|null $response * @param \Throwable|null $throwable + * @param int $maxHits */ - public function __construct(RequestMatcherInterface $matcher, ?ResponseInterface $response, ?Throwable $throwable) - { + public function __construct( + RequestMatcherInterface $matcher, + ?ResponseInterface $response, + ?Throwable $throwable, + int $maxHits + ) { $this->matcher = $matcher; $this->response = $response; $this->throwable = $throwable; + $this->maxHits = $maxHits; + $this->hits = 0; } - public function markAsFired(): MockInterface + /** + * @inheritDoc + */ + public function registerHit(): MockInterface { - $this->fired = true; + ++$this->hits; return $this; } /** - * @return bool + * @inheritDoc */ - public function isFired(): bool + public function available(): bool { - return $this->fired; + return $this->hits < $this->maxHits; } /** - * @return \Pock\Matchers\RequestMatcherInterface + * @inheritDoc */ public function getMatcher(): RequestMatcherInterface { @@ -70,7 +83,7 @@ class Mock implements MockInterface } /** - * @return \Psr\Http\Message\ResponseInterface|null + * @inheritDoc */ public function getResponse(): ?ResponseInterface { @@ -78,7 +91,7 @@ class Mock implements MockInterface } /** - * @return \Throwable|null + * @inheritDoc */ public function getThrowable(): ?Throwable { diff --git a/src/MockInterface.php b/src/MockInterface.php index 77d75d9..31e6833 100644 --- a/src/MockInterface.php +++ b/src/MockInterface.php @@ -22,18 +22,18 @@ use Throwable; interface MockInterface { /** - * Marks mock as already used. + * Registers a hit to the mock. * * @return \Pock\MockInterface */ - public function markAsFired(): MockInterface; + public function registerHit(): MockInterface; /** - * Returns true if mock was not used yet. + * Returns true if mock is still can be used. * * @return bool */ - public function isFired(): bool; + public function available(): bool; /** * Returns matcher for the request. diff --git a/src/PockBuilder.php b/src/PockBuilder.php index aaf1f95..075602d 100644 --- a/src/PockBuilder.php +++ b/src/PockBuilder.php @@ -35,6 +35,9 @@ class PockBuilder /** @var \Throwable|null */ private $throwable; + /** @var int */ + private $maxHits; + /** @var \Pock\MockInterface[] */ private $mocks; @@ -54,7 +57,7 @@ class PockBuilder * * @param string $scheme * - * @return $this + * @return self */ public function matchScheme(string $scheme = RequestScheme::HTTP): PockBuilder { @@ -66,7 +69,7 @@ class PockBuilder * * @param string $host * - * @return $this + * @return self */ public function matchHost(string $host): PockBuilder { @@ -100,6 +103,23 @@ class PockBuilder return $this; } + /** + * Repeat this mock provided amount of times. + * For example, if you pass 2 as an argument mock will be able to handle two identical requests. + * + * @param int $hits + * + * @return $this + */ + public function repeat(int $hits): PockBuilder + { + if ($hits > 0) { + $this->maxHits = $hits; + } + + return $this; + } + /** * Resets the builder. * @@ -110,6 +130,7 @@ class PockBuilder $this->matcher = new MultipleMatcher(); $this->response = null; $this->throwable = null; + $this->maxHits = 1; $this->mocks = []; return $this; @@ -143,9 +164,11 @@ class PockBuilder $this->matcher->addMatcher(new AnyRequestMatcher()); } - $this->mocks[] = new Mock($this->matcher, $this->response, $this->throwable); + $this->mocks[] = new Mock($this->matcher, $this->response, $this->throwable, $this->maxHits); + $this->matcher = new MultipleMatcher(); $this->response = null; $this->throwable = null; + $this->maxHits = 1; } } } diff --git a/src/Promise/HttpRejectedPromise.php b/src/Promise/HttpRejectedPromise.php index c0b6b16..0a36c51 100644 --- a/src/Promise/HttpRejectedPromise.php +++ b/src/Promise/HttpRejectedPromise.php @@ -62,6 +62,8 @@ class HttpRejectedPromise implements Promise /** * @inheritDoc * @throws \Throwable + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) */ public function wait($unwrap = true): void { diff --git a/src/Serializer/JmsSerializerDecorator.php b/src/Serializer/JmsSerializerDecorator.php new file mode 100644 index 0000000..565cca9 --- /dev/null +++ b/src/Serializer/JmsSerializerDecorator.php @@ -0,0 +1,48 @@ +serializer = $serializer; + $this->format = $format; + } + + /** + * @inheritDoc + */ + public function serialize($data): string + { + if (method_exists($this->serializer, 'serialize')) { + return $this->serializer->serialize($data, $this->format); + } + + return ''; + } +} diff --git a/src/Serializer/SerializerInterface.php b/src/Serializer/SerializerInterface.php new file mode 100644 index 0000000..7080a22 --- /dev/null +++ b/src/Serializer/SerializerInterface.php @@ -0,0 +1,28 @@ +add('Pock\\Tests', __DIR__ . '/src'); +$loader->add('Pock\\TestUtils', __DIR__ . '/utils'); diff --git a/tests/.gitkeep b/tests/src/.gitkeep similarity index 100% rename from tests/.gitkeep rename to tests/src/.gitkeep diff --git a/tests/src/Creator/JmsJsonSerializerCreatorTest.php b/tests/src/Creator/JmsJsonSerializerCreatorTest.php new file mode 100644 index 0000000..b87474a --- /dev/null +++ b/tests/src/Creator/JmsJsonSerializerCreatorTest.php @@ -0,0 +1,32 @@ +serialize(new SimpleObject())); + } +} diff --git a/tests/src/Creator/JmsXmlSerializerCreatorTest.php b/tests/src/Creator/JmsXmlSerializerCreatorTest.php new file mode 100644 index 0000000..073569d --- /dev/null +++ b/tests/src/Creator/JmsXmlSerializerCreatorTest.php @@ -0,0 +1,32 @@ +serialize(new SimpleObject())); + } +} diff --git a/tests/src/Factory/JsonSerializerFactoryTest.php b/tests/src/Factory/JsonSerializerFactoryTest.php new file mode 100644 index 0000000..ead50b4 --- /dev/null +++ b/tests/src/Factory/JsonSerializerFactoryTest.php @@ -0,0 +1,32 @@ +serialize(new SimpleObject())); + } +} diff --git a/tests/src/Factory/XmlSerializerFactoryTest.php b/tests/src/Factory/XmlSerializerFactoryTest.php new file mode 100644 index 0000000..3884abe --- /dev/null +++ b/tests/src/Factory/XmlSerializerFactoryTest.php @@ -0,0 +1,32 @@ +serialize(new SimpleObject())); + } +} diff --git a/tests/src/Matchers/AbstractRequestMatcherTest.php b/tests/src/Matchers/AbstractRequestMatcherTest.php new file mode 100644 index 0000000..f58d6e3 --- /dev/null +++ b/tests/src/Matchers/AbstractRequestMatcherTest.php @@ -0,0 +1,53 @@ +createRequest(static::TEST_METHOD, static::TEST_URI); + } + + /** + * @return \Nyholm\Psr7\Factory\Psr17Factory + */ + protected static function getPsr17Factory(): Psr17Factory + { + if (null === static::$psr17Factory) { + static::$psr17Factory = new Psr17Factory(); + } + + return static::$psr17Factory; + } +} diff --git a/tests/src/Matchers/AnyRequestMatcherTest.php b/tests/src/Matchers/AnyRequestMatcherTest.php new file mode 100644 index 0000000..e0c6d02 --- /dev/null +++ b/tests/src/Matchers/AnyRequestMatcherTest.php @@ -0,0 +1,26 @@ +matches(static::getTestRequest())); + } +} diff --git a/tests/src/Matchers/HostMatcherTest.php b/tests/src/Matchers/HostMatcherTest.php new file mode 100644 index 0000000..448fdcb --- /dev/null +++ b/tests/src/Matchers/HostMatcherTest.php @@ -0,0 +1,26 @@ +matches(static::getTestRequest())); + } +} diff --git a/tests/src/Matchers/MultipleMatcherTest.php b/tests/src/Matchers/MultipleMatcherTest.php new file mode 100644 index 0000000..7b792ba --- /dev/null +++ b/tests/src/Matchers/MultipleMatcherTest.php @@ -0,0 +1,34 @@ +addMatcher(new AnyRequestMatcher()); + + self::assertTrue($matcher->matches(static::getTestRequest())); + self::assertFalse($matcher->matches(static::getPsr17Factory() + ->createRequest(RequestMethod::GET, 'https://test.com'))); + } +} diff --git a/tests/src/Matchers/SchemeMatcherTest.php b/tests/src/Matchers/SchemeMatcherTest.php new file mode 100644 index 0000000..327d018 --- /dev/null +++ b/tests/src/Matchers/SchemeMatcherTest.php @@ -0,0 +1,27 @@ +matches(static::getTestRequest())); + } +} diff --git a/tests/src/Matchers/UriMatcherTest.php b/tests/src/Matchers/UriMatcherTest.php new file mode 100644 index 0000000..16c700e --- /dev/null +++ b/tests/src/Matchers/UriMatcherTest.php @@ -0,0 +1,28 @@ +matches(static::getTestRequest())); + self::assertTrue((new UriMatcher(static::getPsr17Factory()->createUri(self::TEST_URI))) + ->matches(static::getTestRequest())); + } +} diff --git a/tests/utils/SimpleObject.php b/tests/utils/SimpleObject.php new file mode 100644 index 0000000..26e23c3 --- /dev/null +++ b/tests/utils/SimpleObject.php @@ -0,0 +1,38 @@ + + + + + +EOF; + + /** + * @var string + * + * @JMS\Type("string") + * @JMS\SerializedName("field") + */ + private $field = 'test'; +}