diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 5a052e5e..03810ee2 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -15,6 +15,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ mariadb-client \ vim \ zip \ + wget \ && apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* # Install PHP extensions @@ -36,9 +37,9 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ pdo \ pdo_mysql \ zip \ - && pecl install imagick-3.7.0 && docker-php-ext-enable imagick \ + # && pecl install imagick-3.7.0 && docker-php-ext-enable imagick \ # https://github.com/Imagick/imagick/issues/689 && pecl install -o -f redis && docker-php-ext-enable redis \ - && pecl install xdebug-3.3.1 && docker-php-ext-enable xdebug \ + && pecl install xdebug-3.4.0 && docker-php-ext-enable xdebug \ && apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* # Install composer diff --git a/.devcontainer/config/web/default.conf b/.devcontainer/config/web/default.conf index 08055d68..a9f9a3d5 100644 --- a/.devcontainer/config/web/default.conf +++ b/.devcontainer/config/web/default.conf @@ -1,6 +1,8 @@ server { listen 80 default; listen [::]:80; + listen 8080 default_server; + listen [::]:8080 default_server; # listen 443 ssl; # listen [::]:443 ssl ipv6only=on; @@ -11,7 +13,7 @@ server { add_header X-Content-Type-Options "nosniff"; charset utf-8; - server_name web.local web; + server_name _; root /roots/app/public; index index.php index.html; diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index c370c1a2..01d001ca 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -5,7 +5,7 @@ services: build: context: . dockerfile: Dockerfile - image: roots/dev-8.3 + image: roots/dev-8.4 extra_hosts: - 'host.docker.internal:host-gateway' environment: @@ -29,6 +29,9 @@ services: image: nginx:latest ports: - '${FORWARD_WEB_PORT:-8080}:80' + + expose: + - '8080' environment: - NGINX_ENTRYPOINT_WORKER_PROCESSES_AUTOTUNE=1 volumes: diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 00000000..cdfc6c2f --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,40 @@ +name: Integration + +on: [push, pull_request, workflow_dispatch] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Copy .env file + working-directory: .devcontainer + run: cp config/app/.env.example .env + + - name: Start dev container and test + uses: devcontainers/ci@v0.3 + with: + configFile: .devcontainer/devcontainer.json + runCmd: | + cd /roots/app + composer install + # ??? + composer remove roots/acorn + composer require roots/acorn --no-interaction + composer require --dev nunomaduro/collision + composer require --dev spatie/laravel-ignition + # ??? + while ! mysqladmin ping -h"database" --silent; do + sleep 1 + done + wp core install --url=http://web:8080 --title=Acorn --admin_user=admin --admin_password=admin --admin_email=admin@example.com --skip-email --allow-root + wp acorn optimize:clear + + cd /roots/acorn + composer install + composer run-script test tests/Integration/Routing + + - name: Verify routes + run: | + curl -s http://localhost:8080/test/ | grep "Howdy" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fb326f9c..27ae9b13 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,4 +47,4 @@ jobs: run: composer run-script lint - name: Execute the Composer test script - run: composer run-script test + run: composer test -- --exclude-group=integration diff --git a/tests/Integration/Routing/RoutingTest.php b/tests/Integration/Routing/RoutingTest.php new file mode 100644 index 00000000..cbbd915c --- /dev/null +++ b/tests/Integration/Routing/RoutingTest.php @@ -0,0 +1,81 @@ +group('integration'); + +expect()->extend('toHaveBodyClass', function (string $class) { + preg_match('/]*class=["\']([^"\']*)["\']/', $this->value, $matches); + expect($matches)->toHaveCount(2, 'No body tag with class attribute found'); + expect($matches[1])->toContain($class); + + return $this; +}); + +it('handles the test route', function () { + $client = new Client([ + 'verify' => false, + ]); + + $response = $client->request('GET', 'http://web:8080/test/'); + expect($response->getStatusCode())->toBe(200); + expect((string) $response->getBody())->toContain('Howdy'); +}); + +it('does not intercept WordPress admin routes', function () { + $client = new Client([ + 'verify' => false, + 'allow_redirects' => false, + ]); + + $response = $client->request('GET', 'http://web:8080/wp/wp-admin/'); + expect($response->getStatusCode())->toBe(302); + expect($response->getHeader('Location')[0])->toContain('wp-login.php'); +}); + +it('handles non-existent routes with 404', function () { + $client = new Client([ + 'verify' => false, + 'http_errors' => false, + ]); + + $response = $client->request('GET', 'http://web:8080/non-existent-'.time()); + expect($response->getStatusCode())->toBe(404); + expect((string) $response->getBody())->toContain('Page not found'); + expect((string) $response->getBody())->toHaveBodyClass('error404'); +}); + +it('handles default homepage', function () { + $client = new Client([ + 'verify' => false, + ]); + + $response = $client->request('GET', 'http://web:8080/'); + expect($response->getStatusCode())->toBe(200); + expect((string) $response->getBody())->toHaveBodyClass('home'); +}); + +it('handles WordPress REST API routes', function () { + $client = new Client([ + 'verify' => false, + ]); + + $response = $client->request('GET', 'http://web:8080/wp-json/'); + expect($response->getStatusCode())->toBe(200); + + $data = json_decode((string) $response->getBody(), true); + expect($data)->toHaveKey('name'); + expect($data)->toHaveKey('url'); +}); + +it('handles WordPress search route', function () { + $client = new Client([ + 'verify' => false, + ]); + + $response = $client->request('GET', 'http://web:8080/search/test/'); + expect($response->getStatusCode())->toBe(200); + expect((string) $response->getBody())->toHaveBodyClass('search'); +}); diff --git a/tests/Integration/Routing/RoutingTestCase.php b/tests/Integration/Routing/RoutingTestCase.php new file mode 100644 index 00000000..ff0ebe1b --- /dev/null +++ b/tests/Integration/Routing/RoutingTestCase.php @@ -0,0 +1,72 @@ +clearStubs(); + + // Ensure routes directory exists + if (! is_dir('/roots/app/public/routes')) { + mkdir('/roots/app/public/routes', 0777, true); + } + + // Create web.php routes file + $webRoutes = <<<'PHP' +group(function () { + Route::get('/test', fn() => 'Howdy')->name('test'); +}); +PHP; + + file_put_contents('/roots/app/public/routes/web.php', $webRoutes); + + // Ensure mu-plugins directory exists + if (! is_dir('/roots/app/public/content/mu-plugins')) { + mkdir('/roots/app/public/content/mu-plugins', 0777, true); + } + + // Create or update the Acorn boot mu-plugin + $bootPlugin = <<<'PHP' +withMiddleware(function (Middleware $middleware) { + // + }) + ->withExceptions(function (Exceptions $exceptions) { + // + }) + ->withRouting( + web: '/roots/app/public/routes/web.php' + ) + ->boot(); +}, 0); +PHP; + + file_put_contents('/roots/app/public/content/mu-plugins/01-acorn-boot.php', $bootPlugin); + } +}