Adding Battle Validation
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIt doesn't make any sense to create a battle without a Programmer or a Project. But guess what - you can! Or at least, you kind of can: we don't have validation to prevent that yet!
The validation system we created in earlier courses is air-tight: as long as we add the constraint annotations, it just works. So normally, I might not write a test for failing validation. But I will now... because we're going to add a twist.
Testing for Validation
Add a new public function testPOSTBattleValidationErrors()
:
// ... lines 1 - 6 | |
class BattleControllerTest extends ApiTestCase | |
{ | |
// ... lines 9 - 44 | |
public function testPOSTBattleValidationErrors() | |
{ | |
// ... lines 47 - 64 | |
} | |
} |
Copy the first bits from the previous function that create the data and make the request:
// ... lines 1 - 6 | |
class BattleControllerTest extends ApiTestCase | |
{ | |
// ... lines 9 - 44 | |
public function testPOSTBattleValidationErrors() | |
{ | |
$programmer = $this->createProgrammer([ | |
'nickname' => 'Fred' | |
], 'weaverryan'); | |
$data = array( | |
'projectId' => null, | |
'programmerId' => $programmer->getId() | |
); | |
// 1) Create a programmer resource | |
$response = $this->client->post('/api/battles', [ | |
'body' => json_encode($data), | |
'headers' => $this->getAuthorizedHeaders('weaverryan') | |
]); | |
// ... lines 62 - 64 | |
} | |
} |
But, don't actually create a project! Instead, send null
for the projectId
. Since starting a battle against nothing is nonsense, assert that 400 is the response status code. This follows the pattern we did before in ProgrammerControllerTest
.
And actually, that test shows off the validation errors response format: there should be an errors
key with field names for the errors below that. Each field could technically have multiple errors, so that's an array:
// ... lines 1 - 6 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
// ... lines 9 - 211 | |
public function testValidationErrors() | |
{ | |
// ... lines 214 - 230 | |
$this->asserter()->assertResponsePropertyExists($response, 'errors.nickname'); | |
$this->asserter()->assertResponsePropertyEquals($response, 'errors.nickname[0]', 'Please enter a clever nickname'); | |
// ... lines 233 - 234 | |
} | |
// ... lines 236 - 288 | |
} |
Check for the error in our code with $this->asserter()->assertResponsePropertyExists()
: the field should be errors.projectId
:
// ... lines 1 - 6 | |
class BattleControllerTest extends ApiTestCase | |
{ | |
// ... lines 9 - 44 | |
public function testPOSTBattleValidationErrors() | |
{ | |
// ... lines 47 - 61 | |
$this->assertEquals(400, $response->getStatusCode()); | |
$this->asserter()->assertResponsePropertyExists($response, 'errors.projectId'); | |
// ... line 64 | |
} | |
} |
Next, check for the exact message: assertResponsePropertyEquals()
with errors.projectId[0]
- so the first and only error - set to This value should not be blank.
:
// ... lines 1 - 6 | |
class BattleControllerTest extends ApiTestCase | |
{ | |
// ... lines 9 - 44 | |
public function testPOSTBattleValidationErrors() | |
{ | |
// ... lines 47 - 61 | |
$this->assertEquals(400, $response->getStatusCode()); | |
$this->asserter()->assertResponsePropertyExists($response, 'errors.projectId'); | |
$this->asserter()->assertResponsePropertyEquals($response, 'errors.projectId[0]', 'This value should not be blank.'); | |
} | |
} |
Why that message? That's the default message for Symfony's NotBlank
constraint.
Before we code this up, copy the method name and run the test:
./vendor/bin/phpunit --filter testPOSTBattleValidationErrors
It explodes with a 500 error! This is what happens when you're lazy and forget to add validation: the BattleManager
panics because there is no Project
. We do not want 500 errors, they are not hipster.
Adding Basic Validation
We know how to fix this! Go to BattleModel
. Remember, this is the class that's bound to the form: so the annotations should go here. First, add the use
statement. Type use NotBlank
, let it auto-complete, delete the last part and add the normal as Assert
:
// ... lines 1 - 6 | |
use Symfony\Component\Validator\Constraints as Assert; | |
// ... lines 8 - 40 |
That's my shortcut to get the use
statement.
Now, above project
, add @Assert\NotBlank()
. Do the same above programmer
: @Assert\NotBlank()
:
// ... lines 1 - 8 | |
class BattleModel | |
{ | |
/** | |
* @Assert\NotBlank() | |
*/ | |
private $project; | |
/** | |
* @Assert\NotBlank() | |
*/ | |
private $programmer; | |
// ... lines 20 - 39 | |
} |
Done! Now run the test:
./vendor/bin/phpunit --filter testPostBattleValidationErrors
We're awesome! Or are we... there's a deeper problem! What prevents an API client from starting a battle with a Programmer
that they do not own? Right now - nothing, besides karma and trusting that humankind will do the right thing. Unfortunately, that doesn't usually pass a security audit. Let's be heros and fix this security hole!