yoaquim/zemi

View on GitHub
tests/core.test.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import express, { NextFunction, Request, Response } from "express";
import request from "supertest";
import Zemi, {
  ZemiMethod,
  ZemiRequest,
  ZemiRequestHandler,
  ZemiResponse,
  ZemiRoute,
  ZemiRouteDefinition,
} from "../src";

const { GET, POST } = ZemiMethod;

class RequestTester {
  private app;

  constructor(routes: Array<ZemiRoute>) {
    this.app = express();
    this.app.use("/", Zemi(routes));
  }

  get(path: string) {
    return request(this.app).get(path);
  }

  post(path: string, data: object) {
    return request(this.app).post(path).set("Accept", "application/json").send(data);
  }
}

async function testGET(path: string, routes: Array<ZemiRoute>) {
  const app = express();
  app.use("/", Zemi(routes));
  return request(app).get(path);
}

async function testPOST(path: string, data: object, routes: Array<ZemiRoute>) {
  const app = express();
  app.use(express.json());
  app.use("/", Zemi(routes));
  return request(app).post(path).set("Accept", "application/json").send(data);
}

function buildRoute(
  name: string,
  path: string,
  method: ZemiMethod,
  handler: ZemiRequestHandler
): ZemiRoute {
  return {
    name,
    path,
    [method]: handler,
  };
}

function buildBasicPetsRoute(handler: ZemiRequestHandler) {
  return buildRoute("pets", "/pets", GET, handler);
}

function basicIdResponse(request, response, payload: object) {
  const id = request.params.id;
  const data = Object.assign({}, { id }, payload);
  response.status(200).json(data);
}

function basicResponse(request, response, payload: object) {
  response.status(200).json(payload);
}

