Author: Shivam Mathur (shivammathur)
Committer: GitHub (web-flow)
Pusher: shivammathur
Date: 2025-01-27T13:19:03+05:30

Commit: 
https://github.com/php/web-downloads/commit/43504195a964736baed80082ad3b07cc92304792
Raw diff: 
https://github.com/php/web-downloads/commit/43504195a964736baed80082ad3b07cc92304792.diff

Merge pull request #1 from php/tests

Add tests

Changed paths:
  A  .github/workflows/tests.yml
  A  .gitignore
  A  composer.json
  A  composer.lock
  A  phpunit.xml.dist
  A  src/Helpers/Helpers.php
  A  tests/Actions/FetchArtifactTest.php
  A  tests/Actions/GetArtifactsTest.php
  A  tests/AuthTest.php
  A  tests/BaseControllerTest.php
  A  tests/CommandTest.php
  A  tests/Console/Command/PeclCommandTest.php
  A  tests/Console/Command/PhpCommandTest.php
  A  tests/Console/Command/WinlibsCommandTest.php
  A  tests/ControllerInterfaceTest.php
  A  tests/Helpers/HelpersTest.php
  A  tests/Http/Controllers/IndexControllerTest.php
  A  tests/Http/Controllers/PeclControllerTest.php
  A  tests/Http/Controllers/PhpControllerTest.php
  A  tests/Http/Controllers/WinlibsControllerTest.php
  A  tests/RouterTest.php
  A  tests/ValidatorTest.php
  M  src/Actions/FetchArtifact.php
  M  src/Actions/GetArtifacts.php
  M  src/Console/Command.php
  M  src/Console/Command/PeclCommand.php
  M  src/Console/Command/PhpCommand.php
  M  src/Console/Command/WinlibsCommand.php
  M  src/Http/BaseController.php
  M  src/Http/Controllers/IndexController.php
  M  src/Http/Controllers/PeclController.php
  M  src/Http/Controllers/PhpController.php
  M  src/Http/Controllers/WinlibsController.php
  M  src/Router.php


