Laravel, Docker, Pest and Browser Tests

Posted: 2025-12-16 11:44:20 by Alasdair Keyes

Direct Link | RSS feed


In recent years the PHP testing tool Pest has gained much traction. So much so, the Laravel testing documentation shows test suite examples in Pest as well as PHPUnit.

On the whole I'm happy with PHPUnit, however when performing browser-based tests with Laravel I have had no end of problems with getting Laravel's Dusk to work within Docker.

Pest has built-in browser support so I was interested in seeing how well it worked and how easily the browser tests were to set up.

Pest is a superset of PHPUnit so if you want to use Pest, you can still keep some of your existing PHPUnit tests and just use Pest where you want. Alternatively Pest does provide a tool to migrate your PHPUnit tests to Pest.

For the examples below, I'm assuming that you are running your Laravel/Vite/JS development environment in a single container or Virtual Machine.

Setting up Pest

The following is a migration from PHPUnit to Pest.

# Remove PHPUnit
composer remove phpunit/phpunit --dev

# Install Pest basic suite
composer require pestphp/pest --dev --with-all-dependencies
./vendor/bin/pest --init

Once this is done you can run either pest or Laravel's test tool

vendor/bin/pest
# or
./artisan test

Install Pest Browser Testing

composer require pestphp/pest-plugin-browser:^4.0 --dev
npm install playwright@latest
npx playwright install

Install Pest Migration plugin

As mentioned, Pest will continue to run your PHPUnit tests without issue. But if you wish to switch to Pest fully, you can install the Drift plugin

composer require pestphp/pest-plugin-drift --dev

Then convert your tests....

$ ./vendor/bin/pest --drift

  ✔✔✔✔✔✔✔✔✔✔✔.

   INFO  The [tests] directory has been migrated to PEST with 11 files changed.

After you have committed your changes, you can remove the drift plugin

composer remove pestphp/pest-plugin-drift --dev

A quick browser test

Now that we have Pest configured, it's worth checking it's working. Here's a quick test on the Laravel welcome page that comes with a fresh install.

<?php

test('Site gives 200 response', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});

test('Site produces correct output', function () {
    $page = visit('/')
    ->on()->desktop()
    ->inDarkMode();

    $page->assertSee('Laravel');
});

and run it...

$ vendor/bin/pest tests/Feature/PestExampleTest.php

   PASS  Tests\Feature\PestExampleTest
  ✓ Site gives 200 response                  0.13s  
  ✓ Site produces correct output             1.86s  

  Tests:    2 passed (2 assertions)
  Duration: 3.00s

Dockerfile

In development I run Laravel in the php:fpm-x.x Docker image with npm installed through apt for the Javascript/Vite functionality. The following snippet is the changes to make to Dockerfile.

# Existing docker config...

# Begin requirements for Pest/Playwright
RUN apt install -y libxdamage1 libgtk-3-0t64 libpangocairo-1.0-0 \
    libpango-1.0-0 libatk1.0-0t64 libcairo-gobject2 libcairo2 \
    libasound2t64 libgstreamer1.0-0 libgstreamer1.0-0 libgstreamer1.0-dev \
    libgtk-4-1 libgraphene-1.0-0 libxslt1.1 libwoff1 libvpx9 \
    libevent-2.1-7t64 libopus0 libgcrypt20 libgpg-error0 \
    libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 libgstreamer-gl1.0-0 \
    libgstreamer-plugins-bad1.0-0 libgstreamer-plugins-base1.0-0 libflite1 \
    libwebpdemux2 libjxl0.11 libavif16 libharfbuzz-icu0 libwebpmux3 \
    libenchant-2-2 libsecret-1-0 libhyphen0 libmanette-0.2-0 libx264-164 \
    libx264-dev libnspr4 libnss3
RUN docker-php-ext-install sockets
# End requirements for Pest/Playwright

RUN composer install

RUN npm install

# Begin install Playwright for Pest browser testing
RUN npx playwright install
# End install Playwright for Pest browser testing

# Existing docker config...


If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz

VirtualBox and KVM fighting for control on Debian Trixie

Posted: 2025-10-30 08:21:02 by Alasdair Keyes

Direct Link | RSS feed


I've recently updated my dev laptop to Debian Trixie. I waited a while as Debian hadn't ported VirtualBox to their Backports repository and Oracle hadn't released a version that would install correctly on Trixie.

