summary refs log tree commit diff
diff options
context:
space:
mode:
-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();
+				});
+		});
+	});
+});