Step 6

The following test is the only failing one!

  test("Return 500 when username is already in-use", async () => {
    const response = await request
      .post(endpoint)
      .send({
        username: "existing-user-post",
        password: "existing-user-post",
        role: "ADMIN",
      })
      .set("Authorization", `Bearer ${tokens.admin}`);
    expect(response.status).toBe(500);
  });

In our UserDao we don't check for a unique username. The Mongoose model, however, validates this constraint. When it fails, it throws an error. The error, however, does not have an HTTP status code. Therefore, our api/users POST handler crashes when it gets to execute this line:

res.status(err.status).json({ message: err.message });

To fix it, update it as follows.

res.status(err.status || 500).json({ message: err.message });

Save the changes and rerun the test. They must all pass now!

Let's incorporate the global error handler pattern in Express app to deal with error handling more effectively.

Add the following to the end of server/index.js right before the export statement!

// Global error handler!
app.use((err, req, res, next) => {
  if (err) {
    // debug(err);
    return res
      .status(err.status || 500)
      .json({message: err.message || "Internal server error!"});
  }
  next();
});

Then update the api/users POST handler as follows:

- router.post("/api/users", checkAdmin, async (req, res) => {
+ router.post("/api/users", checkAdmin, async (req, res, next) => {
    try {
      const { username, password, role } = req.body;
      const data = await users.create({ username, password, role });
      res.status(201).json({ data });
    } catch (err) {
-     res.status(err.status || 500).json({ message: err.message });
+     next(err);
    }
  });

Save the changes and rerun the test. All tests must pass!

Let's update all users route handlers to use the global error handler.

The update server/routes/users.js must look like this!
const express = require("express");
const UserDao = require("../data/UserDao");
const ApiError = require("../model/ApiError");
const { verifyToken, decodeToken, parseBearer } = require("../util/token");

const router = express.Router();
const users = new UserDao();

const checkAdmin = async (req, res, next) => {
  const { authorization } = req.headers;
  const token = authorization ? parseBearer(authorization) : "";
  const valid = await verifyToken(token);
  const user = decodeToken(token);
  if (!valid || user.role !== "ADMIN") {
    next(new ApiError(403, "You are not authorized to perform this action."));
  }
  next();
};

router.get("/api/users", checkAdmin, async (req, res, next) => {
  try {
    const {username, role} = req.query;
    if (username && role) {
      throw new ApiError(
        400,
        "You must query the database based on either a username or user role."
      );
    } else {
      const data = username
        ? await users.readOne(username)
        : await users.readAll(role);
      res.json({data: data ? data : []});
    }
  } catch (err) {
    next(err);
  }
});

router.get("/api/users/:id", checkAdmin, async (req, res, next) => {
  try {
    const { id } = req.params;
    const data = await users.read(id);
    res.json({ data: data ? data : [] });
  } catch (err) {
    next(err);
  }
});

router.post("/api/users", checkAdmin, async (req, res, next) => {
  try {
    const { username, password, role } = req.body;
    const data = await users.create({ username, password, role });
    res.status(201).json({ data });
  } catch (err) {
    next(err);
  }
});

router.delete("/api/users/:id", checkAdmin, async (req, res, next) => {
  try {
    const { id } = req.params;
    const data = await users.delete(id);
    res.json({ data });
  } catch (err) {
    next(err);
  }
});

router.put("/api/users/:id", checkAdmin, async (req, res, next) => {
  try {
    const { id } = req.params;
    const { password, role } = req.body;
    if (!password && !role) {
      throw new ApiError(400, "You must provide at least one user attribute!");
    }
    const data = await users.update(id, { password, role });
    res.json({ data });
  } catch (err) {
    next(err);
  }
});

module.exports = router;

Save the changes and rerun the test to ensure the refactoring has not introduced any errors!