Once Oracle finally released the .deb package for VirtualBox I rebuilt my machine only to get the following error upon starting a VM...

VT-x is being used by another hypervisor (VERR_VMX_IN_VMX_ROOT_MODE)

VirtualBox can't operate in VMX root mode. Please disable the KVM kernel extension, recompile your kernel and reboot.

The message is fairly self explanatory as to the problem. It looks like VirtualBox now doesn't like to share hypervisor duties.

VirtualBox is detecting that the KVM kernel modules are loaded and throwing the error. You can get VirtualBox VMs started by unloading the KVM Kernel modules

$ lsmod | grep kvm
kvm_intel             413696  0
kvm                  1396736  1 kvm_intel
irqbypass              12288  1 kvm

$ sudo rmmod kvm_intel kvm

$ lsmod | grep kvm
$

Note: If you run AMD processors, you'll need to adjust the kvm_intel to kvm_amd. Also Note: Loading and unloading is order-specific, you need to put the architecture specific kvm_xxx module before the standard kvm module.

Solutions

I have created a couple of aliases in my ~/.bashrc file to load the KVM modules if I want by running load-kvm-modules and unload them with unload-kvm-modules

alias unload-kvm-modules="sudo rmmod kvm_intel kvm"
alias load-kvm-modules="sudo modprobe kvm_intel kvm"

Using VirtualBox more than KVM

I use VirtualBox far more than KVM so the simple fix is that I stop the KVM modules being loaded at boot by blacklisting the KVM modules. I created a new file called /etc/modprobe.d/no-kvm.conf with the following contents.

blacklist kvm_intel
blacklist kvm

Using KVM more than VirtualBox

If you use KVM more than VirtualBox, the modules are already present at boot, you can just use KVM as normal. You don't need to do anything other than create the same aliases as above to load/unload the modules when required.


If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz

Sysadmin Appreciation Day 2025

Posted: 2025-07-25 08:05:10 by Alasdair Keyes

Direct Link | RSS feed


It's Sysadmin day 2025.

Give thanks to your sysadmin. If you are a sysadmin, have a beer.

https://sysadminday.com/


If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz

BASH one-liner to enable strict typing on all PHP files in a code base

Posted: 2025-07-05 13:02:41 by Alasdair Keyes

Direct Link | RSS feed


Often, if I'm updating an old code base, or have installed a new instance of Laravel, I want to enable PHP's strict typing.

This has to be enabled on each file and can be a bit of a pain so I knocked up a bash one-liner to do this quickly. It works recursively for any *.php file in a given directory.

find codebase/ -name *.php \
    | xargs -i grep -L '^\s*declare\s*(\s*strict_types\s*=\s*1\s*)\s*;.*$' '{}' \
    | xargs -i sed -i 's/\(<\?php\)/\1\n\ndeclare(strict_types=1);/' '{}'

Because blindly running random BASH commands from the internet is a bad idea, a quick break-down of each line....

  1. Search codebase/ directory for every .php file
  2. Check if the file contains the declare(strict_types=1); declaration
  3. If the strict types declaration is not present and the file has the <?php tag, add the strict type declaration after the <?php tag leaving one blank line between the two.

The command is idempotent, so re-running it will not add duplicate strict type declarations into your files.

A word of advice, be selective on which folder you execute it on. If you run it on the root of your code base and have a vendor/ folder, it will update all the PHP files within.


If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz

Blocking SMTP authentication failures with Fail2ban on Debian with Postfix, saslauthd

Posted: 2025-04-14 21:05:39 by Alasdair Keyes

Direct Link | RSS feed


I've been running a mail server for about 20 years. For years I was using Exim (https://www.exim.org/) but due to one-too-many high security CVEs I decided to switch to Postfix (https://www.postfix.org/) about 2 years ago.

One thing that any mail server admin will attest to is that your server is constantly probed by bots trying to access mailboxes and send emails.

I store mail logs for ten weeks and in that time over half a million failed login attempts.

# zgrep 'SASL LOGIN authentication failed' /var/log/mail.log* | wc -l
570813

Even with logrotate and compressed logs, this is starting to take up a fair amount of disk space, mainly through the un-compressed logs before logrotate starts zipping them up.