Diff:

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..9ed2d42
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,21 @@
+name: Run tests
+
+on: [push, pull_request]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+
+    - name: Set up Php
+      uses: shivammathur/setup-php@v2
+      with:
+        php-version: '8.2'
+
+    - name: Install dependencies
+      uses: ramsey/composer-install@v3
+
+    - name: Run tests
+      run: vendor/bin/phpunit --coverage-text
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f394aa8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/vendor/
+/builds/
+xdebug-filter.php
+.phpunit.result.cache
+.phpunit.cache
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..db69f41
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,18 @@
+{
+    "name": "php/web-downloads",
+    "type": "project",
+    "license": "MIT",
+    "require": {
+      "ext-fileinfo": "*",
+      "ext-curl": "*",
+      "ext-zip": "*"
+    },
+    "autoload": {
+        "classmap": [
+            "src/"
+        ]
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^11"
+    }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..3e2d86e
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,1708 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at 
https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies";,
+        "This file is @generated automatically"
+    ],
+    "content-hash": "361bbdabce9f9fd3219dcda42e05e0d8",
+    "packages": [],
+    "packages-dev": [
+        {
+            "name": "myclabs/deep-copy",
+            "version": "1.12.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/myclabs/DeepCopy.git";,
+                "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845";,
+                "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1 || ^8.0"
+            },
+            "conflict": {
+                "doctrine/collections": "<1.6.8",
+                "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+            },
+            "require-dev": {
+                "doctrine/collections": "^1.6.8",
+                "doctrine/common": "^2.13.3 || ^3.2.2",
+                "phpspec/prophecy": "^1.10",
+                "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/DeepCopy/deep_copy.php"
+                ],
+                "psr-4": {
+                    "DeepCopy\\": "src/DeepCopy/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "MIT"
+            ],
+            "description": "Create deep copies (clones) of your objects",
+            "keywords": [
+                "clone",
+                "copy",
+                "duplicate",
+                "object",
+                "object graph"
+            ],
+            "support": {
+                "issues": "https://github.com/myclabs/DeepCopy/issues";,
+                "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1";
+            },
+            "funding": [
+                {
+                    "url": 
"https://tidelift.com/funding/github/packagist/myclabs/deep-copy";,
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-11-08T17:47:46+00:00"
+        },
+        {
+            "name": "nikic/php-parser",
+            "version": "v5.3.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/nikic/PHP-Parser.git";,
+                "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b";,
+                "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-json": "*",
+                "ext-tokenizer": "*",
+                "php": ">=7.4"
+            },
+            "require-dev": {
+                "ircmaxell/php-yacc": "^0.0.7",
+                "phpunit/phpunit": "^9.0"
+            },
+            "bin": [
+                "bin/php-parse"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PhpParser\\": "lib/PhpParser"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Nikita Popov"
+                }
+            ],
+            "description": "A PHP parser written in PHP",
+            "keywords": [
+                "parser",
+                "php"
+            ],
+            "support": {
+                "issues": "https://github.com/nikic/PHP-Parser/issues";,
+                "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1";
+            },
+            "time": "2024-10-08T18:51:32+00:00"
+        },
+        {
+            "name": "phar-io/manifest",
+            "version": "2.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phar-io/manifest.git";,
+                "reference": "54750ef60c58e43759730615a392c31c80e23176"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176";,
+                "reference": "54750ef60c58e43759730615a392c31c80e23176",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-libxml": "*",
+                "ext-phar": "*",
+                "ext-xmlwriter": "*",
+                "phar-io/version": "^3.0.1",
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "a...@blankerts.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Heuer",
+                    "email": "sebast...@phpeople.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Component for reading phar.io manifest information 
from a PHP Archive (PHAR)",
+            "support": {
+                "issues": "https://github.com/phar-io/manifest/issues";,
+                "source": "https://github.com/phar-io/manifest/tree/2.0.4";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/theseer";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-03-03T12:33:53+00:00"
+        },
+        {
+            "name": "phar-io/version",
+            "version": "3.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phar-io/version.git";,
+                "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74";,
+                "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "a...@blankerts.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Heuer",
+                    "email": "sebast...@phpeople.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Library for handling version information and 
constraints",
+            "support": {
+                "issues": "https://github.com/phar-io/version/issues";,
+                "source": "https://github.com/phar-io/version/tree/3.2.1";
+            },
+            "time": "2022-02-21T01:04:05+00:00"
+        },
+        {
+            "name": "phpunit/php-code-coverage",
+            "version": "11.0.8",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/php-code-coverage.git";,
+                "reference": "418c59fd080954f8c4aa5631d9502ecda2387118"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118";,
+                "reference": "418c59fd080954f8c4aa5631d9502ecda2387118",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-libxml": "*",
+                "ext-xmlwriter": "*",
+                "nikic/php-parser": "^5.3.1",
+                "php": ">=8.2",
+                "phpunit/php-file-iterator": "^5.1.0",
+                "phpunit/php-text-template": "^4.0.1",
+                "sebastian/code-unit-reverse-lookup": "^4.0.1",
+                "sebastian/complexity": "^4.0.1",
+                "sebastian/environment": "^7.2.0",
+                "sebastian/lines-of-code": "^3.0.1",
+                "sebastian/version": "^5.0.2",
+                "theseer/tokenizer": "^1.2.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.5.0"
+            },
+            "suggest": {
+                "ext-pcov": "PHP extension that provides line coverage",
+                "ext-xdebug": "PHP extension that provides line coverage as 
well as branch and path coverage"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "11.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that provides collection, processing, and 
rendering functionality for PHP code coverage information.",
+            "homepage": 
"https://github.com/sebastianbergmann/php-code-coverage";,
+            "keywords": [
+                "coverage",
+                "testing",
+                "xunit"
+            ],
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/php-code-coverage/issues";,
+                "security": 
"https://github.com/sebastianbergmann/php-code-coverage/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.8";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-12-11T12:34:27+00:00"
+        },
+        {
+            "name": "phpunit/php-file-iterator",
+            "version": "5.1.0",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/php-file-iterator.git";,
+                "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6";,
+                "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "5.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "FilterIterator implementation that filters files 
based on a list of suffixes.",
+            "homepage": 
"https://github.com/sebastianbergmann/php-file-iterator/";,
+            "keywords": [
+                "filesystem",
+                "iterator"
+            ],
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/php-file-iterator/issues";,
+                "security": 
"https://github.com/sebastianbergmann/php-file-iterator/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-08-27T05:02:59+00:00"
+        },
+        {
+            "name": "phpunit/php-invoker",
+            "version": "5.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-invoker.git";,
+                "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2";,
+                "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "ext-pcntl": "*",
+                "phpunit/phpunit": "^11.0"
+            },
+            "suggest": {
+                "ext-pcntl": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "5.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Invoke callables with a timeout",
+            "homepage": "https://github.com/sebastianbergmann/php-invoker/";,
+            "keywords": [
+                "process"
+            ],
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/php-invoker/issues";,
+                "security": 
"https://github.com/sebastianbergmann/php-invoker/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/php-invoker/tree/5.0.1";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-07-03T05:07:44+00:00"
+        },
+        {
+            "name": "phpunit/php-text-template",
+            "version": "4.0.1",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/php-text-template.git";,
+                "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964";,
+                "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "4.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Simple template engine.",
+            "homepage": 
"https://github.com/sebastianbergmann/php-text-template/";,
+            "keywords": [
+                "template"
+            ],
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/php-text-template/issues";,
+                "security": 
"https://github.com/sebastianbergmann/php-text-template/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/php-text-template/tree/4.0.1";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-07-03T05:08:43+00:00"
+        },
+        {
+            "name": "phpunit/php-timer",
+            "version": "7.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-timer.git";,
+                "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3";,
+                "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "7.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Utility class for timing",
+            "homepage": "https://github.com/sebastianbergmann/php-timer/";,
+            "keywords": [
+                "timer"
+            ],
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/php-timer/issues";,
+                "security": 
"https://github.com/sebastianbergmann/php-timer/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/php-timer/tree/7.0.1";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-07-03T05:09:35+00:00"
+        },
+        {
+            "name": "phpunit/phpunit",
+            "version": "11.5.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/phpunit.git";,
+                "reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2b94d4f2450b9869fa64a46fd8a6a41997aef56a";,
+                "reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-json": "*",
+                "ext-libxml": "*",
+                "ext-mbstring": "*",
+                "ext-xml": "*",
+                "ext-xmlwriter": "*",
+                "myclabs/deep-copy": "^1.12.1",
+                "phar-io/manifest": "^2.0.4",
+                "phar-io/version": "^3.2.1",
+                "php": ">=8.2",
+                "phpunit/php-code-coverage": "^11.0.7",
+                "phpunit/php-file-iterator": "^5.1.0",
+                "phpunit/php-invoker": "^5.0.1",
+                "phpunit/php-text-template": "^4.0.1",
+                "phpunit/php-timer": "^7.0.1",
+                "sebastian/cli-parser": "^3.0.2",
+                "sebastian/code-unit": "^3.0.1",
+                "sebastian/comparator": "^6.2.1",
+                "sebastian/diff": "^6.0.2",
+                "sebastian/environment": "^7.2.0",
+                "sebastian/exporter": "^6.3.0",
+                "sebastian/global-state": "^7.0.2",
+                "sebastian/object-enumerator": "^6.0.1",
+                "sebastian/type": "^5.1.0",
+                "sebastian/version": "^5.0.2",
+                "staabm/side-effects-detector": "^1.0.5"
+            },
+            "suggest": {
+                "ext-soap": "To be able to generate mocks based on WSDL files"
+            },
+            "bin": [
+                "phpunit"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "11.5-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/Framework/Assert/Functions.php"
+                ],
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "The PHP Unit Testing framework.",
+            "homepage": "https://phpunit.de/";,
+            "keywords": [
+                "phpunit",
+                "testing",
+                "xunit"
+            ],
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/phpunit/issues";,
+                "security": 
"https://github.com/sebastianbergmann/phpunit/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/phpunit/tree/11.5.1";
+            },
+            "funding": [
+                {
+                    "url": "https://phpunit.de/sponsors.html";,
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                },
+                {
+                    "url": 
"https://tidelift.com/funding/github/packagist/phpunit/phpunit";,
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-12-11T10:52:48+00:00"
+        },
+        {
+            "name": "sebastian/cli-parser",
+            "version": "3.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/cli-parser.git";,
+                "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180";,
+                "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library for parsing CLI options",
+            "homepage": "https://github.com/sebastianbergmann/cli-parser";,
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/cli-parser/issues";,
+                "security": 
"https://github.com/sebastianbergmann/cli-parser/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/cli-parser/tree/3.0.2";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-07-03T04:41:36+00:00"
+        },
+        {
+            "name": "sebastian/code-unit",
+            "version": "3.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/code-unit.git";,
+                "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca";,
+                "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Collection of value objects that represent the PHP 
code units",
+            "homepage": "https://github.com/sebastianbergmann/code-unit";,
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/code-unit/issues";,
+                "security": 
"https://github.com/sebastianbergmann/code-unit/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/code-unit/tree/3.0.2";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-12-12T09:59:06+00:00"
+        },
+        {
+            "name": "sebastian/code-unit-reverse-lookup",
+            "version": "4.0.1",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/code-unit-reverse-lookup.git";,
+                "reference": "183a9b2632194febd219bb9246eee421dad8d45e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e";,
+                "reference": "183a9b2632194febd219bb9246eee421dad8d45e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "4.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Looks up which function or method a line of code 
belongs to",
+            "homepage": 
"https://github.com/sebastianbergmann/code-unit-reverse-lookup/";,
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues";,
+                "security": 
"https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-07-03T04:45:54+00:00"
+        },
+        {
+            "name": "sebastian/comparator",
+            "version": "6.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/comparator.git";,
+                "reference": "43d129d6a0f81c78bee378b46688293eb7ea3739"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/comparator/zipball/43d129d6a0f81c78bee378b46688293eb7ea3739";,
+                "reference": "43d129d6a0f81c78bee378b46688293eb7ea3739",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-mbstring": "*",
+                "php": ">=8.2",
+                "sebastian/diff": "^6.0",
+                "sebastian/exporter": "^6.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "6.2-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                },
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthej...@gmail.com"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "git...@wallbash.com"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschus...@2bepublished.at"
+                }
+            ],
+            "description": "Provides the functionality to compare PHP values 
for equality",
+            "homepage": "https://github.com/sebastianbergmann/comparator";,
+            "keywords": [
+                "comparator",
+                "compare",
+                "equality"
+            ],
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/comparator/issues";,
+                "security": 
"https://github.com/sebastianbergmann/comparator/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/comparator/tree/6.2.1";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-10-31T05:30:08+00:00"
+        },
+        {
+            "name": "sebastian/complexity",
+            "version": "4.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/complexity.git";,
+                "reference": "ee41d384ab1906c68852636b6de493846e13e5a0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0";,
+                "reference": "ee41d384ab1906c68852636b6de493846e13e5a0",
+                "shasum": ""
+            },
+            "require": {
+                "nikic/php-parser": "^5.0",
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "4.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library for calculating the complexity of PHP code 
units",
+            "homepage": "https://github.com/sebastianbergmann/complexity";,
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/complexity/issues";,
+                "security": 
"https://github.com/sebastianbergmann/complexity/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/complexity/tree/4.0.1";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-07-03T04:49:50+00:00"
+        },
+        {
+            "name": "sebastian/diff",
+            "version": "6.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/diff.git";,
+                "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544";,
+                "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.0",
+                "symfony/process": "^4.2 || ^5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "6.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                },
+                {
+                    "name": "Kore Nordmann",
+                    "email": "m...@kore-nordmann.de"
+                }
+            ],
+            "description": "Diff implementation",
+            "homepage": "https://github.com/sebastianbergmann/diff";,
+            "keywords": [
+                "diff",
+                "udiff",
+                "unidiff",
+                "unified diff"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/diff/issues";,
+                "security": 
"https://github.com/sebastianbergmann/diff/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/diff/tree/6.0.2";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-07-03T04:53:05+00:00"
+        },
+        {
+            "name": "sebastian/environment",
+            "version": "7.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/environment.git";,
+                "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5";,
+                "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.0"
+            },
+            "suggest": {
+                "ext-posix": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "7.2-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Provides functionality to handle HHVM/PHP 
environments",
+            "homepage": "https://github.com/sebastianbergmann/environment";,
+            "keywords": [
+                "Xdebug",
+                "environment",
+                "hhvm"
+            ],
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/environment/issues";,
+                "security": 
"https://github.com/sebastianbergmann/environment/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/environment/tree/7.2.0";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-07-03T04:54:44+00:00"
+        },
+        {
+            "name": "sebastian/exporter",
+            "version": "6.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/exporter.git";,
+                "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3";,
+                "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3",
+                "shasum": ""
+            },
+            "require": {
+                "ext-mbstring": "*",
+                "php": ">=8.2",
+                "sebastian/recursion-context": "^6.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "6.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                },
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthej...@gmail.com"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "git...@wallbash.com"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "ahar...@php.net"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschus...@gmail.com"
+                }
+            ],
+            "description": "Provides the functionality to export PHP variables 
for visualization",
+            "homepage": "https://www.github.com/sebastianbergmann/exporter";,
+            "keywords": [
+                "export",
+                "exporter"
+            ],
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/exporter/issues";,
+                "security": 
"https://github.com/sebastianbergmann/exporter/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/exporter/tree/6.3.0";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-12-05T09:17:50+00:00"
+        },
+        {
+            "name": "sebastian/global-state",
+            "version": "7.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/global-state.git";,
+                "reference": "3be331570a721f9a4b5917f4209773de17f747d7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7";,
+                "reference": "3be331570a721f9a4b5917f4209773de17f747d7",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2",
+                "sebastian/object-reflector": "^4.0",
+                "sebastian/recursion-context": "^6.0"
+            },
+            "require-dev": {
+                "ext-dom": "*",
+                "phpunit/phpunit": "^11.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "7.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Snapshotting of global state",
+            "homepage": 
"https://www.github.com/sebastianbergmann/global-state";,
+            "keywords": [
+                "global state"
+            ],
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/global-state/issues";,
+                "security": 
"https://github.com/sebastianbergmann/global-state/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/global-state/tree/7.0.2";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-07-03T04:57:36+00:00"
+        },
+        {
+            "name": "sebastian/lines-of-code",
+            "version": "3.0.1",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/lines-of-code.git";,
+                "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a";,
+                "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a",
+                "shasum": ""
+            },
+            "require": {
+                "nikic/php-parser": "^5.0",
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library for counting the lines of code in PHP 
source code",
+            "homepage": "https://github.com/sebastianbergmann/lines-of-code";,
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/lines-of-code/issues";,
+                "security": 
"https://github.com/sebastianbergmann/lines-of-code/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-07-03T04:58:38+00:00"
+        },
+        {
+            "name": "sebastian/object-enumerator",
+            "version": "6.0.1",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/object-enumerator.git";,
+                "reference": "f5b498e631a74204185071eb41f33f38d64608aa"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa";,
+                "reference": "f5b498e631a74204185071eb41f33f38d64608aa",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2",
+                "sebastian/object-reflector": "^4.0",
+                "sebastian/recursion-context": "^6.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "6.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Traverses array structures and object graphs to 
enumerate all referenced objects",
+            "homepage": 
"https://github.com/sebastianbergmann/object-enumerator/";,
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/object-enumerator/issues";,
+                "security": 
"https://github.com/sebastianbergmann/object-enumerator/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-07-03T05:00:13+00:00"
+        },
+        {
+            "name": "sebastian/object-reflector",
+            "version": "4.0.1",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/object-reflector.git";,
+                "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9";,
+                "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "4.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Allows reflection of object attributes, including 
inherited and non-public ones",
+            "homepage": 
"https://github.com/sebastianbergmann/object-reflector/";,
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/object-reflector/issues";,
+                "security": 
"https://github.com/sebastianbergmann/object-reflector/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/object-reflector/tree/4.0.1";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-07-03T05:01:32+00:00"
+        },
+        {
+            "name": "sebastian/recursion-context",
+            "version": "6.0.2",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/recursion-context.git";,
+                "reference": "694d156164372abbd149a4b85ccda2e4670c0e16"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16";,
+                "reference": "694d156164372abbd149a4b85ccda2e4670c0e16",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "6.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                },
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthej...@gmail.com"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "ahar...@php.net"
+                }
+            ],
+            "description": "Provides functionality to recursively process PHP 
variables",
+            "homepage": 
"https://github.com/sebastianbergmann/recursion-context";,
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/recursion-context/issues";,
+                "security": 
"https://github.com/sebastianbergmann/recursion-context/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/recursion-context/tree/6.0.2";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-07-03T05:10:34+00:00"
+        },
+        {
+            "name": "sebastian/type",
+            "version": "5.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/type.git";,
+                "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac";,
+                "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "5.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Collection of value objects that represent the 
types of the PHP type system",
+            "homepage": "https://github.com/sebastianbergmann/type";,
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/type/issues";,
+                "security": 
"https://github.com/sebastianbergmann/type/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/type/tree/5.1.0";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-09-17T13:12:04+00:00"
+        },
+        {
+            "name": "sebastian/version",
+            "version": "5.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/version.git";,
+                "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874";,
+                "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "5.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that helps with managing the version 
number of Git-hosted PHP projects",
+            "homepage": "https://github.com/sebastianbergmann/version";,
+            "support": {
+                "issues": 
"https://github.com/sebastianbergmann/version/issues";,
+                "security": 
"https://github.com/sebastianbergmann/version/security/policy";,
+                "source": 
"https://github.com/sebastianbergmann/version/tree/5.0.2";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-10-09T05:16:32+00:00"
+        },
+        {
+            "name": "staabm/side-effects-detector",
+            "version": "1.0.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/staabm/side-effects-detector.git";,
+                "reference": "d8334211a140ce329c13726d4a715adbddd0a163"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163";,
+                "reference": "d8334211a140ce329c13726d4a715adbddd0a163",
+                "shasum": ""
+            },
+            "require": {
+                "ext-tokenizer": "*",
+                "php": "^7.4 || ^8.0"
+            },
+            "require-dev": {
+                "phpstan/extension-installer": "^1.4.3",
+                "phpstan/phpstan": "^1.12.6",
+                "phpunit/phpunit": "^9.6.21",
+                "symfony/var-dumper": "^5.4.43",
+                "tomasvotruba/type-coverage": "1.0.0",
+                "tomasvotruba/unused-public": "1.0.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "lib/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "MIT"
+            ],
+            "description": "A static analysis tool to detect side effects in 
PHP code",
+            "keywords": [
+                "static analysis"
+            ],
+            "support": {
+                "issues": 
"https://github.com/staabm/side-effects-detector/issues";,
+                "source": 
"https://github.com/staabm/side-effects-detector/tree/1.0.5";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/staabm";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-10-20T05:08:20+00:00"
+        },
+        {
+            "name": "theseer/tokenizer",
+            "version": "1.2.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/theseer/tokenizer.git";,
+                "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2";,
+                "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-tokenizer": "*",
+                "ext-xmlwriter": "*",
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "a...@blankerts.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "A small library for converting tokenized PHP 
source code into XML and potentially other formats",
+            "support": {
+                "issues": "https://github.com/theseer/tokenizer/issues";,
+                "source": "https://github.com/theseer/tokenizer/tree/1.2.3";
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/theseer";,
+                    "type": "github"
+                }
+            ],
+            "time": "2024-03-03T12:36:25+00:00"
+        }
+    ],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": {},
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": {
+        "ext-fileinfo": "*",
+        "ext-curl": "*",
+        "ext-zip": "*"
+    },
+    "platform-dev": {},
+    "plugin-api-version": "2.6.0"
+}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..659c34b
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
+         bootstrap="vendor/autoload.php"
+         cacheDirectory=".phpunit.cache"
+         executionOrder="depends,defects"
+         shortenArraysForExportThreshold="10"
+         beStrictAboutOutputDuringTests="true"
+         displayDetailsOnPhpunitDeprecations="true"
+         failOnPhpunitDeprecation="true"
+         failOnRisky="true"
+         failOnWarning="true">
+    <testsuites>
+        <testsuite name="default">
+            <directory>tests</directory>
+        </testsuite>
+    </testsuites>
+
+    <source ignoreIndirectDeprecations="true" restrictNotices="true" 
restrictWarnings="true">
+        <include>
+            <directory>src</directory>
+        </include>
+    </source>
+</phpunit>
diff --git a/src/Actions/FetchArtifact.php b/src/Actions/FetchArtifact.php
index 2339e1c..5f096a3 100644
--- a/src/Actions/FetchArtifact.php
+++ b/src/Actions/FetchArtifact.php
@@ -4,12 +4,12 @@
 
 class FetchArtifact
 {
-    public static function handle($url, $filepath, $token = null): void
+    public function handle($url, $filepath, $token = null): void
     {
         $ch = curl_init();
+        $fp = fopen($filepath, 'w');
         curl_setopt($ch, CURLOPT_URL, $url);
         curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
-        $fp = fopen($filepath, 'w+');
         curl_setopt($ch, CURLOPT_FILE, $fp);
         if (str_contains($url, 'api.github.com')) {
             $headers = [
diff --git a/src/Actions/GetArtifacts.php b/src/Actions/GetArtifacts.php
index 140480e..0dc4c5a 100644
--- a/src/Actions/GetArtifacts.php
+++ b/src/Actions/GetArtifacts.php
@@ -2,9 +2,11 @@
 
 namespace App\Actions;
 
+use App\Helpers\Helpers;
+
 class GetArtifacts
 {
-    public static function handle($workflow_run_id, $token): void
+    public function handle($workflow_run_id, $token): void
     {
         $ch = curl_init();
 
@@ -33,14 +35,13 @@ public static function handle($workflow_run_id, $token): 
void
             $artifacts = json_decode($response, true);
             $workflowRunDirectory = getenv('BUILDS_DIRECTORY') . "/winlibs/" . 
$workflow_run_id;
             if (is_dir($workflowRunDirectory)) {
-                rmdir($workflowRunDirectory);
+                (new Helpers)->rmdirr($workflowRunDirectory);
             }
             mkdir($workflowRunDirectory, 0755, true);
             foreach ($artifacts['artifacts'] as $artifact) {
                 $filepath = $workflowRunDirectory . "/" . $artifact['name'] . 
".zip";
-                FetchArtifact::handle($artifact['archive_download_url'], 
$filepath, $token);
+                (new FetchArtifact)->handle($artifact['archive_download_url'], 
$filepath, $token);
             }
         }
     }
-
 }
\ No newline at end of file
diff --git a/src/Console/Command.php b/src/Console/Command.php
index fcb12eb..56d9ac6 100644
--- a/src/Console/Command.php
+++ b/src/Console/Command.php
@@ -4,9 +4,9 @@
 
 abstract class Command
 {
-    public const int SUCCESS = 0;
-    public const int FAILURE = 1;
-    public const int INVALID = 2;
+    public const SUCCESS = 0;
+    public const FAILURE = 1;
+    public const INVALID = 2;
 
     protected string $signature = '';
 
@@ -57,11 +57,18 @@ public function getDescription(): string {
         return $this->description;
     }
 
-    public function getArgument($index) {
+    public function getArgument($index): mixed
+    {
         return $this->arguments[$index] ?? null;
     }
 
-    public function getOption($name) {
+    public function getOption($name): mixed
+    {
         return $this->options[$name] ?? null;
     }
+
+    public function setOption($name, $value): void
+    {
+        $this->options[$name] = $value;
+    }
 }
\ No newline at end of file
diff --git a/src/Console/Command/PeclCommand.php 
b/src/Console/Command/PeclCommand.php
index 807a785..53fb0e3 100644
--- a/src/Console/Command/PeclCommand.php
+++ b/src/Console/Command/PeclCommand.php
@@ -20,7 +20,13 @@ public function handle(): int
                 throw new Exception('Base directory is required');
             }
 
-            $files = glob(getenv('BUILDS_DIRECTORY') . '/php/*.zip');
+            $zips_directory = getenv('BUILDS_DIRECTORY') . '/pecl';
+            if(!is_dir($zips_directory)) {
+                return Command::SUCCESS;
+            }
+
+            $files = glob($zips_directory . '/*.zip');
+
 
             // We lock the files we are working on
             // so that we don't process them again if the command is run again
diff --git a/src/Console/Command/PhpCommand.php 
b/src/Console/Command/PhpCommand.php
index 005febc..596b3dc 100644
--- a/src/Console/Command/PhpCommand.php
+++ b/src/Console/Command/PhpCommand.php
@@ -3,6 +3,7 @@
 namespace App\Console\Command;
 
 use App\Console\Command;
+use App\Helpers\Helpers;
 use DateTimeImmutable;
 use Exception;
 use ZipArchive;
@@ -22,7 +23,12 @@ public function handle(): int
                 throw new Exception('Base directory is required');
             }
 
-            $files = glob(getenv('BUILDS_DIRECTORY') . '/php/*.zip');
+            $zips_directory = getenv('BUILDS_DIRECTORY') . '/php';
+            if(!is_dir($zips_directory)) {
+                return Command::SUCCESS;
+            }
+
+            $files = glob($zips_directory . '/*.zip');
 
             // We lock the files we are working on
             // so that we don't process them again if the command is run again
@@ -40,19 +46,18 @@ public function handle(): int
                 $tempDirectory = "/tmp/php-" . $hash;
 
                 if (is_dir($tempDirectory)) {
-                    rmdir($tempDirectory);
+                    (new Helpers)->rmdirr($tempDirectory);
                 }
                 mkdir($tempDirectory, 0755, true);
 
                 $zip = new ZipArchive();
-
                 if ($zip->open($filepath) === TRUE) {
                     if ($zip->extractTo($tempDirectory) === FALSE) {
-                        throw new Exception('Failed to extract the extension 
build');
+                        throw new Exception('Failed to extract the php build');
                     }
                     $zip->close();
                 } else {
-                    throw new Exception('Failed to extract the extension');
+                    throw new Exception('Failed to extract the php build');
                 }
 
                 unlink($filepath);
@@ -63,20 +68,33 @@ public function handle(): int
 
                 $this->generateListing($destinationDirectory);
 
-                rmdir($tempDirectory);
+                (new Helpers)->rmdirr($tempDirectory);
 
                 unlink($filepath . '.lock');
             }
             return Command::SUCCESS;
         } catch (Exception $e) {
             echo $e->getMessage();
+            $tempDirectories = glob('/tmp/php-*');
+            if($tempDirectories) {
+                foreach ($tempDirectories as $tempDirectory) {
+                    (new Helpers)->rmdirr($tempDirectory);
+                }
+            }
             return Command::FAILURE;
         }
     }
 
+    /**
+     * @throws Exception
+     */
     private function getDestinationDirectory(string $tempDirectory): string
     {
-        $testPackFile = basename(glob($tempDirectory . 
'/php-test-pack-*.zip')[0]);
+        $testPackFiles = glob($tempDirectory . '/php-test-pack-*.zip');
+        if(empty($testPackFiles)) {
+            throw new Exception('No test pack found in the artifact');
+        }
+        $testPackFile = basename($testPackFiles[0]);
         $testPackFileName = str_replace('.zip', '', $testPackFile);
         $version = explode('-', $testPackFileName)[3];
         return $this->baseDirectory . (preg_match('/^\d+\.\d+\.\d+$/', 
$version) ? '/releases' : '/qa');
@@ -95,7 +113,7 @@ private function moveBuild(string $tempDirectory, string 
$destinationDirectory):
                 $destination = $destinationDirectory . '/' . $fileName;
                 rename($file, $destination);
             }
-            rmdir($tempDirectory);
+            (new Helpers)->rmdirr($tempDirectory);
             $this->copyBuildsToArchive($destinationDirectory, $version);
         } else {
             throw new Exception('No builds found in the artifact');
@@ -105,11 +123,14 @@ private function moveBuild(string $tempDirectory, string 
$destinationDirectory):
     private function copyBuildsToArchive(string $directory, string $version): 
void
     {
         $version_short = substr($version, 0, 3);
-        $files = glob($directory . '/php*-' . $version_short . '-*.zip');
+        $files = glob($directory . '/php*' . $version_short . '*.zip');
+        if(!is_dir($directory . '/archive')) {
+            mkdir($directory . '/archive', 0755, true);
+        }
         foreach ($files as $file) {
             $fileVersion = $this->getFileVersion($file);
             if ($fileVersion) {
-                copy($directory . '/' . basename($file), $directory . 
'/archive/' . $file);
+                copy($directory . '/' . basename($file), $directory . 
'/archive/' . basename($file));
                 if (version_compare($fileVersion, $version) < 0) {
                     unlink($file);
                 }
@@ -119,8 +140,15 @@ private function copyBuildsToArchive(string $directory, 
string $version): void
 
     private function getFileVersion(string $file): string
     {
-        $file = preg_replace('/^php-((debug|devel|test)-pack)?/', '', $file);
-        return explode('-', $file)[0];
+        $file = basename($file);
+        if(preg_match('/^php-((debug|devel|test)-pack-).*/', $file)) {
+            $pattern = '/^php-((debug|devel|test)-pack-)?/';
+        } else {
+            $pattern = '/php-/';
+        }
+        $file = preg_replace($pattern, '', $file);
+        $parts = explode('-', $file);
+        return str_replace('.zip', '', $parts[0]);
     }
 
     /**
@@ -147,9 +175,9 @@ private function generateListing(string $directory): void
             }
             $releases[$version_short][$key]['mtime'] = $mtime;
             $releases[$version_short][$key]['zip'] = [
-                'path' => $file_ori,
+                'path' => basename($file_ori),
                 'size' => $this->bytes2string(filesize($file_ori)),
-                'sha256' => $sha256sums[strtolower($file_ori)]
+                'sha256' => $sha256sums[strtolower(basename($file_ori))]
             ];
             $namingPattern = $parts['version'] . ($parts['nts'] ? '-' . 
$parts['nts'] : '') . '-Win32-' . $parts['vc'] . '-' . $parts['arch'] . 
($parts['ts'] ? '-' . $parts['ts'] : '');
             $build_types = [
@@ -162,10 +190,19 @@ private function generateListing(string $directory): void
             foreach ($build_types as $type => $fileName) {
                 $filePath = $directory . '/' . $fileName;
                 if (file_exists($filePath)) {
-                    $releases[$version_short][$type] = [
-                        'path' => $fileName,
-                        'size' => $this->bytes2string(filesize($filePath))
-                    ];
+                    if(in_array($type, ['test_pack', 'source'])) {
+                        $releases[$version_short][$type] = [
+                            'path' => $fileName,
+                            'size' => $this->bytes2string(filesize($filePath)),
+                            'sha256' => 
$sha256sums[strtolower(basename($file_ori))]
+                        ];
+                    } else {
+                        $releases[$version_short][$key][$type] = [
+                            'path' => $fileName,
+                            'size' => $this->bytes2string(filesize($filePath)),
+                            'sha256' => 
$sha256sums[strtolower(basename($file_ori))]
+                        ];
+                    }
                 }
             }
         }
@@ -206,16 +243,30 @@ private function updateReleasesJson(array $releases, 
string $directory): void
 
     private function updateLatestBuilds($releases, $directory): void
     {
+        if(!is_dir($directory . '/latest')) {
+            mkdir($directory . '/latest', 0755, true);
+        }
         foreach ($releases as $versionShort => $release) {
-            $latestFileName = str_replace($release['version'], $versionShort, 
$release['path']);
-            $latestFileName = str_replace('.zip', '-latest.zip', 
$latestFileName);
-            copy($directory . '/' . $release['path'], $directory . '/latest/' 
. $latestFileName);
+            foreach ($release as $value) {
+                $filePath = $value['path'] ?? $value['zip']['path'] ?? null;
+                if($filePath === null) {
+                    continue;
+                } else {
+                    $filePath = basename($filePath);
+                }
+                $latestFileName = str_replace($release['version'], 
$versionShort, $filePath);
+                $latestFileName = str_replace('.zip', '-latest.zip', 
$latestFileName);
+                copy($directory . '/' . $filePath, $directory . '/latest/' . 
$latestFileName);
+            }
         }
     }
 
     private function getSha256Sums($directory): array
     {
         $result = [];
+        if(!file_exists("$directory/sha256sum.txt")) {
+            file_put_contents("$directory/sha256sum.txt", '');
+        }
         $sha_file = fopen("$directory/sha256sum.txt", 'w');
         foreach (scandir($directory) as $filename) {
             if (pathinfo($filename, PATHINFO_EXTENSION) !== 'zip') {
@@ -229,7 +280,7 @@ private function getSha256Sums($directory): array
         return $result;
     }
 
-    private function bytes2string(int $size): float
+    private function bytes2string(int $size): string
     {
         $sizes = ['YB', 'ZB', 'EB', 'PB', 'TB', 'GB', 'MB', 'kB', 'B'];
 
diff --git a/src/Console/Command/WinlibsCommand.php 
b/src/Console/Command/WinlibsCommand.php
index 92543cc..ebec0f4 100644
--- a/src/Console/Command/WinlibsCommand.php
+++ b/src/Console/Command/WinlibsCommand.php
@@ -3,6 +3,7 @@
 namespace App\Console\Command;
 
 use App\Console\Command;
+use App\Helpers\Helpers;
 use Exception;
 
 class WinlibsCommand extends Command
@@ -34,16 +35,16 @@ public function handle(): int
             }
 
             foreach ($filteredDirectories as $directoryPath) {
-                $data = json_decode(file_get_contents($directoryPath . 
'/data.json'), true);
+                $data = json_decode(file_get_contents($directoryPath . 
'/data.json'), true, 512, JSON_THROW_ON_ERROR);
                 extract($data);
                 $files = glob($directoryPath . '/*.zip');
                 $files = $this->parseFiles($files);
                 if ($files) {
-                    $this->copyFiles($files, $library, $ref, 
$vs_version_targets);
-                    $this->updateSeriesFiles($files, $library, $ref, 
$php_versions, $vs_version_targets, $stability);
+                    $this->copyFiles($files, $library, $vs_version_targets);
+                    $this->updateSeriesFiles($files, $library, $php_versions, 
$vs_version_targets, $stability);
                 }
 
-                rmdir($directoryPath);
+                (new Helpers)->rmdirr($directoryPath);
 
                 unlink($directoryPath . '.lock');
             }
@@ -60,20 +61,21 @@ private function parseFiles(array $files): array
         foreach ($files as $file) {
             $fileName = basename($file);
             $fileNameParts = explode('.', $fileName);
-            $parsedFileNameParts = explode('-', $fileNameParts[0]);
+            $parsedFileNameParts = explode('-', $fileName);
+            $archParts = explode('.', $parsedFileNameParts[3]);
             $data[] = [
                 'file_path' => $file,
                 'file_name' => $fileName,
-                'extension' => $fileNameParts[1],
+                'extension' => $fileNameParts[count($fileNameParts)-1],
                 'artifact_name' => $parsedFileNameParts[0],
-                'vs_version' => $parsedFileNameParts[1],
-                'arch' => $parsedFileNameParts[2],
+                'vs_version' => $parsedFileNameParts[2],
+                'arch' => $archParts[0],
             ];
         }
         return $data;
     }
 
-    private function copyFiles(array $files, string $library, string $ref, 
string $vs_version_targets): void
+    private function copyFiles(array $files, string $library, string 
$vs_version_targets): void
     {
         $baseDirectory = $this->baseDirectory . "/php-sdk/deps";
         if (!is_dir($baseDirectory)) {
@@ -83,7 +85,10 @@ private function copyFiles(array $files, string $library, 
string $ref, string $v
         foreach ($files as $file) {
             foreach ($vs_version_targets as $vs_version_target) {
                 $destinationDirectory = $baseDirectory . '/' . 
$vs_version_target . '/' . $file['arch'];
-                $destinationFileName = str_replace($file['artifact_name'], 
$library . '-' . $ref, $file['file_name']);
+                if (!is_dir($destinationDirectory)) {
+                    mkdir($destinationDirectory, 0755, true);
+                }
+                $destinationFileName = str_replace($file['artifact_name'], 
$library, $file['file_name']);
                 copy($file['file_path'], $destinationDirectory . '/' . 
$destinationFileName);
             }
         }
@@ -92,7 +97,6 @@ private function copyFiles(array $files, string $library, 
string $ref, string $v
     private function updateSeriesFiles(
         array  $files,
         string $library,
-        string $ref,
         string $php_versions,
         string $vs_version_targets,
         string $stability
@@ -104,17 +108,30 @@ private function updateSeriesFiles(
 
         $baseDirectory = $this->baseDirectory . "/php-sdk/deps/series";
 
+        if (!is_dir($baseDirectory)) {
+            mkdir($baseDirectory, 0755, true);
+        }
+
         foreach ($php_versions as $php_version) {
             foreach ($vs_version_targets as $vs_version_target) {
                 foreach ($stability_values as $stability_value) {
                     foreach ($files as $file) {
-                        $fileName = str_replace($file['artifact_name'], 
$library . '-' . $ref, $file['file_name']);
+                        $fileName = str_replace($file['artifact_name'], 
$library, $file['file_name']);
                         $arch = $file['arch'];
                         $seriesFile = $baseDirectory . 
"/packages-$php_version-$vs_version_target-$arch-$stability_value.txt";
-                        $file_lines = file($seriesFile, FILE_IGNORE_NEW_LINES);
-                        foreach ($file_lines as $no => $line) {
-                            if (str_starts_with($line, $library)) {
-                                $file_lines[$no] = $fileName;
+                        if (!file_exists($seriesFile)) {
+                            $file_lines = [$fileName];
+                        } else {
+                            $file_lines = file($seriesFile, 
FILE_IGNORE_NEW_LINES);
+                            $found = false;
+                            foreach ($file_lines as $no => $line) {
+                                if (str_starts_with($line, $library)) {
+                                    $file_lines[$no] = $fileName;
+                                    $found = true;
+                                }
+                            }
+                            if (!$found) {
+                                $file_lines[] = $fileName;
                             }
                         }
                         file_put_contents($seriesFile, implode("\n", 
$file_lines));
diff --git a/src/Helpers/Helpers.php b/src/Helpers/Helpers.php
new file mode 100644
index 0000000..7a7556b
--- /dev/null
+++ b/src/Helpers/Helpers.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Helpers;
+
+class Helpers
+{
+    public function rmdirr($node): bool
+    {
+        if (!file_exists($node)) {
+            return false;
+        }
+        if (is_file($node) || is_link($node)) {
+            return unlink($node);
+        }
+        $dir = dir($node);
+        while (false !== $leaf = $dir->read()) {
+            if ($leaf == '.' || $leaf == '..') {
+                continue;
+            }
+            $this->rmdirr($node . DIRECTORY_SEPARATOR . $leaf);
+        }
+        $dir->close();
+        return rmdir($node);
+    }
+}
\ No newline at end of file
diff --git a/src/Http/BaseController.php b/src/Http/BaseController.php
index 9bc8f35..b8cd681 100644
--- a/src/Http/BaseController.php
+++ b/src/Http/BaseController.php
@@ -2,12 +2,20 @@
 
 namespace App\Http;
 
+use JsonException;
+
 abstract class BaseController implements ControllerInterface
 {
+    public function __construct(protected string $inputPath = "php://input") {
+        //
+    }
+
+    /**
+     * @throws JsonException
+     */
     public function handle(): void
     {
-        $data = json_decode(file_get_contents('php://input'), true);
-
+        $data = json_decode(file_get_contents($this->inputPath), true, 512, 
JSON_THROW_ON_ERROR);
         if ($this->validate($data)) {
             $this->execute($data);
         }
diff --git a/src/Http/Controllers/IndexController.php 
b/src/Http/Controllers/IndexController.php
index c80478a..4296a95 100644
--- a/src/Http/Controllers/IndexController.php
+++ b/src/Http/Controllers/IndexController.php
@@ -11,12 +11,12 @@ public function handle(): void
         echo 'Welcome!';
     }
 
-    protected function validate(array $data): bool
+    public function validate(array $data): bool
     {
         return true;
     }
 
-    protected function execute(array $data): void
+    public function execute(array $data): void
     {
         //
     }
diff --git a/src/Http/Controllers/PeclController.php 
b/src/Http/Controllers/PeclController.php
index 25423d0..43b0b4d 100644
--- a/src/Http/Controllers/PeclController.php
+++ b/src/Http/Controllers/PeclController.php
@@ -9,7 +9,7 @@
 
 class PeclController extends BaseController
 {
-    protected function validate(mixed $data): bool
+    public function validate(array $data): bool
     {
         $validator = new Validator([
             'url' => 'required|url',
@@ -29,7 +29,7 @@ protected function validate(mixed $data): bool
         return $valid;
     }
 
-    protected function execute(array $data): void
+    public function execute(array $data): void
     {
         try {
             extract($data);
@@ -43,11 +43,11 @@ protected function execute(array $data): void
     /**
      * @throws Exception
      */
-    private function fetchExtension(string $extension, string $ref, string 
$url, string $token): void
+    protected function fetchExtension(string $extension, string $ref, string 
$url, string $token): void
     {
         $filepath = getenv('BUILDS_DIRECTORY') . "/pecl/$extension-$ref-" . 
hash('sha256', $url) . strtotime('now') . ".zip";
 
-        FetchArtifact::handle($url, $filepath, $token);
+        (new FetchArtifact)->handle($url, $filepath, $token);
 
         if (!file_exists($filepath) || mime_content_type($filepath) !== 
'application/zip') {
             throw new Exception('Failed to fetch the extension');
diff --git a/src/Http/Controllers/PhpController.php 
b/src/Http/Controllers/PhpController.php
index 3711a47..4c25442 100644
--- a/src/Http/Controllers/PhpController.php
+++ b/src/Http/Controllers/PhpController.php
@@ -48,7 +48,7 @@ private function fetchPhpBuild(string $url, string $token): 
void
 
         $filepath = getenv('BUILDS_DIRECTORY') . "/php/php-" . $hash . 
".tar.gz";
 
-        FetchArtifact::handle($url, $filepath, $token);
+        (new FetchArtifact)->handle($url, $filepath, $token);
 
         if (!file_exists($filepath) || mime_content_type($filepath) !== 
'application/zip') {
             throw new Exception('Failed to fetch the PHP build');
diff --git a/src/Http/Controllers/WinlibsController.php 
b/src/Http/Controllers/WinlibsController.php
index 363e681..88f4d0d 100644
--- a/src/Http/Controllers/WinlibsController.php
+++ b/src/Http/Controllers/WinlibsController.php
@@ -36,7 +36,7 @@ protected function validate(array $data): bool
     protected function execute(array $data): void
     {
         extract($data);
-        GetArtifacts::handle($workflow_run_id, $token);
+        (new GetArtifacts)->handle($workflow_run_id, $token);
         $directory = getenv('BUILDS_DIRECTORY') . '/winlibs/' . 
$workflow_run_id;
         file_put_contents($directory . '/data.json', json_encode($data));
     }
diff --git a/src/Router.php b/src/Router.php
index 06e7ca4..59c5861 100644
--- a/src/Router.php
+++ b/src/Router.php
@@ -3,6 +3,7 @@
 namespace App;
 
 use App\Http\BaseController;
+use JsonException;
 
 class Router
 {
@@ -22,6 +23,9 @@ public function registerRoute(string $path, string $method, 
string $handler, boo
         ];
     }
 
+    /**
+     * @throws JsonException
+     */
     public function handleRequest(): void
     {
         $path = $_SERVER['REQUEST_URI'];
diff --git a/tests/Actions/FetchArtifactTest.php 
b/tests/Actions/FetchArtifactTest.php
new file mode 100644
index 0000000..27a956e
--- /dev/null
+++ b/tests/Actions/FetchArtifactTest.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Actions;
+
+use App\Actions\FetchArtifact;
+use PHPUnit\Framework\TestCase;
+
+class MockFetchArtifact extends FetchArtifact {
+
+    public function handle($url, $filepath, $token = null): void
+    {
+        file_put_contents($filepath, $url . $token);
+    }
+}
+
+class FetchArtifactTest extends TestCase {
+    public function testHandleWithValidData() {
+        $url = "https://example.com";;
+        $filepath = "test.txt";
+        $token = "test_token";
+        $fetchArtifact = new MockFetchArtifact();
+        $fetchArtifact->handle($url, $filepath, $token);
+        $this->assertFileExists($filepath);
+        $this->assertEquals($url . $token, file_get_contents($filepath));
+        unlink($filepath);
+    }
+}
diff --git a/tests/Actions/GetArtifactsTest.php 
b/tests/Actions/GetArtifactsTest.php
new file mode 100644
index 0000000..72e279c
--- /dev/null
+++ b/tests/Actions/GetArtifactsTest.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Actions;
+
+use App\Actions\GetArtifacts;
+use App\Helpers\Helpers;
+use PHPUnit\Framework\TestCase;
+
+class MockGetArtifacts extends GetArtifacts {
+
+    public function handle($workflow_run_id, $token): void
+    {
+        $data = [
+            'artifacts' => [
+                [
+                    'name' => 'test1',
+                    'archive_download_url' => 'https://example1.com'
+                ],
+                [
+                    'name' => 'test2',
+                    'archive_download_url' => 'https://example2.com'
+                ],
+            ]
+        ];
+
+        $workflowRunDirectory = getenv('BUILDS_DIRECTORY') . "/winlibs/" . 
$workflow_run_id;
+        if (is_dir($workflowRunDirectory)) {
+            (new Helpers)->rmdirr($workflowRunDirectory);
+        }
+        mkdir($workflowRunDirectory, 0755, true);
+        foreach ($data['artifacts'] as $artifact) {
+            $filepath = $workflowRunDirectory . "/" . $artifact['name'] . 
".zip";
+            (new MockFetchArtifact)->handle($artifact['archive_download_url'], 
$filepath, $token);
+        }
+    }
+}
+
+class GetArtifactsTest extends TestCase {
+
+    public function setUp(): void
+    {
+        $temp_dir = sys_get_temp_dir();
+        putenv("BUILDS_DIRECTORY=$temp_dir");
+    }
+
+    public function testHandleWithValidData(): void
+    {
+        $workflow_run_id = 123456;
+        $token = "test_token";
+        $getArtifacts = new MockGetArtifacts();
+        $getArtifacts->handle($workflow_run_id, $token);
+        $this->assertDirectoryExists(getenv('BUILDS_DIRECTORY') . "/winlibs/" 
. $workflow_run_id);
+        $this->assertFileExists(getenv('BUILDS_DIRECTORY') . "/winlibs/" . 
$workflow_run_id . "/test1.zip");
+        $this->assertFileExists(getenv('BUILDS_DIRECTORY') . "/winlibs/" . 
$workflow_run_id . "/test2.zip");
+    }
+
+    public function tearDown(): void
+    {
+        $workflowRunDirectory = getenv('BUILDS_DIRECTORY') . "/winlibs/123456";
+        if (is_dir($workflowRunDirectory)) {
+            (new Helpers)->rmdirr($workflowRunDirectory);
+        }
+    }
+}
diff --git a/tests/AuthTest.php b/tests/AuthTest.php
new file mode 100644
index 0000000..4a8b497
--- /dev/null
+++ b/tests/AuthTest.php
@@ -0,0 +1,27 @@
+<?php
+
+use App\Auth;
+use PHPUnit\Framework\TestCase;
+
+class AuthTest extends TestCase {
+    public function testAuthenticateWithValidToken() {
+        $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer valid_token';
+        putenv('AUTH_TOKEN=valid_token');
+        $auth = new Auth();
+        $this->assertTrue($auth->authenticate(), 'Authentication should 
succeed with valid token.');
+    }
+
+    public function testAuthenticateWithInvalidToken() {
+        $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer invalid_token';
+        putenv('AUTH_TOKEN=valid_token');
+        $auth = new Auth();
+        $this->assertFalse($auth->authenticate(), 'Authentication should fail 
with invalid token.');
+    }
+
+    public function testAuthenticateWithNoToken() {
+        unset($_SERVER['HTTP_AUTHORIZATION']);
+        putenv('AUTH_TOKEN=valid_token');
+        $auth = new Auth();
+        $this->assertFalse($auth->authenticate(), 'Authentication should fail 
with no token provided.');
+    }
+}
diff --git a/tests/BaseControllerTest.php b/tests/BaseControllerTest.php
new file mode 100644
index 0000000..c3e8099
--- /dev/null
+++ b/tests/BaseControllerTest.php
@@ -0,0 +1,60 @@
+<?php
+
+use PHPUnit\Framework\TestCase;
+use App\Http\BaseController;
+
+class MockBaseController extends BaseController {
+    protected function validate(array $data): bool {
+        return isset($data['key']);
+    }
+
+    protected function execute(array $data): void {
+        echo "Executed";
+    }
+}
+
+class BaseControllerTest extends TestCase {
+    private string $tempFile;
+
+    protected function setUp(): void {
+        parent::setUp();
+        $this->tempFile = tempnam(sys_get_temp_dir(), 'phpunit');
+    }
+
+    protected function tearDown(): void {
+        if (file_exists($this->tempFile)) {
+            unlink($this->tempFile);
+        }
+        parent::tearDown();
+    }
+
+    /**
+     * @throws JsonException
+     */
+    public function testHandleWithValidData() {
+        $data = json_encode(["key" => "value"]);
+        file_put_contents($this->tempFile, $data);
+        $controller = new MockBaseController($this->tempFile);
+        $this->expectOutputString("Executed");
+        $controller->handle();
+    }
+
+    /**
+     * @throws JsonException
+     */
+    public function testHandleWithInvalidData() {
+        $data = json_encode([]);
+        file_put_contents($this->tempFile, $data);
+        $controller = new MockBaseController($this->tempFile);
+        $this->expectOutputString('');
+        $controller->handle();
+    }
+
+    public function testHandleWithMalformedJson() {
+        $data = "{key: 'value'}";
+        file_put_contents($this->tempFile, $data);
+        $controller = new MockBaseController($this->tempFile);
+        $this->expectException(JsonException::class);
+        $controller->handle();
+    }
+}
diff --git a/tests/CommandTest.php b/tests/CommandTest.php
new file mode 100644
index 0000000..3d0f799
--- /dev/null
+++ b/tests/CommandTest.php
@@ -0,0 +1,27 @@
+<?php
+use PHPUnit\Framework\TestCase;
+use App\Console\Command;
+
+class TestCommand extends Command {
+    protected string $signature = "test {arg} {--option=}";
+
+    public function handle(): int {
+        return Command::SUCCESS;
+    }
+}
+
+class CommandTest extends TestCase {
+    public function testParseArgumentsAndOptions() {
+        $argv = ["script.php", "value", "--option=optValue"];
+        $command = new TestCommand(count($argv), $argv);
+
+        $this->assertEquals("value", $command->getArgument("arg"), "Argument 
parsing failed.");
+        $this->assertEquals("optValue", $command->getOption("option"), "Option 
parsing failed.");
+
+        $command->setOption("option", "newOptValue");
+        $this->assertEquals("newOptValue", $command->getOption("option"), 
"Option setting failed.");
+
+        $this->assertEquals("", $command->getDescription());
+        $this->assertEquals("test", $command->getSignature());
+    }
+}
diff --git a/tests/Console/Command/PeclCommandTest.php 
b/tests/Console/Command/PeclCommandTest.php
new file mode 100644
index 0000000..3810b11
--- /dev/null
+++ b/tests/Console/Command/PeclCommandTest.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Console\Command;
+
+use App\Helpers\Helpers;
+use PHPUnit\Framework\TestCase;
+use App\Console\Command\PeclCommand;
+use ZipArchive;
+
+class PeclCommandTest extends TestCase
+{
+    private string $baseDirectory;
+    private string $buildsDirectory;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->baseDirectory = sys_get_temp_dir() . '/pecl_test_base';
+        $this->buildsDirectory = sys_get_temp_dir() . '/builds';
+
+        mkdir($this->baseDirectory, 0755, true);
+        mkdir($this->buildsDirectory . '/pecl', 0755, true);
+
+        putenv("BUILDS_DIRECTORY=$this->buildsDirectory");
+
+        $zipPath = $this->buildsDirectory . '/pecl/test.zip';
+        $zip = new ZipArchive();
+        if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
+            $zip->addFromString("test_file.txt", "content");
+            $zip->close();
+        }
+    }
+
+    protected function tearDown(): void
+    {
+        parent::tearDown();
+
+        (new Helpers)->rmdirr($this->baseDirectory);
+        (new Helpers)->rmdirr($this->buildsDirectory);
+    }
+
+    public function testPeclAddSuccessfullyExtractsZip(): void
+    {
+        $command = new PeclCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+        $result = $command->handle();
+        $this->assertEquals(0, $result);
+
+        $extractedFiles = glob($this->baseDirectory . '/pecl/releases/*.*');
+        $this->assertCount(1, $extractedFiles);
+        $this->assertStringContainsString('test_file.txt', $extractedFiles[0]);
+
+        $content = file_get_contents($extractedFiles[0]);
+        $this->assertEquals('content', $content);
+    }
+
+    public function testPeclAddFailsWithoutBaseDirectory(): void
+    {
+        $command = new PeclCommand();
+        ob_start();
+        $result = $command->handle();
+        $output = ob_get_clean();
+        $this->assertEquals('Base directory is required', $output);
+        $this->assertEquals(1, $result);
+
+        (new Helpers)->rmdirr($this->buildsDirectory . '/pecl');
+        $command->setOption('base-directory', $this->baseDirectory);
+        $result = $command->handle();
+        $this->assertEquals(0, $result);
+    }
+
+    public function testPeclAddFailsWithBrokenZip(): void
+    {
+        $zipPath = $this->buildsDirectory . '/pecl/broken.zip';
+        file_put_contents($zipPath, 'broken zip');
+
+        $command = new PeclCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+        ob_start();
+        $result = $command->handle();
+        $output = ob_get_clean();
+        $this->assertEquals('Failed to extract the extension', $output);
+        $this->assertEquals(1, $result);
+    }
+
+    public function testPeclAddFailsToExtractBuild(): void
+    {
+        $destinationDirectory = $this->baseDirectory . '/pecl/releases';
+        mkdir($destinationDirectory, 0555, true);
+        $command = new PeclCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+        ob_start();
+        $result = @$command->handle();
+        $output = ob_get_clean();
+        chmod($destinationDirectory, 0755);
+        $this->assertStringContainsString('Failed to extract the extension 
build', $output);
+        $this->assertEquals(1, $result);
+    }
+}
diff --git a/tests/Console/Command/PhpCommandTest.php 
b/tests/Console/Command/PhpCommandTest.php
new file mode 100644
index 0000000..30808ce
--- /dev/null
+++ b/tests/Console/Command/PhpCommandTest.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Console\Command;
+
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+use App\Console\Command\PhpCommand;
+use App\Helpers\Helpers;
+use ZipArchive;
+
+class PhpCommandTest extends TestCase
+{
+    private string $baseDirectory;
+    private string $buildsDirectory;
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        // Set up temporary directories
+        $this->baseDirectory = sys_get_temp_dir() . '/php_command_base';
+        $this->buildsDirectory = sys_get_temp_dir() . '/builds';
+
+        mkdir($this->baseDirectory . '/releases', 0755, true);
+        mkdir($this->baseDirectory . '/qa', 0755, true);
+        mkdir($this->buildsDirectory . '/php', 0755, true);
+
+        putenv("BUILDS_DIRECTORY=$this->buildsDirectory");
+    }
+
+    protected function tearDown(): void
+
+    {
+        parent::tearDown();
+        // Clean up directories
+        (new Helpers)->rmdirr($this->baseDirectory);
+        (new Helpers)->rmdirr($this->buildsDirectory);
+    }
+
+    public static function buildsProvider(): array
+    {
+        return [
+            [[
+                'php-8.4.1-Win32-vs17-x64.zip',
+                'php-8.4.1-Win32-vs17-x86.zip',
+                'php-8.4.1-nts-Win32-vs17-x64.zip',
+                'php-8.4.1-nts-Win32-vs17-x86.zip',
+                'php-8.4.1-src.zip',
+                'php-debug-pack-8.4.1-Win32-vs17-x64.zip',
+                'php-debug-pack-8.4.1-Win32-vs17-x86.zip',
+                'php-debug-pack-8.4.1-nts-Win32-vs17-x64.zip',
+                'php-debug-pack-8.4.1-nts-Win32-vs17-x86.zip',
+                'php-devel-pack-8.4.1-Win32-vs17-x64.zip',
+                'php-devel-pack-8.4.1-Win32-vs17-x86.zip',
+                'php-devel-pack-8.4.1-nts-Win32-vs17-x64.zip',
+                'php-devel-pack-8.4.1-nts-Win32-vs17-x86.zip',
+                'php-test-pack-8.4.1.zip',
+            ]],[[
+                'php-8.4.0-dev-Win32-vs17-x64.zip',
+                'php-8.4.0-dev-Win32-vs17-x86.zip',
+                'php-8.4.0-dev-nts-Win32-vs17-x64.zip',
+                'php-8.4.0-dev-nts-Win32-vs17-x86.zip',
+                'php-8.4.0-dev-src.zip',
+                'php-debug-pack-8.4.0-dev-Win32-vs17-x64.zip',
+                'php-debug-pack-8.4.0-dev-Win32-vs17-x86.zip',
+                'php-debug-pack-8.4.0-dev-nts-Win32-vs17-x64.zip',
+                'php-debug-pack-8.4.0-dev-nts-Win32-vs17-x86.zip',
+                'php-devel-pack-8.4.0-dev-Win32-vs17-x64.zip',
+                'php-devel-pack-8.4.0-dev-Win32-vs17-x86.zip',
+                'php-devel-pack-8.4.0-dev-nts-Win32-vs17-x64.zip',
+                'php-devel-pack-8.4.0-dev-nts-Win32-vs17-x86.zip',
+                'php-test-pack-8.4.0-dev.zip',
+            ]]
+        ];
+    }
+
+    private function stageBuilds(array $phpZips, $zipPath): void
+    {
+        $zip = new ZipArchive();
+        if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
+            foreach ($phpZips as $zipFileName) {
+                $zipFilePath = $this->buildsDirectory . '/php/' . $zipFileName;
+                $innerZip = new ZipArchive();
+                if ($innerZip->open($zipFilePath, ZipArchive::CREATE) === 
TRUE) {
+                    $innerZip->addFromString("test_file.php", "<?php echo 
'Hello, world!'; ?>");
+                    $innerZip->close();
+                }
+                $zip->addFile($zipFilePath, $zipFileName);
+            }
+            $zip->close();
+        }
+        foreach ($phpZips as $zipFileName) {
+            unlink($this->buildsDirectory . '/php/' . $zipFileName);
+        }
+    }
+
+    #[DataProvider('buildsProvider')]
+    public function testCommandHandlesSuccessfulExecution(array $phpZips): void
+    {
+        $command = new PhpCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+
+        $this->stageBuilds($phpZips, $this->buildsDirectory . '/php/test.zip');
+
+        $result = $command->handle();
+
+        $this->assertEquals(0, $result, "Command should return success.");
+
+        $expectedDestination = $this->baseDirectory . '/releases';
+        $this->assertDirectoryExists($expectedDestination, "Destination 
directory should exist.");
+    }
+
+    public function testCommandHandlerWithMissingTestPackZip(): void
+    {
+        $command = new PhpCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+
+        $this->stageBuilds(['php-8.4.0-dev-Win32-vs17-x64.zip'], 
$this->buildsDirectory . '/php/test.zip');
+        ob_start();
+        $result = $command->handle();
+        $output = ob_get_clean();
+        $this->assertEquals('No test pack found in the artifact', $output);
+        $this->assertEquals(1, $result, "Command should return failure.");
+    }
+
+    public function testCommandHandlesMissingBaseDirectory(): void
+    {
+        $command = new PhpCommand();
+        ob_start();
+        $result = $command->handle();
+        $output = ob_get_clean();
+        $this->assertEquals('Base directory is required', $output);
+        $this->assertEquals(1, $result);
+    }
+
+    public function testFailsToOpenZip(): void
+    {
+        $zipPath = $this->buildsDirectory . '/php/broken.zip';
+        file_put_contents($zipPath, "invalid zip content");
+        $command = new PhpCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+        ob_start();
+        $result = $command->handle();
+        ob_get_clean();
+        $this->assertEquals(1, $result, "Command should return failure on 
broken zip.");
+    }
+
+    public function testCleanupAfterCommand(): void
+    {
+        $command = new PhpCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+        $command->handle();
+        $tempDirectory = "/tmp/php-*";
+        $this->assertEmpty(glob($tempDirectory));
+    }
+}
diff --git a/tests/Console/Command/WinlibsCommandTest.php 
b/tests/Console/Command/WinlibsCommandTest.php
new file mode 100644
index 0000000..1d105f4
--- /dev/null
+++ b/tests/Console/Command/WinlibsCommandTest.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace Console\Command;
+
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+use App\Console\Command\WinlibsCommand;
+use App\Helpers\Helpers;
+use ZipArchive;
+
+class WinlibsCommandTest extends TestCase
+{
+    private string $baseDirectory;
+    
+    private string $winlibsDirectory;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->baseDirectory = sys_get_temp_dir() . '/winlibs_test';
+        mkdir($this->baseDirectory, 0755, true);
+        putenv("BASE_DIRECTORY=$this->baseDirectory");
+
+        $this->winlibsDirectory = $this->baseDirectory . '/winlibs';
+    }
+
+    protected function tearDown(): void
+    {
+        parent::tearDown();
+        (new Helpers)->rmdirr($this->baseDirectory);
+    }
+
+    #[DataProvider('versionProvider')]
+    public function testSuccessfulFileOperations($phpVersion, $vsVersion, 
$arch, $stability): void
+    {
+        mkdir($this->winlibsDirectory . '/lib', 0755, true);
+
+        $library = 'lib';
+        $ref = '2.0.0';
+        $seriesFilePath = $this->baseDirectory . 
"/php-sdk/deps/series/packages-$phpVersion-$vsVersion-$arch-$stability.txt";
+
+        file_put_contents($this->winlibsDirectory . '/lib/data.json', 
json_encode([
+            'library' => $library,
+            'ref' => $ref,
+            'vs_version_targets' => $vsVersion,
+            'php_versions' => $phpVersion,
+            'stability' => $stability
+        ]));
+
+        $zipPath = $this->winlibsDirectory . 
"/lib/lib-$ref-$vsVersion-$arch.zip";
+        $zip = new ZipArchive();
+        if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
+            $zip->addFromString("dummy_file.txt", "dummy content");
+            $zip->close();
+        }
+
+        $command = new WinlibsCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+
+        $result = $command->handle();
+
+        $this->assertEquals(0, $result, "Command should return success.");
+        $this->assertStringEqualsFile($seriesFilePath, 
"lib-$ref-$vsVersion-$arch.zip", "Series file should be updated correctly.");
+    }
+
+    #[DataProvider('versionProvider')]
+    public function 
testSuccessfulFileOperationsWithExistingSeriesFile($phpVersion, $vsVersion, 
$arch, $stability): void
+    {
+        mkdir($this->winlibsDirectory . '/lib', 0755, true);
+        mkdir($this->baseDirectory . '/php-sdk/deps/series', 0755, true);
+
+        $library = 'lib';
+        $ref = '2.0.0';
+        $seriesFilePath = $this->baseDirectory . 
"/php-sdk/deps/series/packages-$phpVersion-$vsVersion-$arch-$stability.txt";
+
+        file_put_contents($this->winlibsDirectory . '/lib/data.json', 
json_encode([
+            'library' => $library,
+            'ref' => $ref,
+            'vs_version_targets' => $vsVersion,
+            'php_versions' => $phpVersion,
+            'stability' => $stability
+        ]));
+
+        file_put_contents($seriesFilePath, 
"existing-$ref-$vsVersion-$arch.zip");
+
+        $zipPath = $this->winlibsDirectory . 
"/lib/lib-$ref-$vsVersion-$arch.zip";
+        $zip = new ZipArchive();
+        if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
+            $zip->addFromString("dummy_file.txt", "dummy content");
+            $zip->close();
+        }
+
+        $command = new WinlibsCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+
+        $result = $command->handle();
+
+        $this->assertEquals(0, $result, "Command should return success.");
+        $this->assertStringContainsString("lib-$ref-$vsVersion-$arch.zip", 
file_get_contents($seriesFilePath), "Series file should be updated correctly.");
+    }
+
+    #[DataProvider('versionProvider')]
+    public function 
testSuccessfulFileOperationsWithExistingOldLibraryInSeriesFile($phpVersion, 
$vsVersion, $arch, $stability): void
+    {
+        mkdir($this->winlibsDirectory . '/lib', 0755, true);
+        mkdir($this->baseDirectory . '/php-sdk/deps/series', 0755, true);
+
+        $library = 'lib';
+        $ref = '2.0.0';
+        $seriesFilePath = $this->baseDirectory . 
"/php-sdk/deps/series/packages-$phpVersion-$vsVersion-$arch-$stability.txt";
+
+        file_put_contents($this->winlibsDirectory . '/lib/data.json', 
json_encode([
+            'library' => $library,
+            'ref' => $ref,
+            'vs_version_targets' => $vsVersion,
+            'php_versions' => $phpVersion,
+            'stability' => $stability
+        ]));
+
+        file_put_contents($seriesFilePath, "lib-1.0.0-$vsVersion-$arch.zip");
+
+        $zipPath = $this->winlibsDirectory . 
"/lib/lib-$ref-$vsVersion-$arch.zip";
+        $zip = new ZipArchive();
+        if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
+            $zip->addFromString("dummy_file.txt", "dummy content");
+            $zip->close();
+        }
+
+        $command = new WinlibsCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+
+        $result = $command->handle();
+
+        $this->assertEquals(0, $result, "Command should return success.");
+        $this->assertStringContainsString("lib-$ref-$vsVersion-$arch.zip", 
file_get_contents($seriesFilePath), "Series file should be updated correctly.");
+        $this->assertStringNotContainsString("lib-1.0.0-$vsVersion-$arch.zip", 
file_get_contents($seriesFilePath), "Series file should be updated correctly.");
+    }
+
+    public static function versionProvider(): array
+    {
+        return [
+            ['7.4', 'vs15', 'x86', 'stable'],
+            ['8.0', 'vs16', 'x64', 'staging'],
+            ['8.1', 'vs17', 'x86', 'stable'],
+        ];
+    }
+
+    public function testCommandHandlesMissingBaseDirectory(): void
+    {
+        $command = new WinlibsCommand();
+        ob_start();
+        $result = $command->handle();
+        $output = ob_get_clean();
+        $this->assertEquals('Base directory is required', $output);
+        $this->assertEquals(1, $result);
+    }
+
+    public function testHandlesCorruptDataFile(): void
+    {
+        $this->winlibsDirectory = $this->baseDirectory . '/winlibs/lib';
+        mkdir($this->winlibsDirectory, 0755, true);
+        file_put_contents($this->winlibsDirectory . '/data.json', '{corrupt 
json');
+
+        $command = new WinlibsCommand();
+        $command->setOption('base-directory', $this->baseDirectory);
+        ob_start();
+        $result = $command->handle();
+        $output = ob_get_clean();
+        $this->assertStringContainsString('Syntax error', $output);
+        $this->assertEquals(1, $result);
+    }
+}
diff --git a/tests/ControllerInterfaceTest.php 
b/tests/ControllerInterfaceTest.php
new file mode 100644
index 0000000..cb97400
--- /dev/null
+++ b/tests/ControllerInterfaceTest.php
@@ -0,0 +1,8 @@
+<?php
+use PHPUnit\Framework\TestCase;
+
+class ControllerInterfaceTest extends TestCase {
+    public function testInterfaceExists() {
+        
$this->assertTrue(interface_exists(App\Http\ControllerInterface::class), 
"ControllerInterface should exist.");
+    }
+}
diff --git a/tests/Helpers/HelpersTest.php b/tests/Helpers/HelpersTest.php
new file mode 100644
index 0000000..a89d18a
--- /dev/null
+++ b/tests/Helpers/HelpersTest.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Helpers;
+
+use PHPUnit\Framework\TestCase;
+use App\Helpers\Helpers;
+
+class HelpersTest extends TestCase
+{
+    private string $testDir;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->testDir = sys_get_temp_dir() . '/testDir';
+    }
+
+    protected function tearDown(): void
+    {
+        parent::tearDown();
+        // Ensure all files and directories are cleaned up after each test
+        if (file_exists($this->testDir)) {
+            $helper = new Helpers();
+            $helper->rmdirr($this->testDir);
+        }
+    }
+
+    public function testRemoveNonExistentDirectory(): void
+    {
+        $helper = new Helpers();
+        $this->assertFalse($helper->rmdirr($this->testDir . '/nonexistent'));
+    }
+
+    public function testRemoveDirectoryWithFiles(): void
+    {
+        mkdir($this->testDir, 0777, true);
+        file_put_contents($this->testDir . '/file.txt', 'Hello World');
+
+        $helper = new Helpers();
+        $result = $helper->rmdirr($this->testDir);
+        $this->assertTrue($result);
+        $this->assertDirectoryDoesNotExist($this->testDir);
+    }
+
+    public function testRemoveDirectoryWithNestedDirectories(): void
+    {
+        mkdir($this->testDir . '/nested', 0777, true);
+        file_put_contents($this->testDir . '/nested/file.txt', 'Hello World');
+
+        $helper = new Helpers();
+        $result = $helper->rmdirr($this->testDir);
+        $this->assertTrue($result);
+        $this->assertDirectoryDoesNotExist($this->testDir);
+    }
+}
diff --git a/tests/Http/Controllers/IndexControllerTest.php 
b/tests/Http/Controllers/IndexControllerTest.php
new file mode 100644
index 0000000..7f1bca7
--- /dev/null
+++ b/tests/Http/Controllers/IndexControllerTest.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Http\Controllers;
+
+use App\Http\Controllers\IndexController;
+use PHPUnit\Framework\TestCase;
+
+class IndexControllerTest extends TestCase {
+    public function testHandle() {
+        $controller = new IndexController();
+        $controller->handle();
+        $this->expectOutputString('Welcome!');
+        $this->assertTrue($controller->validate([]));
+        ob_start();
+        $controller->execute([]);
+        $this->assertEmpty(ob_get_clean());
+    }
+}
diff --git a/tests/Http/Controllers/PeclControllerTest.php 
b/tests/Http/Controllers/PeclControllerTest.php
new file mode 100644
index 0000000..ffae927
--- /dev/null
+++ b/tests/Http/Controllers/PeclControllerTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Http\Controllers;
+
+use App\Http\BaseController;
+use PHPUnit\Framework\TestCase;
+
+class MockPeclController extends BaseController {
+    protected function validate(array $data): bool {
+        return isset($data['key']);
+    }
+
+    protected function execute(array $data): void {
+        echo "Executed";
+    }
+
+    public function handle(): void
+    {
+        $data = json_decode(file_get_contents($this->inputPath), true);
+
+        if ($this->validate($data)) {
+            $this->execute($data);
+        }
+    }
+}
+
+class PeclControllerTest extends TestCase {
+    public function testHandleWithValidData() {
+        $data = json_encode(["key" => "value"]);
+        $tempFile = tempnam(sys_get_temp_dir(), 'phpunit');
+        file_put_contents($tempFile, $data);
+        $controller = new MockPeclController($tempFile);
+        $this->expectOutputString("Executed");
+        $controller->handle();
+        unlink($tempFile);
+    }
+}
diff --git a/tests/Http/Controllers/PhpControllerTest.php 
b/tests/Http/Controllers/PhpControllerTest.php
new file mode 100644
index 0000000..d677f7f
--- /dev/null
+++ b/tests/Http/Controllers/PhpControllerTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Http\Controllers;
+
+use App\Http\Controllers\PhpController;
+use PHPUnit\Framework\TestCase;
+
+class MockPhpController extends PhpController {
+    protected function validate(array $data): bool {
+        return isset($data['key']);
+    }
+
+    protected function execute(array $data): void {
+        echo "Executed";
+    }
+
+    public function handle(): void
+    {
+        $data = json_decode(file_get_contents($this->inputPath), true);
+
+        if ($this->validate($data)) {
+            $this->execute($data);
+        }
+    }
+}
+
+class PhpControllerTest extends TestCase {
+    public function testHandleWithValidData() {
+        $data = json_encode(["key" => "value"]);
+        $tempFile = tempnam(sys_get_temp_dir(), 'phpunit');
+        file_put_contents($tempFile, $data);
+        $controller = new MockPhpController($tempFile);
+        $this->expectOutputString("Executed");
+        $controller->handle();
+        unlink($tempFile);
+    }
+}
diff --git a/tests/Http/Controllers/WinlibsControllerTest.php 
b/tests/Http/Controllers/WinlibsControllerTest.php
new file mode 100644
index 0000000..f4ed100
--- /dev/null
+++ b/tests/Http/Controllers/WinlibsControllerTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Http\Controllers;
+
+use App\Http\Controllers\WinlibsController;
+use PHPUnit\Framework\TestCase;
+
+class MockWinlibsController extends WinlibsController {
+    protected function validate(array $data): bool {
+        return isset($data['key']);
+    }
+
+    protected function execute(array $data): void {
+        echo "Executed";
+    }
+
+    public function handle(): void
+    {
+        $data = json_decode(file_get_contents($this->inputPath), true);
+
+        if ($this->validate($data)) {
+            $this->execute($data);
+        }
+    }
+}
+
+class WinlibsControllerTest extends TestCase {
+    public function testHandleWithValidData() {
+        $data = json_encode(["key" => "value"]);
+        $tempFile = tempnam(sys_get_temp_dir(), 'phpunit');
+        file_put_contents($tempFile, $data);
+        $controller = new MockWinlibsController($tempFile);
+        $this->expectOutputString("Executed");
+        $controller->handle();
+        unlink($tempFile);
+    }
+}
diff --git a/tests/RouterTest.php b/tests/RouterTest.php
new file mode 100644
index 0000000..de3a176
--- /dev/null
+++ b/tests/RouterTest.php
@@ -0,0 +1,51 @@
+<?php
+use PHPUnit\Framework\TestCase;
+use App\Router;
+
+class RouterTest extends TestCase {
+    public function testHandleIndexRequest() {
+        $_SERVER['REQUEST_URI'] = '/';
+        $_SERVER['REQUEST_METHOD'] = 'GET';
+        $_SERVER['HTTP_AUTHORIZATION'] = '';
+        $router = new Router();
+        $router->registerRoute('/', 'GET', 
'App\Http\Controllers\IndexController'
+        );
+        ob_start();
+        $router->handleRequest();
+        $output = ob_get_clean();
+        $this->assertEquals('Welcome!', $output, 'Should respond with Welcome! 
for index route.');
+    }
+
+    public function testHandleRequestUnauthorized() {
+        $_SERVER['REQUEST_URI'] = '/protected';
+        $_SERVER['REQUEST_METHOD'] = 'GET';
+        $_SERVER['HTTP_AUTHORIZATION'] = '';
+        $router = new Router();
+        $router->registerRoute('/protected', 'GET', 'TestHandler', true);
+        ob_start();
+        $router->handleRequest();
+        $output = ob_get_clean();
+        $this->assertEquals('Unauthorized', $output, 'Should respond with 
Unauthorized for protected routes.');
+    }
+
+    public function testHandleRequestMethodNotAllowed() {
+        $_SERVER['REQUEST_URI'] = '/test';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $router = new Router();
+        $router->registerRoute('/test', 'GET', 'TestHandler');
+        ob_start();
+        $router->handleRequest();
+        $output = ob_get_clean();
+        $this->assertStringContainsString('Method Not Allowed', $output, 
'Should respond with Method Not Allowed.');
+    }
+
+    public function testHandleRequestNotFound() {
+        $_SERVER['REQUEST_URI'] = '/nonexistent';
+        $_SERVER['REQUEST_METHOD'] = 'GET';
+        $router = new Router();
+        ob_start();
+        $router->handleRequest();
+        $output = ob_get_clean();
+        $this->assertEquals('Not Found', $output, 'Should respond with Not 
Found for unregistered routes.');
+    }
+}
diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php
new file mode 100644
index 0000000..c355ffb
--- /dev/null
+++ b/tests/ValidatorTest.php
@@ -0,0 +1,54 @@
+<?php
+
+use App\Validator;
+use PHPUnit\Framework\TestCase;
+
+class ValidatorTest extends TestCase {
+    public function testValidateRequiredField() {
+        $validator = new Validator(['name' => 'required']);
+        $validator->validate(['name' => 'John']);
+        $this->assertTrue($validator->isValid(), 'Validation should pass when 
required field is present.');
+    }
+
+    public function testValidateMissingRequiredField() {
+        $validator = new Validator(['name' => 'required']);
+        $validator->validate([]);
+        $this->assertFalse($validator->isValid(), 'Validation should fail when 
required field is missing.');
+    }
+
+    public function testValidateStringField() {
+        $validator = new Validator(['name' => 'string']);
+        $validator->validate(['name' => 123]);
+        $this->assertFalse($validator->isValid(), 'Validation should fail when 
required field is missing.');
+    }
+
+    public function testValidationRegexField() {
+        $validator = new Validator(['date' => 'regex:/\d{4}-\d{2}-\d{2}/']);
+        $validator->validate(['date' => '01/01/2025']);
+        $this->assertFalse($validator->isValid(), 'Validation should fail when 
regex does not match.');
+
+        $validator->validate(['date' => '2025-01-01']);
+        $this->assertTrue($validator->isValid(), 'Validation should pass when 
regex matches.');
+    }
+
+    public function testValidateUrlField() {
+        $validator = new Validator(['website' => 'url']);
+        $validator->validate(['website' => 'https://example.com']);
+        $this->assertTrue($validator->isValid(), 'Validation should pass for a 
valid URL.');
+    }
+
+    public function testValidateInvalidUrlField() {
+        $validator = new Validator(['website' => 'url']);
+        $validator->validate(['website' => 'invalid-url']);
+        $this->assertFalse($validator->isValid(), 'Validation should fail for 
an invalid URL.');
+    }
+
+    public function testErrorMessages() {
+        $validator = new Validator(['website' => 'url']);
+        $validator->validate(['website' => 'invalid-url']);
+        $errors = $validator->errors();
+        $this->assertNotEmpty($errors, 'Errors should not be empty for invalid 
validation.');
+        $this->assertStringContainsString('must be a valid URL', 
$errors['website'][0], 'Error message should match.');
+        $this->assertStringContainsString('website: The website field must be 
a valid URL.', $validator->__toString());
+    }
+}

Reply via email to