/RochV001

Primary LanguageTypeScript

Confessions of a Cut and Paste Developer - Express API Testing

About Me

I am a retired civil engineer studying the MEAN stack as an avocation. I've found the software development "community" unparallelled in support for all levels of users. When you hit a roadblock, video tutorials, blogs, Stack Overflow, etc. provide a wealth of content. For me, a quick search, a crtl-c/crlt-v and my problem is solved. No other profession offers more to it colleagues. However getting past the HW or sample app has often been a struggle for me. As I navigate that world with my Roch app, I will document some of the "not so sample" issues and questions that arise.

This GitHub page is the first of several planned blogs on my efforts to get beyond HW. I expect that the document will be updated as I learn more about the MEAN Stack.

Cliff Eby - 2017 - updated Aug 2020

Introduction

In an attempt to learn a little about testing, I tried to write some Mocha tests for my API server in a MEAN stack project. Once I included JWT authorization to the REST routes, I really struggled to get the test framework working. After I abandoned the traditional "ng test" approach (more on that later), I started to ask questions "What should I test?" "Since I control the entire stack, can I adequately test my API on the client side?" "What level of error reporting should my API generate?" and most importantly, "Will my API tests ease front end development – What tradeoffs will I encounter?"

A search for "best practices" for API and/or REST testing produced very little. Articles suggested consistency, comprehensiveness, middleware and documentation, but none addressed it in the context of an authenticated/authorized self-owned MEAN stack.

The following is a guide for the above context. It contains principles and a framework along with some "How to" suggestions.

Context, Caveats and Tools

Context and caveats:

  1. Authenticated/authorized self-owned MEAN stack
  2. REST API for CRUD operations
  3. I have not yet deployed this server to production. I suspect that opinions and tests may change once deployed

Tools

  1. Mongo/Mongoose, Express, Angular, Node/nodemon
  2. Angular CLI
  3. Node - express sever uses JavaScript – few Typescript node server implementations when I started.
  4. Auth0 – for authorization and JWT generation
  5. Four JWT libraries - express-jwt, express-jwt-authz, jwt_decode, and jwks-rsa
  6. POSTMAN for test development and a test runner
  7. Newman for command line testing

Principles for endpoint design and testing

Design - General and Error reporting

  • Use plural nouns for endpoint. WHY: Convention – most developers expect plural endpoints and singular POST, PUT, PATCH, and GET requests are clear with _a /ID appended. _
  • API responses should use a limited set of http "status-code" responses. They are:
    • 200 – OK
    • 401 – Unauthorized
    • 404 – Not Found
    • 500 – Server Error
      • Localhost refused to connect - ERR_CONNECTION_REFUSED

WHY:There are over 70 HTTP status codes. If you use status codes that are not common, you will force developers to search for your intent.

  • API responses should contain specific messages. WHY: It's helpful to know the requested endpoint and result. There are three ways to send this data:
    • Express middleware – e.g: 'app.use(function(req, res) {res.status(404).send({url: req.originalUrl + ' not found'})});
    • Express endpoint: A res property can return any desired server data - e.g. res.send(500, {status:500, message: 'internal error', type:'internal'});
    • Console.log: Log to server terminal window. I used this approach so I can easily remove logging from my production server. Unlike a public API, there is no need for front end users to get these server messages.
  • Modify the 200 – OK response from an endpoint or endpoint/ID not found. WHY: Misspelled endpoints and wrong IDs are common errors during development. Make sure they are caught early. See middleware below.
  • Do not "document" expected error codes and messages in a separate document – e.g. Postman, Swagger,… WHY: While often a simple click to create, that document is just another maintenance requirement and is never at your fingertips. Instead, make sure your server response and testing tool provide the needed info.
  • Unit test for known responses. WHY: Make sure that your server response catches all conditions.

Design – Middleware

There are many articles and opinions on the proper use of express-server middleware for catching and reporting errors. With two exceptions, I struggle to see the benefit of detailed status codes and unique messages. The exceptions are:

  • a "404 – Not Found" app.get('*', func…) fall through is provided by the express-generator for unspecified endpoints and paths. However, when the path is valid and the id for a GET, PUT, or DELETE is not found, the default express server will return a null body and a 200 status code. I use the following code to report a 404.

Score.findById(req.params.id)

