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.
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.
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!
"Houston: no signs of life"
Start the conversation!
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.0.*", // v3.0.3
"doctrine/orm": "^2.5", // v2.5.4
"doctrine/doctrine-bundle": "^1.6", // 1.6.2
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.3.11
"symfony/monolog-bundle": "^2.8", // v2.10.0
"sensio/distribution-bundle": "^5.0", // v5.0.4
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.14
"incenteev/composer-parameter-handler": "~2.0", // v2.1.2
"jms/serializer-bundle": "^1.1.0", // 1.1.0
"white-october/pagerfanta-bundle": "^1.0", // v1.0.5
"lexik/jwt-authentication-bundle": "^1.4", // v1.4.3
"willdurand/hateoas-bundle": "^1.1" // 1.1.1
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.0.6
"symfony/phpunit-bridge": "^3.0", // v3.0.3
"behat/behat": "~3.1@dev", // dev-master
"behat/mink-extension": "~2.2.0", // v2.2
"behat/mink-goutte-driver": "~1.2.0", // v1.2.1
"behat/mink-selenium2-driver": "~1.3.0", // v1.3.1
"phpunit/phpunit": "~4.6.0", // 4.6.10
"doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
}
}