✨ jest automatic tests
This commit is contained in:
parent
1e331b7c9d
commit
a7bf295591
56
api/jest/getRouteDescriptions.ts
Normal file
56
api/jest/getRouteDescriptions.ts
Normal file
@ -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;
|
||||||
|
}
|
15
api/jest/globalSetup.js
Normal file
15
api/jest/globalSetup.js
Normal file
@ -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();
|
||||||
|
// });
|
@ -1,2 +1,2 @@
|
|||||||
jest.spyOn(global.console, "log").mockImplementation(() => jest.fn());
|
// jest.spyOn(global.console, "log").mockImplementation(() => jest.fn());
|
||||||
jest.spyOn(global.console, "info").mockImplementation(() => jest.fn());
|
// jest.spyOn(global.console, "info").mockImplementation(() => jest.fn());
|
||||||
|
@ -28,12 +28,11 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RouteSchema = string; // typescript interface name
|
export type RouteResponse = { status?: number; body?: `${string}Response`; headers?: Record<string, string> };
|
||||||
export type RouteResponse = { status?: number; body?: RouteSchema; headers?: Record<string, string> };
|
|
||||||
|
|
||||||
export interface RouteOptions {
|
export interface RouteOptions {
|
||||||
permission?: PermissionResolvable;
|
permission?: PermissionResolvable;
|
||||||
body?: RouteSchema;
|
body?: `${string}Schema`; // typescript interface name
|
||||||
response?: RouteResponse;
|
response?: RouteResponse;
|
||||||
example?: {
|
example?: {
|
||||||
body?: any;
|
body?: any;
|
||||||
|
@ -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
|
|
54
api/tests/routes.test.ts
Normal file
54
api/tests/routes.test.ts
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user