From b8a3f6d3b18c56c0a08c449fb69dc1791e36c599 Mon Sep 17 00:00:00 2001 From: Thomas Meinusch Date: Thu, 23 Jan 2025 13:52:20 +0100 Subject: [PATCH 01/19] #23382 feat(*): run rector over source --- .gitignore | 3 +++ composer.json | 2 +- rector.php | 12 ++++++++++++ src/Context/HttpStreamContext.php | 7 ++----- src/Context/SftpStreamContext.php | 16 ++++++---------- src/Context/StreamContext.php | 2 +- src/Exception/FileStreamException.php | 2 +- src/FileStream.php | 8 ++------ 8 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 .gitignore create mode 100644 rector.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4bfee95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea/ +/vendor/ +composer.lock \ No newline at end of file diff --git a/composer.json b/composer.json index b7acdb1..8b5d0da 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ } }, "require": { - "php": ">=7.4", + "php": ">=8.4", "phpseclib/phpseclib": "~3.0" } } diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..b8afc16 --- /dev/null +++ b/rector.php @@ -0,0 +1,12 @@ +withPaths([ + __DIR__ . '/src', + ]) + ->withPhpSets() + ->withTypeCoverageLevel(0); diff --git a/src/Context/HttpStreamContext.php b/src/Context/HttpStreamContext.php index 74a2205..2c53b30 100644 --- a/src/Context/HttpStreamContext.php +++ b/src/Context/HttpStreamContext.php @@ -11,17 +11,14 @@ */ final class HttpStreamContext extends StreamContext { - public const PROTOCOL = 'http'; - - private string $method = 'GET'; + public const string PROTOCOL = 'http'; private array $headers = []; private string $content = ""; private string $userAgent = ""; private float $timeout = 10.0; - private function __construct(string $method) + private function __construct(private readonly string $method) { - $this->method = $method; } /** diff --git a/src/Context/SftpStreamContext.php b/src/Context/SftpStreamContext.php index 5616880..0b0ac90 100644 --- a/src/Context/SftpStreamContext.php +++ b/src/Context/SftpStreamContext.php @@ -14,18 +14,14 @@ */ final class SftpStreamContext extends StreamContext { - private const PROTOCOL = 'sftp'; + private const string PROTOCOL = 'sftp'; - private string $username; - private string $password; - private string $privateKey; - - private function __construct(string $username, string $password, string $privateKey) + private function __construct( + private readonly string $username, + private readonly string $password, + private readonly string $privateKey + ) { - $this->username = $username; - $this->password = $password; - $this->privateKey = $privateKey; - Stream::register(self::PROTOCOL); } diff --git a/src/Context/StreamContext.php b/src/Context/StreamContext.php index 93f7271..cf3f1f4 100644 --- a/src/Context/StreamContext.php +++ b/src/Context/StreamContext.php @@ -22,7 +22,7 @@ public function createStreamContext() $resource = stream_context_create($this->getContextOptions()); if (!is_resource($resource)) { - throw StreamContextException::fromMessage("Can't create stream context for: " . __CLASS__); + throw StreamContextException::fromMessage("Can't create stream context for: " . self::class); } return $resource; diff --git a/src/Exception/FileStreamException.php b/src/Exception/FileStreamException.php index feef989..0e6e321 100644 --- a/src/Exception/FileStreamException.php +++ b/src/Exception/FileStreamException.php @@ -11,7 +11,7 @@ */ class FileStreamException extends Exception { - public static function fromMessage(string $message, Exception $previous = null): self + public static function fromMessage(string $message, ?Exception $previous = null): self { return new self($message, 0, $previous); } diff --git a/src/FileStream.php b/src/FileStream.php index 5231f66..a14bc64 100644 --- a/src/FileStream.php +++ b/src/FileStream.php @@ -13,21 +13,17 @@ */ final class FileStream { - private string $url; private string $mode = "r"; - private ?StreamContext $streamContext; private string $fileExtension = ''; - private function __construct(string $url, ?StreamContext $streamContext) + private function __construct(private readonly string $url, private readonly ?StreamContext $streamContext) { - $this->url = $url; - $this->streamContext = $streamContext; } /** * Named constructor to create an instance base on the given streaming url and context parameters */ - public static function fromUrl(string $url, StreamContext $streamContext = null): self + public static function fromUrl(string $url, ?StreamContext $streamContext = null): self { return new self($url, $streamContext); } From 8be13f43f369bfd97610eb0faca089d23eecab70 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Fri, 24 Jan 2025 10:46:14 +0100 Subject: [PATCH 02/19] feat: Pipeline setup --- .github/workflows/phpstan.yml | 9 +++ .github/workflows/pint.yml | 27 +++++++ CHANGELOG.de.md | 7 -- CHANGELOG.en.md => CHANGELOG.md | 6 +- composer.json | 9 ++- phpstan.neon | 7 ++ pint.json | 128 ++++++++++++++++++++++++++++++++ src/Context/StreamContext.php | 4 +- 8 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/phpstan.yml create mode 100644 .github/workflows/pint.yml delete mode 100644 CHANGELOG.de.md rename CHANGELOG.en.md => CHANGELOG.md (83%) create mode 100644 phpstan.neon create mode 100644 pint.json diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..9600ee2 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,9 @@ +name: PHPStan +on: + pull_request: + push: + branches: + - 1.x +jobs: + phpstan: + uses: artemeon/.shared/.github/workflows/phpstan-php84-upwards.yml@main diff --git a/.github/workflows/pint.yml b/.github/workflows/pint.yml new file mode 100644 index 0000000..3af35de --- /dev/null +++ b/.github/workflows/pint.yml @@ -0,0 +1,27 @@ +name: Pint + +on: + - pull_request + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + pint: + name: Pint (PHP-CS-Fixer) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + coverage: none + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Composer install + run: composer install --no-interaction --no-ansi --no-progress + - name: Run Pint + run: ./vendor/bin/pint --test -v diff --git a/CHANGELOG.de.md b/CHANGELOG.de.md deleted file mode 100644 index 18b0abb..0000000 --- a/CHANGELOG.de.md +++ /dev/null @@ -1,7 +0,0 @@ -# Stream-Context - -## 0.1.0 - - Erstveröffentlichung - -## 0.1.1 - - Das Erzwingen von validen Dateiendungen wurde für File-URL's ohne explizite Dateiendung wie z.B "http://example.com/fetch_some_stream" deaktiviert. diff --git a/CHANGELOG.en.md b/CHANGELOG.md similarity index 83% rename from CHANGELOG.en.md rename to CHANGELOG.md index e916efb..252c3ef 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Stream-Context +## 0.1.1 +- The enforcement of valid file extensions has been deactivated for file URLs without an explicit file extension such as ‘http://example.com/fetch_some_stream’. + ## 0.1.0 - Initial release - -## 0.1.1 -- The enforcement of valid file extensions has been deactivated for file URLs without an explicit file extension such as ‘http://example.com/fetch_some_stream’. \ No newline at end of file diff --git a/composer.json b/composer.json index 8b5d0da..44a288b 100644 --- a/composer.json +++ b/composer.json @@ -5,10 +5,12 @@ "type": "library", "description": "Library to create a stream context", "keywords": [ - "php7", "stream", "context" ], + "scripts": { + "phpstan": "php ./vendor/bin/phpstan analyse --memory-limit=4G" + }, "authors": [ { "name": "Dietmar Simons", @@ -23,5 +25,10 @@ "require": { "php": ">=8.4", "phpseclib/phpseclib": "~3.0" + }, + "require-dev": { + "laravel/pint": "^1.20.0", + "phpstan/phpstan": "^2.1.2", + "rector/rector": "^2.0.7" } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..60afcbd --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phar://phpstan.phar/conf/bleedingEdge.neon + +parameters: + level: 5 + paths: + - src diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..ee8124a --- /dev/null +++ b/pint.json @@ -0,0 +1,128 @@ +{ + "preset": "psr12", + "rules": { + "align_multiline_comment": true, + "array_indentation": true, + "array_push": true, + "array_syntax": { + "syntax": "short" + }, + "assign_null_coalescing_to_coalesce_equal": true, + "binary_operator_spaces": true, + "blank_line_before_statement": true, + "cast_spaces": true, + "clean_namespace": true, + "combine_consecutive_issets": true, + "combine_consecutive_unsets": true, + "compact_nullable_typehint": true, + "concat_space": { + "spacing": "one" + }, + "fully_qualified_strict_types": true, + "function_to_constant": true, + "get_class_to_class_keyword": true, + "is_null": true, + "lambda_not_used_import": true, + "logical_operators": true, + "method_chaining_indentation": true, + "modernize_types_casting": true, + "multiline_whitespace_before_semicolons": true, + "no_empty_comment": true, + "no_empty_phpdoc": true, + "no_empty_statement": true, + "no_extra_blank_lines": { + "tokens": [ + "attribute", + "break", + "case", + "continue", + "curly_brace_block", + "default", + "extra", + "parenthesis_brace_block", + "return", + "square_brace_block", + "switch", + "throw", + "use", + "use_trait" + ] + }, + "no_multiline_whitespace_around_double_arrow": true, + "no_short_bool_cast": true, + "no_singleline_whitespace_before_semicolons": true, + "no_superfluous_elseif": false, + "no_superfluous_phpdoc_tags": true, + "no_trailing_comma_in_singleline": true, + "no_unneeded_control_parentheses": true, + "no_useless_concat_operator": true, + "no_useless_else": true, + "no_useless_nullsafe_operator": true, + "no_useless_return": true, + "no_whitespace_before_comma_in_array": true, + "nullable_type_declaration": true, + "object_operator_without_whitespace": true, + "ordered_imports": { + "imports_order": [ + "class", + "function", + "const" + ], + "sort_algorithm": "alpha" + }, + "ordered_interfaces": true, + "ordered_types": { + "null_adjustment": "always_last" + }, + "phpdoc_align": { + "align": "left" + }, + "phpdoc_indent": true, + "phpdoc_no_useless_inheritdoc": true, + "phpdoc_order": true, + "phpdoc_scalar": true, + "phpdoc_single_line_var_spacing": true, + "phpdoc_summary": true, + "phpdoc_tag_casing": true, + "phpdoc_trim": true, + "phpdoc_trim_consecutive_blank_line_separation": true, + "phpdoc_var_without_name": true, + "php_unit_construct": true, + "php_unit_dedicate_assert": true, + "php_unit_dedicate_assert_internal_type": true, + "php_unit_internal_class": true, + "php_unit_method_casing": true, + "return_assignment": true, + "return_type_declaration": true, + "short_scalar_cast": true, + "single_line_comment_spacing": true, + "single_line_comment_style": true, + "single_quote": true, + "single_space_around_construct": true, + "ternary_to_null_coalescing": true, + "trailing_comma_in_multiline": { + "elements": [ + "arguments", + "arrays", + "match", + "parameters" + ] + }, + "trim_array_spaces": true, + "type_declaration_spaces": true, + "types_spaces": { + "space": "single" + }, + "use_arrow_functions": false, + "void_return": true, + "whitespace_after_comma_in_array": { + "ensure_single_space": true + }, + "yoda_style": { + "equal": false, + "identical": false, + "less_and_greater": false, + "always_move_variable": false + } + } +} diff --git a/src/Context/StreamContext.php b/src/Context/StreamContext.php index cf3f1f4..8adb2a8 100644 --- a/src/Context/StreamContext.php +++ b/src/Context/StreamContext.php @@ -14,7 +14,7 @@ abstract class StreamContext { /** - * @param Resource Context resource created by stream_context_create() + * @return Resource Context resource created by stream_context_create() * @throws StreamContextException */ public function createStreamContext() @@ -29,4 +29,4 @@ public function createStreamContext() } abstract protected function getContextOptions(): array; -} \ No newline at end of file +} From d9739e70263035f376ec382c561b05a5ca7510b5 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Fri, 24 Jan 2025 10:48:51 +0100 Subject: [PATCH 03/19] style: Code Style --- pint.json | 6 +++++ src/Context/HttpStreamContext.php | 32 +++++++++++------------- src/Context/SftpStreamContext.php | 16 ++++++------ src/Context/StreamContext.php | 6 ++--- src/Exception/FileStreamException.php | 5 +--- src/Exception/StreamContextException.php | 5 +--- src/FileStream.php | 10 +++----- 7 files changed, 35 insertions(+), 45 deletions(-) diff --git a/pint.json b/pint.json index ee8124a..5ff7780 100644 --- a/pint.json +++ b/pint.json @@ -20,6 +20,12 @@ }, "fully_qualified_strict_types": true, "function_to_constant": true, + "general_phpdoc_annotation_remove": { + "annotations": [ + "since" + ], + "case_sensitive": false + }, "get_class_to_class_keyword": true, "is_null": true, "lambda_not_used_import": true, diff --git a/src/Context/HttpStreamContext.php b/src/Context/HttpStreamContext.php index 2c53b30..ff8e91c 100644 --- a/src/Context/HttpStreamContext.php +++ b/src/Context/HttpStreamContext.php @@ -5,16 +5,14 @@ namespace Artemeon\StreamContext\Context; /** - * Object to create http://host.com/home/user/filename context streams - * - * @since 0.1 + * Object to create http://host.com/home/user/filename context streams. */ final class HttpStreamContext extends StreamContext { public const string PROTOCOL = 'http'; private array $headers = []; - private string $content = ""; - private string $userAgent = ""; + private string $content = ''; + private string $userAgent = ''; private float $timeout = 10.0; private function __construct(private readonly string $method) @@ -22,7 +20,7 @@ private function __construct(private readonly string $method) } /** - * Named constructor to create an instance for GET requests + * Named constructor to create an instance for GET requests. */ public static function forGet(): self { @@ -30,7 +28,7 @@ public static function forGet(): self } /** - * Named constructor to create an instance for a POST request with the given content string + * Named constructor to create an instance for a POST request with the given content string. */ public static function forPost(string $content): self { @@ -41,7 +39,7 @@ public static function forPost(string $content): self } /** - * Named constructor to create an instance for POST request with url encoded form data + * Named constructor to create an instance for POST request with url encoded form data. */ public static function forPostUrlencoded(array $parameters): self { @@ -53,7 +51,7 @@ public static function forPostUrlencoded(array $parameters): self } /** - * Named constructor to create an instance for a PUT request with the given content string + * Named constructor to create an instance for a PUT request with the given content string. */ public static function forPut(string $content): self { @@ -64,7 +62,7 @@ public static function forPut(string $content): self } /** - * Named constructor to create an instance for PUT request with url encoded form data + * Named constructor to create an instance for PUT request with url encoded form data. */ public static function forPutUrlencoded(array $parameters): self { @@ -76,7 +74,7 @@ public static function forPutUrlencoded(array $parameters): self } /** - * Add additional headers + * Add additional headers. */ public function setHeaders(array $headers): void { @@ -84,7 +82,7 @@ public function setHeaders(array $headers): void } /** - * Set a custom user agent + * Set a custom user agent. */ public function setUserAgent(string $userAgent): void { @@ -92,9 +90,7 @@ public function setUserAgent(string $userAgent): void } /** - * Set a connect timeout in seconds, standard value is 10 seconds - * - * @param float $timeout + * Set a connect timeout in seconds, standard value is 10 seconds. */ public function setTimeout(float $timeout): void { @@ -106,7 +102,7 @@ protected function getContextOptions(): array $context[self::PROTOCOL]['method'] = $this->method; $context[self::PROTOCOL]['timeout'] = $this->timeout; - if ($this->userAgent !== "") { + if ($this->userAgent !== '') { $context[self::PROTOCOL]['user_agent'] = $this->userAgent; } @@ -114,10 +110,10 @@ protected function getContextOptions(): array $context[self::PROTOCOL]['header'] = $this->headers; } - if ($this->content !== "") { + if ($this->content !== '') { $context[self::PROTOCOL]['content'] = $this->content; } return $context; } -} \ No newline at end of file +} diff --git a/src/Context/SftpStreamContext.php b/src/Context/SftpStreamContext.php index 0b0ac90..c4de8a5 100644 --- a/src/Context/SftpStreamContext.php +++ b/src/Context/SftpStreamContext.php @@ -10,7 +10,6 @@ * Object to create sftp://host.com/home/user/filename context based on the phpseclib. * * @see https://phpseclib.com/docs/sftp#customizing-the-protocol - * @since 0.1 */ final class SftpStreamContext extends StreamContext { @@ -19,36 +18,35 @@ final class SftpStreamContext extends StreamContext private function __construct( private readonly string $username, private readonly string $password, - private readonly string $privateKey - ) - { + private readonly string $privateKey, + ) { Stream::register(self::PROTOCOL); } /** - * Named constructor to create a sftp connection with password authentication + * Named constructor to create a sftp connection with password authentication. * * @param string $username Remote username * @param string $password Remote password */ public static function forPasswordAuthentication(string $username, string $password): self { - return new self($username, $password, ""); + return new self($username, $password, ''); } /** - * Named constructor to create a sftp connection with private key authentication + * Named constructor to create a sftp connection with private key authentication. * * @param string $privateKey Private ssh key string */ public static function forPrivateKeyAuthentication(string $privateKey): self { - return new self("", "", $privateKey); + return new self('', '', $privateKey); } protected function getContextOptions(): array { - if ($this->privateKey !== "") { + if ($this->privateKey !== '') { $context[self::PROTOCOL] = [ 'privkey' => $this->privateKey, ]; diff --git a/src/Context/StreamContext.php b/src/Context/StreamContext.php index 8adb2a8..e6e4df6 100644 --- a/src/Context/StreamContext.php +++ b/src/Context/StreamContext.php @@ -7,15 +7,13 @@ use Artemeon\StreamContext\Exception\StreamContextException; /** - * Base class for ale protocol specific stream context options - * - * @since 0.1 + * Base class for ale protocol specific stream context options. */ abstract class StreamContext { /** - * @return Resource Context resource created by stream_context_create() * @throws StreamContextException + * @return Resource Context resource created by stream_context_create() */ public function createStreamContext() { diff --git a/src/Exception/FileStreamException.php b/src/Exception/FileStreamException.php index 0e6e321..d118f2e 100644 --- a/src/Exception/FileStreamException.php +++ b/src/Exception/FileStreamException.php @@ -6,13 +6,10 @@ use Exception; -/** - * @since 0.1 - */ class FileStreamException extends Exception { public static function fromMessage(string $message, ?Exception $previous = null): self { return new self($message, 0, $previous); } -} \ No newline at end of file +} diff --git a/src/Exception/StreamContextException.php b/src/Exception/StreamContextException.php index d29275f..b1ba612 100644 --- a/src/Exception/StreamContextException.php +++ b/src/Exception/StreamContextException.php @@ -6,13 +6,10 @@ use Exception; -/** - * @since 0.1 - */ final class StreamContextException extends Exception { public static function fromMessage(string $message): self { return new self($message); } -} \ No newline at end of file +} diff --git a/src/FileStream.php b/src/FileStream.php index a14bc64..d4ac6e2 100644 --- a/src/FileStream.php +++ b/src/FileStream.php @@ -7,13 +7,11 @@ use Artemeon\StreamContext\Context\StreamContext; /** - * Configuration DTO for the file url and optional StreamContext options - * - * @since 0.1 + * Configuration DTO for the file url and optional StreamContext options. */ final class FileStream { - private string $mode = "r"; + private string $mode = 'r'; private string $fileExtension = ''; private function __construct(private readonly string $url, private readonly ?StreamContext $streamContext) @@ -21,7 +19,7 @@ private function __construct(private readonly string $url, private readonly ?Str } /** - * Named constructor to create an instance base on the given streaming url and context parameters + * Named constructor to create an instance base on the given streaming url and context parameters. */ public static function fromUrl(string $url, ?StreamContext $streamContext = null): self { @@ -64,4 +62,4 @@ public function getFileExtension(): string { return $this->fileExtension; } -} \ No newline at end of file +} From 9d4096528be47bb3189568ba7059b51c339d8ecf Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Fri, 24 Jan 2025 10:49:48 +0100 Subject: [PATCH 04/19] chore: Adjust rector config --- rector.php | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/rector.php b/rector.php index b8afc16..2887610 100644 --- a/rector.php +++ b/rector.php @@ -5,8 +5,30 @@ use Rector\Config\RectorConfig; return RectorConfig::configure() + ->withPhpSets() + ->withRules([ + Rector\CodeQuality\Rector\Ternary\ArrayKeyExistsTernaryThenValueToCoalescingRector::class, + Rector\CodeQuality\Rector\NullsafeMethodCall\CleanupUnneededNullsafeOperatorRector::class, + Rector\CodeQuality\Rector\ClassMethod\InlineArrayReturnAssignRector::class, + Rector\CodeQuality\Rector\Ternary\UnnecessaryTernaryExpressionRector::class, + Rector\DeadCode\Rector\Foreach_\RemoveUnusedForeachKeyRector::class, + Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromStrictFluentReturnRector::class, + Rector\Php80\Rector\Class_\StringableForToStringRector::class, + Rector\CodingStyle\Rector\ArrowFunction\StaticArrowFunctionRector::class, + Rector\CodingStyle\Rector\Closure\StaticClosureRector::class, + Rector\DeadCode\Rector\Node\RemoveNonExistingVarAnnotationRector::class, + Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodParameterRector::class, + Rector\TypeDeclaration\Rector\ClassMethod\BoolReturnTypeFromBooleanStrictReturnsRector::class, + Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromReturnNewRector::class, + Rector\TypeDeclaration\Rector\ClassMethod\ParamTypeByMethodCallTypeRector::class, + Rector\TypeDeclaration\Rector\ClassMethod\NumericReturnTypeFromStrictScalarReturnsRector::class, + Rector\TypeDeclaration\Rector\ClassMethod\AddMethodCallBasedStrictParamTypeRector::class, + Rector\CodeQuality\Rector\If_\ExplicitBoolCompareRector::class, + Rector\CodeQuality\Rector\Foreach_\ForeachItemsAssignToEmptyArrayToAssignRector::class, + Rector\CodeQuality\Rector\Foreach_\ForeachToInArrayRector::class, + Rector\CodeQuality\Rector\BooleanAnd\RemoveUselessIsObjectCheckRector::class, + ]) ->withPaths([ __DIR__ . '/src', ]) - ->withPhpSets() ->withTypeCoverageLevel(0); From a6a51b2d774a7b8b37543c7f6c1f6b963d8f4dec Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Fri, 24 Jan 2025 11:00:12 +0100 Subject: [PATCH 05/19] test: TypeCoverage --- .github/workflows/pest.yml | 23 ++++++++++++++++++ composer.json | 9 ++++++- phpunit.xml | 17 +++++++++++++ src/Context/StreamContext.php | 4 ++-- src/FileObjectFactory.php | 4 +++- tests/Feature/ExampleTest.php | 5 ++++ tests/Pest.php | 45 +++++++++++++++++++++++++++++++++++ tests/TestCase.php | 10 ++++++++ tests/Unit/ExampleTest.php | 5 ++++ 9 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/pest.yml create mode 100644 phpunit.xml create mode 100644 tests/Feature/ExampleTest.php create mode 100644 tests/Pest.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/ExampleTest.php diff --git a/.github/workflows/pest.yml b/.github/workflows/pest.yml new file mode 100644 index 0000000..d6dd32e --- /dev/null +++ b/.github/workflows/pest.yml @@ -0,0 +1,23 @@ +name: Pest + +on: + - pull_request + +jobs: + type-coverage: + name: Pest Type-Coverage + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + coverage: none + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Composer install + run: composer install --no-interaction --no-ansi --no-progress + - name: Run Type-Coverage + run: ./vendor/bin/pest --type-coverage --min=100 diff --git a/composer.json b/composer.json index 44a288b..05b333f 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,13 @@ "require-dev": { "laravel/pint": "^1.20.0", "phpstan/phpstan": "^2.1.2", - "rector/rector": "^2.0.7" + "rector/rector": "^2.0.7", + "pestphp/pest": "^3.7", + "pestphp/pest-plugin-type-coverage": "^3.2" + }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + } } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..0c12bb9 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,17 @@ + + + + + ./tests + + + + + ./src + + + diff --git a/src/Context/StreamContext.php b/src/Context/StreamContext.php index e6e4df6..fb7716c 100644 --- a/src/Context/StreamContext.php +++ b/src/Context/StreamContext.php @@ -13,9 +13,9 @@ abstract class StreamContext { /** * @throws StreamContextException - * @return Resource Context resource created by stream_context_create() + * @return resource Context resource created by stream_context_create() */ - public function createStreamContext() + public function createStreamContext(): mixed { $resource = stream_context_create($this->getContextOptions()); diff --git a/src/FileObjectFactory.php b/src/FileObjectFactory.php index 2961ef3..451cc77 100644 --- a/src/FileObjectFactory.php +++ b/src/FileObjectFactory.php @@ -52,9 +52,11 @@ public static function create(FileStream $fileStream): SplFileObject } /** + * @return resource|null + * * @throws FileStreamException */ - private static function createStreamContext(FileStream $fileStream) + private static function createStreamContext(FileStream $fileStream): mixed { try { if ($fileStream->getStreamContext() === null) { diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..61cd84c --- /dev/null +++ b/tests/Feature/ExampleTest.php @@ -0,0 +1,5 @@ +toBeTrue(); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..fd279ad --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,45 @@ +extend(Tests\TestCase::class)->in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function something() +{ + // .. +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..cfb05b6 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,10 @@ +toBeTrue(); +}); From 539e5423481ea73877213cb4be8733d856f7c251 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Fri, 24 Jan 2025 11:02:55 +0100 Subject: [PATCH 06/19] test: Unit Tests --- .github/workflows/pest.yml | 17 +++++++++++++++++ src/FileObjectFactory.php | 3 +-- tests/Feature/ExampleTest.php | 2 +- tests/Pest.php | 2 +- tests/TestCase.php | 1 - tests/Unit/ExampleTest.php | 2 +- 6 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pest.yml b/.github/workflows/pest.yml index d6dd32e..602649f 100644 --- a/.github/workflows/pest.yml +++ b/.github/workflows/pest.yml @@ -21,3 +21,20 @@ jobs: run: composer install --no-interaction --no-ansi --no-progress - name: Run Type-Coverage run: ./vendor/bin/pest --type-coverage --min=100 + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + coverage: none + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Composer install + run: composer install --no-interaction --no-ansi --no-progress + - name: Run Tests + run: ./vendor/bin/pest --parallel diff --git a/src/FileObjectFactory.php b/src/FileObjectFactory.php index 451cc77..11cd533 100644 --- a/src/FileObjectFactory.php +++ b/src/FileObjectFactory.php @@ -52,9 +52,8 @@ public static function create(FileStream $fileStream): SplFileObject } /** - * @return resource|null - * * @throws FileStreamException + * @return resource|null */ private static function createStreamContext(FileStream $fileStream): mixed { diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 61cd84c..f3aef8e 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -1,5 +1,5 @@ toBeTrue(); }); diff --git a/tests/Pest.php b/tests/Pest.php index fd279ad..4ad4a8f 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -39,7 +39,7 @@ | */ -function something() +function something(): void { // .. } diff --git a/tests/TestCase.php b/tests/TestCase.php index cfb05b6..690e86f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,5 +6,4 @@ abstract class TestCase extends BaseTestCase { - // } diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php index 61cd84c..f3aef8e 100644 --- a/tests/Unit/ExampleTest.php +++ b/tests/Unit/ExampleTest.php @@ -1,5 +1,5 @@ toBeTrue(); }); From 95c29ad266f1a87748cfcaff4f8322b25aa9bc9b Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Fri, 24 Jan 2025 17:38:34 +0100 Subject: [PATCH 07/19] test: Add unit tests --- .github/workflows/pest.yml | 4 +- composer.json | 4 +- src/Context/HttpStreamContext.php | 2 +- src/Context/StreamContext.php | 11 +- src/FileObjectFactory.php | 16 +-- src/FileStream.php | 4 +- tests/Pest.php | 9 -- tests/Unit/ExampleTest.php | 5 - tests/Unit/HttpStreamContextTest.php | 192 +++++++++++++++++++++++++++ tests/Unit/SftpStreamContextTest.php | 50 +++++++ 10 files changed, 255 insertions(+), 42 deletions(-) delete mode 100644 tests/Unit/ExampleTest.php create mode 100644 tests/Unit/HttpStreamContextTest.php create mode 100644 tests/Unit/SftpStreamContextTest.php diff --git a/.github/workflows/pest.yml b/.github/workflows/pest.yml index 602649f..22345ba 100644 --- a/.github/workflows/pest.yml +++ b/.github/workflows/pest.yml @@ -31,10 +31,10 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: 8.4 - coverage: none + coverage: xdebug env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Composer install run: composer install --no-interaction --no-ansi --no-progress - name: Run Tests - run: ./vendor/bin/pest --parallel + run: ./vendor/bin/pest --coverage diff --git a/composer.json b/composer.json index 05b333f..9d27e80 100644 --- a/composer.json +++ b/composer.json @@ -28,10 +28,12 @@ }, "require-dev": { "laravel/pint": "^1.20.0", + "mockery/mockery": "^1.6.12", "phpstan/phpstan": "^2.1.2", "rector/rector": "^2.0.7", "pestphp/pest": "^3.7", - "pestphp/pest-plugin-type-coverage": "^3.2" + "pestphp/pest-plugin-type-coverage": "^3.2", + "pestphp/pest-plugin-faker": "^3.0" }, "config": { "allow-plugins": { diff --git a/src/Context/HttpStreamContext.php b/src/Context/HttpStreamContext.php index ff8e91c..766a6cd 100644 --- a/src/Context/HttpStreamContext.php +++ b/src/Context/HttpStreamContext.php @@ -78,7 +78,7 @@ public static function forPutUrlencoded(array $parameters): self */ public function setHeaders(array $headers): void { - $this->headers = $headers; + $this->headers = [...$this->headers, ...$headers]; } /** diff --git a/src/Context/StreamContext.php b/src/Context/StreamContext.php index fb7716c..9427aa3 100644 --- a/src/Context/StreamContext.php +++ b/src/Context/StreamContext.php @@ -12,18 +12,11 @@ abstract class StreamContext { /** - * @throws StreamContextException - * @return resource Context resource created by stream_context_create() + * @return resource */ public function createStreamContext(): mixed { - $resource = stream_context_create($this->getContextOptions()); - - if (!is_resource($resource)) { - throw StreamContextException::fromMessage("Can't create stream context for: " . self::class); - } - - return $resource; + return stream_context_create($this->getContextOptions()); } abstract protected function getContextOptions(): array; diff --git a/src/FileObjectFactory.php b/src/FileObjectFactory.php index 11cd533..6c27eab 100644 --- a/src/FileObjectFactory.php +++ b/src/FileObjectFactory.php @@ -5,7 +5,6 @@ namespace Artemeon\StreamContext; use Artemeon\StreamContext\Exception\FileStreamException; -use Artemeon\StreamContext\Exception\StreamContextException; use LogicException; use RuntimeException; use SplFileObject; @@ -37,11 +36,11 @@ public static function create(FileStream $fileStream): SplFileObject $hasFileExtension = preg_match("/\.\w+$/", $fileStream->getUrl()) === 1; // isReadable only works for local filesystems - if (!$file->isReadable() && !$isRemoteSource) { + if (!$isRemoteSource && !$file->isReadable()) { throw FileStreamException::fromMessage("File: '{$fileStream->getUrl()}' is not readable"); } - // Enforce file extension check only for file's with an explizit extension + // Enforce file extension check only for files with an explizit extension. if ($hasFileExtension && $fileStream->getFileExtension() !== '') { if ($file->getExtension() !== $fileStream->getFileExtension()) { throw new FileStreamException("'File extension must be lowercase: " . $fileStream->getFileExtension() . ', given: ' . $file->getExtension()); @@ -52,19 +51,10 @@ public static function create(FileStream $fileStream): SplFileObject } /** - * @throws FileStreamException * @return resource|null */ private static function createStreamContext(FileStream $fileStream): mixed { - try { - if ($fileStream->getStreamContext() === null) { - return null; - } - - return $fileStream->getStreamContext()->createStreamContext(); - } catch (StreamContextException $exception) { - throw FileStreamException::fromMessage($exception->getMessage(), $exception); - } + return $fileStream->getStreamContext()?->createStreamContext(); } } diff --git a/src/FileStream.php b/src/FileStream.php index d4ac6e2..f92e46c 100644 --- a/src/FileStream.php +++ b/src/FileStream.php @@ -19,7 +19,7 @@ private function __construct(private readonly string $url, private readonly ?Str } /** - * Named constructor to create an instance base on the given streaming url and context parameters. + * A named constructor to create an instance base on the given streaming url and context parameters. */ public static function fromUrl(string $url, ?StreamContext $streamContext = null): self { @@ -27,7 +27,7 @@ public static function fromUrl(string $url, ?StreamContext $streamContext = null } /** - * @param string $mode Standard mode id read only, use this method to change file modes supporte by the used stream wrapper + * @param string $mode Standard mode id read only, use this method to change file modes supporte by the used stream wrapper. * @see https://www.php.net/manual/de/function.fopen.php */ public function setMode(string $mode): void diff --git a/tests/Pest.php b/tests/Pest.php index 4ad4a8f..43b78c8 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -24,10 +24,6 @@ | */ -expect()->extend('toBeOne', function () { - return $this->toBe(1); -}); - /* |-------------------------------------------------------------------------- | Functions @@ -38,8 +34,3 @@ | global functions to help you to reduce the number of lines of code in your test files. | */ - -function something(): void -{ - // .. -} diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php deleted file mode 100644 index f3aef8e..0000000 --- a/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); diff --git a/tests/Unit/HttpStreamContextTest.php b/tests/Unit/HttpStreamContextTest.php new file mode 100644 index 0000000..ca35826 --- /dev/null +++ b/tests/Unit/HttpStreamContextTest.php @@ -0,0 +1,192 @@ +setAccessible(true); + + $result = $reflection->invoke($context); + + $stream = $context->createStreamContext(); + + expect($result) + ->toHaveKey('http') + ->and($result['http']) + ->toHaveKey('method') + ->and($result['http']['method']) + ->toBe('GET') + ->and($stream) + ->toBeResource(); + }); + + test('forPost()', function () { + $content = fake()->text(); + + $context = HttpStreamContext::forPost($content); + + $reflection = new ReflectionMethod($context, 'getContextOptions'); + $reflection->setAccessible(true); + + $result = $reflection->invoke($context); + + $stream = $context->createStreamContext(); + + expect($result) + ->toHaveKey('http') + ->and($result['http']) + ->toHaveKey('method') + ->and($result['http']['method']) + ->toBe('POST') + ->and($result['http']['content']) + ->toBe($content) + ->and($stream) + ->toBeResource(); + }); + + test('forPostUrlencoded()', function () { + $content = [ + 'foo' => fake()->text() + ]; + + $context = HttpStreamContext::forPostUrlencoded($content); + + $reflection = new ReflectionMethod($context, 'getContextOptions'); + $reflection->setAccessible(true); + + $result = $reflection->invoke($context); + + $stream = $context->createStreamContext(); + + expect($result) + ->toHaveKey('http') + ->and($result['http']) + ->toHaveKey('method') + ->and($result['http']['method']) + ->toBe('POST') + ->and($result['http']['header']) + ->toContain('Content-type: application/x-www-form-urlencoded') + ->and($result['http']['content']) + ->toBe(http_build_query($content)) + ->and($stream) + ->toBeResource(); + }); + + test('forPut()', function () { + $content = fake()->text(); + + $context = HttpStreamContext::forPut($content); + + $reflection = new ReflectionMethod($context, 'getContextOptions'); + $reflection->setAccessible(true); + + $result = $reflection->invoke($context); + + $stream = $context->createStreamContext(); + + expect($result) + ->toHaveKey('http') + ->and($result['http']) + ->toHaveKey('method') + ->and($result['http']['method']) + ->toBe('PUT') + ->and($result['http']['content']) + ->toBe($content) + ->and($stream) + ->toBeResource(); + }); + + test('forPutUrlencoded()', function () { + $content = [ + 'foo' => fake()->text() + ]; + + $context = HttpStreamContext::forPutUrlencoded($content); + + $reflection = new ReflectionMethod($context, 'getContextOptions'); + $reflection->setAccessible(true); + + $result = $reflection->invoke($context); + + $stream = $context->createStreamContext(); + + expect($result) + ->toHaveKey('http') + ->and($result['http']) + ->toHaveKey('method') + ->and($result['http']['method']) + ->toBe('PUT') + ->and($result['http']['header']) + ->toContain('Content-type: application/x-www-form-urlencoded') + ->and($result['http']['content']) + ->toBe(http_build_query($content)) + ->and($stream) + ->toBeResource(); + }); + + test('setHeaders()', function () { + $header = 'X-' . fake()->word() . ': ' . fake()->word(); + + $context = HttpStreamContext::forGet(); + $context->setHeaders([$header]); + + $reflection = new ReflectionMethod($context, 'getContextOptions'); + $reflection->setAccessible(true); + + $result = $reflection->invoke($context); + + $stream = $context->createStreamContext(); + + expect($result['http']['header']) + ->toContain($header) + ->and($stream) + ->toBeResource(); + }); + + test('setUserAgent()', function () { + $userAgent = fake()->userAgent(); + + $context = HttpStreamContext::forGet(); + $context->setUserAgent($userAgent); + + $reflection = new ReflectionMethod($context, 'getContextOptions'); + $reflection->setAccessible(true); + + $result = $reflection->invoke($context); + + $stream = $context->createStreamContext(); + + expect($result['http']) + ->toHaveKey('user_agent') + ->and($result['http']['user_agent']) + ->toBe($userAgent) + ->and($stream) + ->toBeResource(); + }); + + test('setTimeout()', function () { + $timeout = fake()->randomFloat(min: 11.0); + + $context = HttpStreamContext::forGet(); + $context->setTimeout($timeout); + + $reflection = new ReflectionMethod($context, 'getContextOptions'); + $reflection->setAccessible(true); + + $result = $reflection->invoke($context); + + $stream = $context->createStreamContext(); + + expect($result['http']) + ->toHaveKey('timeout') + ->and($result['http']['timeout']) + ->toBe($timeout) + ->and($stream) + ->toBeResource(); + }); +}); diff --git a/tests/Unit/SftpStreamContextTest.php b/tests/Unit/SftpStreamContextTest.php new file mode 100644 index 0000000..fdb161f --- /dev/null +++ b/tests/Unit/SftpStreamContextTest.php @@ -0,0 +1,50 @@ +userName(); + $password = fake()->password(); + + $context = SftpStreamContext::forPasswordAuthentication($username, $password); + + $reflection = new ReflectionMethod($context, 'getContextOptions'); + $reflection->setAccessible(true); + + $result = $reflection->invoke($context); + + expect($result) + ->toHaveKey('sftp') + ->and($result['sftp']) + ->toHaveKey('username') + ->and($result['sftp']['username']) + ->toBe($username) + ->and($result['sftp']) + ->toHaveKey('password') + ->and($result['sftp']['password']) + ->toBe($password); + }); + + test('forPrivateKeyAuthentication()', function () { + $privateKey = fake()->password(); + + $context = SftpStreamContext::forPrivateKeyAuthentication($privateKey); + + $reflection = new ReflectionMethod($context, 'getContextOptions'); + $reflection->setAccessible(true); + + $result = $reflection->invoke($context); + + expect($result) + ->toHaveKey('sftp') + ->and($result['sftp']) + ->toHaveKey('privkey') + ->and($result['sftp']['privkey']) + ->toBe($privateKey); + }); +}); From f6b3414bc01b89eb0fdef4078abfcb12e0c6cd11 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Fri, 24 Jan 2025 19:14:48 +0100 Subject: [PATCH 08/19] style: Code Style --- src/Context/StreamContext.php | 2 -- tests/Unit/HttpStreamContextTest.php | 22 +++++++++++----------- tests/Unit/SftpStreamContextTest.php | 8 +++----- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/Context/StreamContext.php b/src/Context/StreamContext.php index 9427aa3..2cb4e0d 100644 --- a/src/Context/StreamContext.php +++ b/src/Context/StreamContext.php @@ -4,8 +4,6 @@ namespace Artemeon\StreamContext\Context; -use Artemeon\StreamContext\Exception\StreamContextException; - /** * Base class for ale protocol specific stream context options. */ diff --git a/tests/Unit/HttpStreamContextTest.php b/tests/Unit/HttpStreamContextTest.php index ca35826..db732f3 100644 --- a/tests/Unit/HttpStreamContextTest.php +++ b/tests/Unit/HttpStreamContextTest.php @@ -4,8 +4,8 @@ use function Pest\Faker\fake; -describe('HttpStreamContext', function () { - test('forGet()', function () { +describe('HttpStreamContext', function (): void { + test('forGet()', function (): void { $context = HttpStreamContext::forGet(); $reflection = new ReflectionMethod($context, 'getContextOptions'); @@ -25,7 +25,7 @@ ->toBeResource(); }); - test('forPost()', function () { + test('forPost()', function (): void { $content = fake()->text(); $context = HttpStreamContext::forPost($content); @@ -49,9 +49,9 @@ ->toBeResource(); }); - test('forPostUrlencoded()', function () { + test('forPostUrlencoded()', function (): void { $content = [ - 'foo' => fake()->text() + 'foo' => fake()->text(), ]; $context = HttpStreamContext::forPostUrlencoded($content); @@ -77,7 +77,7 @@ ->toBeResource(); }); - test('forPut()', function () { + test('forPut()', function (): void { $content = fake()->text(); $context = HttpStreamContext::forPut($content); @@ -101,9 +101,9 @@ ->toBeResource(); }); - test('forPutUrlencoded()', function () { + test('forPutUrlencoded()', function (): void { $content = [ - 'foo' => fake()->text() + 'foo' => fake()->text(), ]; $context = HttpStreamContext::forPutUrlencoded($content); @@ -129,7 +129,7 @@ ->toBeResource(); }); - test('setHeaders()', function () { + test('setHeaders()', function (): void { $header = 'X-' . fake()->word() . ': ' . fake()->word(); $context = HttpStreamContext::forGet(); @@ -148,7 +148,7 @@ ->toBeResource(); }); - test('setUserAgent()', function () { + test('setUserAgent()', function (): void { $userAgent = fake()->userAgent(); $context = HttpStreamContext::forGet(); @@ -169,7 +169,7 @@ ->toBeResource(); }); - test('setTimeout()', function () { + test('setTimeout()', function (): void { $timeout = fake()->randomFloat(min: 11.0); $context = HttpStreamContext::forGet(); diff --git a/tests/Unit/SftpStreamContextTest.php b/tests/Unit/SftpStreamContextTest.php index fdb161f..52c2946 100644 --- a/tests/Unit/SftpStreamContextTest.php +++ b/tests/Unit/SftpStreamContextTest.php @@ -1,13 +1,11 @@ userName(); $password = fake()->password(); @@ -30,7 +28,7 @@ ->toBe($password); }); - test('forPrivateKeyAuthentication()', function () { + test('forPrivateKeyAuthentication()', function (): void { $privateKey = fake()->password(); $context = SftpStreamContext::forPrivateKeyAuthentication($privateKey); From 4d144dc102a97cb10bee00ecee76aae2bae639e6 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Fri, 24 Jan 2025 23:41:03 +0100 Subject: [PATCH 09/19] test: Adjust tests --- .github/workflows/pest.yml | 4 +- composer.json | 6 +- src/Context/SftpStreamContext.php | 2 +- tests/Unit/HttpStreamContextTest.php | 93 +++++++++++----------------- tests/Unit/SftpStreamContextTest.php | 34 +++++++--- 5 files changed, 70 insertions(+), 69 deletions(-) diff --git a/.github/workflows/pest.yml b/.github/workflows/pest.yml index 22345ba..b097f93 100644 --- a/.github/workflows/pest.yml +++ b/.github/workflows/pest.yml @@ -20,7 +20,7 @@ jobs: - name: Composer install run: composer install --no-interaction --no-ansi --no-progress - name: Run Type-Coverage - run: ./vendor/bin/pest --type-coverage --min=100 + run: composer test:type-coverage unit-tests: name: Unit Tests runs-on: ubuntu-latest @@ -37,4 +37,4 @@ jobs: - name: Composer install run: composer install --no-interaction --no-ansi --no-progress - name: Run Tests - run: ./vendor/bin/pest --coverage + run: composer test diff --git a/composer.json b/composer.json index 9d27e80..26d326b 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,9 @@ "context" ], "scripts": { + "test": "./vendor/bin/pest --mutate", + "test:coverage": "XDEBUG_MODE=coverage ./vendor/bin/pest --coverage --min=60", + "test:type-coverage": "./vendor/bin/pest --type-coverage --min=100", "phpstan": "php ./vendor/bin/phpstan analyse --memory-limit=4G" }, "authors": [ @@ -33,7 +36,8 @@ "rector/rector": "^2.0.7", "pestphp/pest": "^3.7", "pestphp/pest-plugin-type-coverage": "^3.2", - "pestphp/pest-plugin-faker": "^3.0" + "pestphp/pest-plugin-faker": "^3.0", + "pestphp/pest-plugin-watch": "^3.0" }, "config": { "allow-plugins": { diff --git a/src/Context/SftpStreamContext.php b/src/Context/SftpStreamContext.php index c4de8a5..2c9b4fb 100644 --- a/src/Context/SftpStreamContext.php +++ b/src/Context/SftpStreamContext.php @@ -13,7 +13,7 @@ */ final class SftpStreamContext extends StreamContext { - private const string PROTOCOL = 'sftp'; + public const string PROTOCOL = 'sftp'; private function __construct( private readonly string $username, diff --git a/tests/Unit/HttpStreamContextTest.php b/tests/Unit/HttpStreamContextTest.php index db732f3..2944aa7 100644 --- a/tests/Unit/HttpStreamContextTest.php +++ b/tests/Unit/HttpStreamContextTest.php @@ -4,25 +4,22 @@ use function Pest\Faker\fake; +covers(HttpStreamContext::class); describe('HttpStreamContext', function (): void { test('forGet()', function (): void { $context = HttpStreamContext::forGet(); $reflection = new ReflectionMethod($context, 'getContextOptions'); - $reflection->setAccessible(true); - $result = $reflection->invoke($context); - $stream = $context->createStreamContext(); - expect($result) ->toHaveKey('http') ->and($result['http']) ->toHaveKey('method') ->and($result['http']['method']) ->toBe('GET') - ->and($stream) - ->toBeResource(); + ->and($result['http']) + ->not->toHaveKey('content'); }); test('forPost()', function (): void { @@ -31,12 +28,8 @@ $context = HttpStreamContext::forPost($content); $reflection = new ReflectionMethod($context, 'getContextOptions'); - $reflection->setAccessible(true); - $result = $reflection->invoke($context); - $stream = $context->createStreamContext(); - expect($result) ->toHaveKey('http') ->and($result['http']) @@ -44,9 +37,7 @@ ->and($result['http']['method']) ->toBe('POST') ->and($result['http']['content']) - ->toBe($content) - ->and($stream) - ->toBeResource(); + ->toBe($content); }); test('forPostUrlencoded()', function (): void { @@ -57,12 +48,8 @@ $context = HttpStreamContext::forPostUrlencoded($content); $reflection = new ReflectionMethod($context, 'getContextOptions'); - $reflection->setAccessible(true); - $result = $reflection->invoke($context); - $stream = $context->createStreamContext(); - expect($result) ->toHaveKey('http') ->and($result['http']) @@ -72,9 +59,7 @@ ->and($result['http']['header']) ->toContain('Content-type: application/x-www-form-urlencoded') ->and($result['http']['content']) - ->toBe(http_build_query($content)) - ->and($stream) - ->toBeResource(); + ->toBe(http_build_query($content)); }); test('forPut()', function (): void { @@ -83,12 +68,8 @@ $context = HttpStreamContext::forPut($content); $reflection = new ReflectionMethod($context, 'getContextOptions'); - $reflection->setAccessible(true); - $result = $reflection->invoke($context); - $stream = $context->createStreamContext(); - expect($result) ->toHaveKey('http') ->and($result['http']) @@ -96,9 +77,7 @@ ->and($result['http']['method']) ->toBe('PUT') ->and($result['http']['content']) - ->toBe($content) - ->and($stream) - ->toBeResource(); + ->toBe($content); }); test('forPutUrlencoded()', function (): void { @@ -109,13 +88,10 @@ $context = HttpStreamContext::forPutUrlencoded($content); $reflection = new ReflectionMethod($context, 'getContextOptions'); - $reflection->setAccessible(true); - $result = $reflection->invoke($context); - $stream = $context->createStreamContext(); - expect($result) + ->toBeArray() ->toHaveKey('http') ->and($result['http']) ->toHaveKey('method') @@ -124,69 +100,72 @@ ->and($result['http']['header']) ->toContain('Content-type: application/x-www-form-urlencoded') ->and($result['http']['content']) - ->toBe(http_build_query($content)) - ->and($stream) - ->toBeResource(); + ->toBe(http_build_query($content)); }); test('setHeaders()', function (): void { - $header = 'X-' . fake()->word() . ': ' . fake()->word(); + $header1 = 'X-' . fake()->word() . ': ' . fake()->word(); + $header2 = 'X-' . fake()->word() . ': ' . fake()->word(); $context = HttpStreamContext::forGet(); - $context->setHeaders([$header]); + $context->setHeaders([$header1]); $reflection = new ReflectionMethod($context, 'getContextOptions'); - $reflection->setAccessible(true); + $result1 = $reflection->invoke($context); - $result = $reflection->invoke($context); + $context->setHeaders([$header2]); - $stream = $context->createStreamContext(); + $result2 = $reflection->invoke($context); - expect($result['http']['header']) - ->toContain($header) - ->and($stream) - ->toBeResource(); + expect($result1['http']['header']) + ->toContain($header1) + ->and($result2['http']['header']) + ->toContain($header1) + ->and($result2['http']['header']) + ->toContain($header2); }); test('setUserAgent()', function (): void { $userAgent = fake()->userAgent(); $context = HttpStreamContext::forGet(); - $context->setUserAgent($userAgent); $reflection = new ReflectionMethod($context, 'getContextOptions'); - $reflection->setAccessible(true); $result = $reflection->invoke($context); - $stream = $context->createStreamContext(); - expect($result['http']) + ->and($result['http']) + ->not->toHaveKey('user_agent'); + + $context->setUserAgent($userAgent); + + $newResult = $reflection->invoke($context); + + expect($newResult['http']) ->toHaveKey('user_agent') - ->and($result['http']['user_agent']) - ->toBe($userAgent) - ->and($stream) - ->toBeResource(); + ->and($newResult['http']['user_agent']) + ->toBe($userAgent); }); test('setTimeout()', function (): void { - $timeout = fake()->randomFloat(min: 11.0); + $timeout = fake()->randomFloat(); $context = HttpStreamContext::forGet(); $context->setTimeout($timeout); $reflection = new ReflectionMethod($context, 'getContextOptions'); - $reflection->setAccessible(true); $result = $reflection->invoke($context); - $stream = $context->createStreamContext(); - expect($result['http']) ->toHaveKey('timeout') ->and($result['http']['timeout']) - ->toBe($timeout) - ->and($stream) + ->toBe($timeout); + }); + + test('createStreamContext()', function (): void { + expect(HttpStreamContext::forGet()->createStreamContext()) ->toBeResource(); }); }); diff --git a/tests/Unit/SftpStreamContextTest.php b/tests/Unit/SftpStreamContextTest.php index 52c2946..d0e2e1c 100644 --- a/tests/Unit/SftpStreamContextTest.php +++ b/tests/Unit/SftpStreamContextTest.php @@ -4,7 +4,8 @@ use function Pest\Faker\fake; -describe('HttpStreamContext', function (): void { +covers(SftpStreamContext::class); +describe('SftpStreamContext', function (): void { test('forPasswordAuthentication()', function (): void { $username = fake()->userName(); $password = fake()->password(); @@ -33,16 +34,33 @@ $context = SftpStreamContext::forPrivateKeyAuthentication($privateKey); - $reflection = new ReflectionMethod($context, 'getContextOptions'); - $reflection->setAccessible(true); + $optionsReflection = new ReflectionMethod($context, 'getContextOptions'); + $optionsReflection->setAccessible(true); - $result = $reflection->invoke($context); + $usernameReflection = new ReflectionProperty($context, 'username'); + $username = $usernameReflection->getValue($context); - expect($result) + $passwordReflection = new ReflectionProperty($context, 'password'); + $password = $passwordReflection->getValue($context); + + $options = $optionsReflection->invoke($context); + + expect($options) ->toHaveKey('sftp') - ->and($result['sftp']) + ->and($options['sftp']) ->toHaveKey('privkey') - ->and($result['sftp']['privkey']) - ->toBe($privateKey); + ->and($options['sftp']['privkey']) + ->toBe($privateKey) + ->and($username) + ->toBe('') + ->and($password) + ->toBe(''); + }); + + test('isRegistered()', function (): void { + SftpStreamContext::forPrivateKeyAuthentication('foo'); + + expect(stream_get_wrappers()) + ->toContain('sftp'); }); }); From be2bba4298dc77415914cd05a9202d11dde12565 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Mon, 27 Jan 2025 09:59:58 +0100 Subject: [PATCH 10/19] test: 100 percent code coverage --- .github/workflows/pest.yml | 2 +- composer.json | 8 ++-- src/Exception/StreamContextException.php | 15 ------- src/FileObjectFactory.php | 24 ++-------- src/FileStream.php | 8 +++- tests/Unit/FileObjectFactoryTest.php | 57 ++++++++++++++++++++++++ tests/Unit/FileStreamTest.php | 50 +++++++++++++++++++++ tests/fixtures/test.json | 1 + 8 files changed, 124 insertions(+), 41 deletions(-) delete mode 100644 src/Exception/StreamContextException.php create mode 100644 tests/Unit/FileObjectFactoryTest.php create mode 100644 tests/Unit/FileStreamTest.php create mode 100644 tests/fixtures/test.json diff --git a/.github/workflows/pest.yml b/.github/workflows/pest.yml index b097f93..f681742 100644 --- a/.github/workflows/pest.yml +++ b/.github/workflows/pest.yml @@ -5,7 +5,7 @@ on: jobs: type-coverage: - name: Pest Type-Coverage + name: Type-Coverage runs-on: ubuntu-latest steps: - name: Checkout diff --git a/composer.json b/composer.json index 26d326b..698a5a1 100644 --- a/composer.json +++ b/composer.json @@ -9,10 +9,12 @@ "context" ], "scripts": { - "test": "./vendor/bin/pest --mutate", - "test:coverage": "XDEBUG_MODE=coverage ./vendor/bin/pest --coverage --min=60", + "test": "./vendor/bin/pest --order-by=random", + "test:coverage": "XDEBUG_MODE=coverage ./vendor/bin/pest --order-by=random --coverage --min=100", "test:type-coverage": "./vendor/bin/pest --type-coverage --min=100", - "phpstan": "php ./vendor/bin/phpstan analyse --memory-limit=4G" + "phpstan": "php ./vendor/bin/phpstan analyse --memory-limit=4G", + "pint": "./vendor/bin/pint --test -v", + "pint:fix": "./vendor/bin/pint -v" }, "authors": [ { diff --git a/src/Exception/StreamContextException.php b/src/Exception/StreamContextException.php deleted file mode 100644 index b1ba612..0000000 --- a/src/Exception/StreamContextException.php +++ /dev/null @@ -1,15 +0,0 @@ -getUrl(), $fileStream->getMode(), false, - self::createStreamContext($fileStream), + $fileStream->getStreamContext()?->createStreamContext(), ); } catch (LogicException | RuntimeException $e) { throw new FileStreamException($e->getMessage(), $e->getCode(), $e); } - $isRemoteSource = preg_match("/^(?!file)\w+:\/\//", $fileStream->getUrl()) === 1; $hasFileExtension = preg_match("/\.\w+$/", $fileStream->getUrl()) === 1; - // isReadable only works for local filesystems - if (!$isRemoteSource && !$file->isReadable()) { - throw FileStreamException::fromMessage("File: '{$fileStream->getUrl()}' is not readable"); - } - - // Enforce file extension check only for files with an explizit extension. - if ($hasFileExtension && $fileStream->getFileExtension() !== '') { - if ($file->getExtension() !== $fileStream->getFileExtension()) { - throw new FileStreamException("'File extension must be lowercase: " . $fileStream->getFileExtension() . ', given: ' . $file->getExtension()); - } + // Enforce file extension check only for files with an explicit extension. + if ($hasFileExtension && $fileStream->getFileExtension() !== '' && $file->getExtension() !== $fileStream->getFileExtension()) { + throw FileStreamException::fromMessage("File extension must be lowercase: {$fileStream->getFileExtension()}, given: {$file->getExtension()}"); } return $file; } - - /** - * @return resource|null - */ - private static function createStreamContext(FileStream $fileStream): mixed - { - return $fileStream->getStreamContext()?->createStreamContext(); - } } diff --git a/src/FileStream.php b/src/FileStream.php index f92e46c..c11385e 100644 --- a/src/FileStream.php +++ b/src/FileStream.php @@ -30,17 +30,21 @@ public static function fromUrl(string $url, ?StreamContext $streamContext = null * @param string $mode Standard mode id read only, use this method to change file modes supporte by the used stream wrapper. * @see https://www.php.net/manual/de/function.fopen.php */ - public function setMode(string $mode): void + public function setMode(string $mode): self { $this->mode = $mode; + + return $this; } /** * @param string $fileExtension Enforce file extension for security */ - public function enforceFileExtension(string $fileExtension): void + public function enforceFileExtension(string $fileExtension): self { $this->fileExtension = $fileExtension; + + return $this; } public function getUrl(): string diff --git a/tests/Unit/FileObjectFactoryTest.php b/tests/Unit/FileObjectFactoryTest.php new file mode 100644 index 0000000..e7bc63f --- /dev/null +++ b/tests/Unit/FileObjectFactoryTest.php @@ -0,0 +1,57 @@ +toBeInstanceOf(SplFileObject::class) + ->and($sut->getFilename()) + ->toBe('memory'); + }); + + test('create() with local file and enforced file extension', function (): void { + $sut = FileObjectFactory::create( + FileStream::fromUrl('file://' . dirname(__DIR__) . '/fixtures/test.json') + ->enforceFileExtension('json'), + ); + + expect($sut) + ->toBeInstanceOf(SplFileObject::class) + ->and($sut->getFilename()) + ->toBe('test.json'); + }); + + test('create() with directory', function (): void { + FileObjectFactory::create( + FileStream::fromUrl('file://' . dirname(__DIR__)), + ); + })->throws(FileStreamException::class); + + test('create() with incorrect file extension', function (): void { + FileObjectFactory::create( + FileStream::fromUrl('file://' . dirname(__DIR__) . '/fixtures/test.json') + ->enforceFileExtension('html'), + ); + })->throws(FileStreamException::class, 'File extension must be lowercase: html, given: json'); + + test('create() with unreadable file', function (): void { + $path = dirname(__DIR__) . '/fixtures/not-readable.json'; + touch($path); + chmod($path, 222); + + FileObjectFactory::create( + FileStream::fromUrl('file://' . dirname(__DIR__) . '/fixtures/not-readable.json'), + ); + }) + ->throws(FileStreamException::class) + ->after(function (): void { + unlink(dirname(__DIR__) . '/fixtures/not-readable.json'); + }); +}); diff --git a/tests/Unit/FileStreamTest.php b/tests/Unit/FileStreamTest.php new file mode 100644 index 0000000..7c1b602 --- /dev/null +++ b/tests/Unit/FileStreamTest.php @@ -0,0 +1,50 @@ +url(); + + $sut = FileStream::fromUrl($url); + + expect($sut) + ->toBeInstanceOf(FileStream::class) + ->and($sut->getUrl()) + ->toBe($url) + ->and($sut->getStreamContext()) + ->toBeNull(); + }); + + test('enforceFileExtension()', function (): void { + $extension = fake()->fileExtension(); + + $sut = FileStream::fromUrl(fake()->url()); + $sut->enforceFileExtension($extension); + + expect($sut->getFileExtension()) + ->toBe($extension); + }); + + test('setMode()', function (): void { + $mode = fake()->lexify('?'); + + $sut = FileStream::fromUrl(fake()->url()); + $sut->setMode($mode); + + expect($sut->getMode()) + ->toBe($mode); + }); + + test('fromUrl() with StreamContext', function (): void { + $sut = FileStream::fromUrl(fake()->url(), HttpStreamContext::forGet()); + + expect($sut->getStreamContext()) + ->toBeInstanceOf(HttpStreamContext::class); + }); +}); diff --git a/tests/fixtures/test.json b/tests/fixtures/test.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/test.json @@ -0,0 +1 @@ +{} From cbfc9b5aa326bf4e4d4cf050aeb0be8294fa57ff Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Mon, 27 Jan 2025 10:06:08 +0100 Subject: [PATCH 11/19] test: 100 percent code coverage --- .github/workflows/pest.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pest.yml b/.github/workflows/pest.yml index f681742..33c4b72 100644 --- a/.github/workflows/pest.yml +++ b/.github/workflows/pest.yml @@ -14,7 +14,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: 8.4 - coverage: none + coverage: xdebug env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Composer install @@ -37,4 +37,4 @@ jobs: - name: Composer install run: composer install --no-interaction --no-ansi --no-progress - name: Run Tests - run: composer test + run: composer test:coverage From 512cb71f38eab530de7de637fac743cf5c3e10de Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Mon, 27 Jan 2025 11:44:09 +0100 Subject: [PATCH 12/19] test: Mutations --- composer.json | 4 ++-- src/Context/HttpMethod.php | 10 ++++++++++ src/Context/HttpStreamContext.php | 27 ++++++++++++++------------- src/Exception/FileStreamException.php | 4 ---- src/FileObjectFactory.php | 11 +++++------ src/FileStream.php | 4 ++-- tests/Feature/ExampleTest.php | 5 ----- tests/Unit/FileObjectFactoryTest.php | 13 +++++++++++++ tests/Unit/FileStreamTest.php | 1 + tests/Unit/HttpStreamContextTest.php | 22 +++++++++++++--------- tests/fixtures/no-extension | 0 11 files changed, 60 insertions(+), 41 deletions(-) create mode 100644 src/Context/HttpMethod.php delete mode 100644 tests/Feature/ExampleTest.php create mode 100644 tests/fixtures/no-extension diff --git a/composer.json b/composer.json index 698a5a1..ed9dd38 100644 --- a/composer.json +++ b/composer.json @@ -9,8 +9,8 @@ "context" ], "scripts": { - "test": "./vendor/bin/pest --order-by=random", - "test:coverage": "XDEBUG_MODE=coverage ./vendor/bin/pest --order-by=random --coverage --min=100", + "test": "./vendor/bin/pest", + "test:coverage": "XDEBUG_MODE=coverage ./vendor/bin/pest --mutate --parallel --min=100", "test:type-coverage": "./vendor/bin/pest --type-coverage --min=100", "phpstan": "php ./vendor/bin/phpstan analyse --memory-limit=4G", "pint": "./vendor/bin/pint --test -v", diff --git a/src/Context/HttpMethod.php b/src/Context/HttpMethod.php new file mode 100644 index 0000000..18a1b13 --- /dev/null +++ b/src/Context/HttpMethod.php @@ -0,0 +1,10 @@ +content = $content; return $instance; @@ -43,7 +44,7 @@ public static function forPost(string $content): self */ public static function forPostUrlencoded(array $parameters): self { - $instance = new self('POST'); + $instance = new self(HttpMethod::POST); $instance->content = http_build_query($parameters); $instance->headers[] = 'Content-type: application/x-www-form-urlencoded'; @@ -55,7 +56,7 @@ public static function forPostUrlencoded(array $parameters): self */ public static function forPut(string $content): self { - $instance = new self('PUT'); + $instance = new self(HttpMethod::PUT); $instance->content = $content; return $instance; @@ -66,7 +67,7 @@ public static function forPut(string $content): self */ public static function forPutUrlencoded(array $parameters): self { - $instance = new self('PUT'); + $instance = new self(HttpMethod::PUT); $instance->content = http_build_query($parameters); $instance->headers[] = 'Content-type: application/x-www-form-urlencoded'; @@ -99,10 +100,10 @@ public function setTimeout(float $timeout): void protected function getContextOptions(): array { - $context[self::PROTOCOL]['method'] = $this->method; - $context[self::PROTOCOL]['timeout'] = $this->timeout; + $context[self::PROTOCOL]['method'] = $this->method->value; + $context[self::PROTOCOL]['timeout'] = $this->timeout ?? 10.0; - if ($this->userAgent !== '') { + if (!empty($this->userAgent)) { $context[self::PROTOCOL]['user_agent'] = $this->userAgent; } @@ -110,7 +111,7 @@ protected function getContextOptions(): array $context[self::PROTOCOL]['header'] = $this->headers; } - if ($this->content !== '') { + if (!empty($this->content) || in_array($this->method, [HttpMethod::POST, HttpMethod::PUT], true)) { $context[self::PROTOCOL]['content'] = $this->content; } diff --git a/src/Exception/FileStreamException.php b/src/Exception/FileStreamException.php index d118f2e..1102f3f 100644 --- a/src/Exception/FileStreamException.php +++ b/src/Exception/FileStreamException.php @@ -8,8 +8,4 @@ class FileStreamException extends Exception { - public static function fromMessage(string $message, ?Exception $previous = null): self - { - return new self($message, 0, $previous); - } } diff --git a/src/FileObjectFactory.php b/src/FileObjectFactory.php index c94d1ef..2958315 100644 --- a/src/FileObjectFactory.php +++ b/src/FileObjectFactory.php @@ -23,10 +23,9 @@ public static function create(FileStream $fileStream): SplFileObject { try { $file = new SplFileObject( - $fileStream->getUrl(), - $fileStream->getMode(), - false, - $fileStream->getStreamContext()?->createStreamContext(), + filename: $fileStream->getUrl(), + mode: $fileStream->getMode(), + context: $fileStream->getStreamContext()?->createStreamContext(), ); } catch (LogicException | RuntimeException $e) { throw new FileStreamException($e->getMessage(), $e->getCode(), $e); @@ -35,8 +34,8 @@ public static function create(FileStream $fileStream): SplFileObject $hasFileExtension = preg_match("/\.\w+$/", $fileStream->getUrl()) === 1; // Enforce file extension check only for files with an explicit extension. - if ($hasFileExtension && $fileStream->getFileExtension() !== '' && $file->getExtension() !== $fileStream->getFileExtension()) { - throw FileStreamException::fromMessage("File extension must be lowercase: {$fileStream->getFileExtension()}, given: {$file->getExtension()}"); + if ($hasFileExtension && !empty($fileStream->getFileExtension()) && $file->getExtension() !== $fileStream->getFileExtension()) { + throw new FileStreamException("File extension must be lowercase: {$fileStream->getFileExtension()}, given: {$file->getExtension()}"); } return $file; diff --git a/src/FileStream.php b/src/FileStream.php index c11385e..fa66b74 100644 --- a/src/FileStream.php +++ b/src/FileStream.php @@ -12,7 +12,7 @@ final class FileStream { private string $mode = 'r'; - private string $fileExtension = ''; + private ?string $fileExtension = null; private function __construct(private readonly string $url, private readonly ?StreamContext $streamContext) { @@ -62,7 +62,7 @@ public function getStreamContext(): ?StreamContext return $this->streamContext; } - public function getFileExtension(): string + public function getFileExtension(): ?string { return $this->fileExtension; } diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php deleted file mode 100644 index f3aef8e..0000000 --- a/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); diff --git a/tests/Unit/FileObjectFactoryTest.php b/tests/Unit/FileObjectFactoryTest.php index e7bc63f..524ba5c 100644 --- a/tests/Unit/FileObjectFactoryTest.php +++ b/tests/Unit/FileObjectFactoryTest.php @@ -6,6 +6,7 @@ use Artemeon\StreamContext\FileObjectFactory; use Artemeon\StreamContext\FileStream; +covers(FileObjectFactory::class); describe('FileObjectFactory', function (): void { test('create()', function (): void { $sut = FileObjectFactory::create(FileStream::fromUrl('php://memory')); @@ -28,6 +29,18 @@ ->toBe('test.json'); }); + test('create() with local file and no extension and enforced file extension', function (): void { + $sut = FileObjectFactory::create( + FileStream::fromUrl('file://' . dirname(__DIR__) . '/fixtures/no-extension') + ->enforceFileExtension('json'), + ); + + expect($sut) + ->toBeInstanceOf(SplFileObject::class) + ->and($sut->getFilename()) + ->toBe('no-extension'); + }); + test('create() with directory', function (): void { FileObjectFactory::create( FileStream::fromUrl('file://' . dirname(__DIR__)), diff --git a/tests/Unit/FileStreamTest.php b/tests/Unit/FileStreamTest.php index 7c1b602..09a2504 100644 --- a/tests/Unit/FileStreamTest.php +++ b/tests/Unit/FileStreamTest.php @@ -7,6 +7,7 @@ use function Pest\Faker\fake; +covers(FileStream::class); describe('FileStream', function (): void { test('fromUrl()', function (): void { $url = fake()->url(); diff --git a/tests/Unit/HttpStreamContextTest.php b/tests/Unit/HttpStreamContextTest.php index 2944aa7..ee0ef9e 100644 --- a/tests/Unit/HttpStreamContextTest.php +++ b/tests/Unit/HttpStreamContextTest.php @@ -19,12 +19,12 @@ ->and($result['http']['method']) ->toBe('GET') ->and($result['http']) - ->not->toHaveKey('content'); + ->not->toHaveKey('content') + ->and($result['http']['timeout']) + ->toBe(10.0); }); - test('forPost()', function (): void { - $content = fake()->text(); - + test('forPost()', function (mixed $content): void { $context = HttpStreamContext::forPost($content); $reflection = new ReflectionMethod($context, 'getContextOptions'); @@ -38,7 +38,10 @@ ->toBe('POST') ->and($result['http']['content']) ->toBe($content); - }); + })->with([ + 'content' => [fake()->text()], + 'no-content' => [''], + ]); test('forPostUrlencoded()', function (): void { $content = [ @@ -62,9 +65,7 @@ ->toBe(http_build_query($content)); }); - test('forPut()', function (): void { - $content = fake()->text(); - + test('forPut()', function (mixed $content): void { $context = HttpStreamContext::forPut($content); $reflection = new ReflectionMethod($context, 'getContextOptions'); @@ -78,7 +79,10 @@ ->toBe('PUT') ->and($result['http']['content']) ->toBe($content); - }); + })->with([ + 'content' => [fake()->text()], + 'no-content' => [''], + ]); test('forPutUrlencoded()', function (): void { $content = [ diff --git a/tests/fixtures/no-extension b/tests/fixtures/no-extension new file mode 100644 index 0000000..e69de29 From 7da44e5138281fc3d9940a309617050e694730e8 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Mon, 27 Jan 2025 11:46:30 +0100 Subject: [PATCH 13/19] chore: Trigger pipeline From 956a4d45ff666c7ce8a096c5f0af5f64b8f6eb7f Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Mon, 27 Jan 2025 11:59:59 +0100 Subject: [PATCH 14/19] chore: PHPStan Level 6 --- phpstan.neon | 2 +- src/Context/HttpStreamContext.php | 18 ++++++++++++++++++ src/Context/SftpStreamContext.php | 9 +++++++++ src/Context/StreamContext.php | 3 +++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index 60afcbd..5c8de94 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,6 @@ includes: - phar://phpstan.phar/conf/bleedingEdge.neon parameters: - level: 5 + level: 6 paths: - src diff --git a/src/Context/HttpStreamContext.php b/src/Context/HttpStreamContext.php index b1b8009..d0af9b7 100644 --- a/src/Context/HttpStreamContext.php +++ b/src/Context/HttpStreamContext.php @@ -11,6 +11,7 @@ final class HttpStreamContext extends StreamContext { public const string PROTOCOL = 'http'; + /** @var non-empty-string[] */ private array $headers = []; private ?string $content = null; private ?string $userAgent = null; @@ -41,6 +42,8 @@ public static function forPost(string $content): self /** * Named constructor to create an instance for POST request with url encoded form data. + * + * @param array $parameters */ public static function forPostUrlencoded(array $parameters): self { @@ -64,6 +67,8 @@ public static function forPut(string $content): self /** * Named constructor to create an instance for PUT request with url encoded form data. + * + * @param array $parameters */ public static function forPutUrlencoded(array $parameters): self { @@ -76,6 +81,8 @@ public static function forPutUrlencoded(array $parameters): self /** * Add additional headers. + * + * @param non-empty-string[] $headers */ public function setHeaders(array $headers): void { @@ -98,6 +105,17 @@ public function setTimeout(float $timeout): void $this->timeout = $timeout; } + /** + * @return array{ + * http: array{ + * method: 'GET' | 'POST' | 'PUT', + * timeout: float, + * user_agent?: non-empty-string, + * header?: non-empty-string[], + * content?: non-empty-string, + * } + * } + */ protected function getContextOptions(): array { $context[self::PROTOCOL]['method'] = $this->method->value; diff --git a/src/Context/SftpStreamContext.php b/src/Context/SftpStreamContext.php index 2c9b4fb..e854de8 100644 --- a/src/Context/SftpStreamContext.php +++ b/src/Context/SftpStreamContext.php @@ -44,6 +44,15 @@ public static function forPrivateKeyAuthentication(string $privateKey): self return new self('', '', $privateKey); } + /** + * @return array{ + * sftp: array{ + * privkey?: non-empty-string, + * username?: non-empty-string, + * password?: string, + * } + * } + */ protected function getContextOptions(): array { if ($this->privateKey !== '') { diff --git a/src/Context/StreamContext.php b/src/Context/StreamContext.php index 2cb4e0d..2f0ecc4 100644 --- a/src/Context/StreamContext.php +++ b/src/Context/StreamContext.php @@ -17,5 +17,8 @@ public function createStreamContext(): mixed return stream_context_create($this->getContextOptions()); } + /** + * @return array> + */ abstract protected function getContextOptions(): array; } From edd3d744c00b0417a6e256105daa95b27a2abc74 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Mon, 27 Jan 2025 12:02:56 +0100 Subject: [PATCH 15/19] chore: PHPStan Level 7 --- phpstan.neon | 2 +- src/Context/HttpStreamContext.php | 2 +- src/Context/SftpStreamContext.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 5c8de94..c72f192 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,6 @@ includes: - phar://phpstan.phar/conf/bleedingEdge.neon parameters: - level: 6 + level: 7 paths: - src diff --git a/src/Context/HttpStreamContext.php b/src/Context/HttpStreamContext.php index d0af9b7..45b1ea3 100644 --- a/src/Context/HttpStreamContext.php +++ b/src/Context/HttpStreamContext.php @@ -112,7 +112,7 @@ public function setTimeout(float $timeout): void * timeout: float, * user_agent?: non-empty-string, * header?: non-empty-string[], - * content?: non-empty-string, + * content?: string, * } * } */ diff --git a/src/Context/SftpStreamContext.php b/src/Context/SftpStreamContext.php index e854de8..08670a3 100644 --- a/src/Context/SftpStreamContext.php +++ b/src/Context/SftpStreamContext.php @@ -48,7 +48,7 @@ public static function forPrivateKeyAuthentication(string $privateKey): self * @return array{ * sftp: array{ * privkey?: non-empty-string, - * username?: non-empty-string, + * username?: string, * password?: string, * } * } From bfbcd95c2a3fb2e0595a8f5b42ff48ef61bc31ee Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Mon, 27 Jan 2025 12:04:54 +0100 Subject: [PATCH 16/19] chore: PHPStan Level 8 --- phpstan.neon | 2 +- src/Context/HttpStreamContext.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index c72f192..8fc3894 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,6 @@ includes: - phar://phpstan.phar/conf/bleedingEdge.neon parameters: - level: 7 + level: 8 paths: - src diff --git a/src/Context/HttpStreamContext.php b/src/Context/HttpStreamContext.php index 45b1ea3..fbe7917 100644 --- a/src/Context/HttpStreamContext.php +++ b/src/Context/HttpStreamContext.php @@ -110,9 +110,9 @@ public function setTimeout(float $timeout): void * http: array{ * method: 'GET' | 'POST' | 'PUT', * timeout: float, - * user_agent?: non-empty-string, + * user_agent?: non-falsy-string, * header?: non-empty-string[], - * content?: string, + * content?: string|null, * } * } */ From 2eb57389bc95adac1a771967bfcea40db012d225 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Mon, 27 Jan 2025 12:05:09 +0100 Subject: [PATCH 17/19] chore: PHPStan Level 9 --- phpstan.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index 8fc3894..0ec59d3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,6 @@ includes: - phar://phpstan.phar/conf/bleedingEdge.neon parameters: - level: 8 + level: 9 paths: - src From 4b26a17b5ba43bbad3bbdb997510eb442f8d8159 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Mon, 27 Jan 2025 12:07:20 +0100 Subject: [PATCH 18/19] chore: PHPStan Level 10 --- phpstan.neon | 2 +- src/Context/HttpStreamContext.php | 3 +++ src/Context/SftpStreamContext.php | 14 +++++++------- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 0ec59d3..58941a6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,6 @@ includes: - phar://phpstan.phar/conf/bleedingEdge.neon parameters: - level: 9 + level: 10 paths: - src diff --git a/src/Context/HttpStreamContext.php b/src/Context/HttpStreamContext.php index fbe7917..90aef9e 100644 --- a/src/Context/HttpStreamContext.php +++ b/src/Context/HttpStreamContext.php @@ -118,6 +118,9 @@ public function setTimeout(float $timeout): void */ protected function getContextOptions(): array { + $context = [ + self::PROTOCOL => [], + ]; $context[self::PROTOCOL]['method'] = $this->method->value; $context[self::PROTOCOL]['timeout'] = $this->timeout ?? 10.0; diff --git a/src/Context/SftpStreamContext.php b/src/Context/SftpStreamContext.php index 08670a3..9974abd 100644 --- a/src/Context/SftpStreamContext.php +++ b/src/Context/SftpStreamContext.php @@ -55,15 +55,15 @@ public static function forPrivateKeyAuthentication(string $privateKey): self */ protected function getContextOptions(): array { + $context = [ + self::PROTOCOL => [], + ]; + if ($this->privateKey !== '') { - $context[self::PROTOCOL] = [ - 'privkey' => $this->privateKey, - ]; + $context[self::PROTOCOL]['privkey'] = $this->privateKey; } else { - $context[self::PROTOCOL] = [ - 'username' => $this->username, - 'password' => $this->password, - ]; + $context[self::PROTOCOL]['username'] = $this->username; + $context[self::PROTOCOL]['password'] = $this->password; } return $context; From cc3919badf4c4b89e87864d4a6ded1968d0fe5d1 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Mon, 27 Jan 2025 12:09:40 +0100 Subject: [PATCH 19/19] fix: Mutation Tests --- src/Context/HttpStreamContext.php | 16 +++++++--------- src/Context/SftpStreamContext.php | 12 +++++------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/Context/HttpStreamContext.php b/src/Context/HttpStreamContext.php index 90aef9e..dbc5809 100644 --- a/src/Context/HttpStreamContext.php +++ b/src/Context/HttpStreamContext.php @@ -118,24 +118,22 @@ public function setTimeout(float $timeout): void */ protected function getContextOptions(): array { - $context = [ - self::PROTOCOL => [], - ]; - $context[self::PROTOCOL]['method'] = $this->method->value; - $context[self::PROTOCOL]['timeout'] = $this->timeout ?? 10.0; + $context = []; + $context['method'] = $this->method->value; + $context['timeout'] = $this->timeout ?? 10.0; if (!empty($this->userAgent)) { - $context[self::PROTOCOL]['user_agent'] = $this->userAgent; + $context['user_agent'] = $this->userAgent; } if (!empty($this->headers)) { - $context[self::PROTOCOL]['header'] = $this->headers; + $context['header'] = $this->headers; } if (!empty($this->content) || in_array($this->method, [HttpMethod::POST, HttpMethod::PUT], true)) { - $context[self::PROTOCOL]['content'] = $this->content; + $context['content'] = $this->content; } - return $context; + return [self::PROTOCOL => $context]; } } diff --git a/src/Context/SftpStreamContext.php b/src/Context/SftpStreamContext.php index 9974abd..6c75a4c 100644 --- a/src/Context/SftpStreamContext.php +++ b/src/Context/SftpStreamContext.php @@ -55,17 +55,15 @@ public static function forPrivateKeyAuthentication(string $privateKey): self */ protected function getContextOptions(): array { - $context = [ - self::PROTOCOL => [], - ]; + $context = []; if ($this->privateKey !== '') { - $context[self::PROTOCOL]['privkey'] = $this->privateKey; + $context['privkey'] = $this->privateKey; } else { - $context[self::PROTOCOL]['username'] = $this->username; - $context[self::PROTOCOL]['password'] = $this->password; + $context['username'] = $this->username; + $context['password'] = $this->password; } - return $context; + return [self::PROTOCOL => $context]; } }