diff --git a/.gitattributes b/.gitattributes
index 87f3679..2d44cfe 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,6 +2,5 @@
/tests export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
-/Robofile.php export-ignore
/*.md export-ignore
/*.yml export-ignore
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..b01043c
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+version: 2
+updates:
+ -
+ package-ecosystem: composer
+ directory: "/"
+ schedule:
+ interval: monthly
+ versioning-strategy: auto
+ groups:
+ dev-dependencies:
+ dependency-type: "development"
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index c563b02..879203e 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -8,11 +8,11 @@ jobs:
strategy:
matrix:
- php: [7.4, 8.0]
+ php: [8.1, 8.2, 8.3]
steps:
- name: Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -26,6 +26,12 @@ jobs:
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-interaction --no-suggest
+ - name: Execute Code Sniffer
+ run: vendor/bin/phpcs
+
+ - name: Execute PHP Stan
+ run: vendor/bin/phpstan
+
- name: Run test suite
run: |
php -S 127.0.0.1:8000 -t tests/data/app >/dev/null 2>&1 &
diff --git a/composer.json b/composer.json
index 54ac7f8..ce86167 100644
--- a/composer.json
+++ b/composer.json
@@ -1,10 +1,13 @@
{
"name": "codeception/module-phpbrowser",
"description": "Codeception module for testing web application over HTTP",
- "keywords": [ "codeception", "http", "functional-testing" ],
- "homepage": "https://codeception.com/",
- "type": "library",
"license": "MIT",
+ "type": "library",
+ "keywords": [
+ "codeception",
+ "http",
+ "functional-testing"
+ ],
"authors": [
{
"name": "Michael Bodnarchuk"
@@ -13,21 +16,25 @@
"name": "Gintautas Miselis"
}
],
- "minimum-stability": "RC",
+ "homepage": "https://codeception.com/",
"require": {
- "php": "^7.4 | ^8.0",
+ "php": "^8.1",
"ext-json": "*",
+ "codeception/codeception": "*@dev",
+ "codeception/lib-innerbrowser": "*@dev",
"guzzlehttp/guzzle": "^7.4",
- "codeception/lib-innerbrowser": "^2.0 | *@dev",
- "codeception/codeception": "^4.1 | *@dev"
+ "symfony/browser-kit": "^5.4 | ^6.0 | ^7.0"
},
"require-dev": {
"ext-curl": "*",
+ "squizlabs/php_codesniffer": "^3.10",
+ "phpstan/phpstan": "^1.10",
"aws/aws-sdk-php": "^3.199",
"codeception/module-rest": "^2.0 | *@dev"
},
"conflict": {
- "codeception/codeception": "<4.1"
+ "codeception/codeception": "<5.0",
+ "codeception/lib-innerbrowser": "<3.0"
},
"suggest": {
"codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests"
@@ -38,6 +45,7 @@
]
},
"config": {
- "classmap-authoritative": true
+ "classmap-authoritative": true,
+ "sort-packages": true
}
}
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..7b36367
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,23 @@
+
+
+ The coding standard.
+
+
+
+ src
+
+
+
+
+
+
+
+ error
+
+
+
+
+
+ warning
+
+
\ No newline at end of file
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..dec5108
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,4 @@
+parameters:
+ paths:
+ - ./src
+ level: 5
\ No newline at end of file
diff --git a/readme.md b/readme.md
index 144cbb8..3c26e32 100644
--- a/readme.md
+++ b/readme.md
@@ -9,7 +9,7 @@ A Codeception module for testing web application over HTTP.
## Requirements
-* `PHP 7.4` or higher.
+* `PHP 8.0` or higher.
## Installation
diff --git a/src/Codeception/Lib/Connector/Guzzle.php b/src/Codeception/Lib/Connector/Guzzle.php
index 3816b1d..55594bd 100644
--- a/src/Codeception/Lib/Connector/Guzzle.php
+++ b/src/Codeception/Lib/Connector/Guzzle.php
@@ -23,6 +23,9 @@
class Guzzle extends AbstractBrowser
{
+ /**
+ * @var array
+ */
protected array $requestOptions = [
'allow_redirects' => false,
'headers' => [],
@@ -115,7 +118,7 @@ protected function createResponse(Psr7Response $psr7Response): BrowserKitRespons
$contentType = 'text/html';
}
- if (strpos($contentType, 'charset=') === false) {
+ if (str_contains($contentType, 'charset=') === false) {
if (preg_match('#]+charset *= *["\']?([a-zA-Z\-0-9]+)#i', $body, $matches)) {
$contentType .= ';charset=' . $matches[1];
}
@@ -159,10 +162,10 @@ protected function createResponse(Psr7Response $psr7Response): BrowserKitRespons
protected function getAbsoluteUri(string $uri): string
{
$baseUri = $this->client->getConfig('base_uri');
- if (strpos($uri, '://') === false && strpos($uri, '//') !== 0) {
- if (strpos($uri, '/') === 0) {
+ if (str_contains($uri, '://') === false && !str_starts_with($uri, '//')) {
+ if (str_starts_with($uri, '/')) {
$baseUriPath = $baseUri->getPath();
- if (!empty($baseUriPath) && strpos($uri, (string) $baseUriPath) === 0) {
+ if (!empty($baseUriPath) && str_starts_with($uri, (string) $baseUriPath)) {
$uri = substr($uri, strlen($baseUriPath));
}
@@ -178,9 +181,9 @@ protected function getAbsoluteUri(string $uri): string
return Uri::mergeUrls((string)$baseUri, $uri);
}
- protected function doRequest($request)
+ protected function doRequest(object $request)
{
- /** @var $request BrowserKitRequest **/
+ /** @var BrowserKitRequest $request **/
$guzzleRequest = new Psr7Request(
$request->getMethod(),
$request->getUri(),
@@ -190,17 +193,17 @@ protected function doRequest($request)
$options = $this->requestOptions;
$options['cookies'] = $this->extractCookies($guzzleRequest->getUri()->getHost());
$multipartData = $this->extractMultipartFormData($request);
- if (!empty($multipartData)) {
+ if ($multipartData !== []) {
$options['multipart'] = $multipartData;
}
$formData = $this->extractFormData($request);
- if (empty($multipartData) && $formData) {
+ if ($multipartData === [] && $formData) {
$options['form_params'] = $formData;
}
try {
- if (null !== $this->awsCredentials) {
+ if ($this->awsCredentials instanceof AwsCredentials) {
$response = $this->client->send($this->awsSignature->signRequest($guzzleRequest, $this->awsCredentials), $options);
} else {
$response = $this->client->send($guzzleRequest, $options);
@@ -213,6 +216,7 @@ protected function doRequest($request)
$response = $exception->getResponse();
}
+ // @phpstan-ignore-next-line
return $this->createResponse($response);
}
@@ -227,7 +231,7 @@ protected function extractHeaders(BrowserKitRequest $request): array
$contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true];
foreach ($server as $header => $val) {
$header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES);
- if (strpos($header, 'Http-') === 0) {
+ if (str_starts_with($header, 'Http-')) {
$headers[substr($header, 5)] = $val;
} elseif (isset($contentHeaders[$header])) {
$headers[$header] = $val;
@@ -237,6 +241,9 @@ protected function extractHeaders(BrowserKitRequest $request): array
return $headers;
}
+ /**
+ * @return array|null
+ */
protected function extractFormData(BrowserKitRequest $browserKitRequest): ?array
{
if (!in_array(strtoupper($browserKitRequest->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
@@ -257,14 +264,17 @@ protected function extractFormData(BrowserKitRequest $browserKitRequest): ?array
return $browserKitRequest->getParameters();
}
- protected function extractMultipartFormData(BrowserKitRequest $browserKitRequest)
+ /**
+ * @return array
+ */
+ protected function extractMultipartFormData(BrowserKitRequest $browserKitRequest): array
{
if (!in_array(strtoupper($browserKitRequest->getMethod()), ['POST', 'PUT', 'PATCH'])) {
return [];
}
$parts = $this->mapFiles($browserKitRequest->getFiles());
- if (empty($parts)) {
+ if ($parts === []) {
return [];
}
@@ -275,11 +285,14 @@ protected function extractMultipartFormData(BrowserKitRequest $browserKitRequest
return $parts;
}
- protected function formatMultipart($parts, $key, $value)
+ /**
+ * @return array
+ */
+ protected function formatMultipart(mixed $parts, string $key, mixed $value): array
{
if (is_array($value)) {
foreach ($value as $subKey => $subValue) {
- $parts = array_merge($this->formatMultipart([], $key.sprintf('[%s]', $subKey), $subValue), $parts);
+ $parts = array_merge($this->formatMultipart([], $key . sprintf('[%s]', $subKey), $subValue), $parts);
}
return $parts;
@@ -289,7 +302,11 @@ protected function formatMultipart($parts, $key, $value)
return $parts;
}
- protected function mapFiles($requestFiles, $arrayName = ''): array
+ /**
+ * @param array $requestFiles
+ * @return array
+ */
+ protected function mapFiles(array $requestFiles, ?string $arrayName = ''): array
{
$files = [];
foreach ($requestFiles as $name => $info) {
@@ -329,7 +346,7 @@ protected function mapFiles($requestFiles, $arrayName = ''): array
return $files;
}
- protected function extractCookies($host): GuzzleCookieJar
+ protected function extractCookies(string $host): GuzzleCookieJar
{
$jar = [];
$cookies = $this->getCookieJar()->all();
@@ -345,7 +362,7 @@ protected function extractCookies($host): GuzzleCookieJar
return new GuzzleCookieJar(false, $jar);
}
- public static function createHandler($handler): GuzzleHandlerStack
+ public static function createHandler(mixed $handler): GuzzleHandlerStack
{
if ($handler instanceof GuzzleHandlerStack) {
return $handler;
@@ -360,7 +377,7 @@ public static function createHandler($handler): GuzzleHandlerStack
}
if (is_string($handler) && class_exists($handler)) {
- return GuzzleHandlerStack::create(new $handler);
+ return GuzzleHandlerStack::create(new $handler());
}
if (is_callable($handler)) {
@@ -370,7 +387,10 @@ public static function createHandler($handler): GuzzleHandlerStack
return GuzzleHandlerStack::create();
}
- public function setAwsAuth($config): void
+ /**
+ * @param array $config
+ */
+ public function setAwsAuth(array $config): void
{
$this->awsCredentials = new AwsCredentials($config['key'], $config['secret']);
$this->awsSignature = new AwsSignatureV4($config['service'], $config['region']);
diff --git a/src/Codeception/Module/PhpBrowser.php b/src/Codeception/Module/PhpBrowser.php
index edc99b6..b8ce650 100644
--- a/src/Codeception/Module/PhpBrowser.php
+++ b/src/Codeception/Module/PhpBrowser.php
@@ -22,48 +22,42 @@
*
* If test fails stores last shown page in 'output' dir.
*
- * ## Status
- *
- * * Maintainer: **davert**
- * * Stability: **stable**
- * * Contact: codeception@codeception.com
- *
- *
* ## Configuration
*
* * url *required* - start url of your app
* * headers - default headers are set before each test.
- * * handler (default: curl) - Guzzle handler to use. By default curl is used, also possible to pass `stream`, or any valid class name as [Handler](http://docs.guzzlephp.org/en/latest/handlers-and-middleware.html#handlers).
+ * * handler (default: curl) - Guzzle handler to use. By default curl is used, also possible to pass `stream`, or any valid class name as [Handler](https://docs.guzzlephp.org/en/latest/handlers-and-middleware.html#handlers).
* * middleware - Guzzle middlewares to add. An array of valid callables is required.
* * curl - curl options
* * cookies - ...
* * auth - ...
* * verify - ...
- * * .. those and other [Guzzle Request options](http://docs.guzzlephp.org/en/latest/request-options.html)
- *
+ * * .. those and other [Guzzle Request options](https://docs.guzzlephp.org/en/latest/request-options.html)
*
- * ### Example (`acceptance.suite.yml`)
*
- * modules:
- * enabled:
- * - PhpBrowser:
- * url: 'http://localhost'
- * auth: ['admin', '123345']
- * curl:
- * CURLOPT_RETURNTRANSFER: true
- * cookies:
- * cookie-1:
- * Name: userName
- * Value: john.doe
- * cookie-2:
- * Name: authToken
- * Value: 1abcd2345
- * Domain: subdomain.domain.com
- * Path: /admin/
- * Expires: 1292177455
- * Secure: true
- * HttpOnly: false
+ * ### Example (`Acceptance.suite.yml`)
*
+ * ```yaml
+ * modules:
+ * enabled:
+ * - PhpBrowser:
+ * url: 'http://localhost' # Internationalized domain names (IDN) need to be passed in punycode
+ * auth: ['admin', '123345']
+ * curl:
+ * CURLOPT_RETURNTRANSFER: true
+ * cookies:
+ * cookie-1:
+ * Name: userName
+ * Value: john.doe
+ * cookie-2:
+ * Name: authToken
+ * Value: 1abcd2345
+ * Domain: subdomain.domain.com
+ * Path: /admin/
+ * Expires: 1292177455
+ * Secure: true
+ * HttpOnly: false
+ * ```
*
* All SSL certification checks are disabled by default.
* Use Guzzle request options to configure certifications and others.
@@ -74,7 +68,7 @@
*
* Properties:
*
- * * `guzzle` - contains [Guzzle](http://guzzlephp.org/) client instance: `\GuzzleHttp\Client`
+ * * `guzzle` - contains [Guzzle](https://guzzlephp.org/) client instance: `\GuzzleHttp\Client`
* * `client` - Symfony BrowserKit instance.
*
*/
@@ -83,12 +77,12 @@ class PhpBrowser extends InnerBrowser implements Remote, MultiSession
/**
* @var string[]
*/
- protected $requiredFields = ['url'];
+ protected array $requiredFields = ['url'];
/**
- * @var array
+ * @var array
*/
- protected $config = [
+ protected array $config = [
'headers' => [],
'verify' => false,
'expect' => false,
@@ -135,7 +129,7 @@ public function _initialize()
public function _before(TestInterface $test)
{
- if (!$this->client) {
+ if (!$this->client instanceof AbstractBrowser) {
$this->client = new Guzzle();
}
@@ -155,12 +149,14 @@ public function setHeader(string $name, string $value): void
$this->haveHttpHeader($name, $value);
}
- public function amHttpAuthenticated($username, $password): void
+ public function amHttpAuthenticated(string $username, string $password): void
{
- $this->client->setAuth($username, $password);
+ if ($this->client instanceof Guzzle) {
+ $this->client->setAuth($username, $password);
+ }
}
- public function amOnUrl($url): void
+ public function amOnUrl(string $url): void
{
$host = Uri::retrieveHost($url);
$config = $this->config;
@@ -175,7 +171,7 @@ public function amOnUrl($url): void
$this->amOnPage($page);
}
- public function amOnSubdomain($subdomain): void
+ public function amOnSubdomain(string $subdomain): void
{
$url = $this->config['url'];
$url = preg_replace('#(https?://)(.*\.)(.*\.)#', "$1$3", $url); // removing current subdomain
@@ -193,7 +189,7 @@ protected function onReconfigure()
/**
* Low-level API method.
- * If Codeception commands are not enough, use [Guzzle HTTP Client](http://guzzlephp.org/) methods directly
+ * If Codeception commands are not enough, use [Guzzle HTTP Client](https://guzzlephp.org/) methods directly
*
* Example:
*
@@ -206,23 +202,18 @@ protected function onReconfigure()
*
* It is not recommended to use this command on a regular basis.
* If Codeception lacks important Guzzle Client methods, implement them and submit patches.
- *
- * @return mixed
*/
- public function executeInGuzzle(Closure $function)
+ public function executeInGuzzle(Closure $function): mixed
{
return $function($this->guzzle);
}
- /**
- * @return int|string
- */
- public function _getResponseCode()
+ public function _getResponseCode(): int|string
{
return $this->getResponseStatusCode();
}
- public function _initializeSession()
+ public function _initializeSession(): void
{
// independent sessions need independent cookies
$this->client = new Guzzle();
@@ -245,20 +236,23 @@ public function _prepareSession(): void
$defaults['base_uri'] = $this->config['url'];
$defaults['curl'] = $curlOptions;
- $handler = Guzzle::createHandler($this->config['handler']);
- if ($handler && is_array($this->config['middleware'])) {
+ $handlerStack = Guzzle::createHandler($this->config['handler']);
+ if (is_array($this->config['middleware'])) {
foreach ($this->config['middleware'] as $middleware) {
- $handler->push($middleware);
+ $handlerStack->push($middleware);
}
}
- $defaults['handler'] = $handler;
+ $defaults['handler'] = $handlerStack;
$this->guzzle = new GuzzleClient($defaults);
$this->client->setRefreshMaxInterval($this->config['refresh_max_interval']);
$this->client->setClient($this->guzzle);
}
+ /**
+ * @return array
+ */
public function _backupSession()
{
return [
@@ -269,14 +263,20 @@ public function _backupSession()
];
}
- public function _loadSession($session)
+ /**
+ * @param array $session
+ */
+ public function _loadSession($session): void
{
foreach ($session as $key => $val) {
$this->$key = $val;
}
}
- public function _closeSession($session = null)
+ /**
+ * @param ?array $session
+ */
+ public function _closeSession($session = null): void
{
unset($session);
}
diff --git a/tests/_support/UnitTester.php b/tests/_support/UnitTester.php
index e19544a..6aeac82 100644
--- a/tests/_support/UnitTester.php
+++ b/tests/_support/UnitTester.php
@@ -1,6 +1,5 @@
GET();
}
}
-class contentType1 {
- function GET() {
+class contentType1
+{
+ function GET()
+ {
header('Content-Type:', true);
- include __DIR__.'/view/content_type.php';
+ include __DIR__ . '/view/content_type.php';
}
}
-class contentType2 {
- function GET() {
+class contentType2
+{
+ function GET()
+ {
header('Content-Type:', true);
- include __DIR__.'/view/content_type2.php';
+ include __DIR__ . '/view/content_type2.php';
}
}
-class unsetCookie {
- function GET() {
+class unsetCookie
+{
+ function GET()
+ {
header('Set-Cookie: a=; Expires=Thu, 01 Jan 1970 00:00:01 GMT');
}
}
-class basehref {
- function GET() {
- include __DIR__.'/view/basehref.php';
+class basehref
+{
+ function GET()
+ {
+ include __DIR__ . '/view/basehref.php';
}
}
-class jserroronload {
- function GET() {
- include __DIR__.'/view/jserroronload.php';
+class jserroronload
+{
+ function GET()
+ {
+ include __DIR__ . '/view/jserroronload.php';
}
}
-class userAgent {
- function GET() {
+class userAgent
+{
+ function GET()
+ {
echo $_SERVER['HTTP_USER_AGENT'];
}
}
-class minimal {
- function GET() {
- include __DIR__.'/view/minimal.php';
+class minimal
+{
+ function GET()
+ {
+ include __DIR__ . '/view/minimal.php';
}
}
diff --git a/tests/data/app/data.php b/tests/data/app/data.php
index c1877c2..d7dca0e 100755
--- a/tests/data/app/data.php
+++ b/tests/data/app/data.php
@@ -1,9 +1,11 @@
$class) {
- $regex = str_replace('/', '\/', $regex);
- $regex = '^' . $regex . '\/?$';
- if (preg_match("/$regex/i", $path, $matches)) {
- $found = true;
- if (class_exists($class)) {
- $obj = new $class;
- if (method_exists($obj, $method)) {
- $obj->$method($matches);
- } else {
- throw new BadMethodCallException("Method, $method, not supported.");
- }
+ foreach ($urls as $regex => $class) {
+ $regex = str_replace('/', '\/', $regex);
+ $regex = '^' . $regex . '\/?$';
+ if (preg_match("/$regex/i", $path, $matches)) {
+ $found = true;
+ if (class_exists($class)) {
+ $obj = new $class();
+ if (method_exists($obj, $method)) {
+ $obj->$method($matches);
} else {
- throw new Exception("Class, $class, not found.");
+ throw new BadMethodCallException("Method, $method, not supported.");
}
- break;
+ } else {
+ throw new Exception("Class, $class, not found.");
}
+ break;
}
- if (!$found) {
- throw new Exception("URL, $path, not found.");
- }
+ }
+ if (!$found) {
+ throw new Exception("URL, $path, not found.");
}
}
+}
diff --git a/tests/data/app/index.php b/tests/data/app/index.php
index 4c65804..243d816 100644
--- a/tests/data/app/index.php
+++ b/tests/data/app/index.php
@@ -1,10 +1,12 @@
'index',
'/info' => 'info',
@@ -42,5 +44,4 @@
'/jserroronload' => 'jserroronload',
'/minimal' => 'minimal',
);
-
glue::stick($urls);
diff --git a/tests/data/app/view/external_url.php b/tests/data/app/view/external_url.php
index 4b2be70..ca0435a 100644
--- a/tests/data/app/view/external_url.php
+++ b/tests/data/app/view/external_url.php
@@ -1,5 +1,5 @@
- Next
+ Next
\ No newline at end of file
diff --git a/tests/data/app/view/form/bug3824.php b/tests/data/app/view/form/bug3824.php
index 221fb98..64a02ff 100644
--- a/tests/data/app/view/form/bug3824.php
+++ b/tests/data/app/view/form/bug3824.php
@@ -7,9 +7,9 @@