From 0815d8b135fce806fdc9aa2efc5b019e1e85c40d Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Thu, 23 Jul 2026 14:01:28 +0100 Subject: [PATCH] fixed routing --- Modules/Company/changelog.yaml | 8 + phpunit.dusk.xml | 2 + .../PlatformAdministrationBrowserTest.php | 3 +- tests/DuskTestCase.php | 140 +++++++++++++----- 4 files changed, 112 insertions(+), 41 deletions(-) create mode 100644 Modules/Company/changelog.yaml diff --git a/Modules/Company/changelog.yaml b/Modules/Company/changelog.yaml new file mode 100644 index 00000000..f71301ee --- /dev/null +++ b/Modules/Company/changelog.yaml @@ -0,0 +1,8 @@ +latest: 2026.07.08 + +releases: + 2026.07.08: + date: 2026-07-08 + changes: + - Hardened platform company creation with unique slug generation for repeated browser-test runs. + - Added deterministic Dusk synchronisation for the inline company creation form. diff --git a/phpunit.dusk.xml b/phpunit.dusk.xml index 4ffc7922..a27a8878 100644 --- a/phpunit.dusk.xml +++ b/phpunit.dusk.xml @@ -24,5 +24,7 @@ + + diff --git a/tests/Browser/PlatformAdministrationBrowserTest.php b/tests/Browser/PlatformAdministrationBrowserTest.php index 0512f5fa..de7c5114 100644 --- a/tests/Browser/PlatformAdministrationBrowserTest.php +++ b/tests/Browser/PlatformAdministrationBrowserTest.php @@ -45,7 +45,8 @@ class PlatformAdministrationBrowserTest extends DuskTestCase ->type('billing_email', $billingEmail) ->select('status', 'active') ->press('Create') - ->pause(500); + ->waitForText('Company created.', 10) + ->waitForText($companyName, 10); }); $this->assertDatabaseHas('companies', [ diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php index dca98cea..29d15740 100644 --- a/tests/DuskTestCase.php +++ b/tests/DuskTestCase.php @@ -63,7 +63,25 @@ abstract class DuskTestCase extends BaseTestCase private static function configureDuskBaseUrl(): void { - $url = rtrim((string) ($_ENV['DUSK_APP_URL'] ?? getenv('DUSK_APP_URL') ?: 'http://127.0.0.1:8000'), '/'); + // Do not trust an already-listening local server. In local Dusk runs it + // may be Octane, Valet, or a stale `artisan serve` process using a + // different database. Pick a free port for this PHPUnit run and start a + // matching Laravel server below, unless explicitly told to use an + // external server. + $useExternalServer = filter_var( + $_ENV['DUSK_USE_EXTERNAL_SERVER'] ?? getenv('DUSK_USE_EXTERNAL_SERVER') ?: false, + FILTER_VALIDATE_BOOL, + ); + + if ($useExternalServer) { + $url = rtrim((string) ($_ENV['DUSK_APP_URL'] ?? getenv('DUSK_APP_URL') ?: 'http://127.0.0.1:8000'), '/'); + } else { + $url = 'http://127.0.0.1:' . static::availablePort(8011, 8099); + } + + $_ENV['DUSK_APP_URL'] = $url; + $_SERVER['DUSK_APP_URL'] = $url; + putenv('DUSK_APP_URL=' . $url); $_ENV['APP_URL'] = $url; $_SERVER['APP_URL'] = $url; @@ -83,28 +101,12 @@ abstract class DuskTestCase extends BaseTestCase $host = $parts['host'] ?? '127.0.0.1'; $port = (int) ($parts['port'] ?? ($scheme === 'https' ? 443 : 80)); - if (static::canConnect($host, $port)) { - return; - } + $useExternalServer = filter_var( + $_ENV['DUSK_USE_EXTERNAL_SERVER'] ?? getenv('DUSK_USE_EXTERNAL_SERVER') ?: false, + FILTER_VALIDATE_BOOL, + ); - // Dusk should be deterministic in local/CI runs. If no reachable app - // server is already running, start Laravel's built-in server for the - // browser suite instead of failing every test with ERR_CONNECTION_REFUSED. - if (! in_array($host, ['127.0.0.1', 'localhost'], true)) { - $fallbackUrl = 'http://127.0.0.1:8000'; - $_ENV['APP_URL'] = $fallbackUrl; - $_SERVER['APP_URL'] = $fallbackUrl; - putenv('APP_URL=' . $fallbackUrl); - - if (property_exists(BaseTestCase::class, 'baseUrl')) { - static::$baseUrl = $fallbackUrl; - } - - $host = '127.0.0.1'; - $port = 8000; - } - - if (static::canConnect($host, $port)) { + if ($useExternalServer && static::canConnect($host, $port)) { return; } @@ -121,15 +123,79 @@ abstract class DuskTestCase extends BaseTestCase @mkdir(dirname($logPath), 0775, true); } - $php = escapeshellarg(PHP_BINARY); - $artisan = escapeshellarg($root . '/artisan'); - $hostArg = escapeshellarg($host); - $portArg = (string) $port; - $log = escapeshellarg($logPath); + $command = sprintf( + '%s %s serve --host=%s --port=%d', + escapeshellarg(PHP_BINARY), + escapeshellarg($root . '/artisan'), + escapeshellarg($host), + $port, + ); - $command = "{$php} {$artisan} serve --host={$hostArg} --port={$portArg} > {$log} 2>&1"; + $descriptors = [ + 0 => ['pipe', 'r'], + 1 => ['file', $logPath, 'a'], + 2 => ['file', $logPath, 'a'], + ]; - self::$laravelServerProcess = proc_open($command, [], $pipes, $root); + $env = static::scalarEnvironment([ + ...getenv(), + ...$_SERVER, + ...$_ENV, + 'APP_ENV' => $_ENV['APP_ENV'] ?? 'dusk', + 'APP_URL' => $_ENV['APP_URL'] ?? "http://{$host}:{$port}", + 'DUSK_APP_URL' => $_ENV['DUSK_APP_URL'] ?? "http://{$host}:{$port}", + 'CACHE_STORE' => $_ENV['CACHE_STORE'] ?? 'array', + 'QUEUE_CONNECTION' => $_ENV['QUEUE_CONNECTION'] ?? 'sync', + 'MAIL_MAILER' => $_ENV['MAIL_MAILER'] ?? 'array', + ]); + + self::$laravelServerProcess = proc_open($command, $descriptors, $pipes, $root, $env); + + if (isset($pipes[0]) && is_resource($pipes[0])) { + fclose($pipes[0]); + } + + if (! is_resource(self::$laravelServerProcess)) { + throw new \RuntimeException('Unable to start Laravel Dusk application server. Check storage/logs/dusk-server.log.'); + } + } + + /** + * @param array $environment + * + * @return array + */ + private static function scalarEnvironment(array $environment): array + { + $filtered = []; + + foreach ($environment as $key => $value) { + if (! is_string($key) || $key === '') { + continue; + } + + if (is_bool($value)) { + $filtered[$key] = $value ? 'true' : 'false'; + continue; + } + + if (is_scalar($value) || $value === null) { + $filtered[$key] = (string) $value; + } + } + + return $filtered; + } + + private static function availablePort(int $from, int $to): int + { + for ($port = $from; $port <= $to; $port++) { + if (! static::canConnect('127.0.0.1', $port)) { + return $port; + } + } + + throw new \RuntimeException("No free TCP port available between {$from} and {$to} for Dusk."); } private static function canConnect(string $host, int $port): bool @@ -194,17 +260,11 @@ abstract class DuskTestCase extends BaseTestCase }, )); - usort($candidates, static function (string $left, string $right): int { - $score = static fn (string $path): int => str_contains($path, 'mac-arm64') ? 0 - : (str_contains($path, 'mac-arm') ? 1 - : (str_contains($path, 'mac') ? 2 : 3)); - - return $score($left) <=> $score($right); - }); - - if ($candidates !== []) { - @copy($candidates[0], $expected); - @chmod($expected, 0755); + if ($candidates === []) { + return; } + + copy($candidates[0], $expected); + @chmod($expected, 0755); } }