User Feature - Part 2


In this video we will continue writing out the User feature to flesh out the expected scope of handling User data updates in our Symfony-based, RESTful API.

It's worth pointing out here that we will offer a restricted selection of User interactions. By that, I mean that a User will only be able to update their own User object / data, and will not be able to Create new Users, delete their own or other User objects, or retrieve anything other than their own data.

As has already been mentioned, we are using FOSUserBundle for managing the Users inside this application. We will make use of the FOSUserBundle profile form type to achieve the 'Update' (PATCH) verb of our API. Behind the scene,s we will also make use of the UserManager, as you would do normally when using FOSUserBundle.

At the end of this write up, I will include this entire feature in full. But for now, we will step through each Scenario, one at a time.

User Feature Scenarios

  Scenario: User cannot GET a Collection of User objects
    When I send a "GET" request to "/users"
    Then the response code should 405

Starting off, we want to ensure that a logged in User cannot get a list of every other User in the system.

With a RESTful API, it is standard to make a GET request to the the root of the resource and receive a collection / array of results in return.

This would be fine if we were logged in, and wanted all our accounts - GET /user/123/accounts, but in this instance we don't want to allow a regular User to see this collection, so our system should return an error of 405 - Method not allowed.

If using automatic route generation, FOSRESTBundle is helpful, in that it will throw 405 errors if the requested method (POST, PATCH, DELETE, etc) for a given route does not exist. However, for GET we will still need to implement this method. The method will throw a Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException.

For other methods that we won't be implementing for User, such as DELETE and PUT, FOSRESTBundle will throw a 405 error for us without us having to do anything.

As I will cover in later videos, we could implement GET for a collection for our Administrator Users, simply by changing up the handler we will use. Don't worry about this for now, but if that question popped into your head, rest assured (no pun intended) that it will be covered.

  Scenario: User can GET their personal data by their unique ID
    When I send a "GET" request to "/users/u1"
    Then the response code should 200
     And the response header "Content-Type" should be equal to "application/json; charset=utf-8"
     And the response should contain json:
      """
      {
        "id": "u1",
        "email": "peter@test.com",
        "username": "peter",
        "accounts": [
          {
            "id": "a1",
            "name": "account1"
          }
        ]
      }
      """

This is the standard, basic GET request. We want to be able to ask the API for our logged in User data. The API should return a simple JSON representation of our logged in User.

The accounts piece is interesting. This is a nested object - a database relation. We could have multiple Accounts associated with this User, so accounts returns an array. In this example, we only have one entry / relation in our Accounts, but if we had more, they should all show here.

If you remember back in the first video, we covered how an Account might look when returned via our RESTful API:


{
  "id": 9,
  "name": "test-account-name",
  "users": [
    {
      "id":"u1", 
      "username":"peter", 
      "email":"peter@test.com"
    }
  ],
  "files": [
    {
      "id":"f1", 
      "filename":"some file name"
    }
  ],
}

Or similar.

It's important to note that our User result shouldn't return a full representation of every Account a User is part of. We don't have to know every relation of every relation. It would get very messy, very quickly.

Instead, we just want a summary. We will cover how to do this using JMS Serializer exclusions, and groups.

  Scenario: User cannot GET a different User's personal data
    When I send a "GET" request to "/users/u2"
    Then the response code should 403

As already covered, a User should not be able to see any other User's data. A GET to the User endpoint with any other ID then the current User's own ID should return in a 403 - Forbidden error.

  Scenario: User cannot determine if another User ID is active
    When I send a "GET" request to "/users/u100"
    Then the response code should 403

Likewise, a 403 error should show even if the User ID is nonsense. This way a User cannot 'scrape' the DB to figure out which User IDs are even valid.

This is a little extreme, but is more secure for very little extra overhead on our part.

  Scenario: User cannot POST to the Users collection
    When I send a "POST" request to "/users"
    Then the response code should 405

We've discussed that a User should not be able to create new Users.

New Users must be created via the Registration process, which we are currently not implementing. This will come later.

As we don't allow POST in this instance, we should throw a 405 - Method not allowed.

  Scenario: User can PATCH to update their personal data
    When I send a "PATCH" request to "/users/u1" with body:
      """
      {
        "email": "peter@something-else.net",
        "current_password": "testpass"
      }
      """
    Then the response code should 204
     And I send a "GET" request to "/users/u1"
     And the response should contain json:
      """
      {
        "id": "u1",
        "email": "peter@something-else.net",
        "username": "peter",
        "accounts": [
          {
            "id": "a1",
            "name": "account1"
          }
        ]
      }
      """

We do want our Users to be able to update their information. To achieve this, we will make use of the FOSUserBundle Profile form type in the background.

To be able to update in a secure fashion, the profile form will require a User send in their current password when updating their profile.

If the update goes well, the response will contain a Location header value with a link to the updated resource. We don't check that in this test, but instead re-visit (GET) the logged in User's data, and expect it to show the updated email address.

  Scenario: User cannot PATCH without a valid password
    When I send a "PATCH" request to "/users/u1" with body:
      """
      {
        "email": "peter@something-else.net",
        "current_password": "wrong-password"
      }
      """
    Then the response code should 400

