Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Illuminate/Support/Facades/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* @method static void replacer(string $rule, \Closure|string $replacer)
* @method static void includeUnvalidatedArrayKeys()
* @method static void excludeUnvalidatedArrayKeys()
* @method static void fakeDnsLookups(bool $value = true)
* @method static void resolver(\Closure $resolver)
* @method static \Illuminate\Contracts\Translation\Translator getTranslator()
* @method static \Illuminate\Validation\PresenceVerifierInterface getPresenceVerifier()
Expand Down
14 changes: 12 additions & 2 deletions src/Illuminate/Validation/Concerns/ValidatesAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Illuminate\Support\Exceptions\MathException;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Str;
use Illuminate\Validation\FakeDnsGetRecordWrapper;
use Illuminate\Validation\Rules\Exists;
use Illuminate\Validation\Rules\Unique;
use Illuminate\Validation\ValidationData;
Expand Down Expand Up @@ -147,6 +148,16 @@ public function validateActiveUrl($attribute, $value)
*/
protected function getDnsRecords($hostname, $type)
{
if (static::$fakeDnsLookups) {
$hostname = rtrim($hostname, '.');

if (filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false || filter_var($hostname, FILTER_VALIDATE_IP) !== false) {
return false;
}

return [['host' => $hostname, 'class' => 'IN', 'ttl' => 60, 'type' => 'A', 'ip' => '127.0.0.1']];
}

return dns_get_record($hostname, $type);
}

Expand Down Expand Up @@ -959,14 +970,13 @@ public function validateEmail($attribute, $value, $parameters)
->unique()
->map(fn ($validation) => match (true) {
$validation === 'strict' => new NoRFCWarningsValidation(),
$validation === 'dns' => new DNSCheckValidation(),
$validation === 'dns' => new DNSCheckValidation(static::$fakeDnsLookups ? new FakeDnsGetRecordWrapper : null),
$validation === 'spoof' => new SpoofCheckValidation(),
$validation === 'filter' => new FilterEmailValidation(),
$validation === 'filter_unicode' => FilterEmailValidation::unicode(),
is_string($validation) && class_exists($validation) => $this->container->make($validation),
default => new RFCValidation(),
})
->values()
->all() ?: [new RFCValidation];

$emailValidator = Container::getInstance()->make(EmailValidator::class);
Expand Down
11 changes: 11 additions & 0 deletions src/Illuminate/Validation/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,17 @@ public function excludeUnvalidatedArrayKeys()
$this->excludeUnvalidatedArrayKeys = true;
}

/**
* Fake the DNS lookups performed by validation rules so they always succeed.
*
* @param bool $value
* @return void
*/
public function fakeDnsLookups($value = true)
{
Validator::fakeDnsLookups($value);
}

/**
* Set the Validator instance resolver.
*
Expand Down
21 changes: 21 additions & 0 deletions src/Illuminate/Validation/FakeDnsGetRecordWrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Illuminate\Validation;

use Egulias\EmailValidator\Validation\DNSGetRecordWrapper;
use Egulias\EmailValidator\Validation\DNSRecords;

class FakeDnsGetRecordWrapper extends DNSGetRecordWrapper
{
/**
* Get a synthetic DNS record for the given host.
*
* @param string $host
* @param int $type
* @return \Egulias\EmailValidator\Validation\DNSRecords
*/
public function getRecords(string $host, int $type): DNSRecords
{
return new DNSRecords([['type' => 'A']]);
}
}
19 changes: 19 additions & 0 deletions src/Illuminate/Validation/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,13 @@ class Validator implements ValidatorContract
*/
protected static $placeholderHash;

/**
* Indicates if DNS lookups performed by validation rules should be faked to always succeed.
*
* @var bool
*/
protected static $fakeDnsLookups = false;

/**
* The exception to throw upon failure.
*
Expand Down Expand Up @@ -1739,6 +1746,17 @@ protected static function decodeAttributeWithPlaceholder(string $attribute)
return str_replace('__dot__'.static::$placeholderHash, '\\.', $attribute);
}

/**
* Fake the DNS lookups performed by validation rules so they always succeed.
*
* @param bool $value
* @return void
*/
public static function fakeDnsLookups($value = true)
{
static::$fakeDnsLookups = $value;
}