I decided to install fail2ban (https://github.com/fail2ban/fail2ban) to kurb this. fail2ban is a tool that scans logs in real-time for lines that indicate probes/attacks and ban the IPs using the firewall. In their terminology a filter scans the logs for lines indicating an attack and a jail takes action of some kind to block the IPs.

I used to use fail2ban years ago when I first started managing servers, but stopped using it well over a decade ago as I wasn't finding it too much help at the time.

After installation, noticed that my specific setup with Postfix using saslauthd for SMTP authentication is not captured by the standard fail2ban Postfix filter.

A standard line in my logfile looks like...

2025-04-10T15:49:08.591090+01:00 myhostname postfix/smtpd[55089]: warning: unknown[1.2.3.4]: SASL LOGIN authentication failed: authentication failure, sasl_username=probedemail@domain.com

Instead of trying to bodge the existing filter/jail, I decided I'd write my own to detect just these lines. With a bit of trial and error I came up with the following two config files...

/etc/fail2ban/filter.d/custom-postfix-auth.conf

# Fail2Ban filter for Postfix with SASL auth failures
#
#

[Definition]

failregex = warning:\s+\S+\[<HOST>\]: SASL [A-Z\-\d]+ authentication failed: authentication failure, sasl_username=.*$

ignoreregex =

journalmatch = _SYSTEMD_UNIT=postfix@-.service

datepattern = {^LN-BEG}Epoch
              {^LN-BEG}

# Author: Alasdair Keyes

/etc/fail2ban/jail.d/custom-postfix-auth.conf

[custom-postfix-auth]

enabled = true
maxretry = 30
bantime  = 86400
backend = systemd
findtime = 86400
banaction = iptables-allports

Once these were in place I ran a test, this shows that it does indeed detect 64383 entries in the current log file.

# fail2ban-regex systemd-journal /etc/fail2ban/filter.d/custom-postfix-auth.conf 

Running tests
=============

Use   failregex filter file : custom-postfix-auth, basedir: /etc/fail2ban
Use      datepattern : {^LN-BEG}Epoch
{^LN-BEG} : Default Detectors
Use         systemd journal
Use         encoding : UTF-8
Use    journal match : _SYSTEMD_UNIT=postfix@-.service


Results
=======

Failregex: 9562 total
|-  #) [# of hits] regular expression
|   1) [9562] warning:\s+\S+\[<HOST>\]: SASL [A-Z\-\d]+ authentication failed: authentication failure, sasl_username=.*$
`-

Ignoreregex: 0 total

Date template hits:

Lines: 44414 lines, 0 ignored, 9562 matched, 34852 missed
[processed in 2.96 sec]

Missed line(s): too many to print.  Use --print-all-missed to print all 34852 lines

I reloaded fail2ban systemctl reload fail2ban and can now see I'm starting to block those troublesome IPs.

# fail2ban-client status custom-postfix-auth
Status for the jail: custom-postfix-auth
|- Filter
|  |- Currently failed:	8
|  |- Total failed:	9
|  `- Journal matches:	_SYSTEMD_UNIT=postfix@-.service
`- Actions
   |- Currently banned:	3
   |- Total banned:	3
   `- Banned IP list:	1.2.3.4 40.50.60.70 6.7.8.9


If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz

Return of Diggnation

Posted: 2025-03-31 16:46:32 by Alasdair Keyes

Direct Link | RSS feed


I've just found out that Diggnation has been relaunched after it ended back in 2011. It's twelve episodes in and feels much like it was before... albeit with more grey hair.

Back in the mid-2000s when I first started working in tech, and browsing https://www.digg.com/; Diggnation was a regular watch on Saturday mornings with a cup of tea and a bowl of cereal. It was probably the first podcast I saw. This was in a time where podcasts weren't really a 'thing'.

Long may it continue.

Watch it at https://www.diggnation.show/.


If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz

Modernising and refactoring PHP with Rector

Posted: 2025-01-26 12:31:00 by Alasdair Keyes

Direct Link | RSS feed


I've recently heard some good things about the PHP tool Rector (https://getrector.com/). It provides automatic refactoring for your PHP code to increase code quality.

