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
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.
- Authenticated/authorized self-owned MEAN stack
- REST API for CRUD operations
- I have not yet deployed this server to production. I suspect that opinions and tests may change once deployed
- Mongo/Mongoose, Express, Angular, Node/nodemon
- Angular CLI
- Node - express sever uses JavaScript – few Typescript node server implementations when I started.
- Auth0 – for authorization and JWT generation
- Four JWT libraries - express-jwt, express-jwt-authz, jwt_decode, and jwks-rsa
- POSTMAN for test development and a test runner
- Newman for command line testing
- 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.
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.
- 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
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.
- 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
- Create a compliance grid for workflow and each endpoint
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')
|
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 |
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.
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/