/**
* Flush the validator's global state.
*
Expand All @@ -1747,6 +1765,7 @@ protected static function decodeAttributeWithPlaceholder(string $attribute)
public static function flushState()
{
static::$placeholderHash = null;
static::$fakeDnsLookups = false;
}

/**
Expand Down
15 changes: 15 additions & 0 deletions tests/Validation/ValidationFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@
use Illuminate\Validation\Validator;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use ReflectionProperty;

class ValidationFactoryTest extends TestCase
{
protected function tearDown(): void
{
Validator::flushState();

parent::tearDown();
}

public function testMakeMethodCreatesValidValidator()
{
$translator = m::mock(TranslatorInterface::class);
Expand Down Expand Up @@ -146,4 +154,11 @@ public function testSetContainer()

$this->assertSame($container, $factory->setContainer($container)->getContainer());
}

public function testFakeDnsLookupsDelegatesToTheValidator()
{
(new Factory(m::mock(TranslatorInterface::class)))->fakeDnsLookups();

$this->assertTrue((new ReflectionProperty(Validator::class, 'fakeDnsLookups'))->getValue());
}
}
82 changes: 70 additions & 12 deletions tests/Validation/ValidationValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,24 @@
use Illuminate\Validation\Validator;
use InvalidArgumentException;
use Mockery as m;
use Mockery\MockInterface;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\TestCase;
use ReflectionProperty;
use RuntimeException;
use stdClass;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;

class ValidationValidatorTest extends TestCase
{
protected function tearDown(): void
{
Validator::flushState();

parent::tearDown();
}

public function testNestedErrorMessagesAreRetrievedFromLocalArray()
{
$trans = $this->getIlluminateArrayTranslator();
Expand Down Expand Up @@ -5327,18 +5334,10 @@ public static function invalidUrls()
#[DataProvider('activeUrlDataProvider')]
public function testValidateActiveUrl($data, $outcome)
{
Validator::fakeDnsLookups();

$trans = $this->getIlluminateArrayTranslator();
$v = m::mock(
new Validator($trans, $data, ['x' => 'active_url']),
function (MockInterface $mock) {
$mock
->shouldAllowMockingProtectedMethods()
->shouldReceive('getDnsRecords')
->withAnyArgs()
->zeroOrMoreTimes()
->andReturn(['hit']);
}
);
$v = new Validator($trans, $data, ['x' => 'active_url']);
$this->assertEquals($outcome, $v->passes());
}

@SjorsO SjorsO Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mock was added 4 years ago in #43781 to solve flaky tests in Laravel's own test suite


Expand Down Expand Up @@ -5368,6 +5367,65 @@ public static function activeUrlDataProvider()
];
}

public function testValidateActiveUrlWithFakedDnsLookups()
{
Validator::fakeDnsLookups();

$trans = $this->getIlluminateArrayTranslator();

$v = new Validator($trans, ['x' => 'https://this-domain-does-not-exist.invalid'], ['x' => 'active_url']);
$this->assertTrue($v->passes());

$v = new Validator($trans, ['x' => 'aslsdlks'], ['x' => 'active_url']);
$this->assertFalse($v->passes());

$v = new Validator($trans, ['x' => ['not-a-string']], ['x' => 'active_url']);
$this->assertFalse($v->passes());

$v = new Validator($trans, ['x' => 'http://foo..com'], ['x' => 'active_url']);
$this->assertFalse($v->passes());

$v = new Validator($trans, ['x' => 'http://127.0.0.1'], ['x' => 'active_url']);
$this->assertFalse($v->passes());
}

public function testValidateEmailWithDnsCheckWithFakedDnsLookups()
{
Validator::fakeDnsLookups();

$trans = $this->getIlluminateArrayTranslator();

$v = new Validator($trans, ['x' => 'taylor@this-domain-does-not-exist.com'], ['x' => 'email:dns']);
$this->assertTrue($v->passes());

$v = new Validator($trans, ['x' => 'taylor@this-domain-does-not-exist.com'], ['x' => 'email:rfc,dns']);
$this->assertTrue($v->passes());

$v = new Validator($trans, ['x' => 'not-an-email'], ['x' => 'email:dns']);
$this->assertFalse($v->passes());

$v = new Validator($trans, ['x' => '.invalid@gmail.com'], ['x' => 'email:dns']);
$this->assertTrue($v->passes());

$v = new Validator($trans, ['x' => 'taylor@this-domain-does-not-exist.invalid'], ['x' => 'email:dns']);
$this->assertFalse($v->passes());
}

public function testFakedDnsLookupsCanBeToggledAndAreFlushed()
{
$property = new ReflectionProperty(Validator::class, 'fakeDnsLookups');

Validator::fakeDnsLookups();
$this->assertTrue($property->getValue());

Validator::fakeDnsLookups(false);
$this->assertFalse($property->getValue());

Validator::fakeDnsLookups();
Validator::flushState();
$this->assertFalse($property->getValue());
}

public function testValidateImage()
{
$trans = $this->getIlluminateArrayTranslator();
Expand Down
Loading