diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/User.test.js | 43 | ||||
-rw-r--r-- | tests/antman.jpg | bin | 0 -> 259112 bytes | |||
-rw-r--r-- | tests/cdn_endpoints.test.js | 238 | ||||
-rw-r--r-- | tests/filestorage.test.js | 27 | ||||
-rw-r--r-- | tests/routes.test.ts | 155 | ||||
-rw-r--r-- | tests/routes/auth/login.test.js | 33 | ||||
-rw-r--r-- | tests/routes/auth/register.test.js | 27 | ||||
-rw-r--r-- | tests/routes/ping.test.js | 12 | ||||
-rw-r--r-- | tests/setupJest.js | 23 |
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}`, + }; + }, +}); |