diff --git a/CHANGELOG.md b/CHANGELOG.md index 86b2ea85927b..432ee0620dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ # Release Notes for 11.x -## [Unreleased](https://github.com/laravel/framework/compare/v11.39.1...11.x) +## [Unreleased](https://github.com/laravel/framework/compare/v11.40.0...11.x) + +## [v11.40.0](https://github.com/laravel/framework/compare/v11.39.1...v11.40.0) - 2025-01-24 + +* draft: fix: Don't release lock for ShouldBeUniqueUntilProcessing Job that gets released by [@mathiasgrimm](https://github.com/mathiasgrimm) in https://github.com/laravel/framework/pull/54261 +* [11.x] Add Laravel Pint by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/53835 +* Add self to HasCollection type param in Model by [@thena-seer-sfg](https://github.com/thena-seer-sfg) in https://github.com/laravel/framework/pull/54311 +* [11.x] Add pending attributes by [@tontonsb](https://github.com/tontonsb) in https://github.com/laravel/framework/pull/53720 +* fix: `schedule:test` on commands using runInBackground by [@dallyger](https://github.com/dallyger) in https://github.com/laravel/framework/pull/54321 +* [11.x] Helper methods to dump responses of the Laravel HTTP client by [@morrislaptop](https://github.com/morrislaptop) in https://github.com/laravel/framework/pull/54317 +* Add support for cursor editor in ResolvesDumpSource by [@tuxfamily](https://github.com/tuxfamily) in https://github.com/laravel/framework/pull/54318 +* [11.x] Add Customizable Date Validation Rule with Flexible Date Constraints by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/53465 +* [11.x] start syncing StyleCI rules to Pint by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/54326 +* [11.x] apply our new Pint rule to the `/tests` directory by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/54325 +* fix(Collection::pop()): count < 1 by [@artumi-richard](https://github.com/artumi-richard) in https://github.com/laravel/framework/pull/54340 +* Patch CVE-2025-22145 in nesbot/carbon package by [@dennis-koster](https://github.com/dennis-koster) in https://github.com/laravel/framework/pull/54335 +* [11.x] Prevent unintended serialization and compression by [@JeppeKnockaert](https://github.com/JeppeKnockaert) in https://github.com/laravel/framework/pull/54337 +* [11.x] Pass collection of models to `whereMorphedTo` / `whereNotMorphedTo` by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/54324 ## [v11.39.1](https://github.com/laravel/framework/compare/v11.39.0...v11.39.1) - 2025-01-22 diff --git a/pint.json b/pint.json index 453fd4b31462..c36f1f8282e2 100644 --- a/pint.json +++ b/pint.json @@ -1,7 +1,11 @@ { "preset": "empty", "rules": { + "align_multiline_comment": true, "array_indentation": true, + "array_syntax": { + "syntax": "short" + }, "binary_operator_spaces": { "default": "single_space" }, @@ -13,7 +17,18 @@ ] }, "blank_line_between_import_groups": true, + "blank_lines_before_namespace": true, + "braces_position": { + "control_structures_opening_brace": "same_line", + "functions_opening_brace": "next_line_unless_newline_at_signature_end", + "anonymous_functions_opening_brace": "same_line", + "classes_opening_brace": "next_line_unless_newline_at_signature_end", + "anonymous_classes_opening_brace": "next_line_unless_newline_at_signature_end", + "allow_single_line_empty_anonymous_classes": false, + "allow_single_line_anonymous_functions": false + }, "cast_spaces": true, + "class_definition": true, "class_reference_name_casing": true, "clean_namespace": true, "compact_nullable_type_declaration": true, @@ -21,6 +36,7 @@ "constant_case": { "case": "lower" }, + "control_structure_braces": true, "declare_equal_normalize": true, "elseif": true, "encoding": true, @@ -28,9 +44,16 @@ "function_declaration": true, "heredoc_to_nowdoc": true, "include": true, + "increment_style": { + "style": "post" + }, "indentation_type": true, "integer_literal_case": true, + "lambda_not_used_import": true, "line_ending": true, + "list_syntax": { + "syntax": "short" + }, "lowercase_cast": true, "lowercase_keywords": true, "lowercase_static_reference": true, @@ -40,6 +63,9 @@ "on_multiline": "ignore" }, "method_chaining_indentation": true, + "multiline_whitespace_before_semicolons": { + "strategy": "no_multi_line" + }, "native_function_casing": true, "native_type_declaration_casing": true, "no_alternative_syntax": true, @@ -58,11 +84,21 @@ }, "no_leading_import_slash": true, "no_leading_namespace_whitespace": true, + "no_mixed_echo_print": { + "use": "echo" + }, "no_multiline_whitespace_around_double_arrow": true, "no_short_bool_cast": true, "no_singleline_whitespace_before_semicolons": true, "no_space_around_double_colon": true, + "no_spaces_around_offset": { + "positions": [ + "inside", + "outside" + ] + }, "no_spaces_after_function_name": true, + "no_trailing_comma_in_singleline": true, "no_trailing_whitespace": true, "no_trailing_whitespace_in_comment": true, "no_unneeded_braces": true, @@ -85,13 +121,55 @@ "function" ] }, + "phpdoc_align": { + "align": "left", + "spacing": { + "param": 2 + } + }, "phpdoc_indent": true, "phpdoc_inline_tag_normalizer": true, "phpdoc_no_access": true, "phpdoc_no_package": true, "phpdoc_no_useless_inheritdoc": true, + "phpdoc_order": { + "order": [ + "param", + "return", + "throws" + ] + }, "phpdoc_return_self_reference": true, "phpdoc_scalar": true, + "phpdoc_separation": { + "groups": [ + [ + "deprecated", + "link", + "see", + "since" + ], + [ + "author", + "copyright", + "license" + ], + [ + "category", + "package", + "subpackage" + ], + [ + "property", + "property-read", + "property-write" + ], + [ + "param", + "return" + ] + ] + }, "phpdoc_single_line_var_spacing": true, "phpdoc_summary": true, "phpdoc_trim": true, @@ -103,15 +181,18 @@ "short_scalar_cast": true, "single_blank_line_at_eof": true, "single_class_element_per_statement": true, + "single_import_per_statement": true, "single_line_after_imports": true, "single_line_comment_style": true, "single_quote": true, "space_after_semicolon": true, + "spaces_inside_parentheses": true, "standardize_not_equals": true, "switch_case_semicolon_to_colon": true, "switch_case_space": true, "switch_continue_to_break": true, "ternary_operator_spaces": true, + "trailing_comma_in_multiline": true, "trim_array_spaces": true, "type_declaration_spaces": true, "types_spaces": true, diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index 3e559f296b54..4aede1ae6767 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -261,6 +261,8 @@ public function once(array $credentials = []) return true; } + $this->fireFailedEvent($this->lastAttempted, $credentials); + return false; } diff --git a/src/Illuminate/Cache/RateLimiter.php b/src/Illuminate/Cache/RateLimiter.php index 8d8afe30a14c..12f76fee6e2a 100644 --- a/src/Illuminate/Cache/RateLimiter.php +++ b/src/Illuminate/Cache/RateLimiter.php @@ -4,6 +4,7 @@ use Closure; use Illuminate\Contracts\Cache\Repository as Cache; +use Illuminate\Redis\Connections\PhpRedisConnection; use Illuminate\Support\Collection; use Illuminate\Support\InteractsWithTime; @@ -165,12 +166,16 @@ public function increment($key, $decaySeconds = 60, $amount = 1) $key.':timer', $this->availableAt($decaySeconds), $decaySeconds ); - $added = $this->cache->add($key, 0, $decaySeconds); + $added = $this->withoutSerializationOrCompression( + fn () => $this->cache->add($key, 0, $decaySeconds) + ); $hits = (int) $this->cache->increment($key, $amount); if (! $added && $hits == 1) { - $this->cache->put($key, 1, $decaySeconds); + $this->withoutSerializationOrCompression( + fn () => $this->cache->put($key, 1, $decaySeconds) + ); } return $hits; @@ -199,7 +204,7 @@ public function attempts($key) { $key = $this->cleanRateLimiterKey($key); - return $this->cache->get($key, 0); + return $this->withoutSerializationOrCompression(fn () => $this->cache->get($key, 0)); } /** @@ -282,6 +287,29 @@ public function cleanRateLimiterKey($key) return preg_replace('/&([a-z])[a-z]+;/i', '$1', htmlentities($key)); } + /** + * Execute the given callback without serialization or compression when applicable. + * + * @param callable $callback + * @return mixed + */ + protected function withoutSerializationOrCompression(callable $callback) + { + $store = $this->cache->getStore(); + + if (! $store instanceof RedisStore) { + return $callback(); + } + + $connection = $store->connection(); + + if (! $connection instanceof PhpRedisConnection) { + return $callback(); + } + + return $connection->withoutSerializationOrCompression($callback); + } + /** * Resolve the rate limiter name. * diff --git a/src/Illuminate/Cache/RedisStore.php b/src/Illuminate/Cache/RedisStore.php index ebdd0febbd82..39f1a0777ea0 100755 --- a/src/Illuminate/Cache/RedisStore.php +++ b/src/Illuminate/Cache/RedisStore.php @@ -432,10 +432,6 @@ public function setPrefix($prefix) protected function pack($value, $connection) { if ($connection instanceof PhpRedisConnection) { - if ($this->shouldBeStoredWithoutSerialization($value)) { - return $value; - } - if ($connection->serialized()) { return $connection->pack([$value])[0]; } diff --git a/src/Illuminate/Database/Connectors/PostgresConnector.php b/src/Illuminate/Database/Connectors/PostgresConnector.php index ef806dd9ab2f..31d2ff4732ca 100755 --- a/src/Illuminate/Database/Connectors/PostgresConnector.php +++ b/src/Illuminate/Database/Connectors/PostgresConnector.php @@ -50,65 +50,6 @@ public function connect(array $config) return $connection; } - /** - * Set the connection transaction isolation level. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureIsolationLevel($connection, array $config) - { - if (isset($config['isolation_level'])) { - $connection->prepare("set session characteristics as transaction isolation level {$config['isolation_level']}")->execute(); - } - } - - /** - * Set the timezone on the connection. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureTimezone($connection, array $config) - { - if (isset($config['timezone'])) { - $timezone = $config['timezone']; - - $connection->prepare("set time zone '{$timezone}'")->execute(); - } - } - - /** - * Set the "search_path" on the database connection. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureSearchPath($connection, $config) - { - if (isset($config['search_path']) || isset($config['schema'])) { - $searchPath = $this->quoteSearchPath( - $this->parseSearchPath($config['search_path'] ?? $config['schema']) - ); - - $connection->prepare("set search_path to {$searchPath}")->execute(); - } - } - - /** - * Format the search path for the DSN. - * - * @param array $searchPath - * @return string - */ - protected function quoteSearchPath($searchPath) - { - return count($searchPath) === 1 ? '"'.$searchPath[0].'"' : '"'.implode('", "', $searchPath).'"'; - } - /** * Create a DSN string from a configuration. * @@ -171,6 +112,65 @@ protected function addSslOptions($dsn, array $config) return $dsn; } + /** + * Set the connection transaction isolation level. + * + * @param \PDO $connection + * @param array $config + * @return void + */ + protected function configureIsolationLevel($connection, array $config) + { + if (isset($config['isolation_level'])) { + $connection->prepare("set session characteristics as transaction isolation level {$config['isolation_level']}")->execute(); + } + } + + /** + * Set the timezone on the connection. + * + * @param \PDO $connection + * @param array $config + * @return void + */ + protected function configureTimezone($connection, array $config) + { + if (isset($config['timezone'])) { + $timezone = $config['timezone']; + + $connection->prepare("set time zone '{$timezone}'")->execute(); + } + } + + /** + * Set the "search_path" on the database connection. + * + * @param \PDO $connection + * @param array $config + * @return void + */ + protected function configureSearchPath($connection, $config) + { + if (isset($config['search_path']) || isset($config['schema'])) { + $searchPath = $this->quoteSearchPath( + $this->parseSearchPath($config['search_path'] ?? $config['schema']) + ); + + $connection->prepare("set search_path to {$searchPath}")->execute(); + } + } + + /** + * Format the search path for the DSN. + * + * @param array $searchPath + * @return string + */ + protected function quoteSearchPath($searchPath) + { + return count($searchPath) === 1 ? '"'.$searchPath[0].'"' : '"'.implode('", "', $searchPath).'"'; + } + /** * Configure the synchronous_commit setting. * @@ -180,10 +180,8 @@ protected function addSslOptions($dsn, array $config) */ protected function configureSynchronousCommit($connection, array $config) { - if (! isset($config['synchronous_commit'])) { - return; + if (isset($config['synchronous_commit'])) { + $connection->prepare("set synchronous_commit to '{$config['synchronous_commit']}'")->execute(); } - - $connection->prepare("set synchronous_commit to '{$config['synchronous_commit']}'")->execute(); } } diff --git a/src/Illuminate/Database/Console/Migrations/MigrateCommand.php b/src/Illuminate/Database/Console/Migrations/MigrateCommand.php index 2db166a88937..6345985bf06f 100755 --- a/src/Illuminate/Database/Console/Migrations/MigrateCommand.php +++ b/src/Illuminate/Database/Console/Migrations/MigrateCommand.php @@ -9,6 +9,7 @@ use Illuminate\Database\Migrations\Migrator; use Illuminate\Database\SQLiteDatabaseDoesNotExistException; use Illuminate\Database\SqlServerConnection; +use Illuminate\Support\Str; use PDOException; use RuntimeException; use Symfony\Component\Console\Attribute\AsCommand; @@ -163,26 +164,41 @@ protected function repositoryExists() { return retry(2, fn () => $this->migrator->repositoryExists(), 0, function ($e) { try { - if ($e->getPrevious() instanceof SQLiteDatabaseDoesNotExistException) { - return $this->createMissingSqliteDatabase($e->getPrevious()->path); - } - - $connection = $this->migrator->resolveConnection($this->option('database')); - - if ( - $e->getPrevious() instanceof PDOException && - $e->getPrevious()->getCode() === 1049 && - in_array($connection->getDriverName(), ['mysql', 'mariadb'])) { - return $this->createMissingMysqlDatabase($connection); - } - - return false; + return $this->handleMissingDatabase($e->getPrevious()); } catch (Throwable) { return false; } }); } + /** + * Attempt to create the database if it is missing. + * + * @param \Throwable $e + * @return bool + */ + protected function handleMissingDatabase(Throwable $e) + { + if ($e instanceof SQLiteDatabaseDoesNotExistException) { + return $this->createMissingSqliteDatabase($e->path); + } + + $connection = $this->migrator->resolveConnection($this->option('database')); + + if (! $e instanceof PDOException) { + return false; + } + + if (($e->getCode() === 1049 && in_array($connection->getDriverName(), ['mysql', 'mariadb'])) || + (($e->errorInfo[0] ?? null) == '08006' && + $connection->getDriverName() == 'pgsql' && + Str::contains($e->getMessage(), '"'.$connection->getDatabaseName().'"'))) { + return $this->createMissingMySqlOrPgsqlDatabase($connection); + } + + return false; + } + /** * Create a missing SQLite database. * @@ -213,13 +229,14 @@ protected function createMissingSqliteDatabase($path) } /** - * Create a missing MySQL database. + * Create a missing MySQL or Postgres database. * + * @param \Illuminate\Database\Connection $connection * @return bool * * @throws \RuntimeException */ - protected function createMissingMysqlDatabase($connection) + protected function createMissingMySqlOrPgsqlDatabase($connection) { if ($this->laravel['config']->get("database.connections.{$connection->getName()}.database") !== $connection->getDatabaseName()) { return false; @@ -238,15 +255,25 @@ protected function createMissingMysqlDatabase($connection) throw new RuntimeException('Database was not created. Aborting migration.'); } } - try { - $this->laravel['config']->set("database.connections.{$connection->getName()}.database", null); + $this->laravel['config']->set( + "database.connections.{$connection->getName()}.database", + match ($connection->getDriverName()) { + 'mysql', 'mariadb' => null, + 'pgsql' => 'postgres', + }, + ); $this->laravel['db']->purge(); $freshConnection = $this->migrator->resolveConnection($this->option('database')); - return tap($freshConnection->unprepared("CREATE DATABASE IF NOT EXISTS `{$connection->getDatabaseName()}`"), function () { + return tap($freshConnection->unprepared( + match ($connection->getDriverName()) { + 'mysql', 'mariadb' => "CREATE DATABASE IF NOT EXISTS `{$connection->getDatabaseName()}`", + 'pgsql' => 'CREATE DATABASE "'.$connection->getDatabaseName().'"', + } + ), function () { $this->laravel['db']->purge(); }); } finally { diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index cd91699f9f7e..bbb5165a119e 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -45,7 +45,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '11.40.0'; + const VERSION = '11.41.0'; /** * The base path for the Laravel installation. diff --git a/src/Illuminate/Redis/Connections/PacksPhpRedisValues.php b/src/Illuminate/Redis/Connections/PacksPhpRedisValues.php index cc7f31094883..526a764ede89 100644 --- a/src/Illuminate/Redis/Connections/PacksPhpRedisValues.php +++ b/src/Illuminate/Redis/Connections/PacksPhpRedisValues.php @@ -82,6 +82,43 @@ public function pack(array $values): array return array_map($processor, $values); } + /** + * Execute the given callback without serialization or compression when applicable. + * + * @param callable $callback + * @return mixed + */ + public function withoutSerializationOrCompression(callable $callback) + { + $client = $this->client; + + $oldSerializer = null; + + if ($this->serialized()) { + $oldSerializer = $client->getOption($client::OPT_SERIALIZER); + $client->setOption($client::OPT_SERIALIZER, $client::SERIALIZER_NONE); + } + + $oldCompressor = null; + + if ($this->compressed()) { + $oldCompressor = $client->getOption($client::OPT_COMPRESSION); + $client->setOption($client::OPT_COMPRESSION, $client::COMPRESSION_NONE); + } + + try { + return $callback(); + } finally { + if ($oldSerializer !== null) { + $client->setOption($client::OPT_SERIALIZER, $oldSerializer); + } + + if ($oldCompressor !== null) { + $client->setOption($client::OPT_COMPRESSION, $oldCompressor); + } + } + } + /** * Determine if serialization is enabled. * diff --git a/src/Illuminate/Session/Store.php b/src/Illuminate/Session/Store.php index c57d260d963c..5c5d447a3413 100755 --- a/src/Illuminate/Session/Store.php +++ b/src/Illuminate/Session/Store.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Session\Session; use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Date; use Illuminate\Support\MessageBag; use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; @@ -785,7 +786,7 @@ public function setPreviousUrl($url) */ public function passwordConfirmed() { - $this->put('auth.password_confirmed_at', time()); + $this->put('auth.password_confirmed_at', Date::now()->unix()); } /** diff --git a/src/Illuminate/Support/Facades/Lang.php b/src/Illuminate/Support/Facades/Lang.php index a341b5fab640..7dc08da5197d 100755 --- a/src/Illuminate/Support/Facades/Lang.php +++ b/src/Illuminate/Support/Facades/Lang.php @@ -11,6 +11,7 @@ * @method static void load(string $namespace, string $group, string $locale) * @method static \Illuminate\Translation\Translator handleMissingKeysUsing(callable|null $callback) * @method static void addNamespace(string $namespace, string $hint) + * @method static void addPath(string $path) * @method static void addJsonPath(string $path) * @method static array parseKey(string $key) * @method static void determineLocalesUsing(callable $callback) diff --git a/src/Illuminate/Support/composer.json b/src/Illuminate/Support/composer.json index 20fc5edc8253..286a90b0a76e 100644 --- a/src/Illuminate/Support/composer.json +++ b/src/Illuminate/Support/composer.json @@ -23,7 +23,7 @@ "illuminate/conditionable": "^11.0", "illuminate/contracts": "^11.0", "illuminate/macroable": "^11.0", - "nesbot/carbon": "^2.72.2|^3.4", + "nesbot/carbon": "^2.72.6|^3.8.4", "voku/portable-ascii": "^2.0.2" }, "conflict": { diff --git a/src/Illuminate/Testing/TestComponent.php b/src/Illuminate/Testing/TestComponent.php index 0965789b3854..31b3831a316e 100644 --- a/src/Illuminate/Testing/TestComponent.php +++ b/src/Illuminate/Testing/TestComponent.php @@ -2,12 +2,17 @@ namespace Illuminate\Testing; +use Illuminate\Support\Traits\Macroable; use Illuminate\Testing\Assert as PHPUnit; use Illuminate\Testing\Constraints\SeeInOrder; use Stringable; class TestComponent implements Stringable { + use Macroable { + __call as macroCall; + } + /** * The original component. * @@ -162,6 +167,10 @@ public function __get($attribute) */ public function __call($method, $parameters) { + if (static::hasMacro($method)) { + return $this->macroCall($method, $parameters); + } + return $this->component->{$method}(...$parameters); } } diff --git a/src/Illuminate/Translation/Translator.php b/src/Illuminate/Translation/Translator.php index 1f82b4bd5c3f..a24f1d6f542f 100755 --- a/src/Illuminate/Translation/Translator.php +++ b/src/Illuminate/Translation/Translator.php @@ -404,6 +404,17 @@ public function addNamespace($namespace, $hint) $this->loader->addNamespace($namespace, $hint); } + /** + * Add a new path to the loader. + * + * @param string $path + * @return void + */ + public function addPath($path) + { + $this->loader->addPath($path); + } + /** * Add a new JSON path to the loader. * diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 91ea33338629..223181af9766 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -299,6 +299,8 @@ protected function checkDateTimeOrder($format, $first, $second, $operator) { $firstDate = $this->getDateTimeWithOptionalFormat($format, $first); + $format = $this->getDateFormat($second) ?: $format; + if (! $secondDate = $this->getDateTimeWithOptionalFormat($format, $second)) { if (is_null($second = $this->getValue($second))) { return true; diff --git a/src/Illuminate/Validation/ValidationRuleParser.php b/src/Illuminate/Validation/ValidationRuleParser.php index 0119b6d3a803..2d8f5026f091 100644 --- a/src/Illuminate/Validation/ValidationRuleParser.php +++ b/src/Illuminate/Validation/ValidationRuleParser.php @@ -9,6 +9,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use Illuminate\Validation\Rules\Date; use Illuminate\Validation\Rules\Exists; use Illuminate\Validation\Rules\Unique; @@ -95,11 +96,17 @@ protected function explodeExplicitRule($rule, $attribute) return Arr::wrap($this->prepareRule($rule, $attribute)); } - return array_map( - [$this, 'prepareRule'], - $rule, - array_fill((int) array_key_first($rule), count($rule), $attribute) - ); + $rules = []; + + foreach ($rule as $value) { + if ($value instanceof Date) { + $rules = array_merge($rules, explode('|', (string) $value)); + } else { + $rules[] = $this->prepareRule($value, $attribute); + } + } + + return $rules; } /** diff --git a/tests/Cache/CacheRateLimiterTest.php b/tests/Cache/CacheRateLimiterTest.php index b1cbec87598a..a4e7d5e2a099 100644 --- a/tests/Cache/CacheRateLimiterTest.php +++ b/tests/Cache/CacheRateLimiterTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Cache; +use Illuminate\Cache\ArrayStore; use Illuminate\Cache\RateLimiter; use Illuminate\Contracts\Cache\Repository as Cache; use Mockery as m; @@ -20,6 +21,7 @@ public function testTooManyAttemptsReturnTrueIfAlreadyLockedOut() $cache->shouldReceive('get')->once()->with('key', 0)->andReturn(1); $cache->shouldReceive('has')->once()->with('key:timer')->andReturn(true); $cache->shouldReceive('add')->never(); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $rateLimiter = new RateLimiter($cache); $this->assertTrue($rateLimiter->tooManyAttempts('key', 1)); @@ -31,6 +33,7 @@ public function testHitProperlyIncrementsAttemptCount() $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 1)->andReturn(true); $cache->shouldReceive('add')->once()->with('key', 0, 1)->andReturn(true); $cache->shouldReceive('increment')->once()->with('key', 1)->andReturn(1); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $rateLimiter = new RateLimiter($cache); $rateLimiter->hit('key', 1); @@ -42,6 +45,7 @@ public function testIncrementProperlyIncrementsAttemptCount() $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 1)->andReturn(true); $cache->shouldReceive('add')->once()->with('key', 0, 1)->andReturn(true); $cache->shouldReceive('increment')->once()->with('key', 5)->andReturn(5); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $rateLimiter = new RateLimiter($cache); $rateLimiter->increment('key', 1, 5); @@ -53,6 +57,7 @@ public function testDecrementProperlyDecrementsAttemptCount() $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 1)->andReturn(true); $cache->shouldReceive('add')->once()->with('key', 0, 1)->andReturn(true); $cache->shouldReceive('increment')->once()->with('key', -5)->andReturn(-5); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $rateLimiter = new RateLimiter($cache); $rateLimiter->decrement('key', 1, 5); @@ -65,6 +70,7 @@ public function testHitHasNoMemoryLeak() $cache->shouldReceive('add')->once()->with('key', 0, 1)->andReturn(false); $cache->shouldReceive('increment')->once()->with('key', 1)->andReturn(1); $cache->shouldReceive('put')->once()->with('key', 1, 1); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $rateLimiter = new RateLimiter($cache); $rateLimiter->hit('key', 1); @@ -74,6 +80,7 @@ public function testRetriesLeftReturnsCorrectCount() { $cache = m::mock(Cache::class); $cache->shouldReceive('get')->once()->with('key', 0)->andReturn(3); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $rateLimiter = new RateLimiter($cache); $this->assertEquals(2, $rateLimiter->retriesLeft('key', 5)); @@ -84,6 +91,7 @@ public function testClearClearsTheCacheKeys() $cache = m::mock(Cache::class); $cache->shouldReceive('forget')->once()->with('key'); $cache->shouldReceive('forget')->once()->with('key:timer'); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $rateLimiter = new RateLimiter($cache); $rateLimiter->clear('key'); @@ -93,6 +101,7 @@ public function testAvailableInReturnsPositiveValues() { $cache = m::mock(Cache::class); $cache->shouldReceive('get')->andReturn(now()->subSeconds(60)->getTimestamp(), null); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $rateLimiter = new RateLimiter($cache); $this->assertTrue($rateLimiter->availableIn('key:timer') >= 0); @@ -106,6 +115,7 @@ public function testAttemptsCallbackReturnsTrue() $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 1); $cache->shouldReceive('add')->once()->with('key', 0, 1)->andReturns(1); $cache->shouldReceive('increment')->once()->with('key', 1)->andReturn(1); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $executed = false; @@ -124,6 +134,7 @@ public function testAttemptsCallbackReturnsCallbackReturn() $cache->shouldReceive('add')->times(6)->with('key:timer', m::type('int'), 1); $cache->shouldReceive('add')->times(6)->with('key', 0, 1)->andReturns(1); $cache->shouldReceive('increment')->times(6)->with('key', 1)->andReturn(1); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $rateLimiter = new RateLimiter($cache); @@ -157,6 +168,7 @@ public function testAttemptsCallbackReturnsFalse() $cache = m::mock(Cache::class); $cache->shouldReceive('get')->once()->with('key', 0)->andReturn(2); $cache->shouldReceive('has')->once()->with('key:timer')->andReturn(true); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $executed = false; @@ -174,6 +186,7 @@ public function testKeysAreSanitizedFromUnicodeCharacters() $cache->shouldReceive('get')->once()->with('john', 0)->andReturn(1); $cache->shouldReceive('has')->once()->with('john:timer')->andReturn(true); $cache->shouldReceive('add')->never(); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $rateLimiter = new RateLimiter($cache); $this->assertTrue($rateLimiter->tooManyAttempts('jôhn', 1)); @@ -190,6 +203,7 @@ public function testKeyIsSanitizedOnlyOnce() $cache->shouldReceive('get')->once()->with($cleanedKey, 0)->andReturn(1); $cache->shouldReceive('has')->once()->with("$cleanedKey:timer")->andReturn(true); $cache->shouldReceive('add')->never(); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $this->assertTrue($rateLimiter->tooManyAttempts($key, 1)); } diff --git a/tests/Cache/RedisCacheIntegrationTest.php b/tests/Cache/RedisCacheIntegrationTest.php index ff99dd0bfd81..57c6362b84d8 100644 --- a/tests/Cache/RedisCacheIntegrationTest.php +++ b/tests/Cache/RedisCacheIntegrationTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Cache; +use Illuminate\Cache\RateLimiter; use Illuminate\Cache\RedisStore; use Illuminate\Cache\Repository; use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; @@ -37,6 +38,22 @@ public function testRedisCacheAddTwice($driver) $this->assertGreaterThan(3500, $this->redis[$driver]->connection()->ttl('k')); } + /** + * @param string $driver + */ + #[DataProvider('redisDriverProvider')] + public function testRedisCacheRateLimiter($driver) + { + $store = new RedisStore($this->redis[$driver]); + $repository = new Repository($store); + $rateLimiter = new RateLimiter($repository); + + $this->assertFalse($rateLimiter->tooManyAttempts('key', 1)); + $this->assertEquals(1, $rateLimiter->hit('key', 60)); + $this->assertTrue($rateLimiter->tooManyAttempts('key', 1)); + $this->assertFalse($rateLimiter->tooManyAttempts('key', 2)); + } + /** * Breaking change. * diff --git a/tests/Foundation/Testing/Concerns/InteractsWithViewsTest.php b/tests/Foundation/Testing/Concerns/InteractsWithViewsTest.php index 8d56d4222d8a..87c1c4092326 100644 --- a/tests/Foundation/Testing/Concerns/InteractsWithViewsTest.php +++ b/tests/Foundation/Testing/Concerns/InteractsWithViewsTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Foundation\Testing\Concerns; use Illuminate\Foundation\Testing\Concerns\InteractsWithViews; +use Illuminate\Testing\TestComponent; use Illuminate\View\Component; use Orchestra\Testbench\TestCase; @@ -40,4 +41,21 @@ public function render() $this->assertSame('hello', $component->speak()); $component->assertSee('content'); } + + public function testComponentMacroable() + { + TestComponent::macro('foo', fn (): string => 'bar'); + + $exampleComponent = new class extends Component + { + public function render() + { + return 'rendered content'; + } + }; + + $component = $this->component(get_class($exampleComponent)); + + $this->assertSame('bar', $component->foo()); + } } diff --git a/tests/Integration/Cache/RedisStoreTest.php b/tests/Integration/Cache/RedisStoreTest.php index 25ee5ccbe721..af731995b7de 100644 --- a/tests/Integration/Cache/RedisStoreTest.php +++ b/tests/Integration/Cache/RedisStoreTest.php @@ -252,6 +252,8 @@ public function testPutManyCallsPutWhenClustered() public function testIncrementWithSerializationEnabled() { + $this->markTestSkipped('Test makes no sense anymore. Application must explicitly wrap such code in runClean() when used with serialization/compression enabled.'); + /** @var \Illuminate\Cache\RedisStore $store */ $store = Cache::store('redis'); /** @var \Redis $client */ diff --git a/tests/Integration/Queue/RateLimitedTest.php b/tests/Integration/Queue/RateLimitedTest.php index de6e0e0f785a..334ec657536e 100644 --- a/tests/Integration/Queue/RateLimitedTest.php +++ b/tests/Integration/Queue/RateLimitedTest.php @@ -63,6 +63,7 @@ public function testRateLimitedJobsAreNotExecutedOnLimitReached2() $cache->shouldReceive('add')->andReturn(true, true); $cache->shouldReceive('increment')->andReturn(1); $cache->shouldReceive('has')->andReturn(true); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $rateLimiter = new RateLimiter($cache); $this->app->instance(RateLimiter::class, $rateLimiter); diff --git a/tests/Validation/ValidationArrayRuleTest.php b/tests/Validation/ValidationArrayRuleTest.php index fcf4dfbe10de..9df05e81430a 100644 --- a/tests/Validation/ValidationArrayRuleTest.php +++ b/tests/Validation/ValidationArrayRuleTest.php @@ -2,7 +2,10 @@ namespace Illuminate\Tests\Validation; +use Illuminate\Translation\ArrayLoader; +use Illuminate\Translation\Translator; use Illuminate\Validation\Rule; +use Illuminate\Validation\Validator; use PHPUnit\Framework\TestCase; include_once 'Enums.php'; @@ -35,4 +38,21 @@ public function testItCorrectlyFormatsAStringVersionOfTheRule() $this->assertSame('array:key_1,key_2,key_3', (string) $rule); } + + public function testArrayValidation() + { + $trans = new Translator(new ArrayLoader, 'en'); + + $v = new Validator($trans, ['foo' => 'not an array'], ['foo' => Rule::array()]); + $this->assertTrue($v->fails()); + + $v = new Validator($trans, ['foo' => ['bar']], ['foo' => (string) Rule::array()]); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => ['key_1' => 'bar', 'key_2' => '']], ['foo' => Rule::array(['key_1', 'key_2'])]); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => ['key_1' => 'bar', 'key_2' => '']], ['foo' => ['required', Rule::array(['key_1', 'key_2'])]]); + $this->assertTrue($v->passes()); + } } diff --git a/tests/Validation/ValidationDateRuleTest.php b/tests/Validation/ValidationDateRuleTest.php index 6c903a6f956a..1db535b0da5d 100644 --- a/tests/Validation/ValidationDateRuleTest.php +++ b/tests/Validation/ValidationDateRuleTest.php @@ -134,5 +134,15 @@ public function testDateValidation() ); $this->assertEmpty($validator->errors()->first('date')); + + $rule = Rule::date()->between('2024/01/01', '2024/02/01')->format('Y/m/d'); + + $validator = new Validator( + $trans, + ['date' => '2024/01/15'], + ['date' => [$rule]] + ); + + $this->assertEmpty($validator->errors()->first('date')); } } diff --git a/tests/Validation/ValidationDimensionsRuleTest.php b/tests/Validation/ValidationDimensionsRuleTest.php index 8ff8c385eb52..6bd2d0326e06 100644 --- a/tests/Validation/ValidationDimensionsRuleTest.php +++ b/tests/Validation/ValidationDimensionsRuleTest.php @@ -73,5 +73,16 @@ public function testGeneratesTheCorrectValidationMessages() $trans->get('validation.dimensions', ['width' => 100, 'height' => 100, 'min_ratio' => 0.5, 'max_ratio' => 0.4]), $validator->errors()->first('image') ); + + $validator = new Validator( + $trans, + ['image' => $image], + ['image' => [$rule]] + ); + + $this->assertSame( + $trans->get('validation.dimensions', ['width' => 100, 'height' => 100, 'min_ratio' => 0.5, 'max_ratio' => 0.4]), + $validator->errors()->first('image') + ); } } diff --git a/tests/Validation/ValidationExcludeIfTest.php b/tests/Validation/ValidationExcludeIfTest.php index d3e4727d3ca8..5467f66c55d2 100644 --- a/tests/Validation/ValidationExcludeIfTest.php +++ b/tests/Validation/ValidationExcludeIfTest.php @@ -3,7 +3,10 @@ namespace Illuminate\Tests\Validation; use Exception; +use Illuminate\Translation\ArrayLoader; +use Illuminate\Translation\Translator; use Illuminate\Validation\Rules\ExcludeIf; +use Illuminate\Validation\Validator; use InvalidArgumentException; use PHPUnit\Framework\TestCase; use stdClass; @@ -57,4 +60,31 @@ public function testItThrowsExceptionIfRuleIsNotSerializable() return true; })); } + + public function testExcludeIfRuleValidation() + { + $ruleTrue = new ExcludeIf(true); + + $ruleFalse = new ExcludeIf(false); + + $trans = new Translator(new ArrayLoader, 'en'); + + $data = ['foo' => 'FOO', 'bar' => 'BAR']; + + $v = new Validator($trans, $data, ['foo' => $ruleTrue, 'bar' => 'nullable']); + $this->assertTrue($v->passes()); + $this->assertSame(['bar' => 'BAR'], $v->validated()); + + $v = new Validator($trans, $data, ['foo' => (string) $ruleTrue, 'bar' => 'nullable']); + $this->assertTrue($v->passes()); + $this->assertSame(['bar' => 'BAR'], $v->validated()); + + $v = new Validator($trans, $data, ['foo' => [$ruleTrue], 'bar' => 'nullable']); + $this->assertTrue($v->passes()); + $this->assertSame(['bar' => 'BAR'], $v->validated()); + + $v = new Validator($trans, $data, ['foo' => $ruleFalse, 'bar' => 'nullable']); + $this->assertTrue($v->passes()); + $this->assertSame($data, $v->validated()); + } } diff --git a/tests/Validation/ValidationExistsRuleTest.php b/tests/Validation/ValidationExistsRuleTest.php index bf4654532f78..bcdf95b50c93 100644 --- a/tests/Validation/ValidationExistsRuleTest.php +++ b/tests/Validation/ValidationExistsRuleTest.php @@ -245,6 +245,24 @@ public function testItOnlyTrashedSoftDeletes() $this->assertSame('exists:table,NULL,softdeleted_at,"NOT_NULL"', (string) $rule); } + public function testItIsAPartOfListRules() + { + $rule = new Exists('users', 'id'); + + User::create(['id' => '1', 'type' => 'foo']); + User::create(['id' => '2', 'type' => 'bar']); + User::create(['id' => '3', 'type' => 'baz']); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, [], ['id' => ['required', $rule]]); + $v->setPresenceVerifier(new DatabasePresenceVerifier(Eloquent::getConnectionResolver())); + + $v->setData(['id' => 1]); + $this->assertTrue($v->passes()); + $v->setData(['id' => 2]); + $this->assertTrue($v->passes()); + } + protected function createSchema() { $this->schema('default')->create('users', function ($table) { diff --git a/tests/Validation/ValidationInRuleTest.php b/tests/Validation/ValidationInRuleTest.php index 25814076ea4f..5ed6dce69b42 100644 --- a/tests/Validation/ValidationInRuleTest.php +++ b/tests/Validation/ValidationInRuleTest.php @@ -3,8 +3,11 @@ namespace Illuminate\Tests\Validation; use Illuminate\Tests\Validation\fixtures\Values; +use Illuminate\Translation\ArrayLoader; +use Illuminate\Translation\Translator; use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\In; +use Illuminate\Validation\Validator; use PHPUnit\Framework\TestCase; include_once 'Enums.php'; @@ -69,4 +72,21 @@ public function testItCorrectlyFormatsAStringVersionOfTheRule() $this->assertSame('in:"one"', (string) $rule); } + + public function testInRuleValidation() + { + $trans = new Translator(new ArrayLoader, 'en'); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => Rule::in('foo', 'bar')]); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => (string) Rule::in('foo', 'bar')]); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => [Rule::in('bar', 'baz')]]); + $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => ['required', Rule::in('foo', 'bar')]]); + $this->assertTrue($v->passes()); + } } diff --git a/tests/Validation/ValidationNotInRuleTest.php b/tests/Validation/ValidationNotInRuleTest.php index ba00d81a3a51..847b1adf9098 100644 --- a/tests/Validation/ValidationNotInRuleTest.php +++ b/tests/Validation/ValidationNotInRuleTest.php @@ -3,8 +3,11 @@ namespace Illuminate\Tests\Validation; use Illuminate\Tests\Validation\fixtures\Values; +use Illuminate\Translation\ArrayLoader; +use Illuminate\Translation\Translator; use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\NotIn; +use Illuminate\Validation\Validator; use PHPUnit\Framework\TestCase; include_once 'Enums.php'; @@ -65,4 +68,21 @@ public function testItCorrectlyFormatsAStringVersionOfTheRule() $this->assertSame('not_in:"one"', (string) $rule); } + + public function testNotInRuleValidation() + { + $trans = new Translator(new ArrayLoader, 'en'); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => Rule::notIn('bar', 'baz')]); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => (string) Rule::notIn('bar', 'baz')]); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => [Rule::notIn('foo', 'bar')]]); + $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => ['required', Rule::notIn('bar', 'baz')]]); + $this->assertTrue($v->passes()); + } } diff --git a/tests/Validation/ValidationProhibitedIfTest.php b/tests/Validation/ValidationProhibitedIfTest.php index 255f0cbfed7e..c68eb88e95eb 100644 --- a/tests/Validation/ValidationProhibitedIfTest.php +++ b/tests/Validation/ValidationProhibitedIfTest.php @@ -3,7 +3,10 @@ namespace Illuminate\Tests\Validation; use Exception; +use Illuminate\Translation\ArrayLoader; +use Illuminate\Translation\Translator; use Illuminate\Validation\Rules\ProhibitedIf; +use Illuminate\Validation\Validator; use InvalidArgumentException; use PHPUnit\Framework\TestCase; use stdClass; @@ -57,4 +60,28 @@ public function testItThrowsExceptionIfRuleIsNotSerializable() return true; })); } + + public function testProhibitedIfRuleValidation() + { + $trans = new Translator(new ArrayLoader, 'en'); + + $rule = new ProhibitedIf(true); + + $v = new Validator($trans, ['y' => 'foo'], ['x' => $rule]); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['y' => 'foo'], ['x' => (string) $rule]); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['y' => 'foo'], ['x' => [$rule]]); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => ['string', $rule]]); + $this->assertTrue($v->fails()); + + $rule = new ProhibitedIf(false); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => ['string', $rule]]); + $this->assertTrue($v->passes()); + } } diff --git a/tests/Validation/ValidationRequiredIfTest.php b/tests/Validation/ValidationRequiredIfTest.php index f0122fb3a02d..619e7c72f566 100644 --- a/tests/Validation/ValidationRequiredIfTest.php +++ b/tests/Validation/ValidationRequiredIfTest.php @@ -3,7 +3,10 @@ namespace Illuminate\Tests\Validation; use Exception; +use Illuminate\Translation\ArrayLoader; +use Illuminate\Translation\Translator; use Illuminate\Validation\Rules\RequiredIf; +use Illuminate\Validation\Validator; use InvalidArgumentException; use PHPUnit\Framework\TestCase; @@ -51,4 +54,28 @@ public function testItReturnedRuleIsNotSerializable() return true; })); } + + public function testRequiredIfRuleValidation() + { + $trans = new Translator(new ArrayLoader, 'en'); + + $rule = new RequiredIf(true); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => $rule]); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => ''], ['x' => (string) $rule]); + $this->assertTrue($v->fails()); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => [$rule]]); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => ['string', $rule]]); + $this->assertTrue($v->passes()); + + $rule = new RequiredIf(false); + + $v = new Validator($trans, ['x' => 'foo'], ['x' => ['string', $rule]]); + $this->assertTrue($v->passes()); + } } diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index df36e080ef71..49c2e8f6700d 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -6316,6 +6316,12 @@ public function testBeforeAndAfterWithFormat() $v = new Validator($trans, ['x' => '1970-01-02', '2018-05-12' => '1970-01-01'], ['x' => 'date_format:Y-m-d|after:2018-05-12']); $this->assertTrue($v->fails()); + + $v = new Validator($trans, ['from' => '2020-08-05', 'to' => '2020-06-08'], ['from' => 'date_format:Y-m-d|before:to', 'to' => 'date_format:Y-d-m']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['from' => '2020-05-08', 'to' => '2020-08-06'], ['from' => 'date_format:Y-m-d', 'to' => 'date_format:Y-d-m|after:from']); + $this->assertTrue($v->passes()); } public function testWeakBeforeAndAfter() @@ -6417,6 +6423,12 @@ public function testWeakBeforeAndAfter() $v = new Validator($trans, ['foo' => '2012-01-15 11:00', 'bar' => null], ['foo' => 'before_or_equal:bar', 'bar' => 'nullable']); $this->assertTrue($v->fails()); + + $v = new Validator($trans, ['from' => '2020-08-05', 'to' => '2020-05-08'], ['from' => 'date_format:Y-m-d|before_or_equal:to', 'to' => 'date_format:Y-d-m']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['from' => '2020-05-08', 'to' => '2020-08-05'], ['from' => 'date_format:Y-m-d', 'to' => 'date_format:Y-d-m|after_or_equal:from']); + $this->assertTrue($v->passes()); } public function testSometimesAddingRules()