summary refs log tree commit diff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/User.test.js43
-rw-r--r--tests/antman.jpgbin0 -> 259112 bytes
-rw-r--r--tests/cdn_endpoints.test.js238
-rw-r--r--tests/filestorage.test.js27
-rw-r--r--tests/routes.test.ts155
-rw-r--r--tests/routes/auth/login.test.js33
-rw-r--r--tests/routes/auth/register.test.js27
-rw-r--r--tests/routes/ping.test.js12
-rw-r--r--tests/setupJest.js23
9 files changed, 558 insertions, 0 deletions
diff --git a/tests/User.test.js b/tests/User.test.js
new file mode 100644
index 00000000..c0852ebc
--- /dev/null
+++ b/tests/User.test.js
@@ -0,0 +1,43 @@
+const { initDatabase, closeDatabase } = require("../dist/util/Database");
+const { User } = require("../dist/entities/User");
+jest.setTimeout(20000);
+
+beforeAll((done) => {
+	initDatabase().then(() => {
+		done();
+	});
+});
+
+afterAll(() => {
+	closeDatabase();
+});
+
+describe("User", () => {
+	test("valid discriminator: 1", async () => {
+		new User({ discriminator: "1" }).validate();
+	});
+	test("invalid discriminator: test", async () => {
+		expect(() => {
+			new User({ discriminator: "test" }).validate();
+		}).toThrow();
+	});
+
+	test("invalid discriminator: 0", async () => {
+		expect(() => {
+			new User({ discriminator: "0" }).validate();
+		}).toThrow();
+	});
+
+	test("add guild", async () => {
+		try {
+			await new User({ guilds: [], discriminator: "1" }, { id: "0" }).save();
+			const user = await User.find("0");
+
+			user.guilds.push(new Guild({ name: "test" }));
+
+			user.save();
+		} catch (error) {
+			console.error(error);
+		}
+	});
+});
diff --git a/tests/antman.jpg b/tests/antman.jpg
new file mode 100644
index 00000000..56af9063
--- /dev/null
+++ b/tests/antman.jpg
Binary files differdiff --git a/tests/cdn_endpoints.test.js b/tests/cdn_endpoints.test.js
new file mode 100644
index 00000000..5a543e54
--- /dev/null
+++ b/tests/cdn_endpoints.test.js
@@ -0,0 +1,238 @@
+const dotenv = require("dotenv");
+const path = require("path");
+const fs = require("fs");
+dotenv.config();
+
+// TODO: write unittest to check if FileStorage.ts is working
+// TODO: write unitest to check if env vars are defined
+
+if (!process.env.STORAGE_PROVIDER) process.env.STORAGE_PROVIDER = "file";
+// TODO:nodejs path.join trailing slash windows compatible
+if (process.env.STORAGE_PROVIDER === "file") {
+	if (process.env.STORAGE_LOCATION) {
+		if (!process.env.STORAGE_LOCATION.startsWith("/")) {
+			process.env.STORAGE_LOCATION = path.join(
+				__dirname,
+				"..",
+				process.env.STORAGE_LOCATION,
+				"/"
+			);
+		}
+	} else {
+		process.env.STORAGE_LOCATION = path.join(__dirname, "..", "files", "/");
+	}
+	if(!fs.existsSync(process.env.STORAGE_LOCATION)) fs.mkdirSync(process.env.STORAGE_LOCATION, {recursive:true});
+}
+const { CDNServer } = require("../dist/Server");
+const { Config } = require("@fosscord/util");
+const supertest = require("supertest");
+const request = supertest("http://localhost:3003");
+const server = new CDNServer({ port: Number(process.env.PORT) || 3003 });
+
+beforeAll(async () => {
+	await server.start();
+	return server;
+});
+
+afterAll(() => {
+	return server.stop();
+});
+
+describe("/ping", () => {
+	describe("GET", () => {
+		describe("without signature specified", () => {
+			test("route should respond with 200", async () => {
+				let response = await request.get("/ping");
+				expect(response.text).toBe("pong");
+			});
+		});
+	});
+});
+
+describe("/attachments", () => {
+	describe("POST", () => {
+		describe("without signature specified", () => {
+			test("route should respond with 400", async () => {
+				const response = await request.post("/attachments/123456789");
+				expect(response.statusCode).toBe(400);
+			});
+		});
+		describe("with signature specified, without file specified", () => {
+			test("route should respond with 400", async () => {
+				const response = await request
+					.post("/attachments/123456789")
+					.set({ signature: Config.get().security.requestSignature });
+				expect(response.statusCode).toBe(400);
+			});
+		});
+		describe("with signature specified, with file specified ", () => {
+			test("route should respond with Content-type: application/json, 200 and res.body.url", async () => {
+				const response = await request
+					.post("/attachments/123456789")
+					.set({ signature: Config.get().security.requestSignature })
+					.attach("file", __dirname + "/antman.jpg");
+				expect(response.statusCode).toBe(200);
+				expect(response.headers["content-type"]).toEqual(
+					expect.stringContaining("json")
+				);
+				expect(response.body.url).toBeDefined();
+			});
+		});
+	});
+	describe("GET", () => {
+		describe("getting uploaded image by url returned by POST /attachments", () => {
+			test("route should respond with 200", async () => {
+				let response = await request
+					.post("/attachments/123456789")
+					.set({ signature: Config.get().security.requestSignature })
+					.attach("file", __dirname + "/antman.jpg");
+				request
+					.get(response.body.url.replace("http://localhost:3003", ""))
+					.then((x) => {
+						expect(x.statusCode).toBe(200);
+					});
+			});
+		});
+	});
+	describe("DELETE", () => {
+		describe("deleting uploaded image by url returned by POST /attachments", () => {
+			test("route should respond with res.body.success", async () => {
+				let response = await request
+					.post("/attachments/123456789")
+					.set({ signature: Config.get().security.requestSignature })
+					.attach("file", __dirname + "/antman.jpg");
+				request
+					.delete(
+						response.body.url.replace("http://localhost:3003", "")
+					)
+					.then((x) => {
+						expect(x.body.success).toBeDefined();
+					});
+			});
+		});
+	});
+});
+
+describe("/avatars", () => {
+	describe("POST", () => {
+		describe("without signature specified", () => {
+			test("route should respond with 400", async () => {
+				const response = await request.post("/avatars/123456789");
+				expect(response.statusCode).toBe(400);
+			});
+		});
+		describe("with signature specified, without file specified", () => {
+			test("route should respond with 400", async () => {
+				const response = await request
+					.post("/avatars/123456789")
+					.set({ signature: Config.get().security.requestSignature });
+				expect(response.statusCode).toBe(400);
+			});
+		});
+		describe("with signature specified, with file specified ", () => {
+			test("route should respond with Content-type: application/json, 200 and res.body.url", async () => {
+				const response = await request
+					.post("/avatars/123456789")
+					.set({ signature: Config.get().security.requestSignature })
+					.attach("file", __dirname + "/antman.jpg");
+				expect(response.statusCode).toBe(200);
+				expect(response.headers["content-type"]).toEqual(
+					expect.stringContaining("json")
+				);
+				expect(response.body.url).toBeDefined();
+			});
+		});
+	});
+	describe("GET", () => {
+		describe("getting uploaded image by url returned by POST /avatars", () => {
+			test("route should respond with 200", async () => {
+				let response = await request
+					.post("/avatars/123456789")
+					.set({ signature: Config.get().security.requestSignature })
+					.attach("file", __dirname + "/antman.jpg");
+				request
+					.get(response.body.url.replace("http://localhost:3003", ""))
+					.then((x) => {
+						expect(x.statusCode).toBe(200);
+					});
+			});
+		});
+	});
+	describe("DELETE", () => {
+		describe("deleting uploaded image by url returned by POST /avatars", () => {
+			test("route should respond with res.body.success", async () => {
+				let response = await request
+					.post("/avatars/123456789")
+					.set({ signature: Config.get().security.requestSignature })
+					.attach("file", __dirname + "/antman.jpg");
+				request
+					.delete(
+						response.body.url.replace("http://localhost:3003", "")
+					)
+					.then((x) => {
+						expect(x.body.success).toBeDefined();
+					});
+			});
+		});
+	});
+});
+
+describe("/external", () => {
+	describe("POST", () => {
+		describe("without signature specified", () => {
+			test("route should respond with 400", async () => {
+				const response = await request.post("/external");
+				expect(response.statusCode).toBe(400);
+			});
+		});
+		describe("with signature specified, without file specified", () => {
+			test("route should respond with 400", async () => {
+				const response = await request
+					.post("/external")
+					.set({ signature: Config.get().security.requestSignature });
+				expect(response.statusCode).toBe(400);
+			});
+		});
+		describe("with signature specified, with file specified ", () => {
+			test("route should respond with Content-type: application/json, 200 and res.body.url", async () => {
+				const response = await request
+					.post("/external")
+					.set({ signature: Config.get().security.requestSignature })
+					.send({
+						url: "https://i.ytimg.com/vi_webp/TiXzhQr5AUc/mqdefault.webp",
+					});
+				expect(response.statusCode).toBe(200);
+				expect(response.headers["content-type"]).toEqual(
+					expect.stringContaining("json")
+				);
+				expect(response.body.id).toBeDefined();
+			});
+		});
+		describe("with signature specified, with falsy url specified ", () => {
+			test("route should respond with 400", async () => {
+				const response = await request
+					.post("/external")
+					.set({ signature: Config.get().security.requestSignature })
+					.send({
+						url: "notavalidurl.123",
+					});
+				expect(response.statusCode).toBe(400);
+			});
+		});
+	});
+	describe("GET", () => {
+		describe("getting uploaded image by url returned by POST /avatars", () => {
+			test("route should respond with 200", async () => {
+				let response = await request
+					.post("/external")
+					.set({ signature: Config.get().security.requestSignature })
+					.send({
+						url: "https://i.ytimg.com/vi_webp/TiXzhQr5AUc/mqdefault.webp",
+					});
+				request.get(`external/${response.body.id}`).then((x) => {
+					expect(x.statusCode).toBe(200);
+				});
+			});
+		});
+	});
+});
diff --git a/tests/filestorage.test.js b/tests/filestorage.test.js
new file mode 100644
index 00000000..78036602
--- /dev/null
+++ b/tests/filestorage.test.js
@@ -0,0 +1,27 @@
+const path = require("path");
+process.env.STORAGE_LOCATION = path.join(__dirname, "..", "files", "/");
+
+const { FileStorage } = require("../dist/util/FileStorage");
+const storage = new FileStorage();
+const fs = require("fs");
+
+const file = fs.readFileSync(path.join(__dirname, "antman.jpg"));
+
+describe("FileStorage", () => {
+	describe("saving a file", () => {
+		test("saving a buffer", async () => {
+			await storage.set("test_saving_file", file);
+		});
+	});
+	describe("getting a file", () => {
+		test("getting buffer with given name", async () => {
+			const buffer2 = await storage.get("test_saving_file");
+			expect(Buffer.compare(file, buffer2)).toBeTruthy();
+		});
+	});
+	describe("deleting a file", () => {
+		test("deleting buffer with given name", async () => {
+			await storage.delete("test_saving_file");
+		});
+	});
+});
diff --git a/tests/routes.test.ts b/tests/routes.test.ts
new file mode 100644
index 00000000..c915fab9
--- /dev/null
+++ b/tests/routes.test.ts
@@ -0,0 +1,155 @@
+// 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 { join } from "path";
+import fs from "fs";
+import Ajv from "ajv";
+import addFormats from "ajv-formats";
+import fetch from "node-fetch";
+import { Event, User, events, Guild, Channel } from "@fosscord/util";
+
+const SchemaPath = join(__dirname, "..", "assets", "schemas.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,
+	coerceTypes: true
+});
+addFormats(ajv);
+
+let token: string;
+let user: User;
+let guild: Guild;
+let channel: Channel;
+
+const request = async (path: string, opts: any = {}): Promise<any> => {
+	const response = await fetch(`http://localhost:3001/api${path}`, {
+		...opts,
+		method: opts.method || opts.body ? "POST" : "GET",
+		body: opts.body && JSON.stringify(opts.body),
+		headers: {
+			authorization: token,
+			...(opts.body ? { "content-type": "application/json" } : {}),
+			...(opts.header || {})
+		}
+	});
+	if (response.status === 204) return;
+
+	let data = await response.text();
+	try {
+		data = JSON.parse(data);
+		if (response.status >= 400) throw data;
+		return data;
+	} catch (error) {
+		throw data;
+	}
+};
+
+beforeAll(async (done) => {
+	try {
+		const response = await request("/auth/register", {
+			body: {
+				fingerprint: "805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw",
+				username: "tester",
+				invite: null,
+				consent: true,
+				date_of_birth: "2000-01-01",
+				gift_code_sku_id: null,
+				captcha_key: null
+			}
+		});
+		token = response.token;
+		user = await request(`/users/@me`);
+		const { id: guild_id } = await request("/guilds", { body: { name: "test server" } });
+		guild = await request(`/guilds/${guild_id}`);
+		channel = (await request(`/guilds/${guild_id}/channels`))[0];
+
+		done();
+	} catch (error) {
+		done(error);
+	}
+});
+
+const emit = events.emit;
+events.emit = (event: string | symbol, ...args: any[]) => {
+	events.emit("event", args[0]);
+	return emit(event, ...args);
+};
+
+describe("Automatic unit tests with route description middleware", () => {
+	const routes = getRouteDescriptions();
+
+	routes.forEach((route, pathAndMethod) => {
+		const [path, method] = pathAndMethod.split("|");
+
+		test(`${method.toUpperCase()} ${path}`, async (done) => {
+			if (!route.test) {
+				console.log(`${(route as any).file}\nrouter.${method} is missing the test property`);
+				return done();
+			}
+			const urlPath =
+				path.replace(":id", user.id).replace(":guild_id", guild.id).replace(":channel_id", channel.id) || route.test?.path;
+			let validate: any;
+			if (route.test.body) {
+				validate = ajv.getSchema(route.test.body);
+				if (!validate) return done(new Error(`Response schema ${route.test.body} not found`));
+			}
+
+			let body = "";
+			let eventEmitted = Promise.resolve();
+
+			if (route.test.event) {
+				if (!Array.isArray(route.test.event)) route.test.event = [route.test.event];
+
+				eventEmitted = new Promise((resolve, reject) => {
+					const timeout = setTimeout(() => reject, 1000);
+					const received = [];
+
+					events.on("event", (event: Event) => {
+						if (!route.test.event.includes(event.event)) return;
+
+						received.push(event.event);
+						if (received.length === route.test.event.length) resolve();
+					});
+				});
+			}
+
+			try {
+				const response = await fetch(`http://localhost:3001/api${urlPath}`, {
+					method: method.toUpperCase(),
+					body: JSON.stringify(route.test.body),
+					headers: { ...route.test.headers, authorization: token }
+				});
+
+				body = await response.text();
+
+				expect(response.status, body).toBe(route.test.response.status || 200);
+
+				// TODO: check headers
+				// TODO: expect event
+
+				if (validate) {
+					body = JSON.parse(body);
+					const valid = validate(body);
+					if (!valid) return done(validate.errors);
+				}
+			} catch (error) {
+				return done(error);
+			}
+
+			try {
+				await eventEmitted;
+			} catch (error) {
+				return done(new Error(`Event ${route.test.event} was not emitted`));
+			}
+
+			return done();
+		});
+	});
+});
diff --git a/tests/routes/auth/login.test.js b/tests/routes/auth/login.test.js
new file mode 100644
index 00000000..d4b52444
--- /dev/null
+++ b/tests/routes/auth/login.test.js
@@ -0,0 +1,33 @@
+const supertest = require("supertest");
+const request = supertest("http://localhost:3001");
+
+describe("/api/auth/login", () => {
+	describe("POST", () => {
+		test("without body", async () => {
+			const response = await request.post("/api/auth/login").send({});
+			expect(response.statusCode).toBe(400);
+		});
+		test("with body", async () => {
+			const user = {
+				login: "fortnitefortnite@gmail.com",
+				password: "verysecurepassword"
+			};
+
+			await request.post("/api/auth/register").send({
+				fingerprint: "805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw",
+				email: user.login,
+				username: user.login.split("@")[0],
+				password: user.password,
+				invite: null,
+				consent: true,
+				date_of_birth: "2000-04-04",
+				gift_code_sku_id: null,
+				captcha_key: null
+			});
+
+			const response = await request.post("/api/auth/login").send(user);
+
+			expect(response.statusCode).toBe(200);
+		});
+	});
+});
diff --git a/tests/routes/auth/register.test.js b/tests/routes/auth/register.test.js
new file mode 100644
index 00000000..5d7b4eaa
--- /dev/null
+++ b/tests/routes/auth/register.test.js
@@ -0,0 +1,27 @@
+const supertest = require("supertest");
+const request = supertest("http://localhost:3001");
+
+describe("/api/auth/register", () => {
+	describe("POST", () => {
+		test("without body", async () => {
+			const response = await request.post("/api/auth/register").send({});
+
+			expect(response.statusCode).toBe(400);
+		});
+		test("with body", async () => {
+			const response = await request.post("/api/auth/register").send({
+				fingerprint: "805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw",
+				email: "qo8etzvaf@gmail.com",
+				username: "qp39gr98",
+				password: "wtp9gep9gw",
+				invite: null,
+				consent: true,
+				date_of_birth: "2000-04-04",
+				gift_code_sku_id: null,
+				captcha_key: null
+			});
+
+			expect(response.statusCode).toBe(200);
+		});
+	});
+});
diff --git a/tests/routes/ping.test.js b/tests/routes/ping.test.js
new file mode 100644
index 00000000..6fa4b160
--- /dev/null
+++ b/tests/routes/ping.test.js
@@ -0,0 +1,12 @@
+const supertest = require("supertest");
+const request = supertest("http://localhost:3001");
+
+describe("/ping", () => {
+	describe("GET", () => {
+		test("should return 200 and pong", async () => {
+			let response = await request.get("/api/ping");
+			expect(response.text).toBe("pong");
+			expect(response.statusCode).toBe(200);
+		});
+	});
+});
diff --git a/tests/setupJest.js b/tests/setupJest.js
new file mode 100644
index 00000000..378d72d5
--- /dev/null
+++ b/tests/setupJest.js
@@ -0,0 +1,23 @@
+const { performance } = require("perf_hooks");
+const fs = require("fs");
+const path = require("path");
+
+// fs.unlinkSync(path.join(__dirname, "..", "database.db"));
+
+global.expect.extend({
+	toBeFasterThan: async (func, target) => {
+		const start = performance.now();
+		let error;
+		try {
+			await func();
+		} catch (e) {
+			error = e.toString();
+		}
+		const time = performance.now() - start;
+
+		return {
+			pass: time < target && !error,
+			message: () => error || `${func.name} took ${time}ms of maximum ${target}`,
+		};
+	},
+});