diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..607e5677 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.git +.github + +**/node_modules +vendor + +playwright-report +test-results +coverage + +.env +.env.* +!.env.example + +checksums.json +*.rej diff --git a/.github/workflows/e2e-base-image.yml b/.github/workflows/e2e-base-image.yml new file mode 100644 index 00000000..08b545ca --- /dev/null +++ b/.github/workflows/e2e-base-image.yml @@ -0,0 +1,45 @@ +name: Publish E2E Base Image + +on: + workflow_dispatch: + inputs: + tag: + description: E2E base image tag to publish. + required: true + default: php8.2-node22-panel1.12.2 + +permissions: + contents: read + packages: write + +env: + E2E_BASE_IMAGE: ghcr.io/${{ github.repository_owner }}/notur-e2e-base:${{ inputs.tag }} + +jobs: + publish: + name: Publish E2E Base Image + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and publish E2E base image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/e2e/Dockerfile.base + push: true + tags: ${{ env.E2E_BASE_IMAGE }} + cache-from: type=gha,scope=e2e-base + cache-to: type=gha,scope=e2e-base,mode=max diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 56d24dc6..8df10d0e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -19,6 +19,7 @@ on: - 'package.json' - 'package-lock.json' - '.github/workflows/e2e.yml' + - '.github/workflows/e2e-base-image.yml' pull_request: branches: [master] paths: @@ -37,13 +38,24 @@ on: - 'package.json' - 'package-lock.json' - '.github/workflows/e2e.yml' + - '.github/workflows/e2e-base-image.yml' workflow_dispatch: +permissions: + contents: read + packages: read + +env: + E2E_BASE_IMAGE: ghcr.io/${{ github.repository_owner }}/notur-e2e-base:php8.2-node22-panel1.12.2 + E2E_FALLBACK_BASE_IMAGE: notur/e2e-base:php8.2-node22-panel1.12.2 + E2E_APP_IMAGE: notur/e2e-app:ci + E2E_TEST_RUNNER_IMAGE: notur/e2e-test-runner:ci + jobs: e2e: name: E2E Test Suite runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 45 steps: - uses: actions/checkout@v4 @@ -51,299 +63,82 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Cache Docker layers - uses: actions/cache@v4 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-e2e-docker-${{ hashFiles('docker/e2e/Dockerfile') }} - restore-keys: ${{ runner.os }}-e2e-docker- + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and run E2E tests + - name: Resolve E2E base image + id: base + run: | + if docker pull "$E2E_BASE_IMAGE"; then + echo "image=$E2E_BASE_IMAGE" >> "$GITHUB_OUTPUT" + echo "build=false" >> "$GITHUB_OUTPUT" + else + echo "::warning::Missing E2E base image: $E2E_BASE_IMAGE" + echo "::warning::Run the 'Publish E2E Base Image' workflow to avoid rebuilding the base layer." + echo "image=$E2E_FALLBACK_BASE_IMAGE" >> "$GITHUB_OUTPUT" + echo "build=true" >> "$GITHUB_OUTPUT" + fi + + - name: Build fallback E2E base image + if: steps.base.outputs.build == 'true' + uses: docker/build-push-action@v6 + with: + context: . + file: docker/e2e/Dockerfile.base + load: true + tags: ${{ env.E2E_FALLBACK_BASE_IMAGE }} + cache-from: type=gha,scope=e2e-base + cache-to: type=gha,scope=e2e-base,mode=max + + - name: Build E2E app image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/e2e/Dockerfile + target: app + load: true + tags: ${{ env.E2E_APP_IMAGE }} + build-args: | + E2E_BASE_IMAGE=${{ steps.base.outputs.image }} + cache-from: type=gha,scope=e2e-app + cache-to: type=gha,scope=e2e-app,mode=max + + - name: Build E2E test-runner image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/e2e/Dockerfile + target: test-runner + load: true + tags: ${{ env.E2E_TEST_RUNNER_IMAGE }} + build-args: | + E2E_BASE_IMAGE=${{ steps.base.outputs.image }} + cache-from: type=gha,scope=e2e-test-runner + cache-to: type=gha,scope=e2e-test-runner,mode=max + + - name: Run shell and browser E2E tests + run: bash docker/e2e/run-e2e.sh --no-build --keep + + - name: Reset E2E containers before install/uninstall suite run: | cd docker/e2e - docker compose build - docker compose up -d db app - - # Wait for MySQL health check - echo "Waiting for MySQL..." - retries=0 - until docker compose exec -T db mysqladmin ping -h 127.0.0.1 -u root -pnotur_e2e --silent 2>/dev/null; do - retries=$((retries + 1)) - if [ "$retries" -ge 60 ]; then - echo "MySQL did not become ready" - docker compose logs db - exit 1 - fi - sleep 2 - done - echo "MySQL is ready" - - # Install Notur into the panel using current branch code - docker compose exec -T app bash -c ' - cd /var/www/pterodactyl - set -euo pipefail - export NODE_OPTIONS=--openssl-legacy-provider - - # Install from current branch via path repository (tests latest code, not Packagist) - git config --global --add safe.directory /opt/notur - composer config platform.php 8.2.30 --no-interaction - composer config audit.block-insecure false --no-interaction - composer config audit.ignore "PKSA-8qx3-n5y5-vvnd,PKSA-w7xr-vk7n-rstm" --no-interaction - composer config repositories.notur "{\"type\":\"path\",\"url\":\"/opt/notur\",\"options\":{\"symlink\":false}}" --no-interaction - if ! COMPOSER_ALLOW_SUPERUSER=1 COMPOSER_DISABLE_AUDIT=1 composer require notur/notur:@dev --no-interaction --with-all-dependencies 2>&1; then - echo "Composer require failed. Retrying once with verbose output..." - COMPOSER_ALLOW_SUPERUSER=1 COMPOSER_DISABLE_AUDIT=1 composer require notur/notur:@dev --no-interaction --with-all-dependencies -vvv - exit 1 - fi - - # Ensure DB settings in .env match docker-compose environment - if [ -f ".env" ]; then - sed -i "s/^DB_CONNECTION=.*/DB_CONNECTION=mysql/" .env - sed -i "s/^DB_HOST=.*/DB_HOST=db/" .env - sed -i "s/^DB_PORT=.*/DB_PORT=3306/" .env - sed -i "s/^DB_DATABASE=.*/DB_DATABASE=panel/" .env - sed -i "s/^DB_USERNAME=.*/DB_USERNAME=pterodactyl/" .env - sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=pterodactyl/" .env - fi - - # Inject Notur scripts into Blade layout - SCRIPTS_BLADE="resources/views/layouts/scripts.blade.php" - WRAPPER_BLADE="resources/views/templates/wrapper.blade.php" - if [ -f "${SCRIPTS_BLADE}" ]; then - TARGET_BLADE="${SCRIPTS_BLADE}" - elif [ -f "${WRAPPER_BLADE}" ]; then - TARGET_BLADE="${WRAPPER_BLADE}" - fi - - if [ -n "${TARGET_BLADE:-}" ] && ! grep -q "notur::scripts" "${TARGET_BLADE}"; then - echo "" >> "${TARGET_BLADE}" - echo "@include(\"notur::scripts\")" >> "${TARGET_BLADE}" - fi - - # Apply React source patches (skip reverse patches) - # Mirror install.sh detection: try composer.lock first (in case the - # panel was installed as a dependency), then fall back to - # config/app.php (where the standalone panel surfaces its version). - # pterodactyl/panel is the root package in a self-hosted install so - # it does NOT appear in composer.lock packages — config/app.php is - # the authoritative source there. - PATCH_DIR="" - PANEL_VERSION="" - if [ -f "composer.lock" ]; then - PANEL_VERSION=$(php -r " \ - \$lock = json_decode(file_get_contents(\"composer.lock\"), true); \ - if (!is_array(\$lock)) { exit(0); } \ - foreach ([\"packages\", \"packages-dev\"] as \$section) { \ - if (empty(\$lock[\$section]) || !is_array(\$lock[\$section])) { continue; } \ - foreach (\$lock[\$section] as \$pkg) { \ - if ((\$pkg[\"name\"] ?? \"\") === \"pterodactyl/panel\") { \ - echo (string) (\$pkg[\"version\"] ?? \"\"); \ - exit(0); \ - } \ - } \ - }" 2>/dev/null || true) - fi - if [ -z "${PANEL_VERSION}" ] && [ -f "config/app.php" ]; then - PANEL_VERSION=$(grep "'"'"'version'"'"'" config/app.php | head -1 | sed "s/.*'"'"'version'"'"'.*'"'"'\([^'"'"']*\)'"'"'.*/\1/" || echo "") - fi - - echo "Detected panel version: ${PANEL_VERSION:-unknown}" - - # Mirror install.sh: only v1.12.x is supported; hard-fail otherwise. - # Accept both "1.12.0" and "v1.12.0" forms. - PANEL_VERSION_NORMALIZED="${PANEL_VERSION#v}" - case "${PANEL_VERSION_NORMALIZED}" in - 1.12.*) - PATCH_DIR="vendor/notur/notur/installer/patches/v1.12" - ;; - "") - echo "E2E: could not detect Pterodactyl panel version. Notur requires v1.12.x." >&2 - exit 1 - ;; - *) - echo "E2E: unsupported Pterodactyl version '\''${PANEL_VERSION}'\''. Notur supports v1.12.x only." >&2 - exit 1 - ;; - esac - - if [ ! -d "${PATCH_DIR}" ]; then - echo "E2E: patch directory ${PATCH_DIR} not found" >&2 - exit 1 - fi - - echo "Using patch set: ${PATCH_DIR##*/}" - for patch in "${PATCH_DIR}"/*.patch; do - [ -f "${patch}" ] || continue - PATCH_NAME=$(basename "${patch}") - echo "${PATCH_NAME}" | grep -q "reverse" && continue - echo "Applying patch: ${PATCH_NAME}" - if patch --dry-run -p1 < "${patch}" &>/dev/null; then - patch -p1 < "${patch}" - echo "Applied: ${PATCH_NAME}" - else - echo "Skipping: ${PATCH_NAME} (already applied or incompatible)" - fi - done - - # Rebuild frontend with patches applied - echo "Installing frontend dependencies..." - yarn install --frozen-lockfile 2>&1 || npm install 2>&1 - - echo "Building production assets..." - if yarn run build:production 2>&1; then - echo "Frontend build succeeded" - else - echo "Frontend build failed" - exit 1 - fi - - if [ ! -f "public/assets/manifest.json" ]; then - echo "Build artifacts missing: public/assets/manifest.json" - ls -la public/assets 2>/dev/null || true - exit 1 - fi - - # Set up Notur directories and copy bridge - mkdir -p notur/extensions public/notur/extensions - BRIDGE_JS="vendor/notur/notur/bridge/dist/bridge.js" - if [ ! -f "${BRIDGE_JS}" ]; then - echo "Bridge runtime missing; building..." - if [ -d "vendor/notur/notur" ]; then - cd vendor/notur/notur - NODE_OPTIONS=--openssl-legacy-provider bun install --frozen-lockfile 2>&1 || NODE_OPTIONS=--openssl-legacy-provider npm install 2>&1 - if ! NODE_OPTIONS=--openssl-legacy-provider bun run build:bridge 2>&1; then - if ! NODE_OPTIONS=--openssl-legacy-provider npm run build:bridge 2>&1; then - if [ -d "bridge" ]; then - cd bridge - if ! NODE_OPTIONS=--openssl-legacy-provider bunx webpack --mode production 2>&1; then - echo "Bridge build failed" - exit 1 - fi - cd .. - else - echo "Bridge build failed (bridge directory missing)" - exit 1 - fi - fi - fi - cd /var/www/pterodactyl - fi - fi - if [ -f "${BRIDGE_JS}" ]; then - cp "${BRIDGE_JS}" public/notur/bridge.js - else - echo "Bridge runtime missing after build" - exit 1 - fi - - # Initialize extensions.json - [ -f notur/extensions.json ] || echo "{\"extensions\":{}}" > notur/extensions.json - - # Ensure package discovery + routes are registered - php artisan package:discover --ansi 2>&1 || true - php artisan config:clear 2>&1 || true - php artisan route:clear 2>&1 || true - - # Run migrations (core + Notur package explicitly) - php artisan migrate --force 2>&1 - php artisan migrate --path=vendor/notur/notur/database/migrations --force 2>&1 - - # Verify Notur tables exist - TABLES=$(mysql --ssl=0 -h db -P 3306 -u pterodactyl -ppterodactyl panel -e "SHOW TABLES LIKE \"notur_%\";" 2>/tmp/mysql-check.err || true) - if ! echo "$TABLES" | grep -q "notur_"; then - echo "Notur tables missing after migration" - if [ -s /tmp/mysql-check.err ]; then - echo "MySQL error output:" - cat /tmp/mysql-check.err - fi - mysql --ssl=0 -h db -P 3306 -u pterodactyl -ppterodactyl panel -e "SHOW TABLES;" 2>/dev/null || true - exit 1 - fi - - # Fix permissions - chown -R www-data:www-data storage bootstrap/cache notur public/notur 2>/dev/null || true - ' - - # Install test extension - docker compose exec -T app bash -c ' - cd /var/www/pterodactyl - set -euo pipefail - - # Pack example extension into a real .notur archive and install via CLI. - rm -rf /tmp/hello-world-src /tmp/hello-world.notur - cp -R /opt/notur/examples/hello-world /tmp/hello-world-src - php -r "require \"vendor/autoload.php\"; \\Notur\\Support\\NoturArchive::pack(\"/tmp/hello-world-src\", \"/tmp/hello-world.notur\");" - php artisan notur:install /tmp/hello-world.notur --force - - # Refresh caches and ensure Notur migrations are applied - php artisan config:clear 2>/dev/null || true - php artisan route:clear 2>/dev/null || true - php artisan cache:clear 2>/dev/null || true - php artisan migrate --path=vendor/notur/notur/database/migrations --force 2>&1 - - # Verify extension routes are registered - if ! php artisan route:list 2>/dev/null | grep -q "notur/hello-world"; then - echo "Notur extension routes not registered" - php artisan route:list 2>/dev/null | grep -i notur || true - exit 1 - fi - - # Re-chown storage AND bootstrap/cache AFTER all artisan calls in - # this block so nginx → php-fpm (www-data) can append to - # laravel-*.log on subsequent HTTP requests. Each artisan call - # here ran as root and would otherwise leave root-owned log/cache - # files behind, producing 500s with - # "could not be opened in append mode: Permission denied" - # the next time a request enters Laravel. - chown -R www-data:www-data storage bootstrap/cache notur public/notur 2>/dev/null || true - ' - - # Run tests - docker compose run --rm test-runner - - # Verify real CLI lifecycle commands inside panel container - docker compose exec -T app bash -c ' - cd /var/www/pterodactyl - set -euo pipefail - - php artisan notur:list > /tmp/notur-list-before.txt - if ! grep -q "notur/hello-world" /tmp/notur-list-before.txt; then - echo "notur:install did not register hello-world in notur:list" - cat /tmp/notur-list-before.txt - exit 1 - fi - - if ! php artisan notur:disable notur/hello-world; then - echo "notur:disable failed for notur/hello-world" - exit 1 - fi - - if ! php artisan notur:enable notur/hello-world; then - echo "notur:enable failed for notur/hello-world" - exit 1 - fi - - if ! printf "yes\n" | php artisan notur:remove notur/hello-world; then - echo "notur:remove failed for notur/hello-world" - exit 1 - fi + docker compose down -v --remove-orphans 2>/dev/null || true - if php artisan notur:list | grep -q "notur/hello-world"; then - echo "notur/hello-world still present after notur:remove" - exit 1 - fi - ' + - name: Run install/uninstall E2E tests + run: bash docker/e2e/run-e2e.sh --no-build --suite install-uninstall --keep - name: Collect logs on failure if: failure() run: | cd docker/e2e - docker compose logs app > /tmp/app-logs.txt 2>&1 - docker compose logs db > /tmp/db-logs.txt 2>&1 + docker compose logs app > /tmp/app-logs.txt 2>&1 || true + docker compose logs db > /tmp/db-logs.txt 2>&1 || true + docker compose logs test-runner > /tmp/test-runner-logs.txt 2>&1 || true - # docker-compose logs only shows supervisord stdout/stderr — the - # actual Laravel/nginx/php-fpm errors live in files inside the - # container. Pull them out so we can debug 500s post-mortem. docker compose exec -T app bash -c ' echo "=== laravel.log (last 200 lines) ===" tail -n 200 /var/www/pterodactyl/storage/logs/laravel.log 2>/dev/null || echo "(missing)" @@ -356,18 +151,11 @@ jobs: || tail -n 100 /var/log/php8.2-fpm.log 2>/dev/null \ || echo "(no fpm log file — fpm logs to stderr by default)" echo - echo "=== storage perms ===" - ls -la /var/www/pterodactyl/storage 2>&1 | head -20 - ls -la /var/www/pterodactyl/storage/framework 2>&1 | head -20 - echo echo "=== .env (with secrets redacted) ===" sed -E "s/(APP_KEY|HASHIDS_SALT|DB_PASSWORD|MAIL_PASSWORD|RECAPTCHA_SECRET_KEY)=.*/\1=***REDACTED***/" /var/www/pterodactyl/.env 2>/dev/null || echo "(missing)" echo - echo "=== public/assets/manifest.json keys ===" - php -r "echo implode(\"\n\", array_keys(json_decode(file_get_contents(\"/var/www/pterodactyl/public/assets/manifest.json\"), true) ?: []));" 2>&1 | head -30 || echo "(missing/invalid)" - echo - echo "=== HEAD / response (verbose) ===" - curl -sSi http://127.0.0.1/ 2>&1 | head -40 || true + echo "=== extensions.json ===" + cat /var/www/pterodactyl/notur/extensions.json 2>/dev/null || echo "(missing)" ' > /tmp/runtime-logs.txt 2>&1 || true - name: Upload logs on failure diff --git a/.gitignore b/.gitignore index ece1d5a3..d533b643 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,8 @@ docs/superpowers/ .phpunit.cache/ *.coverage clover.xml +/playwright-report/ +/test-results/ # Optional: local panel checkout for development (uncomment if needed) /pterodactyl-panel-v1.11.11/ diff --git a/README.md b/README.md index 2a227fd0..97f10514 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,51 @@ npm run build:sdk npm run test:frontend ``` +## Docker E2E + +The repo ships with a real browser-backed E2E environment built on the existing Docker panel setup. It boots a Pterodactyl panel, installs the current Notur checkout into that panel, seeds a deterministic root admin, and runs the shell and Playwright suites against the same environment. + +```bash +# Build the reusable E2E base image once. +# This is the slow dependency layer with OS packages, Node, Composer, Pterodactyl, and browser libraries. +bash docker/e2e/build-base.sh + +# Run the full shell + browser E2E suite +bash docker/e2e/run-e2e.sh + +# Run only the browser suite +bash docker/e2e/run-e2e.sh --suite browser + +# Run the destructive Notur install/uninstall lifecycle suite +# This checks the panel before install, installs Notur, uninstalls Notur, and checks the panel again. +bash docker/e2e/run-e2e.sh --suite install-uninstall + +# Keep containers running for inspection after the suite finishes +bash docker/e2e/run-e2e.sh --keep + +# Force a fully fresh rebuild when debugging Docker image state +bash docker/e2e/run-e2e.sh --no-cache --rebuild-base +``` + +The reusable local base image is named `notur/e2e-base:php8.2-node22-panel1.12.2`. It contains the slow-moving E2E dependencies: PHP extensions, system packages, Node.js, Bun, Composer, the Pterodactyl panel tarball, and browser runtime libraries. Normal runs reuse it and only rebuild the lightweight repo-specific layers. If the base image is missing, `run-e2e.sh` fails with instructions instead of silently downloading all packages again. + +GitHub Actions uses the published GHCR base image `ghcr.io/sak0a/notur-e2e-base:php8.2-node22-panel1.12.2` instead of rebuilding that slow layer on every PR. Publish or refresh it manually from the `Publish E2E Base Image` workflow after changing `docker/e2e/Dockerfile.base` or the PHP/Node/panel version tuple. If the GHCR image is missing, CI falls back to building the base image with the GitHub Actions layer cache so first-time PRs still run. + +The default `all` suite runs the shell and browser E2E suites against a bootstrapped Notur panel. The `install-uninstall` suite is intentionally explicit because it destructively removes Notur from the panel while verifying that the underlying Pterodactyl installation remains usable. + +Seeded admin credentials inside the E2E environment: + +```text +Email: admin@example.com +Password: notur-admin-password +``` + +The browser specs live in `tests/E2E/browser/` and can also be invoked directly against an already running E2E panel with: + +```bash +npm run test:e2e:browser +``` + ## Architecture ``` diff --git a/composer.json b/composer.json index 14a0ff99..d71453e9 100644 --- a/composer.json +++ b/composer.json @@ -53,5 +53,6 @@ "config": { "sort-packages": true }, - "minimum-stability": "stable" + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/docker/e2e/Dockerfile b/docker/e2e/Dockerfile index 8c391f0c..c93eee6c 100644 --- a/docker/e2e/Dockerfile +++ b/docker/e2e/Dockerfile @@ -1,112 +1,24 @@ # Notur E2E Test Environment -# Based on PHP 8.2 + Node 22, installs Pterodactyl Panel + Notur + test extension -FROM php:8.2-fpm - -# Pterodactyl Panel v1.12.x requires Node >= 22 (declared in upstream -# package.json engines). Keep this in sync with .github/workflows/ci.yml's -# frontend-build matrix. -ARG NODE_VERSION=22 -ARG PANEL_VERSION=v1.12.2 - -# Work around OpenSSL 3 compatibility issues in Node 17+ for webpack builds. -ENV NODE_OPTIONS=--openssl-legacy-provider - -# System dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ - git \ - curl \ - unzip \ - zip \ - libzip-dev \ - libpng-dev \ - libonig-dev \ - libxml2-dev \ - libsodium-dev \ - libcurl4-openssl-dev \ - mariadb-client \ - patch \ - ca-certificates \ - gnupg \ - nginx \ - supervisor \ - && docker-php-ext-install \ - pdo_mysql \ - mbstring \ - zip \ - bcmath \ - sodium \ - gd \ - xml \ - && rm -rf /var/lib/apt/lists/* - -# Configure the mysql CLI to skip TLS. The compose db service runs -# MySQL 8.0, which presents a self-signed cert on every connection, -# and our mariadb-client verifies certs by default — so any direct -# `mysql` invocation fails with "TLS/SSL error: self-signed -# certificate in certificate chain". This bites Laravel's migrate -# when it shells out to mysql to load database/schema/mysql-schema.sql, -# but the workflow's own SHOW TABLES probes already pass --ssl=0 -# explicitly. Disabling TLS for the CLI globally inside this test -# container is fine — the db is a private docker-compose service. -RUN printf '[client]\nssl=0\n' > /root/.my.cnf - -# Install Node.js -RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \ - && apt-get install -y nodejs \ - && rm -rf /var/lib/apt/lists/* - -# Install Bun -RUN curl -fsSL https://bun.sh/install | bash \ - && ln -s /root/.bun/bin/bun /usr/local/bin/bun \ - && ln -s /root/.bun/bin/bunx /usr/local/bin/bunx - -# Install Composer -COPY --from=composer:2 /usr/bin/composer /usr/bin/composer - -# Download the Pterodactyl Panel release tarball — same artifact the -# upstream install docs tell operators to fetch: -# https://pterodactyl.io/panel/1.0/getting_started.html -# The tarball ships with the real version baked into config/app.php -# (no 'canary' placeholder), pre-built public/assets/, and the source -# tree intact. This matches the real install path our installer -# targets and removes the need to "git clone + sed-rewrite the version". -RUN mkdir -p /var/www/pterodactyl \ - && curl -fsSL -o /tmp/panel.tar.gz \ - "https://github.com/pterodactyl/panel/releases/download/${PANEL_VERSION}/panel.tar.gz" \ - && tar -xzf /tmp/panel.tar.gz -C /var/www/pterodactyl \ - && rm /tmp/panel.tar.gz +# Depends on the reusable base built from docker/e2e/Dockerfile.base. +ARG E2E_BASE_IMAGE=notur/e2e-base:php8.2-node22-panel1.12.2 -WORKDIR /var/www/pterodactyl +FROM ${E2E_BASE_IMAGE} AS notur-node-deps + +WORKDIR /opt/notur + +COPY package.json package-lock.json ./ + +RUN npm ci --no-audit --no-fund \ + && npx playwright install chromium + +FROM ${E2E_BASE_IMAGE} AS panel-base -# Install panel PHP dependencies (vendor/ is not shipped in the tarball). -# --no-scripts: defer composer's post-install hooks (Pterodactyl's -# composer.json runs `php artisan package:discover` post-autoload-dump, -# which boots SettingsServiceProvider and needs APP_KEY — but .env -# doesn't exist yet at this point). We run the discovery step manually -# below, after the environment is fully set up. -RUN composer install --no-interaction --prefer-dist --optimize-autoloader --no-scripts - -# Install yarn + frontend deps so the post-patch rebuild in the e2e -# workflow has node_modules ready. Skip the initial build:production: -# the tarball already includes pre-built public/assets/ for the no-patch -# baseline, and the workflow rebuilds anyway after applying React patches, -# so building twice would just burn minutes. -RUN npm install -g yarn \ - && yarn install --frozen-lockfile - -# Set up panel environment + run the package discovery we deferred above. -RUN cp .env.example .env \ - && php artisan key:generate --force \ - && php artisan package:discover --ansi - -# Fix storage permissions for www-data (PHP-FPM user) -RUN chown -R www-data:www-data storage bootstrap/cache \ - && chmod -R 775 storage bootstrap/cache - -# Copy Notur source into vendor-like location for testing COPY . /opt/notur -# Nginx configuration for serving the panel +FROM panel-base AS app + +WORKDIR /var/www/pterodactyl + RUN echo 'server { \n\ listen 80; \n\ root /var/www/pterodactyl/public; \n\ @@ -124,7 +36,6 @@ RUN echo 'server { \n\ } \n\ }' > /etc/nginx/sites-available/default -# Supervisor to run PHP-FPM + Nginx together RUN echo '[supervisord] \n\ nodaemon=true \n\ \n\ @@ -138,8 +49,14 @@ command=/usr/sbin/nginx -g "daemon off;" \n\ autostart=true \n\ autorestart=true' > /etc/supervisor/conf.d/panel.conf -# Expose HTTP EXPOSE 80 -# Default: start supervisor (PHP-FPM + Nginx) CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] + +FROM notur-node-deps AS test-runner + +WORKDIR /opt/notur + +COPY . /opt/notur + +WORKDIR /var/www/pterodactyl diff --git a/docker/e2e/Dockerfile.base b/docker/e2e/Dockerfile.base new file mode 100644 index 00000000..06e72b98 --- /dev/null +++ b/docker/e2e/Dockerfile.base @@ -0,0 +1,93 @@ +# Reusable base for the Notur Docker E2E environment. +# +# Build once locally: +# docker build -f docker/e2e/Dockerfile.base -t notur/e2e-base:php8.2-node22-panel1.12.2 . +FROM php:8.2-fpm + +ARG NODE_VERSION=22 +ARG PANEL_VERSION=v1.12.2 + +ENV NODE_OPTIONS=--openssl-legacy-provider + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + curl \ + unzip \ + zip \ + libzip-dev \ + libpng-dev \ + libonig-dev \ + libxml2-dev \ + libsodium-dev \ + libcurl4-openssl-dev \ + mariadb-client \ + patch \ + ca-certificates \ + gnupg \ + nginx \ + supervisor \ + fonts-liberation \ + libasound2t64 \ + libatk-bridge2.0-0t64 \ + libatk1.0-0t64 \ + libatspi2.0-0t64 \ + libcairo2 \ + libcups2t64 \ + libdbus-1-3 \ + libdrm2 \ + libgbm1 \ + libglib2.0-0t64 \ + libnspr4 \ + libnss3 \ + libpango-1.0-0 \ + libx11-6 \ + libx11-xcb1 \ + libxcb1 \ + libxcomposite1 \ + libxdamage1 \ + libxext6 \ + libxfixes3 \ + libxkbcommon0 \ + libxrandr2 \ + libxshmfence1 \ + && docker-php-ext-install \ + pdo_mysql \ + mbstring \ + zip \ + bcmath \ + sodium \ + gd \ + xml \ + && rm -rf /var/lib/apt/lists/* + +RUN printf '[client]\nssl=0\n' > /root/.my.cnf + +RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \ + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL https://bun.sh/install | bash \ + && ln -s /root/.bun/bin/bun /usr/local/bin/bun \ + && ln -s /root/.bun/bin/bunx /usr/local/bin/bunx + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +RUN mkdir -p /var/www/pterodactyl \ + && curl -fsSL -o /tmp/panel.tar.gz \ + "https://github.com/pterodactyl/panel/releases/download/${PANEL_VERSION}/panel.tar.gz" \ + && tar -xzf /tmp/panel.tar.gz -C /var/www/pterodactyl \ + && rm /tmp/panel.tar.gz + +WORKDIR /var/www/pterodactyl + +RUN composer install --no-interaction --prefer-dist --optimize-autoloader --no-scripts + +RUN npm install -g yarn \ + && yarn install --frozen-lockfile + +RUN cp .env.example .env \ + && php artisan key:generate --force \ + && php artisan package:discover --ansi + +RUN chown -R www-data:www-data storage bootstrap/cache \ + && chmod -R 775 storage bootstrap/cache diff --git a/docker/e2e/build-base.sh b/docker/e2e/build-base.sh new file mode 100644 index 00000000..c38b5231 --- /dev/null +++ b/docker/e2e/build-base.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +BASE_IMAGE="${E2E_BASE_IMAGE:-notur/e2e-base:php8.2-node22-panel1.12.2}" + +docker build \ + -f "${SCRIPT_DIR}/Dockerfile.base" \ + -t "$BASE_IMAGE" \ + "$PROJECT_ROOT" diff --git a/docker/e2e/docker-compose.yml b/docker/e2e/docker-compose.yml index 0e312e09..c2fae2b0 100644 --- a/docker/e2e/docker-compose.yml +++ b/docker/e2e/docker-compose.yml @@ -19,16 +19,20 @@ services: - e2e app: + image: ${E2E_APP_IMAGE:-notur/e2e-app:local} build: context: ../.. dockerfile: docker/e2e/Dockerfile + target: app + args: + E2E_BASE_IMAGE: ${E2E_BASE_IMAGE:-notur/e2e-base:php8.2-node22-panel1.12.2} depends_on: db: condition: service_healthy environment: APP_ENV: testing APP_DEBUG: "true" - APP_URL: http://localhost + APP_URL: http://app NODE_OPTIONS: --openssl-legacy-provider # APP_KEY is generated at Docker build time (php artisan key:generate in Dockerfile) DB_CONNECTION: mysql @@ -40,32 +44,48 @@ services: CACHE_DRIVER: file SESSION_DRIVER: file QUEUE_CONNECTION: sync + RECAPTCHA_ENABLED: "false" ports: - "8080:80" - volumes: - - ../../:/opt/notur:ro + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://127.0.0.1/auth/login >/dev/null || curl -fsS http://127.0.0.1/ >/dev/null"] + interval: 5s + timeout: 5s + retries: 60 + start_period: 10s networks: - e2e test-runner: + image: ${E2E_TEST_RUNNER_IMAGE:-notur/e2e-test-runner:local} build: context: ../.. dockerfile: docker/e2e/Dockerfile + target: test-runner + args: + E2E_BASE_IMAGE: ${E2E_BASE_IMAGE:-notur/e2e-base:php8.2-node22-panel1.12.2} depends_on: - - app + app: + condition: service_healthy environment: - APP_URL: http://app + APP_URL: http://127.0.0.1 + PLAYWRIGHT_BASE_URL: http://127.0.0.1 DB_HOST: db DB_PORT: 3306 DB_DATABASE: panel DB_USERNAME: pterodactyl DB_PASSWORD: pterodactyl NODE_OPTIONS: --openssl-legacy-provider - entrypoint: ["bash", "/opt/notur/tests/E2E/e2e-test.sh"] - volumes: - - ../../:/opt/notur:ro - networks: - - e2e + E2E_SUITE: all + E2E_ADMIN_EMAIL: admin@example.com + E2E_ADMIN_USERNAME: admin + E2E_ADMIN_PASSWORD: notur-admin-password + E2E_USER_EMAIL: user@example.com + E2E_USER_USERNAME: user + E2E_USER_PASSWORD: notur-user-password + entrypoint: ["bash", "/opt/notur/tests/E2E/run-tests.sh"] + shm_size: 1gb + network_mode: "service:app" networks: e2e: diff --git a/docker/e2e/install-uninstall.sh b/docker/e2e/install-uninstall.sh new file mode 100755 index 00000000..8a84e0a6 --- /dev/null +++ b/docker/e2e/install-uninstall.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd /var/www/pterodactyl + +panel_available() { + local retries=0 + local max_retries=60 + + until curl -fsS --max-time 10 http://127.0.0.1/auth/login >/dev/null \ + || curl -fsS --max-time 10 http://127.0.0.1/ >/dev/null; do + retries=$((retries + 1)) + if [ "$retries" -ge "$max_retries" ]; then + echo "[E2E] Panel did not become available within ${max_retries} attempts." >&2 + return 1 + fi + sleep 2 + done +} + +mysql_notur_tables() { + mysql --ssl=0 \ + -h "${DB_HOST}" \ + -P "${DB_PORT}" \ + -u "${DB_USERNAME}" \ + -p"${DB_PASSWORD}" \ + "${DB_DATABASE}" \ + -N -B \ + -e "SHOW TABLES LIKE 'notur_%';" +} + +echo "[E2E] Verifying Notur is installed before uninstall..." +panel_available + +if ! php artisan route:list | grep -q "admin/notur/extensions"; then + echo "[E2E] Expected Notur admin routes before uninstall, but they were missing." >&2 + exit 1 +fi + +if ! grep -R "notur::scripts" resources/views >/dev/null 2>&1; then + echo "[E2E] Expected Notur Blade script injection before uninstall, but it was missing." >&2 + exit 1 +fi + +if [ ! -d notur ] || [ ! -d public/notur ]; then + echo "[E2E] Expected Notur runtime directories before uninstall, but one was missing." >&2 + exit 1 +fi + +if ! mysql_notur_tables | grep -q "notur_extensions"; then + echo "[E2E] Expected Notur database tables before uninstall, but notur_extensions was missing." >&2 + exit 1 +fi + +echo "[E2E] Running non-interactive Notur uninstall..." +COMPOSER_ALLOW_SUPERUSER=1 COMPOSER_DISABLE_AUDIT=1 php artisan notur:uninstall --confirm --no-interaction + +echo "[E2E] Verifying panel still responds after Notur uninstall..." +panel_available + +echo "[E2E] Verifying Notur framework state was removed..." +if php artisan route:list | grep -q "admin/notur"; then + echo "[E2E] Notur admin routes are still registered after uninstall." >&2 + php artisan route:list | grep -i notur >&2 || true + exit 1 +fi + +if grep -R "notur::scripts" resources/views >/dev/null 2>&1; then + echo "[E2E] Notur Blade script injection is still present after uninstall." >&2 + grep -R "notur::scripts" resources/views >&2 || true + exit 1 +fi + +if [ -d notur ] || [ -d public/notur ]; then + echo "[E2E] Notur runtime directories still exist after uninstall." >&2 + ls -ld notur public/notur 2>/dev/null >&2 || true + exit 1 +fi + +remaining_tables="$(mysql_notur_tables)" +if [ -n "${remaining_tables}" ]; then + echo "[E2E] Notur database tables still exist after uninstall:" >&2 + echo "${remaining_tables}" >&2 + exit 1 +fi + +if composer show notur/notur >/dev/null 2>&1; then + echo "[E2E] Composer still reports notur/notur as installed after uninstall." >&2 + exit 1 +fi + +echo "[E2E] Install/uninstall lifecycle verification passed." diff --git a/docker/e2e/run-e2e.sh b/docker/e2e/run-e2e.sh index 5206cd2d..7cdf87d5 100755 --- a/docker/e2e/run-e2e.sh +++ b/docker/e2e/run-e2e.sh @@ -2,15 +2,21 @@ set -euo pipefail # Notur E2E Test Runner -# Orchestrates the Docker-based end-to-end test suite. +# Orchestrates the Docker-based shell, browser, and destructive lifecycle end-to-end suites. # -# Usage: bash docker/e2e/run-e2e.sh [--no-build] [--keep] -# --no-build Skip rebuilding Docker images -# --keep Keep containers running after tests +# Usage: bash docker/e2e/run-e2e.sh [--no-build] [--no-cache] [--rebuild-base] [--keep] [--suite all|shell|browser|install-uninstall] +# --no-build Skip rebuilding Docker images +# --no-cache Rebuild Docker images without cache +# --rebuild-base Rebuild the reusable E2E base image before app images +# --keep Keep containers running after tests +# --suite Select which test suite to run (default: all) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yml" +BASE_IMAGE="${E2E_BASE_IMAGE:-notur/e2e-base:php8.2-node22-panel1.12.2}" +APP_IMAGE="${E2E_APP_IMAGE:-notur/e2e-app:local}" +TEST_RUNNER_IMAGE="${E2E_TEST_RUNNER_IMAGE:-notur/e2e-test-runner:local}" RED='\033[0;31m' GREEN='\033[0;32m' @@ -24,16 +30,53 @@ warn() { echo -e "${YELLOW}[E2E]${NC} $1"; } error() { echo -e "${RED}[E2E]${NC} $1" >&2; } NO_BUILD=false +NO_CACHE=false +REBUILD_BASE=false KEEP=false - -for arg in "$@"; do - case "$arg" in - --no-build) NO_BUILD=true ;; - --keep) KEEP=true ;; - *) error "Unknown argument: $arg"; exit 1 ;; +SUITE=all + +while [ "$#" -gt 0 ]; do + case "$1" in + --no-build) + NO_BUILD=true + shift + ;; + --no-cache) + NO_CACHE=true + shift + ;; + --rebuild-base) + REBUILD_BASE=true + shift + ;; + --keep) + KEEP=true + shift + ;; + --suite) + shift + if [ "$#" -eq 0 ]; then + error "--suite requires a value: all, shell, browser, or install-uninstall" + exit 1 + fi + SUITE="$1" + shift + ;; + *) + error "Unknown argument: $1" + exit 1 + ;; esac done +case "$SUITE" in + all|shell|browser|install-uninstall) ;; + *) + error "Invalid suite '$SUITE'. Expected: all, shell, browser, or install-uninstall" + exit 1 + ;; +esac + cleanup() { if [ "$KEEP" = false ]; then info "Cleaning up containers..." @@ -45,26 +88,90 @@ cleanup() { trap cleanup EXIT +compose() { + E2E_BASE_IMAGE="$BASE_IMAGE" \ + E2E_APP_IMAGE="$APP_IMAGE" \ + E2E_TEST_RUNNER_IMAGE="$TEST_RUNNER_IMAGE" \ + docker compose -f "$COMPOSE_FILE" "$@" +} + +check_panel_available() { + local retries=0 + local max_retries=60 + + until compose exec -T app bash -lc 'curl -fsS --max-time 10 http://127.0.0.1/auth/login >/dev/null || curl -fsS --max-time 10 http://127.0.0.1/ >/dev/null'; do + retries=$((retries + 1)) + if [ "$retries" -ge "$max_retries" ]; then + error "Panel did not become available within ${max_retries} attempts" + return 1 + fi + sleep 2 + done +} + info "Starting Notur E2E test suite" echo "" +if [ "$NO_BUILD" = true ]; then + REQUIRED_IMAGES=("$APP_IMAGE") + if [ "$SUITE" != "install-uninstall" ]; then + REQUIRED_IMAGES+=("$TEST_RUNNER_IMAGE") + fi + + for image in "${REQUIRED_IMAGES[@]}"; do + if ! docker image inspect "$image" >/dev/null 2>&1; then + error "Required E2E image is missing: ${image}" + echo "" + echo "Build images first or run without --no-build." + exit 1 + fi + done +fi + # Step 1: Build images if [ "$NO_BUILD" = false ]; then + if [ "$REBUILD_BASE" = true ]; then + info "Building reusable E2E base image: ${BASE_IMAGE}" + "${SCRIPT_DIR}/build-base.sh" + echo "" + elif docker image inspect "$BASE_IMAGE" >/dev/null 2>&1; then + ok "Reusable E2E base image is available: ${BASE_IMAGE}" + echo "" + else + error "Reusable E2E base image is missing: ${BASE_IMAGE}" + echo "" + echo "Build it once with:" + echo " bash docker/e2e/build-base.sh" + echo "" + echo "Or let this runner rebuild it explicitly with:" + echo " bash docker/e2e/run-e2e.sh --rebuild-base --suite ${SUITE}" + exit 1 + fi + info "Building Docker images..." - docker compose -f "$COMPOSE_FILE" build --no-cache + BUILD_SERVICES=(app) + if [ "$SUITE" != "install-uninstall" ]; then + BUILD_SERVICES+=(test-runner) + fi + + if [ "$NO_CACHE" = true ]; then + compose build --no-cache "${BUILD_SERVICES[@]}" + else + compose build "${BUILD_SERVICES[@]}" + fi echo "" fi # Step 2: Start database and app services info "Starting database and app services..." -docker compose -f "$COMPOSE_FILE" up -d db app +compose up -d db app echo "" # Step 3: Wait for MySQL to be ready info "Waiting for MySQL to be ready..." RETRIES=0 MAX_RETRIES=60 -until docker compose -f "$COMPOSE_FILE" exec -T db mysqladmin ping -h 127.0.0.1 -u root -pnotur_e2e --silent 2>/dev/null; do +until compose exec -T db mysqladmin ping -h 127.0.0.1 -u root -pnotur_e2e --silent 2>/dev/null; do RETRIES=$((RETRIES + 1)) if [ "$RETRIES" -ge "$MAX_RETRIES" ]; then error "MySQL did not become ready within ${MAX_RETRIES} attempts" @@ -75,83 +182,30 @@ done ok "MySQL is ready" echo "" -# Step 4: Generate app key and run panel migrations -info "Setting up Pterodactyl Panel..." -docker compose -f "$COMPOSE_FILE" exec -T app bash -c ' - cd /var/www/pterodactyl - php artisan key:generate --force - php artisan migrate --force --seed 2>&1 || php artisan migrate --force 2>&1 -' -ok "Panel migrations complete" -echo "" +if [ "$SUITE" = "install-uninstall" ]; then + info "Checking panel availability before Notur installation..." + check_panel_available + ok "Panel is available before Notur installation" + echo "" +fi -# Step 5: Run Notur installer -info "Running Notur installer..." -docker compose -f "$COMPOSE_FILE" exec -T app bash -c ' - cd /var/www/pterodactyl - - # Simulate composer require by copying Notur into vendor - mkdir -p vendor/notur/notur - cp -r /opt/notur/* vendor/notur/notur/ 2>/dev/null || true - - # Run the install script - bash vendor/notur/notur/installer/install.sh /var/www/pterodactyl 2>&1 || { - echo "Installer failed, attempting manual setup..." - # Manual fallback: run migrations and set up directories - php artisan migrate --force 2>&1 - mkdir -p notur/extensions - mkdir -p public/notur/extensions - echo "{\"extensions\":{}}" > notur/extensions.json - - # Build and copy bridge - if [ -d vendor/notur/notur/bridge ]; then - cd vendor/notur/notur/bridge - bun install --frozen-lockfile 2>/dev/null || true - bun run build 2>/dev/null || bunx webpack --mode production 2>/dev/null || true - cp dist/bridge.js /var/www/pterodactyl/public/notur/bridge.js 2>/dev/null || true - cd /var/www/pterodactyl - fi - } -' -ok "Notur installation complete" +# Step 4: Bootstrap a usable panel with Notur and deterministic E2E data +info "Bootstrapping the Pterodactyl + Notur E2E environment..." +compose exec -T app bash /opt/notur/docker/e2e/setup-panel.sh +ok "Panel bootstrap complete" echo "" -# Step 6: Install hello-world extension -info "Installing hello-world test extension..." -docker compose -f "$COMPOSE_FILE" exec -T app bash -c ' - cd /var/www/pterodactyl - - # Copy hello-world extension - mkdir -p notur/extensions/notur/hello-world - cp -r /opt/notur/examples/hello-world/* notur/extensions/notur/hello-world/ - - # Register it in extensions.json - cat > notur/extensions.json << EXTJSON -{ - "extensions": { - "notur/hello-world": { - "enabled": true, - "version": "1.0.0", - "installed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" - } - } -} -EXTJSON - - # Copy frontend bundle to public directory - mkdir -p public/notur/extensions/notur/hello-world - if [ -f notur/extensions/notur/hello-world/resources/frontend/dist/hello-world.js ]; then - cp notur/extensions/notur/hello-world/resources/frontend/dist/hello-world.js \ - public/notur/extensions/notur/hello-world/hello-world.js - fi -' -ok "Hello-world extension installed" -echo "" +if [ "$SUITE" = "install-uninstall" ]; then + info "Running destructive install/uninstall lifecycle suite..." + compose exec -T app bash /opt/notur/docker/e2e/install-uninstall.sh + ok "Install/uninstall lifecycle suite passed!" + exit 0 +fi -# Step 7: Run the test suite -info "Running E2E tests..." +# Step 5: Run the requested test suite +info "Running E2E suite: ${SUITE}" echo "" -docker compose -f "$COMPOSE_FILE" run --rm test-runner +compose run --rm -e E2E_SUITE="$SUITE" test-runner EXIT_CODE=$? echo "" diff --git a/docker/e2e/setup-panel.sh b/docker/e2e/setup-panel.sh new file mode 100644 index 00000000..ea8bd686 --- /dev/null +++ b/docker/e2e/setup-panel.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd /var/www/pterodactyl + +export NODE_OPTIONS="${NODE_OPTIONS:---openssl-legacy-provider}" + +composer config platform.php 8.2.30 --no-interaction +composer config audit.block-insecure false --no-interaction +composer config audit.ignore "PKSA-8qx3-n5y5-vvnd,PKSA-w7xr-vk7n-rstm" --no-interaction +composer config minimum-stability dev --no-interaction +composer config prefer-stable true --no-interaction +composer config repositories.notur '{"type":"path","url":"/opt/notur","options":{"symlink":false}}' --no-interaction + +if [ -f ".env" ]; then + set_env_var() { + local key="$1" + local value="$2" + + if grep -q "^${key}=" .env; then + sed -i "s|^${key}=.*|${key}=${value}|" .env + else + echo "${key}=${value}" >> .env + fi + } + + set_env_var "APP_URL" "${APP_URL}" + set_env_var "DB_CONNECTION" "mysql" + set_env_var "DB_HOST" "${DB_HOST}" + set_env_var "DB_PORT" "${DB_PORT}" + set_env_var "DB_DATABASE" "${DB_DATABASE}" + set_env_var "DB_USERNAME" "${DB_USERNAME}" + set_env_var "DB_PASSWORD" "${DB_PASSWORD}" + set_env_var "RECAPTCHA_ENABLED" "false" +fi + +if ! COMPOSER_ALLOW_SUPERUSER=1 COMPOSER_DISABLE_AUDIT=1 composer require notur/notur:@dev --no-interaction --with-all-dependencies 2>&1; then + echo "Composer require failed. Retrying once with verbose output..." + COMPOSER_ALLOW_SUPERUSER=1 COMPOSER_DISABLE_AUDIT=1 composer require notur/notur:@dev --no-interaction --with-all-dependencies -vvv + exit 1 +fi + +if ! grep -q '^APP_KEY=base64:' .env; then + php artisan key:generate --force --no-interaction +fi + +bash vendor/notur/notur/installer/install.sh /var/www/pterodactyl + +php artisan package:discover --ansi +php artisan config:clear +php artisan route:clear +php artisan cache:clear +php artisan view:clear +php artisan migrate --force +php artisan migrate --path=vendor/notur/notur/database/migrations --force + +rm -rf /tmp/hello-world-src /tmp/hello-world.notur +cp -R /opt/notur/examples/hello-world /tmp/hello-world-src +php -r 'require "vendor/autoload.php"; \Notur\Support\NoturArchive::pack("/tmp/hello-world-src", "/tmp/hello-world.notur");' +php artisan notur:install /tmp/hello-world.notur --force + +rm -rf notur/extensions/notur/full-extension +mkdir -p notur/extensions/notur +cp -R /opt/notur/examples/full-extension notur/extensions/notur/full-extension + +php /opt/notur/tests/E2E/bootstrap-state.php + +php artisan package:discover --ansi +php artisan config:clear +php artisan route:clear +php artisan cache:clear +php artisan view:clear + +TABLES=$(mysql --ssl=0 -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USERNAME}" -p"${DB_PASSWORD}" "${DB_DATABASE}" -N -B -e "SHOW TABLES LIKE 'notur_%';" 2>/tmp/notur-mysql.err || true) +for table in notur_extensions notur_migrations notur_settings; do + if ! echo "${TABLES}" | grep -q "${table}"; then + echo "E2E bootstrap failed: missing table ${table}" >&2 + [ -s /tmp/notur-mysql.err ] && cat /tmp/notur-mysql.err >&2 + exit 1 + fi +done + +if ! php artisan route:list 2>/tmp/notur-route.err | grep -q "notur/hello-world"; then + echo "E2E bootstrap failed: hello-world routes are not registered." >&2 + cat /tmp/notur-route.err >&2 || true + php artisan route:list | grep -i notur >&2 || true + exit 1 +fi + +if ! grep -R "notur::scripts" resources/views >/dev/null 2>&1; then + echo "E2E bootstrap failed: Notur scripts were not injected into panel views." >&2 + exit 1 +fi + +chown -R www-data:www-data storage bootstrap/cache notur public/notur 2>/dev/null || true diff --git a/package-lock.json b/package-lock.json index d3a24920..72b63b5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "sdk" ], "devDependencies": { + "@playwright/test": "^1.54.2", "@tailwindcss/cli": "^4.1.0", "@testing-library/react": "^12.1.5", "@types/react": "^16.14.0", @@ -864,6 +865,9 @@ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -885,6 +889,9 @@ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -906,6 +913,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -927,6 +937,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -948,6 +961,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -969,6 +985,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1058,6 +1077,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", @@ -1599,6 +1634,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1616,6 +1654,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1633,6 +1674,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1650,6 +1694,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1689,6 +1736,66 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.7.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.7.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", @@ -3977,6 +4084,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3998,6 +4108,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4019,6 +4132,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4040,6 +4156,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4478,6 +4597,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index b613b068..bdaf7fd5 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,11 @@ "build": "npm run build:tailwind && npm run build:bridge && npm run build:sdk", "dev:bridge": "npx webpack-cli --mode development --watch --config bridge/webpack.config.js", "dev:tailwind": "npx @tailwindcss/cli -i resources/tailwind/notur.css -o bridge/dist/tailwind.css --watch", - "test:frontend": "vitest run --config tests/Frontend/vitest.config.js" + "test:frontend": "vitest run --config tests/Frontend/vitest.config.js", + "test:e2e:browser": "npx playwright test --config=playwright.config.ts" }, "devDependencies": { + "@playwright/test": "^1.54.2", "@tailwindcss/cli": "^4.1.0", "@testing-library/react": "^12.1.5", "@types/react": "^16.14.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..891ad06b --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/E2E/browser', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: [['list']], + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? process.env.APP_URL ?? 'http://127.0.0.1:8080', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + launchOptions: { + args: ['--no-sandbox', '--disable-dev-shm-usage'], + }, + }, +}); diff --git a/resources/views/admin/extension-detail.blade.php b/resources/views/admin/extension-detail.blade.php index 16a30be0..94490d1a 100644 --- a/resources/views/admin/extension-detail.blade.php +++ b/resources/views/admin/extension-detail.blade.php @@ -17,14 +17,14 @@ @include('notur::admin.partials.brutalist-styles') @if(session('success')) -
+
{{ session('success') }}
@endif @if(session('error')) -
+
{{ session('error') }}
@@ -354,7 +354,7 @@
-
+ @csrf
@if(!empty($settingsSchema['description'])) @@ -383,7 +383,7 @@ $inputType = 'text'; } @endphp -
+