Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v8.52.0] Invalid JSON was returned from the route. #38178

Closed
kocoten1992 opened this issue Jul 29, 2021 · 3 comments · Fixed by #38192
Closed

[v8.52.0] Invalid JSON was returned from the route. #38178

kocoten1992 opened this issue Jul 29, 2021 · 3 comments · Fixed by #38192

Comments

@kocoten1992
Copy link

kocoten1992 commented Jul 29, 2021

  • Laravel Version: v8.52.0
  • PHP Version: PHP 8.0.8
  • Database Driver & Version: not relevant

Description:

This code work on v8.51.0, but fail on v8.52.0

Steps To Reproduce:

On any controller:

return response()->json(false);

On testing:

$response = $this->get('/');
  
$response->assertStatus(200);
# v8.51.0
   PASS  Tests\Feature\ExampleTest
  ✓ example

  Tests:  2 passed
  Time:   0.10s

# v8.52.0
   FAIL  Tests\Feature\ExampleTest
  ⨯ example

  ---

  • Tests\Feature\ExampleTest > example
  Invalid JSON was returned from the route.

  at tests/Feature/ExampleTest.php:19
     15▕     public function test_example()
     16▕     {
     17▕         $response = $this->get('/');
     18▕ 
  ➜  19▕         $response->assertStatus(200);
     20▕     }
     21▕ }
     22▕ 
@derekmd
Copy link
Contributor

derekmd commented Jul 29, 2021

Laravel currently can't make assertions on response()->json(false)

Your example shows an edge case gap in Illuminate\Testing\TestResponse's ability to make assertions on response()->json(false).

Running the below 3 tests with v8.51.0, the top 2 pass but the last case fails:

public function testResponseJsonNull()
{
    \Illuminate\Support\Facades\Route::get(
        '/json-null',
        fn () => response()->json(null)
    );

    $response = $this->get('/json-null');
    $this->assertSame('{}', $response->content()); // passes
    dump($response->json()); // prints []
    $response->assertStatus(200); // passes
}

public function testResponseJsonTrue()
{
    \Illuminate\Support\Facades\Route::get(
        '/json-true',
        fn () => response()->json(true)
    );

    $response = $this->get('/json-true');
    $this->assertSame('true', $response->content()); // passes
    dump($response->json()); // prints true
    $response->assertStatus(200); // passes
}

public function testResponseJsonFalse()
{
    \Illuminate\Support\Facades\Route::get(
        '/json-false',
        fn () => response()->json(false)
    );

    $response = $this->get('/json-false');
    $this->assertSame('false', $response->content()); // passes
    dump($response->json()); // throws PHPUnit::fail('Invalid JSON was returned from the route.')
    $response->assertStatus(200); // assertion not reached
}

This is caused by the error checking in Illuminate\Testing\TestResponse@decodeResponseJson():

if (is_null($decodedResponse) || $decodedResponse === false) {

The $decodedResponse === false expression is checking for Illuminate\Testing\AssertableJsonString's constructor assigning false to property AssertableJsonString@decoded:

if ($jsonable instanceof JsonSerializable) {
$this->decoded = $jsonable->jsonSerialize();
} elseif ($jsonable instanceof Jsonable) {
$this->decoded = json_decode($jsonable->toJson(), true);
} elseif (is_array($jsonable)) {
$this->decoded = $jsonable;
} else {
$this->decoded = json_decode($jsonable, true);
}

But in this API response, HTTP response body string false is JSON false that should be transformed to PHP bool value false.


v8.52.0 changes causing the exception

The exception is now occurring because v8.52.0 started showing JSON response validation errors in test output on a status code mismatch. Introduced by #38088, a custom PHPUnit feedback message string is computed when asserting the status code integer match like 200 HTTP OK.

So this new code:

if ($this->baseResponse->headers->get('Content-Type') === 'application/json') {
$json = $this->json();
if (isset($json['errors'])) {
return $this->statusMessageWithErrors($expected, $actual, $json);
}
}

Is now attempting to decode the response body JSON string on status code assertions before any assert*Json() methods are called.


Possible framework code fix

A potential fix would be to add a new AssertableJsonString method:

class AssertableJsonString implements ArrayAccess, Countable
{
    // ...

    public function hasInvalidJson()
    {
        return $this->decoded === null || (
            $this->decoded === false && $this->json !== 'false' && ! (
                $this->$json instanceof Jsonable && $jsonable->toJson() === false
            )
        );
    }
}

Then make TestResponse@decodeResponseJson() only throw an exception when that returns true.

I'm not sure why false is being checked since json_decode() returns null upon parse failure. But the decodeResponseJson() method infers that the AssertableJsonString constructor accepting $jsonable value JsonSerializable@jsonSerialize() === false is an error condition.

Edit: Looking at when decodeResponseJson() was added, its false checks seem to be rooted in making assertions on associative array payloads. 5f4a991 You can't make key/value assertions on a false payload. The method was later used by other JSON assertion methods where the false error check doesn't make sense.

@derekmd
Copy link
Contributor

derekmd commented Jul 31, 2021

https://github.com/laravel/framework/compare/8.x...derekmd:assert-status-doesnt-fail-on-json-false?expand=1 will at least fix assertStatus() failing.

Before submitting I'll wait on today's #38189 to be merged to avoid Git conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants
@derekmd @kocoten1992 and others