diff --git a/.github/workflows/pest_coverage_pull_request.yml b/.github/workflows/pest_coverage_pull_request.yml index f811b59..bcd8979 100644 --- a/.github/workflows/pest_coverage_pull_request.yml +++ b/.github/workflows/pest_coverage_pull_request.yml @@ -91,10 +91,10 @@ jobs: run: php -v - name: Execute Pest Coverage - run: XDEBUG_MODE=coverage ./vendor/bin/pest --coverage --min=${{ env.PEST_MIN_COVERAGE }} + run: XDEBUG_MODE=coverage ./vendor/bin/pest --exclude-testsuite=Browser --coverage --min=${{ env.PEST_MIN_COVERAGE }} - name: Execute Pest Type Coverage - run: XDEBUG_MODE=coverage ./vendor/bin/pest --type-coverage --min=${{ env.PEST_MIN_TYPE_COVERAGE }} + run: XDEBUG_MODE=coverage ./vendor/bin/pest --exclude-testsuite=Browser --type-coverage --min=${{ env.PEST_MIN_TYPE_COVERAGE }} - name: Store Log Artifacts if: failure() diff --git a/.github/workflows/pest_database_matrix_pull_request.yml b/.github/workflows/pest_database_matrix_pull_request.yml index 6e33d9b..8361612 100644 --- a/.github/workflows/pest_database_matrix_pull_request.yml +++ b/.github/workflows/pest_database_matrix_pull_request.yml @@ -68,4 +68,4 @@ jobs: run: composer install --no-interaction --no-progress --prefer-dist - name: Pest - run: ./vendor/bin/pest + run: ./vendor/bin/pest --exclude-testsuite=Browser diff --git a/.github/workflows/pest_pull_request.yml b/.github/workflows/pest_pull_request.yml index 78a412a..a10d705 100644 --- a/.github/workflows/pest_pull_request.yml +++ b/.github/workflows/pest_pull_request.yml @@ -85,7 +85,7 @@ jobs: run: npm run build - name: Execute Pest Test - run: ./vendor/bin/pest + run: ./vendor/bin/pest --exclude-testsuite=Browser - name: Store Log Artifacts if: failure() diff --git a/.github/workflows/pest_pull_request_browser.yml b/.github/workflows/pest_pull_request_browser.yml new file mode 100644 index 0000000..7b29b3b --- /dev/null +++ b/.github/workflows/pest_pull_request_browser.yml @@ -0,0 +1,119 @@ +name: Pull Request | CI Pest Browser + +on: pull_request + +# A new push cancels the run it replaces. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + ci_pest_browser: + # Public repository: GitHub-hosted runners only, never the self-hosted group. + runs-on: ubuntu-latest + name: pull-request | ci pest browser + + services: + postgres: + image: postgres:17 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: laravel_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432 + + env: + PHP_VERSION: 8.5 + NODE_VERSION: 26 + + steps: + # CI always tests against PostgreSQL. These variables beat .env and the + # phpunit.xml defaults (in-memory SQLite). The service gets a + # Docker-assigned host port. + - name: Point the app at the Postgres service + run: | + { + echo "DB_CONNECTION=pgsql" + echo "DB_HOST=127.0.0.1" + echo "DB_PORT=${{ job.services.postgres.ports['5432'] }}" + echo "DB_DATABASE=laravel_test" + echo "DB_USERNAME=postgres" + echo "DB_PASSWORD=postgres" + echo "DB_URL=" + } >> "$GITHUB_ENV" + + - name: Checkout code + uses: actions/checkout@v7 + + - name: Setup PHP Environment + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ env.PHP_VERSION }} + extensions: dom, curl, fileinfo, pgsql, pdo_pgsql, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + + - name: Prepare the .env file + run: cp .env.example .env + + - name: Cache Composer Dependencies + uses: actions/cache@v6 + with: + path: | + ~/.composer/cache/files + ~/.composer/cache/repo + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install Composer Dependencies + run: composer install --no-progress --prefer-dist --no-interaction --optimize-autoloader + + - name: Publish Laravel assets + run: php artisan vendor:publish --tag=laravel-assets --ansi --force + + - name: Setup PostgreSQL Database + run: | + until pg_isready -h localhost -p "$DB_PORT" -U postgres; do sleep 1; done + + - name: Generate App Key & Run Migrations + run: | + php artisan key:generate + php artisan migrate:fresh + + - name: Setup Node.js Environment + uses: actions/setup-node@v7 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install NPM Dependencies + run: npm ci + + - name: Build Frontend Assets + run: npm run build + + - name: Install Playwright Browsers + run: npx playwright install --with-deps chromium + + - name: Execute Pest Browser Tests + run: ./vendor/bin/pest tests/Browser + + - name: Store Log Artifacts + if: failure() + uses: actions/upload-artifact@v7 + with: + name: Store report artifacts + path: ./storage/logs + + - name: Store Screenshots + if: failure() + uses: actions/upload-artifact@v7 + with: + name: Browser Screenshots + path: ./tests/Browser/Screenshots + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index e7338bb..e1f991c 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ Thumbs.db /e2e/playwright-report /public/widget /resources/widget + +# Pest browser test failure screenshots +/tests/Browser/Screenshots/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2687e10..2ae930c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to the API and the widget are documented here. The format fo - The widget is no longer published to npm: the API serves it at `/widget/v1/feedback.js` (one script tag, no npm, no CDN, updated with every deploy). `npm run build` compiles it into `resources/widget/v1`. The npm package `@codebar-ag/feedback-widget` 1.0.0 stays available but gets no updates. Removed: release workflow, type definitions, SRI file, publint and attw. - The widget only appears on pages the API accepts: it checks the project config first and stays hidden on a `403` (origin or IP not allowed). Refused `GET` requests now carry `Access-Control-Allow-Origin` so the widget can read the `403`; the preflight for sending stays refused. - Dev dependencies: vite 8.3.5, typescript-eslint 8.72.0. +- Pest browser tests (`tests/Browser`, Playwright Chromium) for the feedback page: tabs, message card, copy, completing and reopening, refused links. New dev dependencies: pestphp/pest-plugin-browser 5.1, playwright 1.64.1. `composer test` leaves them out, `composer test:browser` runs them. ### Fixed diff --git a/README.md b/README.md index c987446..6449ea2 100644 --- a/README.md +++ b/README.md @@ -423,6 +423,7 @@ npm run build # builds the widget and copies it to resources npm --prefix widget run check # lint, types, Vitest, build, size budget npm --prefix widget run e2e # Playwright: Chromium, Firefox, WebKit, mobile npm --prefix widget run e2e:full # API and widget together +composer test:browser # Pest browser tests of the feedback page (npx playwright install chromium first) npm --prefix widget run dev # playground with a mock API on http://localhost:4173/fixtures/index.html ``` diff --git a/app/Checks/DatabaseConnectionCountCheck.php b/app/Checks/DatabaseConnectionCountCheck.php index e669a02..32dfca6 100644 --- a/app/Checks/DatabaseConnectionCountCheck.php +++ b/app/Checks/DatabaseConnectionCountCheck.php @@ -4,32 +4,52 @@ namespace App\Checks; +use Illuminate\Database\ConnectionResolverInterface; +use Illuminate\Support\Str; use Spatie\Health\Checks\Checks\DatabaseConnectionCountCheck as SpatieDatabaseConnectionCountCheck; use Spatie\Health\Checks\Result; /** - * spatie's DatabaseConnectionCountCheck with the agreed thresholds (warn - * above 50, fail above 80), reporting ok for apps without a database and - * for SQLite, which has no connection count. + * Informational only: reports the number of open connections of the app's + * own database and never warns or fails. On PostgreSQL only connections to + * the current database count, because Neon servers are shared between apps. + * Apps without a database and SQLite report ok. */ class DatabaseConnectionCountCheck extends SpatieDatabaseConnectionCountCheck { - protected ?int $warningThreshold = 50; - - protected int $errorThreshold = 80; - public function run(): Result { if (! DatabaseAvailability::isAvailable()) { return Result::make()->shortSummary('no database')->ok(); } - $connection = $this->connectionName ?? $this->getDefaultConnectionName(); - - if (config("database.connections.{$connection}.driver") === 'sqlite') { + if (config("database.connections.{$this->connection()}.driver") === 'sqlite') { return Result::make()->shortSummary('sqlite')->ok(); } - return parent::run(); + $connectionCount = $this->getConnectionCount(); + + return Result::make() + ->ok() + ->meta(['connection_count' => $connectionCount]) + ->shortSummary($connectionCount.' '.Str::plural('connection', $connectionCount)); + } + + protected function getConnectionCount(): int + { + $connection = app(ConnectionResolverInterface::class)->connection($this->connection()); + + if ($connection->getDriverName() !== 'pgsql') { + return parent::getConnectionCount(); + } + + $count = $connection->scalar('select count(*) from pg_stat_activity where datname = current_database()'); + + return is_numeric($count) ? (int) $count : 0; + } + + private function connection(): string + { + return $this->connectionName ?? $this->getDefaultConnectionName(); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 6ae236c..0511c7b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -25,7 +25,6 @@ use Spatie\Health\Checks\Checks\CacheCheck; use Spatie\Health\Checks\Checks\DebugModeCheck; use Spatie\Health\Checks\Checks\EnvironmentCheck; -use Spatie\Health\Checks\Checks\OptimizedAppCheck; use Spatie\Health\Checks\Checks\ScheduleCheck; use Spatie\Health\Facades\Health; use Spatie\SecurityAdvisoriesHealthCheck\SecurityAdvisoriesCheck; @@ -41,8 +40,7 @@ public function boot(): void /** * Checks for Oh Dear (OH_DEAR_HEALTH_CHECK_*) and `php artisan health:check`: - * the standard set agreed for all Laravel Cloud apps. OptimizedAppCheck - * needs `php artisan optimize` in the deploy command. + * the standard set agreed for all Laravel Cloud apps. */ private function configureHealthChecks(): void { @@ -52,7 +50,6 @@ private function configureHealthChecks(): void DatabaseCheck::new(), DatabaseConnectionCountCheck::new(), CacheCheck::new(), - OptimizedAppCheck::new(), ScheduleCheck::new()->heartbeatMaxAgeInMinutes(120), SecurityAdvisoriesCheck::new(), NightwatchIssuesCheck::new()->if(filled(config('default.nightwatch.api_token')) && filled(config('default.nightwatch.application_id'))), diff --git a/composer.json b/composer.json index 19e9e33..c3b6154 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.9", "pestphp/pest": "^5.0", + "pestphp/pest-plugin-browser": "^5.1", "pestphp/pest-plugin-laravel": "^5.0", "pestphp/pest-plugin-type-coverage": "^5.0" }, @@ -55,8 +56,9 @@ "lint": "./vendor/bin/pint --test", "format": "./vendor/bin/pint", "analyse": "./vendor/bin/phpstan analyse --memory-limit=1G --no-progress", - "test": "./vendor/bin/pest --parallel --compact", - "test:coverage": "./vendor/bin/pest --coverage --min=90", + "test": "./vendor/bin/pest --exclude-testsuite=Browser --parallel --compact", + "test:coverage": "./vendor/bin/pest --exclude-testsuite=Browser --coverage --min=90", + "test:browser": "./vendor/bin/pest tests/Browser", "check": [ "@lint", "@analyse", diff --git a/composer.lock b/composer.lock index e0339f3..201c23e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0deb113b18e34126f15b18f7c3608dcd", + "content-hash": "8548e40d846f9b3470b3fec67aaf259e", "packages": [ { "name": "brick/math", @@ -6744,6 +6744,1236 @@ } ], "packages-dev": [ + { + "name": "amphp/amp", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/73c38b323ff8d790abf0f76c56fcc892bda4111a", + "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.1.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-07-19T17:59:20+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T17:10:27+00:00" + }, + { + "name": "amphp/cache", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", + "support": { + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:38:06+00:00" + }, + { + "name": "amphp/dns", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/dns.git", + "reference": "ec5948bfafac808f410406e18bc7b52a2d4629b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/dns/zipball/ec5948bfafac808f410406e18bc7b52a2d4629b7", + "reference": "ec5948bfafac808f410406e18bc7b52a2d4629b7", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "ext-json": "*", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" + ], + "support": { + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-07-26T17:53:54+00:00" + }, + { + "name": "amphp/hpack", + "version": "v3.2.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/hpack.git", + "reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/hpack/zipball/291da27078e7e149a9bad4d08ff05bf7d81c89f4", + "reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "http2jp/hpack-test-case": "^1", + "nikic/php-fuzzer": "^0.0.11", + "phpunit/phpunit": "^7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Amp\\Http\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "HTTP/2 HPack implementation.", + "homepage": "https://github.com/amphp/hpack", + "keywords": [ + "headers", + "hpack", + "http-2" + ], + "support": { + "issues": "https://github.com/amphp/hpack/issues", + "source": "https://github.com/amphp/hpack/tree/v3.2.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-05-03T19:28:59+00:00" + }, + { + "name": "amphp/http", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/http.git", + "reference": "3680d80bd38b5d6f3c2cef2214ca6dd6cef26588" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/http/zipball/3680d80bd38b5d6f3c2cef2214ca6dd6cef26588", + "reference": "3680d80bd38b5d6f3c2cef2214ca6dd6cef26588", + "shasum": "" + }, + "require": { + "amphp/hpack": "^3", + "amphp/parser": "^1.1", + "league/uri-components": "^2.4.2 | ^7.1", + "php": ">=8.1", + "psr/http-message": "^1 | ^2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "league/uri": "^6.8 | ^7.1", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.26.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/constants.php" + ], + "psr-4": { + "Amp\\Http\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Basic HTTP primitives which can be shared by servers and clients.", + "support": { + "issues": "https://github.com/amphp/http/issues", + "source": "https://github.com/amphp/http/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-11-23T14:57:26+00:00" + }, + { + "name": "amphp/http-client", + "version": "v5.3.6", + "source": { + "type": "git", + "url": "https://github.com/amphp/http-client.git", + "reference": "ca155026acafa74a612d776a97202d53077fee86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/http-client/zipball/ca155026acafa74a612d776a97202d53077fee86", + "reference": "ca155026acafa74a612d776a97202d53077fee86", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/hpack": "^3", + "amphp/http": "^2", + "amphp/pipeline": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "league/uri": "^7", + "league/uri-components": "^7", + "league/uri-interfaces": "^7.1", + "php": ">=8.1", + "psr/http-message": "^1 | ^2", + "revolt/event-loop": "^1" + }, + "conflict": { + "amphp/file": "<3 | >=5" + }, + "require-dev": { + "amphp/file": "^3 | ^4", + "amphp/http-server": "^3", + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "ext-json": "*", + "kelunik/link-header-rfc5988": "^1", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "suggest": { + "amphp/file": "Required for file request bodies and HTTP archive logging", + "ext-json": "Required for logging HTTP archives", + "ext-zlib": "Allows using compression for response bodies." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\Http\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "An advanced async HTTP client library for PHP, enabling efficient, non-blocking, and concurrent requests and responses.", + "homepage": "https://amphp.org/http-client", + "keywords": [ + "async", + "client", + "concurrent", + "http", + "non-blocking", + "rest" + ], + "support": { + "issues": "https://github.com/amphp/http-client/issues", + "source": "https://github.com/amphp/http-client/tree/v5.3.6" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-05-15T23:29:38+00:00" + }, + { + "name": "amphp/http-server", + "version": "v3.4.7", + "source": { + "type": "git", + "url": "https://github.com/amphp/http-server.git", + "reference": "aa96ca38c5f822a827236543d5417d0c2a782ef0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/http-server/zipball/aa96ca38c5f822a827236543d5417d0c2a782ef0", + "reference": "aa96ca38c5f822a827236543d5417d0c2a782ef0", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/hpack": "^3", + "amphp/http": "^2", + "amphp/pipeline": "^1", + "amphp/socket": "^2.1", + "amphp/sync": "^2.2", + "league/uri": "^7.1", + "league/uri-interfaces": "^7.1", + "php": ">=8.1", + "psr/http-message": "^1 | ^2", + "psr/log": "^1 | ^2 | ^3", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/http-client": "^5", + "amphp/log": "^2", + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "league/uri-components": "^7.1", + "monolog/monolog": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "suggest": { + "ext-zlib": "Allows GZip compression of response bodies" + }, + "type": "library", + "autoload": { + "files": [ + "src/Driver/functions.php", + "src/Middleware/functions.php", + "src/functions.php" + ], + "psr-4": { + "Amp\\Http\\Server\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "A non-blocking HTTP application server for PHP based on Amp.", + "homepage": "https://github.com/amphp/http-server", + "keywords": [ + "amp", + "amphp", + "async", + "http", + "non-blocking", + "server" + ], + "support": { + "issues": "https://github.com/amphp/http-server/issues", + "source": "https://github.com/amphp/http-server/tree/v3.4.7" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-10-04T15:14:17+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:16:53+00:00" + }, + { + "name": "amphp/pipeline", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/amphp/pipeline.git", + "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17", + "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Pipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" + ], + "support": { + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.7" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-07-26T14:50:43+00:00" + }, + { + "name": "amphp/process", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "583959df17d00304ad7b0b32285373f985935643" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/583959df17d00304ad7b0b32285373f985935643", + "reference": "583959df17d00304ad7b0b32285373f985935643", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-05-31T15:11:55+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/fdf2834d78cebb0205fb2672676c1b1eb84371f0", + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "ext-json": "*", + "ext-zlib": "*", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/v1.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-04-05T15:59:53+00:00" + }, + { + "name": "amphp/socket", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/socket.git", + "reference": "b347be5aff6b2cc025208bb4d896607eb470c018" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/socket/zipball/b347be5aff6b2cc025208bb4d896607eb470c018", + "reference": "b347be5aff6b2cc025208bb4d896607eb470c018", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^7", + "league/uri-interfaces": "^7", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.4.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-08-22T18:13:20+00:00" + }, + { + "name": "amphp/sync", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-08-03T19:31:26+00:00" + }, + { + "name": "amphp/websocket", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/amphp/websocket.git", + "reference": "963904b6a883c4b62d9222d1d9749814fac96a3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/websocket/zipball/963904b6a883c4b62d9222d1d9749814fac96a3b", + "reference": "963904b6a883c4b62d9222d1d9749814fac96a3b", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/socket": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "suggest": { + "ext-zlib": "Required for compression" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Websocket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + } + ], + "description": "Shared code for websocket servers and clients.", + "homepage": "https://github.com/amphp/websocket", + "keywords": [ + "amp", + "amphp", + "async", + "http", + "non-blocking", + "websocket" + ], + "support": { + "issues": "https://github.com/amphp/websocket/issues", + "source": "https://github.com/amphp/websocket/tree/v2.0.4" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-10-28T21:28:45+00:00" + }, + { + "name": "amphp/websocket-client", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/websocket-client.git", + "reference": "dc033fdce0af56295a23f63ac4f579b34d470d6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/websocket-client/zipball/dc033fdce0af56295a23f63ac4f579b34d470d6c", + "reference": "dc033fdce0af56295a23f63ac4f579b34d470d6c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2.1", + "amphp/http": "^2.1", + "amphp/http-client": "^5", + "amphp/socket": "^2.2", + "amphp/websocket": "^2", + "league/uri": "^7.1", + "php": ">=8.1", + "psr/http-message": "^1|^2", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/http-server": "^3", + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/websocket-server": "^3|^4", + "phpunit/phpunit": "^9", + "psalm/phar": "~5.26.1", + "psr/log": "^1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Websocket\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Async WebSocket client for PHP based on Amp.", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "http", + "non-blocking", + "websocket" + ], + "support": { + "issues": "https://github.com/amphp/websocket-client/issues", + "source": "https://github.com/amphp/websocket-client/tree/v2.0.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-08-24T17:25:34+00:00" + }, { "name": "brianium/paratest", "version": "v7.27.4", @@ -6837,6 +8067,50 @@ ], "time": "2026-10-22T06:15:12+00:00" }, + { + "name": "daverandom/libdns", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" + ], + "support": { + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + }, + "time": "2024-04-12T12:12:48+00:00" + }, { "name": "doctrine/deprecations", "version": "1.1.6", @@ -7288,6 +8562,64 @@ }, "time": "2025-03-19T14:43:43+00:00" }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, { "name": "larastan/larastan", "version": "v3.13.0", @@ -7447,6 +8779,90 @@ }, "time": "2026-09-10T10:47:55+00:00" }, + { + "name": "league/uri-components", + "version": "7.9.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-components.git", + "reference": "4e37c1663f7d0daebbab6b3d947631b0835b3943" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-components/zipball/4e37c1663f7d0daebbab6b3d947631b0835b3943", + "reference": "4e37c1663f7d0daebbab6b3d947631b0835b3943", + "shasum": "" + }, + "require": { + "league/uri": "^7.9.0", + "php": "^8.1" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-mbstring": "to use the sorting algorithm of URLSearchParams", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI components manipulation library", + "homepage": "http://uri.thephpleague.com", + "keywords": [ + "authority", + "components", + "fragment", + "host", + "middleware", + "modifier", + "path", + "port", + "query", + "rfc3986", + "scheme", + "uri", + "url", + "userinfo" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-components/tree/7.9.0" + }, + "funding": [ + { + "url": "https://github.com/nyamsprod", + "type": "github" + } + ], + "time": "2026-10-16T15:19:33+00:00" + }, { "name": "mockery/mockery", "version": "1.6.15", @@ -8084,6 +9500,90 @@ ], "time": "2026-07-21T07:48:55+00:00" }, + { + "name": "pestphp/pest-plugin-browser", + "version": "v5.1.2", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-browser.git", + "reference": "3425026b6563ca936cbdfabb9ac979355fab74c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-browser/zipball/3425026b6563ca936cbdfabb9ac979355fab74c1", + "reference": "3425026b6563ca936cbdfabb9ac979355fab74c1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3.1.3", + "amphp/http-server": "^3.4.6", + "amphp/websocket-client": "^2.0.2", + "ext-sockets": "*", + "pestphp/pest": "^5.2.1", + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4", + "symfony/process": "^8.1.7" + }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, + "require-dev": { + "livewire/livewire": "^4.4.7", + "nunomaduro/collision": "^8.9.5", + "orchestra/testbench": "^11.3", + "pestphp/pest-dev-tools": "^5.0.0", + "pestphp/pest-plugin-laravel": "^5.0.1", + "pestphp/pest-plugin-type-coverage": "^5.0.2" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Browser\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Browser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Pest plugin to test browser interactions", + "keywords": [ + "browser", + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-browser/tree/v5.1.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-10-02T10:52:14+00:00" + }, { "name": "pestphp/pest-plugin-laravel", "version": "v5.0.1", @@ -9260,6 +10760,78 @@ ], "time": "2026-10-16T05:56:47+00:00" }, + { + "name": "revolt/event-loop", + "version": "v1.0.9", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "44061cf513e53c6200372fc935ac42271566295d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/44061cf513e53c6200372fc935ac42271566295d", + "reference": "44061cf513e53c6200372fc935ac42271566295d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.9" + }, + "time": "2026-05-16T17:55:38+00:00" + }, { "name": "sebastian/cli-parser", "version": "5.0.1", diff --git a/package-lock.json b/package-lock.json index 8bb184e..b706b1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,41 @@ "name": "codebar-feedback", "hasInstallScript": true, "license": "MIT", + "devDependencies": { + "playwright": "1.64.1" + }, "engines": { "node": ">=26" } + }, + "node_modules/playwright": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", + "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz", + "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } } } } diff --git a/package.json b/package.json index eab0cdd..eaa2e32 100644 --- a/package.json +++ b/package.json @@ -9,5 +9,8 @@ "scripts": { "postinstall": "npm --prefix widget ci --audit false --fund false", "build": "npm --prefix widget run build" + }, + "devDependencies": { + "playwright": "1.64.1" } } diff --git a/phpunit.xml b/phpunit.xml index 810b32a..7feb556 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -14,6 +14,9 @@ tests/Arch + + tests/Browser + diff --git a/tests/Browser/FeedbackPageTest.php b/tests/Browser/FeedbackPageTest.php new file mode 100644 index 0000000..ac6b695 --- /dev/null +++ b/tests/Browser/FeedbackPageTest.php @@ -0,0 +1,145 @@ +project = FeedbackProject::factory()->create(['name' => 'Alpha', 'allowed_origins' => [TEST_ORIGIN]]); +}); + +/** Sends feedback the way the widget does, with screenshot and diagnostics. */ +function sentFeedback(FeedbackProject $project): FeedbackEntry +{ + postFeedback($project, [ + 'screenshot' => fakeJpeg(200, 400), + 'identity' => json_encode(['id' => 'u-1', 'name' => 'Anna Muster']), + 'console_log' => json_encode([['type' => 'error', 'message' => 'Boom', 'count' => 2, 'time' => 1]]), + 'network_log' => json_encode([['method' => 'POST', 'url' => 'https://api.beispiel.ch/save', 'status' => 500, 'duration' => 120, 'count' => 1, 'time' => 1]]), + ])->assertCreated(); + + return FeedbackEntry::query()->latest('id')->firstOrFail(); +} + +it('shows the message, the sender and the screenshot', function (): void { + $entry = sentFeedback($this->project); + + $page = visit(FeedbackLink::page($entry, now()->addHour())); + + $page->assertSee('Alpha') + ->assertSee($entry->id) + ->assertSee('open') + ->assertSee('Die Schaltfläche «Speichern» reagiert nicht.') + ->assertSee('Anna Muster') + ->assertSee('anna@beispiel.ch') + ->assertSee('1 console error') + ->assertVisible('img[alt^="Screenshot of"]') + ->assertNoJavaScriptErrors(); + + expect($page->script('() => document.querySelector(\'img[alt^="Screenshot of"]\').naturalWidth'))->toBe(200); +}); + +it('switches to the technical tab with console errors, failed requests and the environment', function (): void { + $entry = sentFeedback($this->project); + + $page = visit(FeedbackLink::page($entry, now()->addHour())); + + $page->assertDontSee('Failed requests') + ->click('#tab-technical') + ->assertSee('Failed requests') + ->assertSee('Boom') + ->assertSee('https://api.beispiel.ch/save') + ->assertSee('Environment') + ->assertSee('Europe/Zurich') + ->assertDontSee('Die Schaltfläche «Speichern» reagiert nicht.') + ->assertAttribute('#tab-technical', 'aria-selected', 'true') + ->assertNoJavaScriptErrors(); + + expect($page->script('() => location.hash'))->toBe('#technical'); + + $page->click('#tab-feedback') + ->assertSee('Die Schaltfläche «Speichern» reagiert nicht.') + ->assertDontSee('Failed requests'); +}); + +it('opens the technical tab directly from the #technical link', function (): void { + $entry = sentFeedback($this->project); + + visit(FeedbackLink::page($entry, now()->addHour()).'#technical') + ->assertSee('Failed requests') + ->assertAttribute('#tab-technical', 'aria-selected', 'true') + ->assertNoJavaScriptErrors(); +}); + +it('hides and shows the message card', function (): void { + $entry = sentFeedback($this->project); + + visit(FeedbackLink::page($entry, now()->addHour())) + ->click('#card-toggle') + ->assertAttribute('#card-toggle', 'aria-expanded', 'false') + ->assertAttribute('#card-toggle', 'aria-label', 'Show message') + ->click('#card-toggle') + ->assertAttribute('#card-toggle', 'aria-expanded', 'true') + ->assertNoJavaScriptErrors(); +}); + +it('copies the feedback ID and announces it', function (): void { + $entry = sentFeedback($this->project); + + visit(FeedbackLink::page($entry, now()->addHour())) + ->click('[aria-label="Copy feedback ID"]') + ->assertSeeIn('#live', 'Copied to clipboard') + ->assertNoJavaScriptErrors(); +}); + +it('lets the team complete and reopen an entry from a team link', function (): void { + $entry = sentFeedback($this->project); + + $page = visit(FeedbackLink::page($entry, now()->addHour(), manage: true, by: 'Sebastian')); + + $page->press('Mark as completed') + ->assertSee('completed') + ->assertSee('Reopen') + ->assertDontSee('Mark as completed') + ->assertNoJavaScriptErrors(); + + expect($entry->fresh()->completed_at)->not->toBeNull() + ->and(Activity::query()->where('subject_id', $entry->id)->sole()->properties['by'])->toBe('Sebastian'); + + $page->press('Reopen') + ->assertSee('Mark as completed') + ->assertNoJavaScriptErrors(); + + expect($entry->fresh()->completed_at)->toBeNull(); +}); + +it('shows no status buttons on the read-only link the sender gets', function (): void { + $entry = sentFeedback($this->project); + + visit(FeedbackLink::page($entry, now()->addHour())) + ->assertSee('open') + ->assertDontSee('Mark as completed') + ->assertNoJavaScriptErrors(); +}); + +it('rejects expired and tampered links with an empty page', function (string $url): void { + $entry = sentFeedback($this->project); + $url = str_replace('{entry}', $entry->id, $url); + + $page = visit($url === 'expired' ? FeedbackLink::page($entry, now()->subMinute()) : $url); + + expect($page->script('() => document.body.innerText.trim()'))->toBe(''); + $page->assertDontSee('Alpha'); +})->with([ + 'expired' => ['expired'], + 'tampered' => ['/v1/feedback/{entry}?expires=4102444800&signature=forged'], +]); + +it('answers a typed API URL with an empty page', function (): void { + $page = visit("/v1/projects/{$this->project->public_key}/config"); + + expect($page->script('() => document.body.innerText.trim()'))->toBe(''); +}); diff --git a/tests/Feature/HealthCheckTest.php b/tests/Feature/HealthCheckTest.php index be3c838..58758cd 100644 --- a/tests/Feature/HealthCheckTest.php +++ b/tests/Feature/HealthCheckTest.php @@ -35,7 +35,6 @@ 'SecurityAdvisoriesCheck', 'ScheduleCheck', 'CacheCheck', - 'OptimizedAppCheck', 'JobsCheck', 'FailedJobsCheck', 'PendingMigrationsCheck', diff --git a/tests/Pest.php b/tests/Pest.php index 20fda49..f6d5a7e 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -11,6 +11,7 @@ pest()->extend(TestCase::class)->use(RefreshDatabase::class)->in('Feature'); pest()->extend(TestCase::class)->in('Unit'); +pest()->extend(TestCase::class)->use(RefreshDatabase::class)->in('Browser'); const TEST_ORIGIN = 'https://www.beispiel.ch'; diff --git a/tests/Unit/Checks/DatabaseConnectionCountCheckTest.php b/tests/Unit/Checks/DatabaseConnectionCountCheckTest.php index da0dd13..c20513a 100644 --- a/tests/Unit/Checks/DatabaseConnectionCountCheckTest.php +++ b/tests/Unit/Checks/DatabaseConnectionCountCheckTest.php @@ -3,6 +3,9 @@ declare(strict_types=1); use App\Checks\DatabaseConnectionCountCheck; +use Illuminate\Database\Connection; +use Illuminate\Database\ConnectionResolverInterface; +use Illuminate\Database\MySqlConnection; use Spatie\Health\Enums\Status; function connectionCountCheckWith(int $connections): DatabaseConnectionCountCheck @@ -16,6 +19,19 @@ function connectionCountCheckWith(int $connections): DatabaseConnectionCountChec return $check; } +function bindConnection(string $class, string $driver, mixed $count): void +{ + $connection = Mockery::mock($class); + $connection->shouldReceive('getDriverName')->andReturn($driver); + $connection->shouldReceive('scalar')->andReturn($count); + $connection->shouldReceive('selectOne')->andReturn((object) ['COUNT(*)' => $count]); + + $resolver = Mockery::mock(ConnectionResolverInterface::class); + $resolver->shouldReceive('connection')->andReturn($connection); + + app()->instance(ConnectionResolverInterface::class, $resolver); +} + it('reports ok with "no database" when the app has none', function () { config(['health.uses_database' => false]); @@ -34,10 +50,58 @@ function connectionCountCheckWith(int $connections): DatabaseConnectionCountChec ->and($result->shortSummary)->toBe('sqlite'); })->group('unit', 'checks'); -it('applies the agreed thresholds', function (int $connections, Status $status) { - expect(connectionCountCheckWith($connections)->run()->status->equals($status))->toBeTrue(); +it('only reports the count and never warns or fails', function (int $connections, string $summary) { + $result = connectionCountCheckWith($connections)->run(); + + expect($result->status->equals(Status::ok()))->toBeTrue() + ->and($result->shortSummary)->toBe($summary) + ->and($result->meta)->toBe(['connection_count' => $connections]); })->with([ - 'ok' => [10, Status::ok()], - 'warning above 50' => [60, Status::warning()], - 'failed above 80' => [90, Status::failed()], + 'one' => [1, '1 connection'], + 'many' => [500, '500 connections'], ])->group('unit', 'checks'); + +it('counts only the connections of the current database on postgres', function () { + config(['database.connections.health_pgsql' => ['driver' => 'pgsql']]); + bindConnection(Connection::class, 'pgsql', 7); + + $check = new class extends DatabaseConnectionCountCheck + { + public function count(): int + { + return $this->getConnectionCount(); + } + }; + + expect($check->connectionName('health_pgsql')->count())->toBe(7); +})->group('unit', 'checks'); + +it('reports zero when postgres returns no number', function () { + config(['database.connections.health_pgsql' => ['driver' => 'pgsql']]); + bindConnection(Connection::class, 'pgsql', null); + + $check = new class extends DatabaseConnectionCountCheck + { + public function count(): int + { + return $this->getConnectionCount(); + } + }; + + expect($check->connectionName('health_pgsql')->count())->toBe(0); +})->group('unit', 'checks'); + +it('falls back to spatie for other drivers', function () { + config(['database.connections.health_mysql' => ['driver' => 'mysql']]); + bindConnection(MySqlConnection::class, 'mysql', 4); + + $check = new class extends DatabaseConnectionCountCheck + { + public function count(): int + { + return $this->getConnectionCount(); + } + }; + + expect($check->connectionName('health_mysql')->count())->toBe(4); +})->group('unit', 'checks');