From add249472cd192cabcb4f113ff7915f667394141 Mon Sep 17 00:00:00 2001 From: Claudio Dekker <1752195+claudiodekker@users.noreply.github.com> Date: Wed, 3 Mar 2021 20:07:50 +0100 Subject: [PATCH 01/13] Implement Fluent JSON Assertions --- src/Illuminate/Testing/Fluent/Assert.php | 76 ++ .../Testing/Fluent/Concerns/Debugging.php | 20 + .../Testing/Fluent/Concerns/Has.php | 100 +++ .../Testing/Fluent/Concerns/Interaction.php | 41 ++ .../Testing/Fluent/Concerns/Matching.php | 70 ++ src/Illuminate/Testing/TestResponse.php | 20 +- tests/Testing/Fluent/AssertTest.php | 688 ++++++++++++++++++ tests/Testing/Stubs/ArrayableStubObject.php | 25 + tests/Testing/TestResponseTest.php | 22 + 9 files changed, 1059 insertions(+), 3 deletions(-) create mode 100644 src/Illuminate/Testing/Fluent/Assert.php create mode 100644 src/Illuminate/Testing/Fluent/Concerns/Debugging.php create mode 100644 src/Illuminate/Testing/Fluent/Concerns/Has.php create mode 100644 src/Illuminate/Testing/Fluent/Concerns/Interaction.php create mode 100644 src/Illuminate/Testing/Fluent/Concerns/Matching.php create mode 100644 tests/Testing/Fluent/AssertTest.php create mode 100644 tests/Testing/Stubs/ArrayableStubObject.php diff --git a/src/Illuminate/Testing/Fluent/Assert.php b/src/Illuminate/Testing/Fluent/Assert.php new file mode 100644 index 000000000000..a5e89e83294c --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Assert.php @@ -0,0 +1,76 @@ +path = $path; + $this->props = $props; + } + + protected function dotPath($key): string + { + if (is_null($this->path)) { + return $key; + } + + return implode('.', [$this->path, $key]); + } + + protected function prop(string $key = null) + { + return Arr::get($this->props, $key); + } + + protected function scope($key, Closure $callback): self + { + $props = $this->prop($key); + $path = $this->dotPath($key); + + PHPUnit::assertIsArray($props, sprintf('Property [%s] is not scopeable.', $path)); + + $scope = new self($props, $path); + $callback($scope); + $scope->interacted(); + + return $this; + } + + public static function fromArray(array $data): self + { + return new self($data); + } + + public static function fromAssertableJsonString(AssertableJsonString $json): self + { + return self::fromArray($json->json()); + } + + public function toArray() + { + return $this->props; + } +} diff --git a/src/Illuminate/Testing/Fluent/Concerns/Debugging.php b/src/Illuminate/Testing/Fluent/Concerns/Debugging.php new file mode 100644 index 000000000000..d604f0e0b21d --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Concerns/Debugging.php @@ -0,0 +1,20 @@ +prop($prop)); + + return $this; + } + + public function dd(string $prop = null): void + { + dd($this->prop($prop)); + } + + abstract protected function prop(string $key = null); +} diff --git a/src/Illuminate/Testing/Fluent/Concerns/Has.php b/src/Illuminate/Testing/Fluent/Concerns/Has.php new file mode 100644 index 000000000000..28955a742493 --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Concerns/Has.php @@ -0,0 +1,100 @@ +prop($key), + sprintf('Property [%s] does not have the expected size.', $this->dotPath($key)) + ); + + return $this; + } + + public function hasAll($key): self + { + $keys = is_array($key) ? $key : func_get_args(); + + foreach ($keys as $prop => $count) { + if (is_int($prop)) { + $this->has($count); + } else { + $this->has($prop, $count); + } + } + + return $this; + } + + public function has(string $key, $value = null, Closure $scope = null): self + { + $prop = $this->prop(); + + PHPUnit::assertTrue( + Arr::has($prop, $key), + sprintf('Property [%s] does not exist.', $this->dotPath($key)) + ); + + $this->interactsWith($key); + + // When all three arguments are provided, this indicates a short-hand + // expression that combines both a `count`-assertion, followed by + // directly creating a `scope` on the first element. + if (is_int($value) && ! is_null($scope)) { + $prop = $this->prop($key); + $path = $this->dotPath($key); + + PHPUnit::assertTrue($value > 0, sprintf('Cannot scope directly onto the first entry of property [%s] when asserting that it has a size of 0.', $path)); + PHPUnit::assertIsArray($prop, sprintf('Direct scoping is unsupported for non-array like properties such as [%s].', $path)); + + $this->count($key, $value); + + return $this->scope($key.'.'.array_keys($prop)[0], $scope); + } + + if (is_callable($value)) { + $this->scope($key, $value); + } elseif (! is_null($value)) { + $this->count($key, $value); + } + + return $this; + } + + public function missingAll($key): self + { + $keys = is_array($key) ? $key : func_get_args(); + + foreach ($keys as $prop) { + $this->missing($prop); + } + + return $this; + } + + public function missing(string $key): self + { + PHPUnit::assertNotTrue( + Arr::has($this->prop(), $key), + sprintf('Property [%s] was found while it was expected to be missing.', $this->dotPath($key)) + ); + + return $this; + } + + abstract protected function prop(string $key = null); + + abstract protected function dotPath($key): string; + + abstract protected function interactsWith(string $key): void; + + abstract protected function scope($key, Closure $callback); +} diff --git a/src/Illuminate/Testing/Fluent/Concerns/Interaction.php b/src/Illuminate/Testing/Fluent/Concerns/Interaction.php new file mode 100644 index 000000000000..d938438a618c --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Concerns/Interaction.php @@ -0,0 +1,41 @@ +interacted, true)) { + $this->interacted[] = $prop; + } + } + + public function interacted(): void + { + PHPUnit::assertSame( + [], + array_diff(array_keys($this->prop()), $this->interacted), + $this->path + ? sprintf('Unexpected properties were found in scope [%s].', $this->path) + : 'Unexpected properties were found on the root level.' + ); + } + + public function etc(): self + { + $this->interacted = array_keys($this->prop()); + + return $this; + } + + abstract protected function prop(string $key = null); +} diff --git a/src/Illuminate/Testing/Fluent/Concerns/Matching.php b/src/Illuminate/Testing/Fluent/Concerns/Matching.php new file mode 100644 index 000000000000..3edce127d362 --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Concerns/Matching.php @@ -0,0 +1,70 @@ + $value) { + $this->where($key, $value); + } + + return $this; + } + + public function where($key, $expected): self + { + $this->has($key); + + $actual = $this->prop($key); + + if ($expected instanceof Closure) { + PHPUnit::assertTrue( + $expected(is_array($actual) ? Collection::make($actual) : $actual), + sprintf('Property [%s] was marked as invalid using a closure.', $this->dotPath($key)) + ); + + return $this; + } + + if ($expected instanceof Arrayable) { + $expected = $expected->toArray(); + } + + $this->ensureSorted($expected); + $this->ensureSorted($actual); + + PHPUnit::assertSame( + $expected, + $actual, + sprintf('Property [%s] does not match the expected value.', $this->dotPath($key)) + ); + + return $this; + } + + protected function ensureSorted(&$value): void + { + if (! is_array($value)) { + return; + } + + foreach ($value as &$arg) { + $this->ensureSorted($arg); + } + + ksort($value); + } + + abstract protected function dotPath($key): string; + + abstract protected function prop(string $key = null); + + abstract public function has(string $key, $value = null, Closure $scope = null); +} diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index 1bfc75285518..b900418caf79 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -14,6 +14,7 @@ use Illuminate\Support\Traits\Tappable; use Illuminate\Testing\Assert as PHPUnit; use Illuminate\Testing\Constraints\SeeInOrder; +use Illuminate\Testing\Fluent\Assert as FluentAssert; use LogicException; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -507,13 +508,26 @@ public function assertDontSeeText($value, $escape = true) /** * Assert that the response is a superset of the given JSON. * - * @param array $data + * @param array|callable $value * @param bool $strict * @return $this */ - public function assertJson(array $data, $strict = false) + public function assertJson($value, $strict = false) { - $this->decodeResponseJson()->assertSubset($data, $strict); + $json = $this->decodeResponseJson(); + + if (is_array($value)) { + $json->assertSubset($value, $strict); + } else { + $assert = FluentAssert::fromAssertableJsonString($json); + + $value($assert); + + if ($strict) { + $assert->interacted(); + } + } + return $this; } diff --git a/tests/Testing/Fluent/AssertTest.php b/tests/Testing/Fluent/AssertTest.php new file mode 100644 index 000000000000..f02e366f40a0 --- /dev/null +++ b/tests/Testing/Fluent/AssertTest.php @@ -0,0 +1,688 @@ + 'value', + ]); + + $assert->has('prop'); + } + + public function testAssertHasFailsWhenPropMissing() + { + $assert = Assert::fromArray([ + 'bar' => 'value', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [prop] does not exist.'); + + $assert->has('prop'); + } + + public function testAssertHasNestedProp() + { + $assert = Assert::fromArray([ + 'example' => [ + 'nested' => 'nested-value', + ], + ]); + + $assert->has('example.nested'); + } + + public function testAssertHasFailsWhenNestedPropMissing() + { + $assert = Assert::fromArray([ + 'example' => [ + 'nested' => 'nested-value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [example.another] does not exist.'); + + $assert->has('example.another'); + } + + public function testAssertCountItemsInProp() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $assert->has('bar', 2); + } + + public function testAssertCountFailsWhenAmountOfItemsDoesNotMatch() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not have the expected size.'); + + $assert->has('bar', 1); + } + + public function testAssertCountFailsWhenPropMissing() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] does not exist.'); + + $assert->has('baz', 1); + } + + public function testAssertHasFailsWhenSecondArgumentUnsupportedType() + { + $assert = Assert::fromArray([ + 'bar' => 'baz', + ]); + + $this->expectException(TypeError::class); + + $assert->has('bar', 'invalid'); + } + + public function testAssertMissing() + { + $assert = Assert::fromArray([ + 'foo' => [ + 'bar' => true, + ], + ]); + + $assert->missing('foo.baz'); + } + + public function testAssertMissingFailsWhenPropExists() + { + $assert = Assert::fromArray([ + 'prop' => 'value', + 'foo' => [ + 'bar' => true, + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [foo.bar] was found while it was expected to be missing.'); + + $assert->missing('foo.bar'); + } + + public function testAssertMissingAll() + { + $assert = Assert::fromArray([ + 'baz' => 'foo', + ]); + + $assert->missingAll([ + 'foo', + 'bar', + ]); + } + + public function testAssertMissingAllFailsWhenAtLeastOnePropExists() + { + $assert = Assert::fromArray([ + 'baz' => 'foo', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] was found while it was expected to be missing.'); + + $assert->missingAll([ + 'bar', + 'baz', + ]); + } + + public function testAssertMissingAllAcceptsMultipleArgumentsInsteadOfArray() + { + $assert = Assert::fromArray([ + 'baz' => 'foo', + ]); + + $assert->missingAll('foo', 'bar'); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] was found while it was expected to be missing.'); + + $assert->missingAll('bar', 'baz'); + } + + public function testAssertWhereMatchesValue() + { + $assert = Assert::fromArray([ + 'bar' => 'value', + ]); + + $assert->where('bar', 'value'); + } + + public function testAssertWhereFailsWhenDoesNotMatchValue() + { + $assert = Assert::fromArray([ + 'bar' => 'value', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not match the expected value.'); + + $assert->where('bar', 'invalid'); + } + + public function testAssertWhereFailsWhenMissing() + { + $assert = Assert::fromArray([ + 'bar' => 'value', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] does not exist.'); + + $assert->where('baz', 'invalid'); + } + + public function testAssertWhereFailsWhenMachingLoosely() + { + $assert = Assert::fromArray([ + 'bar' => 1, + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not match the expected value.'); + + $assert->where('bar', true); + } + + public function testAssertWhereUsingClosure() + { + $assert = Assert::fromArray([ + 'bar' => 'baz', + ]); + + $assert->where('bar', function ($value) { + return $value === 'baz'; + }); + } + + public function testAssertWhereFailsWhenDoesNotMatchValueUsingClosure() + { + $assert = Assert::fromArray([ + 'bar' => 'baz', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] was marked as invalid using a closure.'); + + $assert->where('bar', function ($value) { + return $value === 'invalid'; + }); + } + + public function testAssertWhereClosureArrayValuesAreAutomaticallyCastedToCollections() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'foo', + 'example' => 'value', + ], + ]); + + $assert->where('bar', function ($value) { + $this->assertInstanceOf(Collection::class, $value); + + return $value->count() === 2; + }); + } + + public function testAssertWhereMatchesValueUsingArrayable() + { + $stub = ArrayableStubObject::make(['foo' => 'bar']); + + $assert = Assert::fromArray([ + 'bar' => $stub->toArray(), + ]); + + $assert->where('bar', $stub); + } + + public function testAssertWhereMatchesValueUsingArrayableWhenSortedDifferently() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'foo', + 'example' => 'value', + ], + ]); + + $assert->where('bar', function ($value) { + $this->assertInstanceOf(Collection::class, $value); + + return $value->count() === 2; + }); + } + + public function testAssertWhereFailsWhenDoesNotMatchValueUsingArrayable() + { + $assert = Assert::fromArray([ + 'bar' => ['id' => 1, 'name' => 'Example'], + 'baz' => [ + 'id' => 1, + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'email_verified_at' => '2021-01-22T10:34:42.000000Z', + 'created_at' => '2021-01-22T10:34:42.000000Z', + 'updated_at' => '2021-01-22T10:34:42.000000Z', + ], + ]); + + $assert + ->where('bar', ArrayableStubObject::make(['name' => 'Example', 'id' => 1])) + ->where('baz', [ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'id' => 1, + 'email_verified_at' => '2021-01-22T10:34:42.000000Z', + 'updated_at' => '2021-01-22T10:34:42.000000Z', + 'created_at' => '2021-01-22T10:34:42.000000Z', + ]); + } + + public function testAssertNestedWhereMatchesValue() + { + $assert = Assert::fromArray([ + 'example' => [ + 'nested' => 'nested-value', + ], + ]); + + $assert->where('example.nested', 'nested-value'); + } + + public function testAssertNestedWhereFailsWhenDoesNotMatchValue() + { + $assert = Assert::fromArray([ + 'example' => [ + 'nested' => 'nested-value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [example.nested] does not match the expected value.'); + + $assert->where('example.nested', 'another-value'); + } + + public function testScope() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $called = false; + $assert->has('bar', function (Assert $assert) use (&$called) { + $called = true; + $assert + ->where('baz', 'example') + ->where('prop', 'value'); + }); + + $this->assertTrue($called, 'The scoped query was never actually called.'); + } + + public function testScopeFailsWhenPropMissing() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] does not exist.'); + + $assert->has('baz', function (Assert $item) { + $item->where('baz', 'example'); + }); + } + + public function testScopeFailsWhenPropSingleValue() + { + $assert = Assert::fromArray([ + 'bar' => 'value', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] is not scopeable.'); + + $assert->has('bar', function (Assert $item) { + // + }); + } + + public function testScopeShorthand() + { + $assert = Assert::fromArray([ + 'bar' => [ + ['key' => 'first'], + ['key' => 'second'], + ], + ]); + + $called = false; + $assert->has('bar', 2, function (Assert $item) use (&$called) { + $item->where('key', 'first'); + $called = true; + }); + + $this->assertTrue($called, 'The scoped query was never actually called.'); + } + + public function testScopeShorthandFailsWhenAssertingZeroItems() + { + $assert = Assert::fromArray([ + 'bar' => [ + ['key' => 'first'], + ['key' => 'second'], + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Cannot scope directly onto the first entry of property [bar] when asserting that it has a size of 0.'); + + $assert->has('bar', 0, function (Assert $item) { + $item->where('key', 'first'); + }); + } + + public function testScopeShorthandFailsWhenAmountOfItemsDoesNotMatch() + { + $assert = Assert::fromArray([ + 'bar' => [ + ['key' => 'first'], + ['key' => 'second'], + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not have the expected size.'); + + $assert->has('bar', 1, function (Assert $item) { + $item->where('key', 'first'); + }); + } + + public function testFailsWhenNotInteractingWithAllPropsInScope() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Unexpected properties were found in scope [bar].'); + + $assert->has('bar', function (Assert $item) { + $item->where('baz', 'example'); + }); + } + + public function testDisableInteractionCheckForCurrentScope() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $assert->has('bar', function (Assert $item) { + $item->etc(); + }); + } + + public function testCannotDisableInteractionCheckForDifferentScopes() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => [ + 'foo' => 'bar', + 'example' => 'value', + ], + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Unexpected properties were found in scope [bar.baz].'); + + $assert->has('bar', function (Assert $item) { + $item + ->etc() + ->has('baz', function (Assert $item) { + // + }); + }); + } + + public function testTopLevelPropInteractionDisabledByDefault() + { + $assert = Assert::fromArray([ + 'foo' => 'bar', + 'bar' => 'baz', + ]); + + $assert->has('foo'); + } + + public function testTopLevelInteractionEnabledWhenInteractedFlagSet() + { + $assert = Assert::fromArray([ + 'foo' => 'bar', + 'bar' => 'baz', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Unexpected properties were found on the root level.'); + + $assert + ->has('foo') + ->interacted(); + } + + public function testAssertWhereAllMatchesValues() + { + $assert = Assert::fromArray([ + 'foo' => [ + 'bar' => 'value', + 'example' => ['hello' => 'world'], + ], + 'baz' => 'another', + ]); + + $assert->whereAll([ + 'foo.bar' => 'value', + 'foo.example' => ArrayableStubObject::make(['hello' => 'world']), + 'baz' => function ($value) { + return $value === 'another'; + }, + ]); + } + + public function testAssertWhereAllFailsWhenAtLeastOnePropDoesNotMatchValue() + { + $assert = Assert::fromArray([ + 'foo' => 'bar', + 'baz' => 'example', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] was marked as invalid using a closure.'); + + $assert->whereAll([ + 'foo' => 'bar', + 'baz' => function ($value) { + return $value === 'foo'; + }, + ]); + } + + public function testAssertHasAll() + { + $assert = Assert::fromArray([ + 'foo' => [ + 'bar' => 'value', + 'example' => ['hello' => 'world'], + ], + 'baz' => 'another', + ]); + + $assert->hasAll([ + 'foo.bar', + 'foo.example', + 'baz', + ]); + } + + public function testAssertHasAllFailsWhenAtLeastOnePropMissing() + { + $assert = Assert::fromArray([ + 'foo' => [ + 'bar' => 'value', + 'example' => ['hello' => 'world'], + ], + 'baz' => 'another', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [foo.baz] does not exist.'); + + $assert->hasAll([ + 'foo.bar', + 'foo.baz', + 'baz', + ]); + } + + public function testAssertHasAllAcceptsMultipleArgumentsInsteadOfArray() + { + $assert = Assert::fromArray([ + 'foo' => [ + 'bar' => 'value', + 'example' => ['hello' => 'world'], + ], + 'baz' => 'another', + ]); + + $assert->hasAll('foo.bar', 'foo.example', 'baz'); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [foo.baz] does not exist.'); + + $assert->hasAll('foo.bar', 'foo.baz', 'baz'); + } + + public function testAssertCountMultipleProps() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'key' => 'value', + 'prop' => 'example', + ], + 'baz' => [ + 'another' => 'value', + ], + ]); + + $assert->hasAll([ + 'bar' => 2, + 'baz' => 1, + ]); + } + + public function testAssertCountMultiplePropsFailsWhenPropMissing() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'key' => 'value', + 'prop' => 'example', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] does not exist.'); + + $assert->hasAll([ + 'bar' => 2, + 'baz' => 1, + ]); + } + + public function testMacroable() + { + Assert::macro('myCustomMacro', function () { + throw new RuntimeException('My Custom Macro was called!'); + }); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('My Custom Macro was called!'); + + $assert = Assert::fromArray(['foo' => 'bar']); + $assert->myCustomMacro(); + } + + public function testTappable() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $called = false; + $assert->has('bar', function (Assert $assert) use (&$called) { + $assert->etc(); + $assert->tap(function (Assert $assert) use (&$called) { + $called = true; + }); + }); + + $this->assertTrue($called, 'The scoped query was never actually called.'); + } +} diff --git a/tests/Testing/Stubs/ArrayableStubObject.php b/tests/Testing/Stubs/ArrayableStubObject.php new file mode 100644 index 000000000000..021440e0b287 --- /dev/null +++ b/tests/Testing/Stubs/ArrayableStubObject.php @@ -0,0 +1,25 @@ +data = $data; + } + + public static function make($data = []) + { + return new self($data); + } + + public function toArray() + { + return $this->data; + } +} diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 42d16e72d3fa..055519925cb9 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -9,6 +9,7 @@ use Illuminate\Encryption\Encrypter; use Illuminate\Filesystem\Filesystem; use Illuminate\Http\Response; +use Illuminate\Testing\Fluent\Assert; use Illuminate\Testing\TestResponse; use JsonSerializable; use Mockery as m; @@ -577,6 +578,27 @@ public function testAssertJsonWithNull() $response->assertJson($resource->jsonSerialize()); } + public function testAssertJsonWithFluent() + { + $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableSingleResourceStub)); + + $response->assertJson(function (Assert $json) { + $json->where('0.foo', 'foo 0'); + }); + } + + public function testAssertJsonWithFluentStrict() + { + $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableSingleResourceStub)); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Unexpected properties were found on the root level.'); + + $response->assertJson(function (Assert $json) { + $json->where('0.foo', 'foo 0'); + }, true); + } + public function testAssertSimilarJsonWithMixed() { $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableMixedResourcesStub)); From f31557f76ac5a790d66b717e074537031c48085e Mon Sep 17 00:00:00 2001 From: Claudio Dekker <1752195+claudiodekker@users.noreply.github.com> Date: Wed, 3 Mar 2021 20:20:34 +0100 Subject: [PATCH 02/13] Apply fixes from StyleCI --- src/Illuminate/Testing/TestResponse.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index b900418caf79..eb983f464a6d 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -528,7 +528,6 @@ public function assertJson($value, $strict = false) } } - return $this; } From 91bd93f92c26cc58c25849f1a1ac6d7ee6d55411 Mon Sep 17 00:00:00 2001 From: Claudio Dekker <1752195+claudiodekker@users.noreply.github.com> Date: Thu, 4 Mar 2021 17:29:43 +0100 Subject: [PATCH 03/13] Add docblocks & minor cleanup --- src/Illuminate/Testing/Fluent/Assert.php | 58 +++++++++- .../Testing/Fluent/Concerns/Debugging.php | 18 ++++ .../Testing/Fluent/Concerns/Has.php | 100 ++++++++++++++---- .../Testing/Fluent/Concerns/Interaction.php | 28 ++++- .../Testing/Fluent/Concerns/Matching.php | 65 +++++++++--- 5 files changed, 230 insertions(+), 39 deletions(-) diff --git a/src/Illuminate/Testing/Fluent/Assert.php b/src/Illuminate/Testing/Fluent/Assert.php index a5e89e83294c..926ef09656ac 100644 --- a/src/Illuminate/Testing/Fluent/Assert.php +++ b/src/Illuminate/Testing/Fluent/Assert.php @@ -19,19 +19,39 @@ class Assert implements Arrayable Macroable, Tappable; - /** @var array */ + /** + * The properties in the current scope. + * + * @var array + */ private $props; - /** @var string */ + /** + * The "dot" path to the current scope. + * + * @var string|null + */ private $path; + /** + * Create a new Assert instance. + * + * @param array $props + * @param string|null $path + */ protected function __construct(array $props, string $path = null) { $this->path = $path; $this->props = $props; } - protected function dotPath($key): string + /** + * Compose the absolute "dot" path to the given key. + * + * @param string $key + * @return string + */ + protected function dotPath(string $key): string { if (is_null($this->path)) { return $key; @@ -40,12 +60,25 @@ protected function dotPath($key): string return implode('.', [$this->path, $key]); } + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ protected function prop(string $key = null) { return Arr::get($this->props, $key); } - protected function scope($key, Closure $callback): self + /** + * Instantiate a new "scope" at the path of the given key. + * + * @param string $key + * @param Closure $callback + * @return $this + */ + protected function scope(string $key, Closure $callback): self { $props = $this->prop($key); $path = $this->dotPath($key); @@ -59,16 +92,33 @@ protected function scope($key, Closure $callback): self return $this; } + /** + * Create a new instance from an array. + * + * @param array $data + * @return static + */ public static function fromArray(array $data): self { return new self($data); } + /** + * Create a new instance from a AssertableJsonString. + * + * @param AssertableJsonString $json + * @return static + */ public static function fromAssertableJsonString(AssertableJsonString $json): self { return self::fromArray($json->json()); } + /** + * Get the instance as an array. + * + * @return array + */ public function toArray() { return $this->props; diff --git a/src/Illuminate/Testing/Fluent/Concerns/Debugging.php b/src/Illuminate/Testing/Fluent/Concerns/Debugging.php index d604f0e0b21d..f51d119074ae 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Debugging.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Debugging.php @@ -4,6 +4,12 @@ trait Debugging { + /** + * Dumps the given props. + * + * @param string|null $prop + * @return $this + */ public function dump(string $prop = null): self { dump($this->prop($prop)); @@ -11,10 +17,22 @@ public function dump(string $prop = null): self return $this; } + /** + * Dumps the given props and exits. + * + * @param string|null $prop + * @return void + */ public function dd(string $prop = null): void { dd($this->prop($prop)); } + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ abstract protected function prop(string $key = null); } diff --git a/src/Illuminate/Testing/Fluent/Concerns/Has.php b/src/Illuminate/Testing/Fluent/Concerns/Has.php index 28955a742493..746fa49b60f1 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Has.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Has.php @@ -8,7 +8,14 @@ trait Has { - protected function count(string $key, $length): self + /** + * Assert that the prop is of the expected size. + * + * @param string $key + * @param int $length + * @return $this + */ + protected function count(string $key, int $length): self { PHPUnit::assertCount( $length, @@ -19,21 +26,14 @@ protected function count(string $key, $length): self return $this; } - public function hasAll($key): self - { - $keys = is_array($key) ? $key : func_get_args(); - - foreach ($keys as $prop => $count) { - if (is_int($prop)) { - $this->has($count); - } else { - $this->has($prop, $count); - } - } - - return $this; - } - + /** + * Ensure that the given prop exists. + * + * @param string $key + * @param null $value + * @param Closure|null $scope + * @return $this + */ public function has(string $key, $value = null, Closure $scope = null): self { $prop = $this->prop(); @@ -69,6 +69,33 @@ public function has(string $key, $value = null, Closure $scope = null): self return $this; } + /** + * Assert that all of the given props exist. + * + * @param array|string $key + * @return $this + */ + public function hasAll($key): self + { + $keys = is_array($key) ? $key : func_get_args(); + + foreach ($keys as $prop => $count) { + if (is_int($prop)) { + $this->has($count); + } else { + $this->has($prop, $count); + } + } + + return $this; + } + + /** + * Assert that none of the given props exist. + * + * @param array|string $key + * @return $this + */ public function missingAll($key): self { $keys = is_array($key) ? $key : func_get_args(); @@ -80,6 +107,12 @@ public function missingAll($key): self return $this; } + /** + * Assert that the given prop does not exist. + * + * @param string $key + * @return $this + */ public function missing(string $key): self { PHPUnit::assertNotTrue( @@ -90,11 +123,36 @@ public function missing(string $key): self return $this; } - abstract protected function prop(string $key = null); - - abstract protected function dotPath($key): string; - + /** + * Compose the absolute "dot" path to the given key. + * + * @param string $key + * @return string + */ + abstract protected function dotPath(string $key): string; + + /** + * Marks the property as interacted. + * + * @param string $key + * @return void + */ abstract protected function interactsWith(string $key): void; - abstract protected function scope($key, Closure $callback); + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ + abstract protected function prop(string $key = null); + + /** + * Instantiate a new "scope" at the path of the given key. + * + * @param string $key + * @param Closure $callback + * @return $this + */ + abstract protected function scope(string $key, Closure $callback); } diff --git a/src/Illuminate/Testing/Fluent/Concerns/Interaction.php b/src/Illuminate/Testing/Fluent/Concerns/Interaction.php index d938438a618c..15e7e9508f55 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Interaction.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Interaction.php @@ -7,9 +7,19 @@ trait Interaction { - /** @var array */ + /** + * The list of interacted properties. + * + * @var array + */ protected $interacted = []; + /** + * Marks the property as interacted. + * + * @param string $key + * @return void + */ protected function interactsWith(string $key): void { $prop = Str::before($key, '.'); @@ -19,6 +29,11 @@ protected function interactsWith(string $key): void } } + /** + * Asserts that all properties have been interacted with. + * + * @return void + */ public function interacted(): void { PHPUnit::assertSame( @@ -30,6 +45,11 @@ public function interacted(): void ); } + /** + * Disables the interaction check. + * + * @return $this + */ public function etc(): self { $this->interacted = array_keys($this->prop()); @@ -37,5 +57,11 @@ public function etc(): self return $this; } + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ abstract protected function prop(string $key = null); } diff --git a/src/Illuminate/Testing/Fluent/Concerns/Matching.php b/src/Illuminate/Testing/Fluent/Concerns/Matching.php index 3edce127d362..052b2d49ab2f 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Matching.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Matching.php @@ -9,16 +9,14 @@ trait Matching { - public function whereAll(array $bindings): self - { - foreach ($bindings as $key => $value) { - $this->where($key, $value); - } - - return $this; - } - - public function where($key, $expected): self + /** + * Asserts that the property matches the expected value. + * + * @param string $key + * @param mixed|callable $expected + * @return $this + */ + public function where(string $key, $expected): self { $this->has($key); @@ -49,6 +47,27 @@ public function where($key, $expected): self return $this; } + /** + * Asserts that all properties match their expected values. + * + * @param array $bindings + * @return $this + */ + public function whereAll(array $bindings): self + { + foreach ($bindings as $key => $value) { + $this->where($key, $value); + } + + return $this; + } + + /** + * Ensures that all properties are sorted the same way, recursively. + * + * @param mixed $value + * @return void + */ protected function ensureSorted(&$value): void { if (! is_array($value)) { @@ -62,9 +81,29 @@ protected function ensureSorted(&$value): void ksort($value); } - abstract protected function dotPath($key): string; + /** + * Compose the absolute "dot" path to the given key. + * + * @param string $key + * @return string + */ + abstract protected function dotPath(string $key): string; + + /** + * Ensure that the given prop exists. + * + * @param string $key + * @param null $value + * @param Closure|null $scope + * @return $this + */ + abstract public function has(string $key, $value = null, Closure $scope = null); + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ abstract protected function prop(string $key = null); - - abstract public function has(string $key, $value = null, Closure $scope = null); } From c5eadc4d9de5a2597dc0c4b64f0d009e287c25a6 Mon Sep 17 00:00:00 2001 From: Claudio Dekker <1752195+claudiodekker@users.noreply.github.com> Date: Thu, 4 Mar 2021 18:11:49 +0100 Subject: [PATCH 04/13] Use FQN in DocBlocks --- src/Illuminate/Testing/Fluent/Assert.php | 4 ++-- src/Illuminate/Testing/Fluent/Concerns/Has.php | 4 ++-- src/Illuminate/Testing/Fluent/Concerns/Matching.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Testing/Fluent/Assert.php b/src/Illuminate/Testing/Fluent/Assert.php index 926ef09656ac..bde937a0bf23 100644 --- a/src/Illuminate/Testing/Fluent/Assert.php +++ b/src/Illuminate/Testing/Fluent/Assert.php @@ -75,7 +75,7 @@ protected function prop(string $key = null) * Instantiate a new "scope" at the path of the given key. * * @param string $key - * @param Closure $callback + * @param \Closure $callback * @return $this */ protected function scope(string $key, Closure $callback): self @@ -106,7 +106,7 @@ public static function fromArray(array $data): self /** * Create a new instance from a AssertableJsonString. * - * @param AssertableJsonString $json + * @param \Illuminate\Testing\AssertableJsonString $json * @return static */ public static function fromAssertableJsonString(AssertableJsonString $json): self diff --git a/src/Illuminate/Testing/Fluent/Concerns/Has.php b/src/Illuminate/Testing/Fluent/Concerns/Has.php index 746fa49b60f1..19b9ad9915ca 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Has.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Has.php @@ -31,7 +31,7 @@ protected function count(string $key, int $length): self * * @param string $key * @param null $value - * @param Closure|null $scope + * @param \Closure|null $scope * @return $this */ public function has(string $key, $value = null, Closure $scope = null): self @@ -151,7 +151,7 @@ abstract protected function prop(string $key = null); * Instantiate a new "scope" at the path of the given key. * * @param string $key - * @param Closure $callback + * @param \Closure $callback * @return $this */ abstract protected function scope(string $key, Closure $callback); diff --git a/src/Illuminate/Testing/Fluent/Concerns/Matching.php b/src/Illuminate/Testing/Fluent/Concerns/Matching.php index 052b2d49ab2f..3cf1f82c471c 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Matching.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Matching.php @@ -13,7 +13,7 @@ trait Matching * Asserts that the property matches the expected value. * * @param string $key - * @param mixed|callable $expected + * @param mixed|\Closure $expected * @return $this */ public function where(string $key, $expected): self @@ -94,7 +94,7 @@ abstract protected function dotPath(string $key): string; * * @param string $key * @param null $value - * @param Closure|null $scope + * @param \Closure|null $scope * @return $this */ abstract public function has(string $key, $value = null, Closure $scope = null); From a5d9b455af1dc91bf73aee95147029fd8b540f78 Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Mon, 8 Mar 2021 17:22:22 +0000 Subject: [PATCH 05/13] [8.x] Support value callback arguments (#36506) * test * feature: support value function callback arguments --- src/Illuminate/Collections/helpers.php | 4 ++-- tests/Support/SupportHelpersTest.php | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Collections/helpers.php b/src/Illuminate/Collections/helpers.php index 6ae6dfe68a9b..67669e5ce1c6 100644 --- a/src/Illuminate/Collections/helpers.php +++ b/src/Illuminate/Collections/helpers.php @@ -179,8 +179,8 @@ function last($array) * @param mixed $value * @return mixed */ - function value($value) + function value($value, ...$args) { - return $value instanceof Closure ? $value() : $value; + return $value instanceof Closure ? $value(...$args) : $value; } } diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index af7de6b40f56..c286809fbe23 100755 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -40,6 +40,9 @@ public function testValue() $this->assertSame('foo', value(function () { return 'foo'; })); + $this->assertSame('foo', value(function ($arg) { + return $arg; + }, 'foo')); } public function testObjectGet() From 84f10d707df3c9a0517b5561bcbc60ccc912aee0 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 8 Mar 2021 11:49:04 -0600 Subject: [PATCH 06/13] formatting --- .../Fluent/{Assert.php => AssertableJson.php} | 5 +- .../Testing/Fluent/Concerns/Has.php | 6 +- src/Illuminate/Testing/TestResponse.php | 4 +- tests/Testing/Fluent/AssertTest.php | 118 +++++++++--------- tests/Testing/TestResponseTest.php | 5 +- 5 files changed, 71 insertions(+), 67 deletions(-) rename src/Illuminate/Testing/Fluent/{Assert.php => AssertableJson.php} (95%) diff --git a/src/Illuminate/Testing/Fluent/Assert.php b/src/Illuminate/Testing/Fluent/AssertableJson.php similarity index 95% rename from src/Illuminate/Testing/Fluent/Assert.php rename to src/Illuminate/Testing/Fluent/AssertableJson.php index bde937a0bf23..07104e114990 100644 --- a/src/Illuminate/Testing/Fluent/Assert.php +++ b/src/Illuminate/Testing/Fluent/AssertableJson.php @@ -10,7 +10,7 @@ use Illuminate\Testing\AssertableJsonString; use PHPUnit\Framework\Assert as PHPUnit; -class Assert implements Arrayable +class AssertableJson implements Arrayable { use Concerns\Has, Concerns\Matching, @@ -34,10 +34,11 @@ class Assert implements Arrayable private $path; /** - * Create a new Assert instance. + * Create a new fluent, assertable JSON data instance. * * @param array $props * @param string|null $path + * @return void */ protected function __construct(array $props, string $path = null) { diff --git a/src/Illuminate/Testing/Fluent/Concerns/Has.php b/src/Illuminate/Testing/Fluent/Concerns/Has.php index 19b9ad9915ca..dd91ee618790 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Has.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Has.php @@ -45,9 +45,9 @@ public function has(string $key, $value = null, Closure $scope = null): self $this->interactsWith($key); - // When all three arguments are provided, this indicates a short-hand - // expression that combines both a `count`-assertion, followed by - // directly creating a `scope` on the first element. + // When all three arguments are provided this indicates a short-hand expression + // that combines both a `count`-assertion, followed by directly creating the + // `scope` on the first element. We can simply handle this correctly here. if (is_int($value) && ! is_null($scope)) { $prop = $this->prop($key); $path = $this->dotPath($key); diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index eb983f464a6d..edc85e2a13df 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -15,6 +15,8 @@ use Illuminate\Testing\Assert as PHPUnit; use Illuminate\Testing\Constraints\SeeInOrder; use Illuminate\Testing\Fluent\Assert as FluentAssert; +use Illuminate\Testing\Fluent\AssertableJson; +use Illuminate\Testing\Fluent\FluentAssertableJson; use LogicException; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -519,7 +521,7 @@ public function assertJson($value, $strict = false) if (is_array($value)) { $json->assertSubset($value, $strict); } else { - $assert = FluentAssert::fromAssertableJsonString($json); + $assert = AssertableJson::fromAssertableJsonString($json); $value($assert); diff --git a/tests/Testing/Fluent/AssertTest.php b/tests/Testing/Fluent/AssertTest.php index f02e366f40a0..acafd07589d2 100644 --- a/tests/Testing/Fluent/AssertTest.php +++ b/tests/Testing/Fluent/AssertTest.php @@ -3,7 +3,7 @@ namespace Illuminate\Tests\Testing\Fluent; use Illuminate\Support\Collection; -use Illuminate\Testing\Fluent\Assert; +use Illuminate\Testing\Fluent\AssertableJson; use Illuminate\Tests\Testing\Stubs\ArrayableStubObject; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; @@ -14,7 +14,7 @@ class AssertTest extends TestCase { public function testAssertHas() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'prop' => 'value', ]); @@ -23,7 +23,7 @@ public function testAssertHas() public function testAssertHasFailsWhenPropMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'value', ]); @@ -35,7 +35,7 @@ public function testAssertHasFailsWhenPropMissing() public function testAssertHasNestedProp() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'example' => [ 'nested' => 'nested-value', ], @@ -46,7 +46,7 @@ public function testAssertHasNestedProp() public function testAssertHasFailsWhenNestedPropMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'example' => [ 'nested' => 'nested-value', ], @@ -60,7 +60,7 @@ public function testAssertHasFailsWhenNestedPropMissing() public function testAssertCountItemsInProp() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -72,7 +72,7 @@ public function testAssertCountItemsInProp() public function testAssertCountFailsWhenAmountOfItemsDoesNotMatch() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -87,7 +87,7 @@ public function testAssertCountFailsWhenAmountOfItemsDoesNotMatch() public function testAssertCountFailsWhenPropMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -102,7 +102,7 @@ public function testAssertCountFailsWhenPropMissing() public function testAssertHasFailsWhenSecondArgumentUnsupportedType() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'baz', ]); @@ -113,7 +113,7 @@ public function testAssertHasFailsWhenSecondArgumentUnsupportedType() public function testAssertMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => [ 'bar' => true, ], @@ -124,7 +124,7 @@ public function testAssertMissing() public function testAssertMissingFailsWhenPropExists() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'prop' => 'value', 'foo' => [ 'bar' => true, @@ -139,7 +139,7 @@ public function testAssertMissingFailsWhenPropExists() public function testAssertMissingAll() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'baz' => 'foo', ]); @@ -151,7 +151,7 @@ public function testAssertMissingAll() public function testAssertMissingAllFailsWhenAtLeastOnePropExists() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'baz' => 'foo', ]); @@ -166,7 +166,7 @@ public function testAssertMissingAllFailsWhenAtLeastOnePropExists() public function testAssertMissingAllAcceptsMultipleArgumentsInsteadOfArray() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'baz' => 'foo', ]); @@ -180,7 +180,7 @@ public function testAssertMissingAllAcceptsMultipleArgumentsInsteadOfArray() public function testAssertWhereMatchesValue() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'value', ]); @@ -189,7 +189,7 @@ public function testAssertWhereMatchesValue() public function testAssertWhereFailsWhenDoesNotMatchValue() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'value', ]); @@ -201,7 +201,7 @@ public function testAssertWhereFailsWhenDoesNotMatchValue() public function testAssertWhereFailsWhenMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'value', ]); @@ -213,7 +213,7 @@ public function testAssertWhereFailsWhenMissing() public function testAssertWhereFailsWhenMachingLoosely() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 1, ]); @@ -225,7 +225,7 @@ public function testAssertWhereFailsWhenMachingLoosely() public function testAssertWhereUsingClosure() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'baz', ]); @@ -236,7 +236,7 @@ public function testAssertWhereUsingClosure() public function testAssertWhereFailsWhenDoesNotMatchValueUsingClosure() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'baz', ]); @@ -250,7 +250,7 @@ public function testAssertWhereFailsWhenDoesNotMatchValueUsingClosure() public function testAssertWhereClosureArrayValuesAreAutomaticallyCastedToCollections() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'foo', 'example' => 'value', @@ -268,7 +268,7 @@ public function testAssertWhereMatchesValueUsingArrayable() { $stub = ArrayableStubObject::make(['foo' => 'bar']); - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => $stub->toArray(), ]); @@ -277,7 +277,7 @@ public function testAssertWhereMatchesValueUsingArrayable() public function testAssertWhereMatchesValueUsingArrayableWhenSortedDifferently() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'foo', 'example' => 'value', @@ -293,7 +293,7 @@ public function testAssertWhereMatchesValueUsingArrayableWhenSortedDifferently() public function testAssertWhereFailsWhenDoesNotMatchValueUsingArrayable() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => ['id' => 1, 'name' => 'Example'], 'baz' => [ 'id' => 1, @@ -319,7 +319,7 @@ public function testAssertWhereFailsWhenDoesNotMatchValueUsingArrayable() public function testAssertNestedWhereMatchesValue() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'example' => [ 'nested' => 'nested-value', ], @@ -330,7 +330,7 @@ public function testAssertNestedWhereMatchesValue() public function testAssertNestedWhereFailsWhenDoesNotMatchValue() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'example' => [ 'nested' => 'nested-value', ], @@ -344,7 +344,7 @@ public function testAssertNestedWhereFailsWhenDoesNotMatchValue() public function testScope() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -352,7 +352,7 @@ public function testScope() ]); $called = false; - $assert->has('bar', function (Assert $assert) use (&$called) { + $assert->has('bar', function (AssertableJson $assert) use (&$called) { $called = true; $assert ->where('baz', 'example') @@ -364,7 +364,7 @@ public function testScope() public function testScopeFailsWhenPropMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -374,28 +374,28 @@ public function testScopeFailsWhenPropMissing() $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Property [baz] does not exist.'); - $assert->has('baz', function (Assert $item) { + $assert->has('baz', function (AssertableJson $item) { $item->where('baz', 'example'); }); } public function testScopeFailsWhenPropSingleValue() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'value', ]); $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Property [bar] is not scopeable.'); - $assert->has('bar', function (Assert $item) { + $assert->has('bar', function (AssertableJson $item) { // }); } public function testScopeShorthand() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ ['key' => 'first'], ['key' => 'second'], @@ -403,7 +403,7 @@ public function testScopeShorthand() ]); $called = false; - $assert->has('bar', 2, function (Assert $item) use (&$called) { + $assert->has('bar', 2, function (AssertableJson $item) use (&$called) { $item->where('key', 'first'); $called = true; }); @@ -413,7 +413,7 @@ public function testScopeShorthand() public function testScopeShorthandFailsWhenAssertingZeroItems() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ ['key' => 'first'], ['key' => 'second'], @@ -423,14 +423,14 @@ public function testScopeShorthandFailsWhenAssertingZeroItems() $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Cannot scope directly onto the first entry of property [bar] when asserting that it has a size of 0.'); - $assert->has('bar', 0, function (Assert $item) { + $assert->has('bar', 0, function (AssertableJson $item) { $item->where('key', 'first'); }); } public function testScopeShorthandFailsWhenAmountOfItemsDoesNotMatch() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ ['key' => 'first'], ['key' => 'second'], @@ -440,14 +440,14 @@ public function testScopeShorthandFailsWhenAmountOfItemsDoesNotMatch() $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Property [bar] does not have the expected size.'); - $assert->has('bar', 1, function (Assert $item) { + $assert->has('bar', 1, function (AssertableJson $item) { $item->where('key', 'first'); }); } public function testFailsWhenNotInteractingWithAllPropsInScope() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -457,28 +457,28 @@ public function testFailsWhenNotInteractingWithAllPropsInScope() $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Unexpected properties were found in scope [bar].'); - $assert->has('bar', function (Assert $item) { + $assert->has('bar', function (AssertableJson $item) { $item->where('baz', 'example'); }); } public function testDisableInteractionCheckForCurrentScope() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', ], ]); - $assert->has('bar', function (Assert $item) { + $assert->has('bar', function (AssertableJson $item) { $item->etc(); }); } public function testCannotDisableInteractionCheckForDifferentScopes() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => [ 'foo' => 'bar', @@ -491,10 +491,10 @@ public function testCannotDisableInteractionCheckForDifferentScopes() $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Unexpected properties were found in scope [bar.baz].'); - $assert->has('bar', function (Assert $item) { + $assert->has('bar', function (AssertableJson $item) { $item ->etc() - ->has('baz', function (Assert $item) { + ->has('baz', function (AssertableJson $item) { // }); }); @@ -502,7 +502,7 @@ public function testCannotDisableInteractionCheckForDifferentScopes() public function testTopLevelPropInteractionDisabledByDefault() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => 'bar', 'bar' => 'baz', ]); @@ -512,7 +512,7 @@ public function testTopLevelPropInteractionDisabledByDefault() public function testTopLevelInteractionEnabledWhenInteractedFlagSet() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => 'bar', 'bar' => 'baz', ]); @@ -527,7 +527,7 @@ public function testTopLevelInteractionEnabledWhenInteractedFlagSet() public function testAssertWhereAllMatchesValues() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => [ 'bar' => 'value', 'example' => ['hello' => 'world'], @@ -546,7 +546,7 @@ public function testAssertWhereAllMatchesValues() public function testAssertWhereAllFailsWhenAtLeastOnePropDoesNotMatchValue() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => 'bar', 'baz' => 'example', ]); @@ -564,7 +564,7 @@ public function testAssertWhereAllFailsWhenAtLeastOnePropDoesNotMatchValue() public function testAssertHasAll() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => [ 'bar' => 'value', 'example' => ['hello' => 'world'], @@ -581,7 +581,7 @@ public function testAssertHasAll() public function testAssertHasAllFailsWhenAtLeastOnePropMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => [ 'bar' => 'value', 'example' => ['hello' => 'world'], @@ -601,7 +601,7 @@ public function testAssertHasAllFailsWhenAtLeastOnePropMissing() public function testAssertHasAllAcceptsMultipleArgumentsInsteadOfArray() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => [ 'bar' => 'value', 'example' => ['hello' => 'world'], @@ -619,7 +619,7 @@ public function testAssertHasAllAcceptsMultipleArgumentsInsteadOfArray() public function testAssertCountMultipleProps() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'key' => 'value', 'prop' => 'example', @@ -637,7 +637,7 @@ public function testAssertCountMultipleProps() public function testAssertCountMultiplePropsFailsWhenPropMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'key' => 'value', 'prop' => 'example', @@ -655,20 +655,20 @@ public function testAssertCountMultiplePropsFailsWhenPropMissing() public function testMacroable() { - Assert::macro('myCustomMacro', function () { + AssertableJson::macro('myCustomMacro', function () { throw new RuntimeException('My Custom Macro was called!'); }); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('My Custom Macro was called!'); - $assert = Assert::fromArray(['foo' => 'bar']); + $assert = AssertableJson::fromArray(['foo' => 'bar']); $assert->myCustomMacro(); } public function testTappable() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -676,9 +676,9 @@ public function testTappable() ]); $called = false; - $assert->has('bar', function (Assert $assert) use (&$called) { + $assert->has('bar', function (AssertableJson $assert) use (&$called) { $assert->etc(); - $assert->tap(function (Assert $assert) use (&$called) { + $assert->tap(function (AssertableJson $assert) use (&$called) { $called = true; }); }); diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 055519925cb9..1ac9ebba7373 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -10,6 +10,7 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Http\Response; use Illuminate\Testing\Fluent\Assert; +use Illuminate\Testing\Fluent\AssertableJson; use Illuminate\Testing\TestResponse; use JsonSerializable; use Mockery as m; @@ -582,7 +583,7 @@ public function testAssertJsonWithFluent() { $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableSingleResourceStub)); - $response->assertJson(function (Assert $json) { + $response->assertJson(function (AssertableJson $json) { $json->where('0.foo', 'foo 0'); }); } @@ -594,7 +595,7 @@ public function testAssertJsonWithFluentStrict() $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Unexpected properties were found on the root level.'); - $response->assertJson(function (Assert $json) { + $response->assertJson(function (AssertableJson $json) { $json->where('0.foo', 'foo 0'); }, true); } From 0798479b95fd241c829b0e36a1d6f8573a586738 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 8 Mar 2021 11:52:35 -0600 Subject: [PATCH 07/13] Apply fixes from StyleCI (#36508) --- src/Illuminate/Testing/TestResponse.php | 2 -- tests/Testing/TestResponseTest.php | 1 - 2 files changed, 3 deletions(-) diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index edc85e2a13df..aeee4fe59c7e 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -14,9 +14,7 @@ use Illuminate\Support\Traits\Tappable; use Illuminate\Testing\Assert as PHPUnit; use Illuminate\Testing\Constraints\SeeInOrder; -use Illuminate\Testing\Fluent\Assert as FluentAssert; use Illuminate\Testing\Fluent\AssertableJson; -use Illuminate\Testing\Fluent\FluentAssertableJson; use LogicException; use Symfony\Component\HttpFoundation\StreamedResponse; diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 1ac9ebba7373..b6a1e5531a54 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -9,7 +9,6 @@ use Illuminate\Encryption\Encrypter; use Illuminate\Filesystem\Filesystem; use Illuminate\Http\Response; -use Illuminate\Testing\Fluent\Assert; use Illuminate\Testing\Fluent\AssertableJson; use Illuminate\Testing\TestResponse; use JsonSerializable; From 55a14074115486b3f4a6e7f4b25dedd9981c5d6e Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Tue, 9 Mar 2021 13:43:43 +0200 Subject: [PATCH 08/13] ability to slow the workers down --- src/Illuminate/Queue/Console/WorkCommand.php | 4 +++- src/Illuminate/Queue/Worker.php | 2 ++ src/Illuminate/Queue/WorkerOptions.php | 11 ++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Queue/Console/WorkCommand.php b/src/Illuminate/Queue/Console/WorkCommand.php index ff092197f53a..da9176be4063 100644 --- a/src/Illuminate/Queue/Console/WorkCommand.php +++ b/src/Illuminate/Queue/Console/WorkCommand.php @@ -33,6 +33,7 @@ class WorkCommand extends Command {--force : Force the worker to run even in maintenance mode} {--memory=128 : The memory limit in megabytes} {--sleep=3 : Number of seconds to sleep when no job is available} + {--rest=0 : Number of seconds to rest between jobs} {--timeout=60 : The number of seconds a child process can run} {--tries=1 : Number of times to attempt a job before logging it failed}'; @@ -134,7 +135,8 @@ protected function gatherWorkerOptions() $this->option('force'), $this->option('stop-when-empty'), $this->option('max-jobs'), - $this->option('max-time') + $this->option('max-time'), + $this->option('rest') ); } diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index f2ba7b1a01ad..4fcacb26d009 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -156,6 +156,8 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $jobsProcessed++; $this->runJob($job, $connectionName, $options); + + $this->sleep($options->rest); } else { $this->sleep($options->sleep); } diff --git a/src/Illuminate/Queue/WorkerOptions.php b/src/Illuminate/Queue/WorkerOptions.php index 766f4676029a..7e99457e70d1 100644 --- a/src/Illuminate/Queue/WorkerOptions.php +++ b/src/Illuminate/Queue/WorkerOptions.php @@ -74,6 +74,13 @@ class WorkerOptions */ public $maxTime; + /** + * The number of seconds to rest between jobs. + * + * @var int + */ + public $rest; + /** * Create a new worker options instance. * @@ -87,10 +94,11 @@ class WorkerOptions * @param bool $stopWhenEmpty * @param int $maxJobs * @param int $maxTime + * @param int $rest * @return void */ public function __construct($name = 'default', $backoff = 0, $memory = 128, $timeout = 60, $sleep = 3, $maxTries = 1, - $force = false, $stopWhenEmpty = false, $maxJobs = 0, $maxTime = 0) + $force = false, $stopWhenEmpty = false, $maxJobs = 0, $maxTime = 0, $rest = 0) { $this->name = $name; $this->backoff = $backoff; @@ -102,5 +110,6 @@ public function __construct($name = 'default', $backoff = 0, $memory = 128, $tim $this->stopWhenEmpty = $stopWhenEmpty; $this->maxJobs = $maxJobs; $this->maxTime = $maxTime; + $this->rest = $rest; } } From d9a33b1812c4c0269fd652fc0e534c140afc489e Mon Sep 17 00:00:00 2001 From: Rodrigo Pedra Brum Date: Tue, 9 Mar 2021 10:28:43 -0300 Subject: [PATCH 09/13] [8.x] Allow to override discover events base path (#36515) * Allow to override discover events base path * Update EventServiceProvider.php Co-authored-by: Taylor Otwell --- .../Support/Providers/EventServiceProvider.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php index 0573563cf5ac..70ea3086efe9 100644 --- a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php +++ b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php @@ -119,7 +119,7 @@ public function discoverEvents() ->reduce(function ($discovered, $directory) { return array_merge_recursive( $discovered, - DiscoverEvents::within($directory, base_path()) + DiscoverEvents::within($directory, $this->eventDiscoveryBasePath()) ); }, []); } @@ -135,4 +135,14 @@ protected function discoverEventsWithin() $this->app->path('Listeners'), ]; } + + /** + * Get the base path to be used during event discovery. + * + * @return string + */ + protected function eventDiscoveryBasePath() + { + return base_path(); + } } From a4678ce95cb9d4f10ec789843cee78df4acceb35 Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Tue, 9 Mar 2021 23:29:22 +1000 Subject: [PATCH 10/13] Add prohibited_if and prohibited_unless validation rules (#36516) --- .../Concerns/ReplacesAttributes.php | 40 +++++++++++ .../Concerns/ValidatesAttributes.php | 42 +++++++++++ src/Illuminate/Validation/Validator.php | 2 + tests/Validation/ValidationValidatorTest.php | 72 +++++++++++++++++++ 4 files changed, 156 insertions(+) diff --git a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php index d645dbd6d5a6..d4a47af146c4 100644 --- a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php @@ -374,6 +374,46 @@ protected function replaceRequiredUnless($message, $attribute, $rule, $parameter return str_replace([':other', ':values'], [$other, implode(', ', $values)], $message); } + /** + * Replace all place-holders for the prohibited_if rule. + * + * @param string $message + * @param string $attribute + * @param string $rule + * @param array $parameters + * @return string + */ + protected function replaceProhibitedIf($message, $attribute, $rule, $parameters) + { + $parameters[1] = $this->getDisplayableValue($parameters[0], Arr::get($this->data, $parameters[0])); + + $parameters[0] = $this->getDisplayableAttribute($parameters[0]); + + return str_replace([':other', ':value'], $parameters, $message); + } + + /** + * Replace all place-holders for the prohibited_unless rule. + * + * @param string $message + * @param string $attribute + * @param string $rule + * @param array $parameters + * @return string + */ + protected function replaceProhibitedUnless($message, $attribute, $rule, $parameters) + { + $other = $this->getDisplayableAttribute($parameters[0]); + + $values = []; + + foreach (array_slice($parameters, 1) as $value) { + $values[] = $this->getDisplayableValue($parameters[0], $value); + } + + return str_replace([':other', ':values'], [$other, implode(', ', $values)], $message); + } + /** * Replace all place-holders for the same rule. * diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index bba094dd61f6..2f1dcf6a2157 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -1434,6 +1434,48 @@ public function validateRequiredIf($attribute, $value, $parameters) return true; } + /** + * Validate that an attribute does not exist when another attribute has a given value. + * + * @param string $attribute + * @param mixed $value + * @param mixed $parameters + * @return bool + */ + public function validateProhibitedIf($attribute, $value, $parameters) + { + $this->requireParameterCount(2, $parameters, 'prohibited_if'); + + [$values, $other] = $this->parseDependentRuleParameters($parameters); + + if (in_array($other, $values, is_bool($other))) { + return ! $this->validateRequired($attribute, $value); + } + + return true; + } + + /** + * Validate that an attribute does not exist unless another attribute has a given value. + * + * @param string $attribute + * @param mixed $value + * @param mixed $parameters + * @return bool + */ + public function validateProhibitedUnless($attribute, $value, $parameters) + { + $this->requireParameterCount(2, $parameters, 'prohibited_unless'); + + [$values, $other] = $this->parseDependentRuleParameters($parameters); + + if (! in_array($other, $values, is_bool($other))) { + return ! $this->validateRequired($attribute, $value); + } + + return true; + } + /** * Indicate that an attribute should be excluded when another attribute has a given value. * diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 48d9946386d6..0aa44d6c197e 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -227,6 +227,8 @@ class Validator implements ValidatorContract 'RequiredWithAll', 'RequiredWithout', 'RequiredWithoutAll', + 'ProhibitedIf', + 'ProhibitedUnless', 'Same', 'Unique', ]; diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index a60b93b9abf4..7f21e4ba1688 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -1146,6 +1146,78 @@ public function testRequiredUnless() $this->assertSame('The last field is required unless first is in taylor, sven.', $v->messages()->first('last')); } + public function testProhibitedIf() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor', 'last' => 'otwell'], ['last' => 'prohibited_if:first,taylor']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor'], ['last' => 'prohibited_if:first,taylor']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor', 'last' => 'otwell'], ['last' => 'prohibited_if:first,taylor,jess']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor'], ['last' => 'prohibited_if:first,taylor,jess']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => true, 'bar' => 'baz'], ['bar' => 'prohibited_if:foo,false']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => true, 'bar' => 'baz'], ['bar' => 'prohibited_if:foo,true']); + $this->assertTrue($v->fails()); + + // error message when passed multiple values (prohibited_if:foo,bar,baz) + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines(['validation.prohibited_if' => 'The :attribute field is prohibited when :other is :value.'], 'en'); + $v = new Validator($trans, ['first' => 'jess', 'last' => 'archer'], ['last' => 'prohibited_if:first,taylor,jess']); + $this->assertFalse($v->passes()); + $this->assertSame('The last field is prohibited when first is jess.', $v->messages()->first('last')); + } + + public function testProhibitedUnless() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'jess', 'last' => 'archer'], ['last' => 'prohibited_unless:first,taylor']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor', 'last' => 'otwell'], ['last' => 'prohibited_unless:first,taylor']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'jess'], ['last' => 'prohibited_unless:first,taylor']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor', 'last' => 'otwell'], ['last' => 'prohibited_unless:first,taylor,jess']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'jess', 'last' => 'archer'], ['last' => 'prohibited_unless:first,taylor,jess']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => false, 'bar' => 'baz'], ['bar' => 'prohibited_unless:foo,false']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => false, 'bar' => 'baz'], ['bar' => 'prohibited_unless:foo,true']); + $this->assertTrue($v->fails()); + + // error message when passed multiple values (prohibited_unless:foo,bar,baz) + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines(['validation.prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.'], 'en'); + $v = new Validator($trans, ['first' => 'tim', 'last' => 'macdonald'], ['last' => 'prohibitedUnless:first,taylor,jess']); + $this->assertFalse($v->passes()); + $this->assertSame('The last field is prohibited unless first is in taylor, jess.', $v->messages()->first('last')); + } + public function testFailedFileUploads() { $trans = $this->getIlluminateArrayTranslator(); From 37e48ba864e2f463517429d41cefd94e88136c1c Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 9 Mar 2021 08:03:10 -0600 Subject: [PATCH 11/13] formatting and fix key usage --- .../Queue/Middleware/ThrottlesExceptions.php | 52 ++++++++++++++----- .../Queue/ThrottlesExceptionsTest.php | 7 ++- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php index edb3dd164d33..3fff4e914b55 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php @@ -8,6 +8,13 @@ class ThrottlesExceptions { + /** + * The developer specified key that the rate limiter should use. + * + * @var string + */ + protected $key; + /** * The maximum number of attempts allowed before rate limiting applies. * @@ -27,14 +34,7 @@ class ThrottlesExceptions * * @var int */ - protected $retryAfterMinutes; - - /** - * The rate limiter key. - * - * @var string - */ - protected $key; + protected $retryAfterMinutes = 0; /** * The callback that determines if rate limiting should apply. @@ -48,7 +48,7 @@ class ThrottlesExceptions * * @var string */ - protected $prefix = 'circuit_breaker:'; + protected $prefix = 'laravel_throttles_exceptions:'; /** * The rate limiter instance. @@ -62,15 +62,13 @@ class ThrottlesExceptions * * @param int $maxAttempts * @param int $decayMinutes - * @param int $retryAfterMinutes * @param string $key + * @return void */ - public function __construct($maxAttempts = 10, $decayMinutes = 10, $retryAfterMinutes = 0, string $key = '') + public function __construct($maxAttempts = 10, $decayMinutes = 10) { $this->maxAttempts = $maxAttempts; $this->decayMinutes = $decayMinutes; - $this->retryAfterMinutes = $retryAfterMinutes; - $this->key = $key; } /** @@ -129,6 +127,19 @@ public function withPrefix(string $prefix) return $this; } + /** + * Specify the number of seconds a job should be delayed when it is released (before it has reached its max exceptions). + * + * @param int $backoff + * @return $this + */ + public function backoff($backoff) + { + $this->retryAfterMinutes = $backoff; + + return $this; + } + /** * Get the cache key associated for the rate limiter. * @@ -137,7 +148,20 @@ public function withPrefix(string $prefix) */ protected function getKey($job) { - return $this->prefix.md5(empty($this->key) ? get_class($job) : $this->key); + return $this->key ? $this->prefix.$this->key : $this->prefix.$job->job->uuid(); + } + + /** + * Set the value that the rate limiter should be keyed by. + * + * @param string $key + * @return $this + */ + public function by($key) + { + $this->key = $key; + + return $this; } /** diff --git a/tests/Integration/Queue/ThrottlesExceptionsTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php index b51190e41654..002acc30c661 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -58,6 +58,7 @@ protected function assertJobWasReleasedImmediately($class) $job->shouldReceive('release')->with(0)->once(); $job->shouldReceive('isReleased')->andReturn(true); $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + $job->shouldReceive('uuid')->andReturn('simple-test-uuid'); $instance->call($job, [ 'command' => serialize($command = new $class), @@ -79,6 +80,7 @@ protected function assertJobWasReleasedWithDelay($class) })->once(); $job->shouldReceive('isReleased')->andReturn(true); $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + $job->shouldReceive('uuid')->andReturn('simple-test-uuid'); $instance->call($job, [ 'command' => serialize($command = new $class), @@ -98,6 +100,7 @@ protected function assertJobRanSuccessfully($class) $job->shouldReceive('isReleased')->andReturn(false); $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(false); $job->shouldReceive('delete')->once(); + $job->shouldReceive('uuid')->andReturn('simple-test-uuid'); $instance->call($job, [ 'command' => serialize($command = new $class), @@ -122,7 +125,7 @@ public function handle() public function middleware() { - return [new ThrottlesExceptions(2, 10, 0, 'test')]; + return [(new ThrottlesExceptions(2, 10))->by('test')]; } } @@ -139,6 +142,6 @@ public function handle() public function middleware() { - return [new ThrottlesExceptions(2, 10, 0, 'test')]; + return [(new ThrottlesExceptions(2, 10))->by('test')]; } } From 0443f1c42c20f6a30d0d81050bc43e94c9c51145 Mon Sep 17 00:00:00 2001 From: Jason McCreary Date: Tue, 9 Mar 2021 09:06:15 -0500 Subject: [PATCH 12/13] Add class argument (#36513) --- .../Database/Console/Seeds/SeedCommand.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Console/Seeds/SeedCommand.php b/src/Illuminate/Database/Console/Seeds/SeedCommand.php index ccca6fd5eeda..058e545c234f 100644 --- a/src/Illuminate/Database/Console/Seeds/SeedCommand.php +++ b/src/Illuminate/Database/Console/Seeds/SeedCommand.php @@ -6,6 +6,7 @@ use Illuminate\Console\ConfirmableTrait; use Illuminate\Database\ConnectionResolverInterface as Resolver; use Illuminate\Database\Eloquent\Model; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; class SeedCommand extends Command @@ -81,7 +82,7 @@ public function handle() */ protected function getSeeder() { - $class = $this->input->getOption('class'); + $class = $this->input->getArgument('class') ?? $this->input->getOption('class'); if (strpos($class, '\\') === false) { $class = 'Database\\Seeders\\'.$class; @@ -109,6 +110,18 @@ protected function getDatabase() return $database ?: $this->laravel['config']['database.default']; } + /** + * Get the console command arguments. + * + * @return array + */ + protected function getArguments() + { + return [ + ['class', InputArgument::OPTIONAL, 'The class name of the root seeder', null], + ]; + } + /** * Get the console command options. * From c6ea49c80a2ac93aebb8fdf2360161b73cec26af Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 9 Mar 2021 08:15:40 -0600 Subject: [PATCH 13/13] formatting --- src/Illuminate/Queue/Worker.php | 4 +++- src/Illuminate/Queue/WorkerOptions.php | 16 ++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index 4fcacb26d009..4229fe701691 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -157,7 +157,9 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $this->runJob($job, $connectionName, $options); - $this->sleep($options->rest); + if ($options->rest > 0) { + $this->sleep($options->rest); + } } else { $this->sleep($options->sleep); } diff --git a/src/Illuminate/Queue/WorkerOptions.php b/src/Illuminate/Queue/WorkerOptions.php index 7e99457e70d1..7b8d8dfeea3b 100644 --- a/src/Illuminate/Queue/WorkerOptions.php +++ b/src/Illuminate/Queue/WorkerOptions.php @@ -39,6 +39,13 @@ class WorkerOptions */ public $sleep; + /** + * The number of seconds to rest between jobs. + * + * @var int + */ + public $rest; + /** * The maximum amount of times a job may be attempted. * @@ -74,13 +81,6 @@ class WorkerOptions */ public $maxTime; - /** - * The number of seconds to rest between jobs. - * - * @var int - */ - public $rest; - /** * Create a new worker options instance. * @@ -103,6 +103,7 @@ public function __construct($name = 'default', $backoff = 0, $memory = 128, $tim $this->name = $name; $this->backoff = $backoff; $this->sleep = $sleep; + $this->rest = $rest; $this->force = $force; $this->memory = $memory; $this->timeout = $timeout; @@ -110,6 +111,5 @@ public function __construct($name = 'default', $backoff = 0, $memory = 128, $tim $this->stopWhenEmpty = $stopWhenEmpty; $this->maxJobs = $maxJobs; $this->maxTime = $maxTime; - $this->rest = $rest; } }