This website (https://www.akeyes.co.uk) is running on PHP 8.x, previously 7.x and with a quick check of my composer.json in Git, I can see that it was even running on PHP 5.6 when the site was built using the Slim Framework (https://www.slimframework.com/).

The earliest version of Laravel that I used was Laravel 5.5 on PHP 7.x, given that it's now on Laravel 11 on PHP 8.2, the code has progressed through many changes to the core language.

During this time, I've tried to keep the code updated. Implementing strict type checking on function parameters and return values. Keeping up with modern standards, but there will always bee a few bits I've missed. I thought this would be an ideal time to try Rector and see how it performs.

First, I checked out the latest version of the codebase on my dev machine and created a new branch.

I've tried to keep my test suite up-to-date which should give me some confidence that any changes made by Rector have not broken anything. We'll see how this confidence holds....

Test current code

$ ./artisan test --compact
.............................................

  Tests:    46 passed (140 assertions)
  Duration: 1.50s

$

Before any changes, everything is passing.

Install Rector

$ composer require rector/rector --dev
./composer.json has been updated
Running composer update rector/rector
...
...
...
Using version ^2.0 for rector/rector
$

Configure Rector

Rector is configured with a file called rector.php. If you don't have this, running Rector for the first time will detect this and offer to put in a basic config.

$ vendor/bin/rector 

 No "rector.php" config found. Should we generate it for you? [yes]:
 > yes

 [OK] The config is added now. Re-run command to make Rector do the work!

Taking a look at the file, it looks to have scanned for the common directories in my repo.

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths([
        __DIR__ . '/app',
        __DIR__ . '/bootstrap',
        __DIR__ . '/config',
        __DIR__ . '/lang',
        __DIR__ . '/public',
        __DIR__ . '/resources',
        __DIR__ . '/routes',
        __DIR__ . '/tests',
    ])
    // uncomment to reach your current PHP version
    // ->withPhpSets()
    ->withTypeCoverageLevel(0)
    ->withDeadCodeLevel(0)
    ->withCodeQualityLevel(0);

I'm going to keep it at the defaults for the time being, the only change I'll make is to uncomment ->withPhpSets(), this will cause Rector to check the PHP version in my composer.json file and test against that (Currently PHP 8.2).

Running rector again outputs it's suggested changes and has updated the PHP files.

$ vendor/bin/rector 
 136/136 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
12 files with changes
=====================

...
... (Remove for brevity)
...

 [OK] 12 files have been changed by Rector

$

Right off the bat, let's see if anything's broken...

$ ./artisan test --compact

.............................................

  Tests:    46 passed (140 assertions)
  Duration: 1.99s

$

So far, so good. Let's look at some of the changes it's suggested.

  1. Removing extra parameters.

Firstly the RemoveExtraParametersRector option has detected that the optional parameters for PHPUnit's assertStatus() can be removed.

-            $response->assertStatus(200, "Assert $uri returns a 200 response");
+            $response->assertStatus(200);

It looks like I added this second parameter to add some debug when I refactored the tests after upgrading to PHPUnit 10.x and hit some problems. It looks like I got confused with Perl's test suite which allows extra parameters. Checking the docs, the assertStatus() function does only accept a single parameter. +1 for Rector

  1. Processing files within bootstrap/cache.

As the path suggests, the files bootstrap/cache/services.php and bootstrap/cache/packages.php are cache files and will be deleted and recreated regularly by Laravel. There is no need to fix these. This is user error and can be resolved by adding the following to my rector.php config file.

    ->withSkip([
        __DIR__ . '/bootstrap/cache',
    ])
  1. Remove unused variable in try/catch.

This is a good catch and can be removed. I probably left it there during debug and didn't remove it. +2 for Rector.

         try {
             $record = $reader->city($ipAddress);
-        } catch (AddressNotFoundException $e) {
+        } catch (AddressNotFoundException) {
             //
         }
  1. Return values for closures.

The AddClosureVoidReturnTypeWhereNoReturnRector option found a few instances in the boilerplate Laravel code within routes/console.php where no return value was specified on closures. +3 for Rector.

-Artisan::command('inspire', function () {
+Artisan::command('inspire', function (): void {
     $this->comment(Inspiring::quote());
 })->purpose('Display an inspiring quote');
  1. Updating closure definition format.

The ClosureToArrowFunctionRector wishes me to convert anonymous functions to the newer format arrow => functions.

-Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
-    return (int) $user->id === (int) $id;
-});
+Broadcast::channel('App.Models.User.{id}', fn($user, $id) => (int) $user->id === (int) $id);

I actually prefer the old style anonymous function. Retaining the function () { ... } format helps my brain easily parse what the function is up to. The arrow format feels like a run-on sentence in my mind.

Although not an issue in this instance, arrow functions include variables from the parent scope and don't require the use keyword to pass variables e.g. function() use ($i) { ... }. Requiring use to bring variables into scope is a good thing in my mind, so I think I'll keep these functions as they are.

To stop this I will just add the following to my rector.php config file.

    ->withSkip([
        \Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector::class,
    ]);

Note: The documentation shows that you can use the non-qualified class name ClosureToArrowFunctionRector::class, however this give an error that the Rector rule does not exist, it may work for some rules, but for this one and possible others, you must give the fully qualified class name to the withSkip() function.

  1. Constructor property promotion.

I like this. The ClassPropertyAssignToConstructorPromotionRector realises that as I'm now running PHP 8, the old style boiler plate class code can be dispensed with and the cleaner constructor property promotion format can be used. This was the only class in my codebase that was affected, but on a large codebase with some big classes this could really slim down the code. +4 for Rector.

@@ @@

 class Repository
 {
-    /** @var string $folderPath */
-    private $folderPath;
-
-    /** @var string $gitBinary */
-    private $gitBinary;
-
     private const GIT_BINARY = 'git';

     private const DEFAULT_TRUNCATED_COMMIT_ID_LENGTH = 10;
@@ @@
      *
      * @param string $folderPath
      */
-    public function __construct(string $folderPath = '.', string $gitBinary = self::GIT_BINARY)
+    public function __construct(private string $folderPath = '.', private string $gitBinary = self::GIT_BINARY)
     {
-        $this->folderPath = $folderPath;
-        $this->gitBinary = $gitBinary;
     }

     /**

Review

Overall, I'm very impressed with Rector. It did nothing wrong, it broke no code and the only changes I had to make were due to my tastes rather than to fix a problem. If anything I wish my codebase was in a worse condition so that I could get more benefit from it.

It appears there's even a PHPStorm/Jetbrains plugin to automatically run Rector on your code base https://plugins.jetbrains.com/plugin/19718-rector-support.

If I took on an old codebase as part of a new role, I would definitely use Rector to incrementally modernise the code.


If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz

Best Albums of 2024

Posted: 2025-01-03 10:00:35 by Alasdair Keyes

Direct Link | RSS feed


My most enjoyed albums of 2024...

  1. Smashing Pumpkins - Aghori Mhori Mei
  2. Sheryl Crow - Evolution
  3. KMFDM - Let Go
  4. Static X - Project Regeneration Vol. II
  5. The Cure - Songs of a Lost World

Special mention to Squarepusher - Dostrotime.


If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz

Cloudee LTD now provides LineageOS installation

Posted: 2024-11-20 15:13:09 by Alasdair Keyes

Direct Link | RSS feed


After my blog post earlier this year about installing LineageOS onto an old OnePlus2 phone (https://www.akeyes.co.uk/blog/installing_lineageos_on_ancient_device); I've had some interest from people asking me to install LineageOS onto their older phones.

Installation of LineageOS is quite a technical endeavour and can be quite difficult for some devices. It seems that regular phone stores on the high street may offer hardware repairs but not Operating System installs.

As such I have expanded some of the services that I offer through my company Cloudee LTD (https://www.cloudee.co.uk/), to include installation of LineageOS onto old mobile devices.

So if you do not feel comfortable doing it yourself or have perhaps already tried and been unsuccessful, feel free to contact Cloudee and we'll see if we can help.


If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz

Sysadmin Appreciation Day 2024

Posted: 2024-07-26 10:50:42 by Alasdair Keyes

Direct Link | RSS feed


Once again, it's Sysadmin day.

Give thanks to your sysadmin. If you are a sysadmin, have a beer.

https://sysadminday.com/


If you found this useful, please feel free to donate via bitcoin to 1NT2ErDzLDBPB8CDLk6j1qUdT6FmxkMmNz

© Alasdair Keyes

IT Consultancy Services

I'm now available for IT consultancy and software development services - Cloudee LTD.



Happy user of Digital Ocean (Affiliate link)


Version:master-b651fea0f8


Validate HTML 5