summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-09-18 01:50:29 +0200
committerFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-09-18 01:50:29 +0200
commita7bf2955910772409ea9c3c6c32c0394c76f34b8 (patch)
tree3a59c1b7817039aebbde76f45327a8a71fa60600
parent:art: use typescript plugin that converts to relative paths (diff)
downloadserver-a7bf2955910772409ea9c3c6c32c0394c76f34b8.tar.xz
:sparkles: jest automatic tests
-rw-r--r--api/jest/getRouteDescriptions.ts56
-rw-r--r--api/jest/globalSetup.js15
-rw-r--r--api/jest/setup.js4
-rw-r--r--api/src/util/route.ts5
-rw-r--r--api/tests/automatic.test.js2
-rw-r--r--api/tests/routes.test.ts54
6 files changed, 129 insertions, 7 deletions
diff --git a/api/jest/getRouteDescriptions.ts b/api/jest/getRouteDescriptions.ts
new file mode 100644

index 00000000..d7d6e0c6 --- /dev/null +++ b/api/jest/getRouteDescriptions.ts
@@ -0,0 +1,56 @@ +import { traverseDirectory } from "lambert-server"; +import path from "path"; +import express from "express"; +import * as RouteUtility from "../dist/util/route"; +const Router = express.Router; + +const routes = new Map<string, RouteUtility.RouteOptions>(); +let currentPath = ""; +let currentFile = ""; +const methods = ["get", "post", "put", "delete", "patch"]; + +function registerPath(file, method, prefix, path, ...args) { + const urlPath = prefix + path; + const sourceFile = file.replace("/dist/", "/src/").replace(".js", ".ts"); + const opts = args.find((x) => typeof x === "object"); + if (opts) { + routes.set(urlPath + "|" + method, opts); + // console.log(method, urlPath, opts); + } else { + console.log(`${sourceFile}\nrouter.${method}("${path}") is missing the "route()" description middleware\n`, args); + } +} + +function routeOptions(opts) { + return opts; +} + +// @ts-ignore +RouteUtility.route = routeOptions; + +express.Router = (opts) => { + const path = currentPath; + const file = currentFile; + const router = Router(opts); + + for (const method of methods) { + router[method] = registerPath.bind(null, file, method, path); + } + + return router; +}; + +export default function getRouteDescriptions() { + const root = path.join(__dirname, "..", "dist", "routes", "/"); + traverseDirectory({ dirname: root, recursive: true }, (file) => { + currentFile = file; + let path = file.replace(root.slice(0, -1), ""); + path = path.split(".").slice(0, -1).join("."); // trancate .js/.ts file extension of path + path = path.replaceAll("#", ":").replaceAll("\\", "/"); // replace # with : for path parameters and windows paths with slashes + if (path.endsWith("/index")) path = path.slice(0, "/index".length * -1); // delete index from path + currentPath = path; + + require(file); + }); + return routes; +} diff --git a/api/jest/globalSetup.js b/api/jest/globalSetup.js new file mode 100644
index 00000000..98e70fb9 --- /dev/null +++ b/api/jest/globalSetup.js
@@ -0,0 +1,15 @@ +const fs = require("fs"); +const path = require("path"); +const { FosscordServer } = require("../dist/Server"); +const Server = new FosscordServer({ port: 3001 }); +global.server = Server; +module.exports = async () => { + try { + fs.unlinkSync(path.join(__dirname, "..", "database.db")); + } catch {} + return await Server.start(); +}; + +// afterAll(async () => { +// return await Server.stop(); +// }); diff --git a/api/jest/setup.js b/api/jest/setup.js
index abc485ae..bd535866 100644 --- a/api/jest/setup.js +++ b/api/jest/setup.js
@@ -1,2 +1,2 @@ -jest.spyOn(global.console, "log").mockImplementation(() => jest.fn()); -jest.spyOn(global.console, "info").mockImplementation(() => jest.fn()); +// jest.spyOn(global.console, "log").mockImplementation(() => jest.fn()); +// jest.spyOn(global.console, "info").mockImplementation(() => jest.fn()); diff --git a/api/src/util/route.ts b/api/src/util/route.ts
index 678ca64c..35ea43ba 100644 --- a/api/src/util/route.ts +++ b/api/src/util/route.ts
@@ -28,12 +28,11 @@ declare global { } } -export type RouteSchema = string; // typescript interface name -export type RouteResponse = { status?: number; body?: RouteSchema; headers?: Record<string, string> }; +export type RouteResponse = { status?: number; body?: `${string}Response`; headers?: Record<string, string> }; export interface RouteOptions { permission?: PermissionResolvable; - body?: RouteSchema; + body?: `${string}Schema`; // typescript interface name response?: RouteResponse; example?: { body?: any; diff --git a/api/tests/automatic.test.js b/api/tests/automatic.test.js deleted file mode 100644
index 2d0a9fcb..00000000 --- a/api/tests/automatic.test.js +++ /dev/null
@@ -1,2 +0,0 @@ -// TODO: check every route based on route() paramters: https://github.com/fosscord/fosscord-server/issues/308 -// TODO: check every route with different database engine diff --git a/api/tests/routes.test.ts b/api/tests/routes.test.ts new file mode 100644
index 00000000..e913e0dc --- /dev/null +++ b/api/tests/routes.test.ts
@@ -0,0 +1,54 @@ +// TODO: check every route based on route() parameters: https://github.com/fosscord/fosscord-server/issues/308 +// TODO: check every route with different database engine + +import getRouteDescriptions from "../jest/getRouteDescriptions"; +import supertest, { Response } from "supertest"; +import path from "path"; +import fs from "fs"; +import Ajv from "ajv"; +import addFormats from "ajv-formats"; +const request = supertest("http://localhost:3001/api"); + +const SchemaPath = path.join(__dirname, "..", "assets", "responses.json"); +const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" })); +export const ajv = new Ajv({ + allErrors: true, + parseDate: true, + allowDate: true, + schemas, + messages: true, + strict: true, + strictRequired: true +}); +addFormats(ajv); + +describe("Automatic unit tests with route description middleware", () => { + const routes = getRouteDescriptions(); + + routes.forEach((route, pathAndMethod) => { + const [path, method] = pathAndMethod.split("|"); + test(path, (done) => { + if (!route.example) { + console.log(`Route ${path} is missing the example property`); + return done(); + } + if (!route.response) { + console.log(`Route ${path} is missing the response property`); + return done(); + } + const urlPath = path || route.example?.path; + const validate = ajv.getSchema(route.response.body); + if (!validate) return done(new Error(`Response schema ${route.response.body} not found`)); + + request[method](urlPath) + .expect(route.response.status) + .expect((err: any, res: Response) => { + if (err) return done(err); + const valid = validate(res.body); + if (!valid) return done(validate.errors); + + return done(); + }); + }); + }); +});