To be sure, we can try a PATCH with an incorrect password, and we should get a 400 - Bad Request error. We might want to make this a little more friendly. Perhaps a 403 would be better here.

  Scenario: User cannot PATCH a different User's personal data
    When I send a "PATCH" request to "/users/u2"
    Then the response code should 403

  Scenario: User cannot PATCH a none existent User
    When I send a "PATCH" request to "/users/u100"
    Then the response code should 403

These two are very similar to our GET scenarios from earlier. We don't want Users to be able to update any other User's data, or determine if that User ID is even valid.

  Scenario: User cannot PUT to replace their personal data
    When I send a "PUT" request to "/users/u1"
    Then the response code should 405

We aren't implementing PUT.

You may wish to implement PUT and disregard PATCH. This is your call entirely. In this instance, PUT and PATCH would be identical.

However, to PUT you should be sending in the whole User object representation, and it's not worth the extra effort for me, when PATCH already does what we need.

  Scenario: User cannot PUT a different User's personal data
    When I send a "PUT" request to "/users/u2"
    Then the response code should 405

  Scenario: User cannot PUT a none existent User
    When I send a "PUT" request to "/users/u100"
    Then the response code should 405

Again, these two are very similar to our GET scenarios from earlier. PUT should throw a 405 error regardless of whether the ID is valid or not.

  Scenario: User cannot DELETE their personal data
    When I send a "DELETE" request to "/users/u1"
    Then the response code should 405

  Scenario: User cannot DELETE a different User's personal data
    When I send a "DELETE" request to "/users/u2"
    Then the response code should 405

  Scenario: User cannot DELETE a none existent User
    When I send a "DELETE" request to "/users/u100"
    Then the response code should 405

Likewise, we don't want Users to be able to DELETE their own, or other User's data.

By simply not including a putAction or a deleteAction inside our UserController, FOSRESTBundle will automatically throw a 405 error for us, if someone tries to access those URIs.

Summary

Of course, none of this actually works at the moment. This is purely our documentation, or more accurately at this point, our expectation of how our system should work.

The next stage is to create the Behat steps behind the scenes to actually make this work.

Once the Behat steps work, we can implement the code that does what we expect. This will lead on to using PHPSpec.

User Feature In Full

Feature: Manage Users data via the RESTful API

  In order to offer the User resource via an hypermedia API
  As a client software developer
  I need to be able to retrieve, create, update, and delete JSON encoded User resources

  Background:
    Given there are Users with the following details:
    | uid | username | email          | password |
    | u1  | peter    | peter@test.com | testpass |
    | u2  | john     | john@test.org  | johnpass |
    And there are Accounts with the following details:
    | uid | name     | users |
    | a1  | account1 | u1    |
    And I am successfully logged in with username: "peter", and password: "testpass"
    And when consuming the endpoint I use the "headers/content-type" of "application/json"

  Scenario: User cannot GET a Collection of User objects
    When I send a "GET" request to "/users"
    Then the response code should 405

  Scenario: User can GET their personal data by their unique ID
    When I send a "GET" request to "/users/u1"
    Then the response code should 200
     And the response header "Content-Type" should be equal to "application/json; charset=utf-8"
     And the response should contain json:
      """
      {
        "id": "u1",
        "email": "peter@test.com",
        "username": "peter",
        "accounts": [
          {
            "id": "a1",
            "name": "account1"
          }
        ]
      }
      """

  Scenario: User cannot GET a different User's personal data
    When I send a "GET" request to "/users/u2"
    Then the response code should 403

  Scenario: User cannot determine if another User ID is active
    When I send a "GET" request to "/users/u100"
    Then the response code should 403

  Scenario: User cannot POST to the Users collection
    When I send a "POST" request to "/users"
    Then the response code should 405

  Scenario: User can PATCH to update their personal data
    When I send a "PATCH" request to "/users/u1" with body:
      """
      {
        "email": "peter@something-else.net",
        "current_password": "testpass"
      }
      """
    Then the response code should 204
     And I send a "GET" request to "/users/u1"
     And the response should contain json:
      """
      {
        "id": "u1",
        "email": "peter@something-else.net",
        "username": "peter",
        "accounts": [
          {
            "id": "a1",
            "name": "account1"
          }
        ]
      }
      """

  Scenario: User cannot PATCH without a valid password
    When I send a "PATCH" request to "/users/u1" with body:
      """
      {
        "email": "peter@something-else.net",
        "current_password": "wrong-password"
      }
      """
    Then the response code should 400

  Scenario: User cannot PATCH a different User's personal data
    When I send a "PATCH" request to "/users/u2"
    Then the response code should 403

  Scenario: User cannot PATCH a none existent User
    When I send a "PATCH" request to "/users/u100"
    Then the response code should 403

  Scenario: User cannot PUT to replace their personal data
    When I send a "PUT" request to "/users/u1"
    Then the response code should 405

  Scenario: User cannot PUT a different User's personal data
    When I send a "PUT" request to "/users/u2"
    Then the response code should 405

  Scenario: User cannot PUT a none existent User
    When I send a "PUT" request to "/users/u100"
    Then the response code should 405

  Scenario: User cannot DELETE their personal data
    When I send a "DELETE" request to "/users/u1"
    Then the response code should 405

  Scenario: User cannot DELETE a different User's personal data
    When I send a "DELETE" request to "/users/u2"
    Then the response code should 405

  Scenario: User cannot DELETE a none existent User
    When I send a "DELETE" request to "/users/u100"
    Then the response code should 405

Code For This Course

Get the code for this course.

Episodes