From 00069b9724b1f02b1ad7d91c31a8ef2087d6ce55 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 14 Oct 2026 13:46:22 +0800 Subject: [PATCH] test: run the customer controller tests against core-api 1.6.69 The suite never ran against the real core until it was tagged: PHP CI could not resolve ^1.6.69. Against it, four tests in CustomerControllerContractsTest fail: - codes sent by SMS are stored hashed and used up when accepted, so the test reads the plain code from the text the provider was handed instead of the row, gives the customer a phone on file so that text exists, checks the accepted code is marked used, and asks for a fresh closure code after the deletion that fails burned the first one - two local verification_codes tables lacked the uuid and status columns that consume() writes - verifyCode() builds a form request from a plain request and validates it; the bootstrap's FormRequest stub gains setContainer(), setRedirector() and validateResolved(), deferring to the request's own failedValidation() so the usual 422 response is attached, and illuminate/validation (a core dev dependency, not a runtime one) is now a dev dependency here too --- composer.json | 1 + scripts/pest-bootstrap.php | 6 ++- .../CustomerControllerContractsTest.php | 43 +++++++++++++++---- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 959d4a8d..29f846c5 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.34.1", + "illuminate/validation": "^10.0", "nunomaduro/collision": "^7.0", "pestphp/pest": "^2.33.2", "phpstan/phpstan": "^1.10.38", diff --git a/scripts/pest-bootstrap.php b/scripts/pest-bootstrap.php index a1355cc6..ffaffe7b 100644 --- a/scripts/pest-bootstrap.php +++ b/scripts/pest-bootstrap.php @@ -291,7 +291,11 @@ function now($tz = null): Illuminate\Support\Carbon } if (!class_exists('Illuminate\Foundation\Http\FormRequest') && class_exists('Illuminate\Http\Request')) { - eval('namespace Illuminate\Foundation\Http; class FormRequest extends \Illuminate\Http\Request { public function authorize() { return true; } public function rules() { return []; } public function responseWithErrors(\Illuminate\Contracts\Validation\Validator $validator) { return $validator; } }'); + // Enough of the real FormRequest for a controller to build one from a plain request and + // validate it the way the router does: setContainer(), setRedirector() and + // validateResolved(), which asks the bound validator factory and throws the usual + // ValidationException on failure. + eval('namespace Illuminate\Foundation\Http; class FormRequest extends \Illuminate\Http\Request { protected $container; public function authorize() { return true; } public function rules() { return []; } public function responseWithErrors(\Illuminate\Contracts\Validation\Validator $validator) { return $validator; } public function setContainer($container) { $this->container = $container; return $this; } public function setRedirector($redirector) { return $this; } public function validateResolved(): void { if (method_exists($this, "prepareForValidation")) { $this->prepareForValidation(); } if (!$this->authorize()) { throw new \Illuminate\Auth\Access\AuthorizationException(); } $factory = $this->container ? $this->container->make("validator") : null; if (!$factory) { return; } $validator = $factory->make($this->all(), $this->rules(), method_exists($this, "messages") ? $this->messages() : [], method_exists($this, "attributes") ? $this->attributes() : []); if ($validator->fails()) { if (method_exists($this, "failedValidation")) { $this->failedValidation($validator); } throw new \Illuminate\Validation\ValidationException($validator); } } }'); } if (!class_exists('Illuminate\Validation\Rules\RequiredIf')) { diff --git a/server/tests/Unit/Http/Controllers/CustomerControllerContractsTest.php b/server/tests/Unit/Http/Controllers/CustomerControllerContractsTest.php index 57ad5fd6..2bfe1640 100644 --- a/server/tests/Unit/Http/Controllers/CustomerControllerContractsTest.php +++ b/server/tests/Unit/Http/Controllers/CustomerControllerContractsTest.php @@ -483,6 +483,25 @@ public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode ], ]); bindCustomerNotificationDispatcher(); + // Codes sent by SMS are stored hashed (core-api 1.6.69), so the plain code is only in + // the text itself: record what the provider was given and read the code back from it. + $twilio = new class { + public array $messages = []; + + public function message(string $to, string $message, array $media = [], array $params = []): object + { + $this->messages[] = $message; + + return (object) ['sid' => 'sms_test']; + } + }; + app()->instance('twilio', $twilio); + Illuminate\Support\Facades\Facade::clearResolvedInstance('twilio'); + $lastSmsCode = function () use ($twilio): ?string { + $message = $twilio->messages ? $twilio->messages[array_key_last($twilio->messages)] : ''; + + return preg_match('/(\d{6})$/', trim($message), $match) ? $match[1] : null; + }; $sentry = new class { public array $exceptions = []; @@ -506,19 +525,21 @@ public function captureException(Throwable $exception): void ); $closureDeliveryFailure = $controller->startAccountClosure(Request::create('/customer/closure', 'POST')); $connection->statement('DROP TRIGGER fail_closure_verification_insert'); + // Core texts the code to the phone on file, so the customer needs one for the text + // (and its plain code) to exist at all. + $connection->table('users')->where('uuid', 'user_uuid')->update(['phone' => '+97699112200']); $phoneConflict = $controller->requestPhoneVerification(Request::create('/customer/phone', 'POST', [ 'phone' => '97699112233', ])); $invalidPhoneCode = $controller->verifyPhoneNumber(Request::create('/customer/phone/verify', 'POST', [ 'code' => 'invalid-code', ])); - $generatedCode = $connection->table('verification_codes') - ->where('for', 'storefront_verify_phone') - ->value('code'); + $generatedCode = $lastSmsCode(); $verifiedPhone = $controller->verifyPhoneNumber(Request::create('/customer/phone/verify', 'POST', [ 'code' => $generatedCode, ])); $closureStarted = $controller->startAccountClosure(Request::create('/customer/closure', 'POST')); + $closureCode = $lastSmsCode(); $connection->statement( "CREATE TRIGGER fail_phone_verification_insert BEFORE INSERT ON verification_codes BEGIN SELECT RAISE(ABORT, 'verification insert failed'); END" ); @@ -527,10 +548,6 @@ public function captureException(Throwable $exception): void 'phone' => '+97699112234', ])); $connection->statement('DROP TRIGGER fail_phone_verification_insert'); - $closureCode = $connection->table('verification_codes') - ->where('for', 'storefront_account_closure') - ->where('meta', 'like', '%+97699112233%') - ->value('code'); $invalidClosure = $controller->confirmAccountClosure(Request::create('/customer/closure/confirm', 'POST', [ 'code' => 'invalid-code', ])); @@ -541,8 +558,11 @@ public function captureException(Throwable $exception): void 'code' => $closureCode, ])); $connection->statement('DROP TRIGGER fail_customer_user_delete'); + // A code is used up by the check that accepted it, so the failed deletion burned this + // one and the customer asks for a new code to try again. + $controller->startAccountClosure(Request::create('/customer/closure', 'POST')); $closed = $controller->confirmAccountClosure(Request::create('/customer/closure/confirm', 'POST', [ - 'code' => $closureCode, + 'code' => $lastSmsCode(), ])); app()->offsetUnset(Illuminate\Contracts\Notifications\Dispatcher::class); app()->forgetInstance('sentry'); @@ -590,7 +610,7 @@ public function captureException(Throwable $exception): void ->and($verifiedPhone)->toBeInstanceOf(Fleetbase\Storefront\Http\Resources\Customer::class) ->and($connection->table('users')->where('uuid', 'user_uuid')->value('phone'))->toBe('+97699112233') ->and($connection->table('contacts')->where('uuid', $customerUuid)->value('phone'))->toBe('+97699112233') - ->and($connection->table('verification_codes')->where('code', $generatedCode)->value('deleted_at'))->not->toBeNull() + ->and($connection->table('verification_codes')->where('for', 'storefront_verify_phone')->value('status'))->toBe('used') ->and(json_encode($closureStarted->getData(true)))->toBe('{"status":"OK"}') ->and($phoneDeliveryFailure->getData(true))->toHaveKey('error') ->and($invalidClosure->getData(true))->toBe(['error' => 'Invalid verification code provided!']) @@ -904,8 +924,10 @@ public function message(string $to, string $message, array $media = [], array $p $schema->dropIfExists('verification_codes'); $schema->create('verification_codes', function ($table) { $table->increments('id'); + $table->string('uuid')->nullable(); $table->string('code')->nullable(); $table->string('for')->nullable(); + $table->string('status')->nullable(); $table->text('meta')->nullable(); $table->timestamp('expires_at')->nullable(); $table->timestamps(); @@ -936,8 +958,10 @@ public function message(string $to, string $message, array $media = [], array $p $schema->dropIfExists('verification_codes'); $schema->create('verification_codes', function ($table) { $table->increments('id'); + $table->string('uuid')->nullable(); $table->string('code')->nullable(); $table->string('for')->nullable(); + $table->string('status')->nullable(); $table->text('meta')->nullable(); $table->timestamp('expires_at')->nullable(); $table->timestamps(); @@ -1741,6 +1765,7 @@ public function sendNow($notifiables, $notification) $table->string('subject_uuid')->nullable(); $table->string('code')->nullable(); $table->string('for')->nullable(); + $table->string('status')->nullable(); $table->text('meta')->nullable(); $table->timestamp('expires_at')->nullable(); $table->timestamps();