Skip to content

Commit

Permalink
Added PHPUnit tests for the API server + coverage. (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexSkrypnyk authored Jan 17, 2025
1 parent fa7eef3 commit 7d2137a
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 44 deletions.
16 changes: 9 additions & 7 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Ignore files for distribution archives.
/.gitattributes export-ignore
/.gitignore export-ignore
/phpcs.xml export-ignore
/phpmd.xml export-ignore
/phpstan.neon export-ignore
/tests export-ignore
/renovate.json export-ignore
/.logs export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.phpunit.cache export-ignore
/phpcs.xml export-ignore
/phpmd.xml export-ignore
/phpstan.neon export-ignore
/renovate.json export-ignore
/tests export-ignore
34 changes: 32 additions & 2 deletions .github/workflows/test-php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,40 @@ jobs:
run: composer lint
continue-on-error: ${{ vars.CI_LINT_IGNORE_FAILURE == '1' }}

- name: Run tests
run: composer test
- name: Run unit tests
run: composer test-coverage
continue-on-error: ${{ vars.CI_TEST_IGNORE_FAILURE == '1' }}

- name: Run BDD tests
run: composer test-bdd
continue-on-error: ${{ vars.CI_TEST_IGNORE_FAILURE == '1' }}

- name: Upload coverage report as an artifact
uses: actions/upload-artifact@v4
with:
name: ${{github.job}}-code-coverage-report-${{ matrix.php-versions }}
path: .logs
include-hidden-files: true
if-no-files-found: error

- name: Upload test results to Codecov
uses: codecov/test-results-action@v1
if: ${{ env.CODECOV_TOKEN != '' }}
with:
files: .logs/phpunit/junit.xml,.logs/behat/test_results/default.xml
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v5
if: ${{ env.CODECOV_TOKEN != '' }}
with:
files: .logs/phpunit/cobertura.xml,.logs/behat/cobertura.xml
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

- name: Setup tmate session
if: ${{ !cancelled() && github.event.inputs.enable_terminal }}
uses: mxschmitt/action-tmate@v3
Expand Down
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
vendor
composer.lock
/.logs
/.phpunit.cache
/composer.lock
/vendor
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
[![GitHub Issues](https://img.shields.io/github/issues/drevops/behat-phpserver.svg)](https://github.com/drevops/behat-phpserver/issues)
[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/drevops/behat-phpserver.svg)](https://github.com/drevops/behat-phpserver/pulls)
[![Test PHP](https://github.com/drevops/behat-phpserver/actions/workflows/test-php.yml/badge.svg)](https://github.com/drevops/behat-phpserver/actions/workflows/test-php.yml)
[![codecov](https://codecov.io/gh/drevops/behat-phpserver/graph/badge.svg?token=KZCCZXN5C4)](https://codecov.io/gh/drevops/behat-phpserver)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/drevops/behat-phpserver)
[![Total Downloads](https://poser.pugx.org/drevops/behat-phpserver/downloads)](https://packagist.org/packages/drevops/behat-phpserver)
![LICENSE](https://img.shields.io/github/license/drevops/behat-phpserver)
Expand Down Expand Up @@ -193,6 +194,7 @@ composer lint-fix

```bash
composer test
composer test-bdd
```

---
Expand Down
73 changes: 43 additions & 30 deletions apiserver/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,21 @@

declare(strict_types=1);

namespace DrevOps\BehatPhpServer\ApiServer;

class ApiServer {

/**
* The received requests.
*
* @var array<int|\Request>
* @var array<int|Request>
*/
protected array $requests = [];

/**
* The queued responses.
*
* @var array<int|\Response>
* @var array<int|Response>
*/
protected array $responses = [];

Expand Down Expand Up @@ -116,8 +118,8 @@ public function __destruct() {
*/
public function handleRequest(): void {
$request = new Request(
is_scalar($_SERVER['REQUEST_METHOD']) && is_string($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET',
is_scalar($_SERVER['REQUEST_URI']) ? (string) strtok(strval($_SERVER['REQUEST_URI']), '?') : '/',
isset($_SERVER['REQUEST_METHOD']) && is_scalar($_SERVER['REQUEST_METHOD']) && is_string($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET',
isset($_SERVER['REQUEST_URI']) && is_scalar($_SERVER['REQUEST_URI']) ? (string) strtok(strval($_SERVER['REQUEST_URI']), '?') : '/',
getallheaders(),
file_get_contents('php://input') ?: ''
);
Expand Down Expand Up @@ -160,7 +162,7 @@ public function handleRequest(): void {
$this->responses[] = $response;
}

$this->handleResponse(new \Response(201, 'Created'));
$this->handleResponse(new Response(201, 'Created'));
}
else {
$this->requests[] = $request;
Expand All @@ -171,7 +173,7 @@ public function handleRequest(): void {
else {
$response = array_shift($this->responses);

if (!$response instanceof \Response) {
if (!$response instanceof Response) {
throw new \Exception(sprintf('Invalid response in queue: %s', print_r($response, TRUE)), 500);
}

Expand All @@ -183,10 +185,10 @@ public function handleRequest(): void {
/**
* Send the response.
*
* @param \Response $response
* @param \DrevOps\BehatPhpServer\ApiServer\Response $response
* The response object.
*/
protected function handleResponse(\Response $response): void {
protected function handleResponse(Response $response): void {
$response->headers += [
'X-Received-Requests' => (string) count($this->requests),
'X-Queued-Responses' => (string) count($this->responses),
Expand All @@ -198,10 +200,10 @@ protected function handleResponse(\Response $response): void {
/**
* Send the response.
*
* @param \Response $response
* @param \DrevOps\BehatPhpServer\ApiServer\Response $response
* The response object.
*/
public static function sendResponse(\Response $response): void {
public static function sendResponse(Response $response): void {
// Set the full status line manually to include the custom reason.
$protocol = is_scalar($_SERVER['SERVER_PROTOCOL']) ? strval($_SERVER['SERVER_PROTOCOL']) : 'HTTP/1.1';
header(sprintf('%s %s %s', $protocol, $response->code, $response->reason));
Expand Down Expand Up @@ -268,7 +270,13 @@ final public function __construct(
* The response object.
*/
public static function fromArray(array $data): static {
$data['method'] = $data['method'] ?? 'GET';
$data += [
'method' => 'GET',
'code' => 200,
'reason' => 'OK',
'headers' => [],
'body' => '',
];

if (!is_string($data['method'])) {
throw new \InvalidArgumentException('Method must be a string.');
Expand All @@ -282,46 +290,51 @@ public static function fromArray(array $data): static {
throw new \InvalidArgumentException('Response code is required.');
}

$data['code'] = intval($data['code']);

if ($data['code'] < 100 || $data['code'] > 599) {
throw new \InvalidArgumentException('Response code must be a number between 100 and 599.');
}

$data['headers'] = $data['headers'] ?? [];
if (!is_array($data['headers'])) {
throw new \InvalidArgumentException('Headers must be an array.');
}

// Check that both keys and values are strings.
foreach ($data['headers'] as $header_name => $header_value) {
if (!is_string($header_name) || !is_scalar($header_value)) {
throw new \InvalidArgumentException(sprintf('Header "%s" value must be a string.', $header_name));
}
}

$data['headers'] = array_map(fn($value): string => is_scalar($value) ? strval($value) : '', $data['headers']);

if (isset($data['body'])) {
if (!is_string($data['body'])) {
throw new \InvalidArgumentException('Body must be a string.');
}

$response_body = base64_decode($data['body']);
// @phpstan-ignore-next-line
if ($response_body === FALSE) {
throw new \InvalidArgumentException('Body is not a valid base64 encoded string.');
}

$data['body'] = $response_body;
}
else {
$data['body'] = '';
$data['body'] = base64_decode($data['body']);
}

if (!empty($data['reason']) && !is_string($data['reason'])) {
if (empty($data['reason']) || !is_string($data['reason'])) {
throw new \InvalidArgumentException('Reason must be a string.');
}

$data['reason'] = $data['reason'] ?: 'OK';

return new static($data['code'], $data['reason'], $data['headers'], $data['body']);
}

}

$server = new ApiServer();
// Allow to skip the script run.
if (getenv('SCRIPT_RUN_SKIP') != 1) {
$server = new ApiServer();

try {
$server->handleRequest();
}
catch (\Throwable $throwable) {
ApiServer::sendResponse(new \Response($throwable->getCode(), $throwable->getMessage(), [], ['error' => $throwable->getMessage()]));
try {
$server->handleRequest();
}
catch (\Throwable $throwable) {
ApiServer::sendResponse(new Response($throwable->getCode(), $throwable->getMessage(), [], ['error' => $throwable->getMessage()]));
}
}
18 changes: 18 additions & 0 deletions behat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,21 @@ default:
sessions:
default:
browserkit_http: ~
DVDoug\Behat\CodeCoverage\Extension:
filter:
include:
directories:
'%paths.base%/src': ~
reports:
text:
showColors: true
showOnlySummary: true
html:
target: '%paths.base%/.logs/behat/.coverage-html'
cobertura:
target: '%paths.base%/.logs/behat/cobertura.xml'

formatters:
pretty: true
junit:
output_path: '%paths.base%/.logs/behat/test_results'
14 changes: 13 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
"behat/mink-browserkit-driver": "^2.2",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"drupal/coder": "^8.3",
"dvdoug/behat-code-coverage": "^5.3",
"ergebnis/composer-normalize": "^2.45",
"escapestudios/symfony2-coding-standard": "^3.15",
"friends-of-behat/mink-extension": "^2.7.5",
"phpstan/phpstan": "^2",
"phpunit/phpunit": "^11",
"rector/rector": "^2.0",
"squizlabs/php_codesniffer": "^3.11.2",
"symfony/http-client": "^6 || ^7.2.2"
Expand All @@ -41,6 +43,14 @@
"autoload": {
"psr-0": {
"DrevOps\\BehatPhpServer": "src/"
},
"classmap": [
"apiserver"
]
},
"autoload-dev": {
"psr-4": {
"DrevOps\\BehatPhpServer\\Tests\\": "tests/phpunit"
}
},
"config": {
Expand All @@ -60,6 +70,8 @@
"phpcbf"
],
"reset": "rm -Rf vendor composer.lock",
"test": "behat"
"test": "phpunit --no-coverage",
"test-bdd": "behat",
"test-coverage": "php -d pcov.directory=. vendor/bin/phpunit"
}
}
19 changes: 19 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,23 @@
<file>apiserver</file>
<file>tests/</file>

<!-- Allow long array lines in tests. -->
<rule ref="Drupal.Arrays.Array.LongLineDeclaration">
<exclude-pattern>*.Test\.php</exclude-pattern>
<exclude-pattern>*.TestCase\.php</exclude-pattern>
<exclude-pattern>*.test</exclude-pattern>
</rule>

<!-- Allow missing class names in tests. -->
<rule ref="Drupal.Commenting.ClassComment.Missing">
<exclude-pattern>*.Test\.php</exclude-pattern>
<exclude-pattern>*.TestCase\.php</exclude-pattern>
<exclude-pattern>*.test</exclude-pattern>
</rule>
<!-- Allow missing function names in tests. -->
<rule ref="Drupal.Commenting.FunctionComment.Missing">
<exclude-pattern>*.Test\.php</exclude-pattern>
<exclude-pattern>*.TestCase\.php</exclude-pattern>
<exclude-pattern>*.test</exclude-pattern>
</rule>
</ruleset>
45 changes: 45 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.4/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
requireCoverageMetadata="true"
beStrictAboutCoverageMetadata="false"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
failOnWarning="true"
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnTestsThatTriggerErrors="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnPhpunitDeprecations="true">
<php>
<env name="SCRIPT_RUN_SKIP" value="1"/>
</php>
<testsuites>
<testsuite name="default">
<directory>tests/phpunit</directory>
</testsuite>
</testsuites>
<source restrictNotices="true"
restrictWarnings="true"
ignoreIndirectDeprecations="true">
<include>
<directory>apiserver</directory>
</include>
<exclude>
<directory>tests</directory>
</exclude>
</source>
<coverage pathCoverage="false"
ignoreDeprecatedCodeUnits="true"
disableCodeCoverageIgnore="false">
<report>
<html outputDirectory=".logs/phpunit/.coverage-html" lowUpperBound="50" highLowerBound="90"/>
<cobertura outputFile=".logs/phpunit/cobertura.xml"/>
</report>
</coverage>
<logging>
<junit outputFile=".logs/phpunit/junit.xml"/>
</logging>
</phpunit>
5 changes: 3 additions & 2 deletions src/DrevOps/BehatPhpServer/ApiServerContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class ApiServerContext extends PhpServerContext {
* """
* {
* "code": 200,
* "reason": "OK",
* "headers": {
* "Content-Type": "application/json"
* },
Expand Down Expand Up @@ -85,9 +86,9 @@ protected function prepareData(string $data): array {
$data += [
'code' => 200,
// @todo Validate reason.
'reason' => '',
'reason' => 'OK',
'headers' => [],
'body' => NULL,
'body' => '',
];

if (!is_numeric($data['code'])) {
Expand Down
Loading

0 comments on commit 7d2137a

Please sign in to comment.