.exec(function(err, score){

if (err) { console.log("Error retrieving score";);

}
else {
  if ( !score) {
    res.statusCode = 404;
    console.log("Not FOUND - get Score");
  }
  res.json(score);
}});`
  • Similarly, an invalid id (Mongoose defined as not equal to a 16-character hex string) will crash the node server. Error checking could be on the server or client, but since the id string is not a user input, I chose not to check for a valid hex value.

Testing - Authorization

  • Test for each JWT scope/permission condition. e.g. does absence of read:score produce "Insufficient Scope"
  • Confirm messages JWT Malformed, No Authorization Token, and Insufficient Scope are produced

Testing – Mock server

None was used for early development. A Test and Dev server were configured. Make sure that your tests (both during test script creation and once complete) do not pollute your server data.

Testing - ALL POST, PUT, GET, and DELETE

  • Is request authorized i.e. Does the JWT contain the appropriate scope/permission
  • Is the response time within acceptable limits
  • Is the status code correct- 200, 400,…
  • Is the response message correct

POST

  • Does the response body contain all of the requested and expected properties
  • Does the response body schema match the expected schema
  • Does a selected property response equal the request

PUT

  • Is the object ID found
  • Does the selected property request get updated

GET - all

  • Is the response body an array

GET - one

  • Is the object ID found

DELETE

  • Is the object ID found

Coverage

  • Create a compliance grid for workflow and each endpoint

Compliance matrix framework

Behavior

Tool/Responsibility

Technique

Login

Auth0

Client-side login uses Auth0 for authentication

Roles

Auth0

Using rules in Auth0, users are granted permissions for each endpoint and CRUD action. A role based JWT is issued by Auth0

JWT security

Auth0

Server developer

Client secret is needed to modify JWT. Must not be made public

Authorize Routes

Developer

Uses

express-jwt, express-jwt-authz, and jwks-rsa

router.route('/scores')
.get(jwtCheck, jwtAuthz(['read:scores'], options), scoreController.getScores);

Test user authorization

POSTMAN and Auth0 Developer to check UnauthorizedError response - JWT Malformed and No Authorization Token

Login to Auth0 via Postman. See Auth0 API documentation for required header and body. TEST access_token for valid and invalid JWTs.

User Identification

POSTMAN and Auth0

Developer to check UnauthorizedError response -Insufficient Scope

TEST id_token for expected user, role and scopes

Endpoint unknown

POSTMAN

Developer

TEST that unrecognized endpoint is handled

Known endpoint - POST

a. Is endpoint accessible

b. Is response timely

c. Requested and expected properties

d. Schema

e. Data

POSTMAN

Developer

TEST unauthorized, authorized, and role/scope status codes and messages

TEST response time

TEST response body for properties: required, requested and expected

TEST schema for correct types

TEST one property for specific data

Known endpoint - PUT

a. Is endpoint accessible

b. Is response timely

c. Data

POSTMAN

Developer

TEST unauthorized, authorized, and role/scope status codes and messages.

TEST response time

TEST not null and one property for specific data update

Known endpoint - GET all

a. Is endpoint accessible

b. Is response timely

c. Data

POSTMAN

Developer

TEST unauthorized, authorized, and role/scope status codes and messages

TEST response time

TEST for array of objects

Known endpoint - GET one

a. Is endpoint accessible

b. Is response timely

c. Data

POSTMAN

Developer

TEST unauthorized, authorized, and role/scope status codes and messages.

TEST response time

TEST body is not null/ID not found

Known endpoint - DELETE

a. Is endpoint accessible

b. Is response timely

c. Data

POSTMAN

Developer

TEST unauthorized, authorized, and role/scope status codes and messages.

TEST response time

TEST body is not null/ID not found

POSTMAN

I previously dabbled in POSTMAN, but rarely got beyond GET and DELETE. In trying debug my basic Mocha/JWT tests, I turned to POSTMAN to see header request and response data. The Auth0 Authentication API and Management API documentation and Lars Bilde's video https://www.youtube.com/watch?v=DVMCq8v5b7I gave me the basics. Along the way, I tried some POSTMAN tests and then the test runner. Once I found that you can export the tests to a Newman command line test runner, I abandoned Mocha and dove in.

In general, POSTMAN is giving me all I need. I like that it's easy to break out a single test from a collection and run it. Having the http data response (html or json) readily viewable and a console log made writing the test script easy. My only frustration was remembering to frequently save and backup my work. For me, it seemed easy to overwrite my work.

The following describes my testing approach using POSTMAN and Newman:

My test scripts make use of postman. {{variables}} to allow for test runner iteration by role. These data are stored in POSTMAN's Environments (roch1) that are easily manipulated.

To get JWT access_ and id_ tokens, Auth0 requires the Headers Keys/Values shown below with Content-Type keys set to application/x-www-form-urlencoded.

loginbody

Prior to the POST, many environmental variables are set and are used by subsequent tests in the collection.

Since this is a POST to Auth0, the only test is to assure that it is successful. Then, JWT access_ and id_ tokens are stored in the postman.setGlobalVariable collection. I chose global variable because these tokens are unreadable and long. It keeps the Environment section readable.

The above POST creates a JWT access_token that allows API access. Setting Authorization in the Header to Bearer followed by the access token, then setting the audience to a unique string defined in Auth0 and changing the Content-type to application/json, is all that is needed. Not shown is that this access_token includes a scope of create:score that matches the server's express router requirement.

router.route("/scores")
.post(jwtCheck, jwtAuthz(["create:score"], options), scoreController.postScore)

Next, the TEST scripts are created. They are basic JavaScript and I use if/else to test for role conditions. Like any testing library, the syntax takes some time to master, but I like that it is concise and all in one location.

The entire Roch collection test can run in POSTMAN or in Newman. Using a file named "data" (json or csv), {{variables}} are defined for each iteration. For example, I change the username and password for each iteration with a pdata.json file:

[{ "username": "admin@roch.com", "password": "Password" },
{ "username": "rplayer@roch.com", "password": "Password" },
{ "username": "rmember@roch.com", "password": "Password" }]

Exporting all or part of the collection creates a json file that can be used as a Newman input to run at the command line.

C:\Users\cliff\WebstormProjects\RochV001\ngApp>newman run server/tests/roch2.postman_collection.json -d server/tests/pdata.json . Alternatively, you can run the collection's iterations in the POSTMAN runner.

Sources

https://derickbailey.com/2014/09/06/proper-error-handling-in-expressjs-route-handlers/

https://www.joyent.com/node-js/production/design/errors

http://blog.restcase.com/rest-api-error-codes-101/

https://kostasbariotis.com/rest-api-error-handling-with-express-js/

https://webapplog.com/intro-to-express-js-parameters-error-handling-and-other-middleware/

http://goldbergyoni.com/checklist-best-practices-of-node-js-error-handling/

https://www.owasp.org/index.php/REST_Security_Cheat_Sheet

https://blog.mwaysolutions.com/2014/06/05/10-best-practices-for-better-restful-api/

https://www.youtube.com/watch?v=DVMCq8v5b7I

https://www.youtube.com/watch?v=gqcBptGjHTo&amp