describe("zemi core functionality can...", () => {
  test("create a route from the definition passed to it.", async () => {
    const routes: Array<ZemiRoute> = [
      buildBasicPetsRoute(function (request: ZemiRequest, response: ZemiResponse) {
        response.status(200).json({ pets: ["dogs", "cats"] });
      }),
    ];
    const response = await testGET("/pets", routes);
    expect(response.status).toEqual(200);
    expect(response.body.pets).toEqual(["dogs", "cats"]);
  });

  test("access params defined in the path via the request.", async () => {
    const routes: Array<ZemiRoute> = [
      buildRoute("petsById", "/pets/:id", GET, function (request: Request, response: Response) {
        basicIdResponse(request, response, { dogs: ["Kali", "Ahkila"] });
      }),
    ];

    const response = await testGET("/pets/99", routes);
    expect(response.status).toEqual(200);
    expect(response.body.id).toEqual("99");
    expect(response.body.dogs).toEqual(["Kali", "Ahkila"]);
  });

  test("access query object in the path via the request.", async () => {
    const routes: Array<ZemiRoute> = [
      buildBasicPetsRoute(function (request: Request, response: Response) {
        response.status(200).json({
          query: request.query,
        });
      }),
    ];

    const response = await testGET("/pets?foo=bar&baz=beez", routes);
    expect(response.status).toEqual(200);
    expect(response.body.query).toEqual({ foo: "bar", baz: "beez" });
  });

  test("create a route with child routes when specified in the definition.", async () => {
    const routes: Array<ZemiRoute> = [
      {
        name: "pets",
        path: "/pets",
        [GET]: function (request: Request, response: Response) {
          response.status(200).json({ pets: ["dogs", "cats"] });
        },
        routes: [
          {
            name: "dogs",
            path: "/dogs",
            [GET]: function (request: Request, response: Response) {
              response.status(200).json({ data: ["Kali", "Ahkila"] });
            },
          },
          {
            name: "cats",
            path: "/cats",
            [GET]: function (request: Request, response: Response) {
              basicResponse(request, response, { data: ["Fufu", "Meow"] });
            },
          },
        ],
      },
    ];

    const baseResponse = await testGET("/pets", routes);
    expect(baseResponse.status).toEqual(200);
    expect(baseResponse.body.pets).toEqual(["dogs", "cats"]);

    const dogRouteResponse = await testGET("/pets/dogs", routes);
    expect(dogRouteResponse.body.data).toEqual(["Kali", "Ahkila"]);
    expect(dogRouteResponse.status).toEqual(200);

    const catRouteResponse = await testGET("/pets/cats", routes);
    expect(catRouteResponse.status).toEqual(200);
    expect(catRouteResponse.body.data).toEqual(["Fufu", "Meow"]);
  });

  test("merge params from parents routes into child routes and allows access to them.", async () => {
    const routes: Array<ZemiRoute> = [
      {
        name: "petsById",
        path: "/pets/:id",
        [GET]: function (request: Request, response: Response) {
          response.status(200).json({ pets: ["dogs", "cats"] });
        },
        routes: [
          {
            name: "dogs",
            path: "/dogs",
            [GET]: function (request: Request, response: Response) {
              basicResponse(request, response, {
                parent_id: request.params.id,
                data: ["Kali", "Ahkila"],
              });
            },
          },
        ],
      },
    ];

    const response = await testGET("/pets/99/dogs", routes);
    expect(response.body.parent_id).toEqual("99");
    expect(response.body.data).toEqual(["Kali", "Ahkila"]);
    expect(response.status).toEqual(200);
  });

  test("create a POST route that receives data.", async () => {
    const routes: Array<ZemiRoute> = [
      {
        name: "newPet",
        path: "/new-pet",
        [POST]: function (request: Request, response: Response) {
          const { new_pet } = request.body;
          basicResponse(request, response, { new_pet });
        },
      },
    ];

    const response = await testPOST("/new-pet", { new_pet: "rabbit" }, routes);
    expect(response.status).toEqual(200);
    expect(response.body.new_pet).toEqual("rabbit");
  });

  test("create a POST route that receives data and can access request params.", async () => {
    const routes: Array<ZemiRoute> = [
      {
        name: "newPetById",
        path: "/new-pet/:id",
        [POST]: function (request: Request, response: Response) {
          const { new_pet } = request.body;
          const { id } = request.params;
          basicResponse(request, response, { new_pet, id });
        },
      },
    ];

    const response = await testPOST("/new-pet/99", { new_pet: "rabbit" }, routes);
    expect(response.status).toEqual(200);
    expect(response.body.new_pet).toEqual("rabbit");
    expect(response.body.id).toEqual("99");
  });

  test("create a POST route and a GET route that both work.", async () => {
    const routes: Array<ZemiRoute> = [
      {
        name: "newItem",
        path: "/new-item",
        [POST]: function (request: Request, response: Response) {
          const { new_item } = request.body;
          response.status(200).json({ new_item });
        },
        [GET]: function (request: Request, response: Response) {
          const { id } = request.params;
          response.status(200).json({ id, message: "items" });
        },
      },
    ];

    const postResponse = await testPOST("/new-item", { new_item: "sponge" }, routes);
    expect(postResponse.status).toEqual(200);
    expect(postResponse.body.new_item).toEqual("sponge");

    const getResponse = await testGET("/new-item", routes);
    expect(getResponse.status).toEqual(200);
    expect(getResponse.body.message).toEqual("items");
  });

  test("build nested routes without having a handler at the current level", async () => {
    const routes: Array<ZemiRoute> = [
      {
        name: "pets",
        path: "/pets",
        routes: [
          {
            name: "dogs",
            path: "/dogs",
            [GET]: function (request: Request, response: Response) {
              response.status(200).json({ dogs: ["Kali", "Ahkila"] });
            },
          },
        ],
      },
    ];

    const nestedResponse = await testGET("/pets/dogs", routes);
    expect(nestedResponse.status).toEqual(200);
    expect(nestedResponse.body).toEqual({ dogs: ["Kali", "Ahkila"] });

    const notFoundResponse = await testGET("/pets", routes);
    expect(notFoundResponse.status).toEqual(404);
  });

  test("store route defintiions in ZemiRequest", async () => {
    const routes: Array<ZemiRoute> = [
      {
        name: "pets",
        path: "/pets/:id",
        [GET]: function (request: ZemiRequest, response: Response) {
          response.status(200).json(request.routeDefinitions);
        },
        routes: [
          {
            name: "dogs",
            path: "/dogs",
          },
          {
            name: "cats",
            path: "/cats",
            routes: [
              {
                name: "tigers",
                path: "/tiggers",
              },
            ],
          },
        ],
      },
    ];

    const response = await testGET("/pets/99", routes);
    expect(response.status).toEqual(200);
    expect(response.body).toEqual({
      pets: {
        name: "pets",
        path: "/pets/:id",
        parameters: ["id"],
      },
      "pets-dogs": {
        name: "pets-dogs",
        path: "/pets/:id/dogs",
        parameters: ["id"],
      },
      "pets-cats": {
        name: "pets-cats",
        path: "/pets/:id/cats",
        parameters: ["id"],
      },
      "pets-cats-tigers": {
        name: "pets-cats-tigers",
        path: "/pets/:id/cats/tiggers",
        parameters: ["id"],
      },
    });
  });

  test("correctly build paths for defined-routes when object being access by nested routes", async () => {
    const petsHandler = function (request: ZemiRequest, response: ZemiResponse) {
      const { routeDefinitions } = request;
      response.status(200).json({ routeDefinitions });
    };

    const dogsHandler = function (request: ZemiRequest, response: ZemiResponse) {
      response.status(200).json({ dogs: ["corgi", "labrador", "poodle"] });
    };

    const catsHandler = function (request: ZemiRequest, response: ZemiResponse) {
      const routeDefs = request.routeDefinitions;
      response.status(200).json({ routeDefs });
    };

    const tigersHandler = function (request: ZemiRequest, response: ZemiResponse) {
      // Tigers are just cats, so redirect to /cats
      const routeDefinitions = request.routeDefinitions;
      response.json({ routeDefinitions });
    };

    const routes: Array<ZemiRoute> = [
      {
        name: "pets",
        path: "/pets",
        [GET]: petsHandler,
        routes: [
          {
            name: "dogs",
            path: "/dogs",
            [GET]: dogsHandler,
          },
          {
            name: "cats",
            path: "/cats",
            [GET]: catsHandler,
          },
          {
            name: "tigers",
            path: "/tigers",
            [GET]: tigersHandler,
          },
        ],
      },
    ];

    const response = await testGET("/pets/tigers", routes);
    expect(response.body).toEqual({
      routeDefinitions: {
        pets: { name: "pets", path: "/pets", parameters: [] },
        "pets-dogs": { name: "pets-dogs", path: "/pets/dogs", parameters: [] },
        "pets-cats": { name: "pets-cats", path: "/pets/cats", parameters: [] },
        "pets-tigers": {
          name: "pets-tigers",
          path: "/pets/tigers",
          parameters: [],
        },
      },
    });
  });

  test("correctly add the complete routeDefinition as the fourth argument in the ZemiRequestHandler fn", async () => {
    const tigersHandler = (
      request: ZemiRequest,
      response: ZemiResponse,
      _: NextFunction,
      routeDefinition: ZemiRouteDefinition
    ) => {
      response.json({ routeDefinition });
    };

    const routes: Array<ZemiRoute> = [
      {
        name: "pets",
        path: "/pets",
        routes: [
          {
            name: "dogs",
            path: "/dogs",
          },
          {
            name: "cats",
            path: "/cats",
            routes: [
              {
                name: "tigers",
                path: "/tigers",
                [GET]: tigersHandler,
              },
            ],
          },
        ],
      },
    ];

    const response = await testGET("/pets/cats/tigers", routes);
    expect(response.body.routeDefinition.name).toEqual("pets-cats-tigers");
    expect(response.body.routeDefinition.path).toEqual("/pets/cats/tigers");
  });

  test("child routes get defined before current route", async () => {
    const routes: Array<ZemiRoute> = [
      {
        name: "pets",
        path: "/pets",
        routes: [
          {
            name: "petsById",
            path: "/:id",
            [GET]: function (request: Request, response: Response) {
              basicIdResponse(request, response, { name: "Boomer" });
            },
            routes: [
              {
                name: "petsByIdWithDetails",
                path: "/details",
                [GET]: function (request: Request, response: Response) {
                  basicIdResponse(request, response, { details: "Fun dog" });
                },
              },
            ],
          },
        ],
      },
    ];

    const responseIdRoute = await new RequestTester(routes).get("/pets/2");
    const responseIdDetailsRoute = await new RequestTester(routes).get("/pets/2/details");

    expect(responseIdRoute.body.id).toEqual("2");
    expect(responseIdRoute.body.name).toEqual("Boomer");
    expect(responseIdDetailsRoute.body.id).toEqual("2");
    expect(responseIdDetailsRoute.body.details).toEqual("Fun dog");
  });
});

describe("zemi middleware functionality can...", () => {
  test("attach middleware to router before routes are defined", async () => {
    const mockOne = jest.fn();
    const mockTwo = jest.fn();
    const routes: Array<ZemiRoute> = [
      {
        name: "pets",
        path: "/pets",
        middleware: [
          function (request: Request, response: Response, next: NextFunction) {
            mockOne();
            next();
          },
          function (request: Request, response: Response, next: NextFunction) {
            mockTwo();
            next();
          },
        ],
        [GET]: function (request: Request, response: Response) {
          response.status(200).json({ pets: ["dogs", "cats"] });
        },
      },
    ];

    const response = await testGET("/pets", routes);
    expect(mockOne).toHaveBeenCalled();
    expect(mockTwo).toHaveBeenCalled();
    expect(response.status).toEqual(200);
    expect(response.body).toEqual({ pets: ["dogs", "cats"] });
  });
});