v1.0.0 #28

Merged
bizcochito merged 147 commits from develop into main 2024-01-14 19:49:34 +00:00
12 changed files with 230 additions and 187 deletions
Showing only changes of commit 1b45481d27 - Show all commits

View File

@ -12,7 +12,6 @@ services:
MONGO_INITDB_DATABASE: bot MONGO_INITDB_DATABASE: bot
volumes: volumes:
- mongodb_data:/data/db - mongodb_data:/data/db
- ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
bot-api: bot-api:
image: oven/bun:1 image: oven/bun:1
@ -28,6 +27,8 @@ services:
MONGODB_USER: "root" MONGODB_USER: "root"
MONGODB_PASS: "password" MONGODB_PASS: "password"
JWTSECRET: "cooljwtsecret" JWTSECRET: "cooljwtsecret"
# DEDICATED_MONGODB_SERVER is for integration testing, for disabling memory server in tests
DEDICATED_MONGODB_SERVER: "true"
volumes: volumes:
- ./:/usr/src/app:ro - ./:/usr/src/app:ro

View File

@ -1,30 +0,0 @@
db.createUser({
user: "root",
pwd: "password",
roles: [
{
role: "readWrite",
db: "admin",
},
{
role: "readWrite",
db: "bot",
},
],
});
db = new Mongo().getDB("bot");
db.images.createIndex({ status: 1 });
db.images.createIndex({ url: 1 }, { unique: true });
db.images.insert({
url: "https://example.com",
status: "consumed",
tags: ["2girls", "sleeping"],
});
db.authorizations.createIndex({ app: 1 });
db.authorizations.insert({
app: "tester",
secret: "test",
});

View File

@ -4,9 +4,11 @@
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
"@types/mongodb-memory-server": "^2.3.0",
"@types/supertest": "^6.0.1", "@types/supertest": "^6.0.1",
"bun-types": "latest", "bun-types": "latest",
"jest": "^29.7.0", "jest": "^29.7.0",
"mongodb-memory-server": "^9.1.3",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"ts-jest": "^29.1.1" "ts-jest": "^29.1.1"
}, },

41
src/app.ts Normal file
View File

@ -0,0 +1,41 @@
import express from "express";
import listEndpoints from "express-list-endpoints";
import imageController from "./controllers/ImageController";
import authControler from "./controllers/AuthControler";
import mongoose from "mongoose";
export const app = express();
app.use(express.json());
app.get("/", (_, res) => {
const endpoints = listEndpoints(app);
res.json({ endpoints });
});
app.get("/images", imageController.getAllImages);
app.post("/images", authControler.authorize, imageController.addImage);
app.post("/login", authControler.login);
export const startApp = async () => {
const port = process.env.PORT || 8080;
const mongo_uri: string = process.env.MONGODB_URI || "";
const mongo_user = process.env.MONGODB_USER;
const mongo_pass = process.env.MONGODB_PASS;
try {
await mongoose.connect(mongo_uri, {
authSource: "admin",
user: mongo_user,
pass: mongo_pass,
});
app.listen(port, () =>
console.log(`Express server listening on port ${port}`)
);
} catch (error) {
console.error(error);
process.exit(1);
}
};
export default app;

View File

@ -1,42 +1,3 @@
import express from "express"; import { startApp } from "./app";
import mongoose from "mongoose";
import listEndpoints from "express-list-endpoints";
import imageController from "./controllers/ImageController";
import authControler from "./controllers/AuthControler";
export const app = express(); startApp();
app.use(express.json());
app.get("/", (_, res) => {
const endpoints = listEndpoints(app);
res.json({ endpoints });
});
app.get("/images", imageController.getAllImages);
app.post("/images", authControler.authorize, imageController.addImage);
app.post("/login", authControler.login);
const start = async () => {
// Set the default port to 8080, or use the PORT environment variable
const port = process.env.PORT || 8080;
const mongo_uri: string = process.env.MONGODB_URI || "";
const mongo_user = process.env.MONGODB_USER;
const mongo_pass = process.env.MONGODB_PASS;
try {
await mongoose.connect(mongo_uri, {
authSource: "admin",
user: mongo_user,
pass: mongo_pass,
});
app.listen(port, () =>
console.log(`Express server listening on port ${port}`)
);
} catch (error) {
console.error(error);
process.exit(1);
}
};
start();

View File

@ -9,11 +9,12 @@ const AuthSchema = new mongoose.Schema({
app: { app: {
type: String, type: String,
required: true, required: true,
index: true,
}, },
secret: { secret: {
type: String, type: String,
required: true, required: true,
}, },
}); }, { collection: "authorizations" });
export default mongoose.model("authorizations", AuthSchema); export default mongoose.model("authorizations", AuthSchema);

View File

@ -10,6 +10,8 @@ const ImageSchema = new mongoose.Schema({
url: { url: {
type: String, type: String,
required: true, required: true,
index: true,
unique: true
}, },
status: { status: {
type: String, type: String,
@ -17,10 +19,11 @@ const ImageSchema = new mongoose.Schema({
values: ["consumed", "unavailable", "available"], values: ["consumed", "unavailable", "available"],
}, },
required: true, required: true,
index: true,
}, },
tags: { tags: {
type: [String], type: [String],
}, },
}); }, { collection: "images" });
export default mongoose.model<Image>("images", ImageSchema); export default mongoose.model<Image>("images", ImageSchema);

View File

@ -1,8 +1,8 @@
import AuthModel, { Auth } from "../models/AuthModel"; import AuthModel, { Auth } from "../models/AuthModel";
class AuthService { class AuthService {
async find(app: String, secret: String): Promise<Auth | null> { async find(app: string, secret: string): Promise<Auth | null> {
const auth = await AuthModel.findOne({ app: app, secret: secret }).exec(); const auth = await AuthModel.findOne({ app: app, secret: secret });
return auth; return auth;
} }
} }

View File

@ -1,134 +1,151 @@
import { afterEach, beforeAll, describe, expect, it, mock } from "bun:test"; import { afterAll, afterEach, beforeAll, describe, expect, it, mock } from "bun:test";
import request from "supertest"; import request from "supertest";
import { app } from "../src"; import app, { startApp } from "../src/app";
import imageService from "../src/services/ImageService"; import imageService from "../src/services/ImageService";
import memoryServer from "./memoryServer";
import populateDatabase from "./populateDatabase";
const imageServiceOriginal = imageService; const imageServiceOriginal = imageService;
const tok = await request(app)
.post("/login") let token: string;
.send({ app: "tester", secret: "test" });
const token = tok.body.token; beforeAll(async () => {
if (!process.env.DEDICATED_MONGODB_SERVER)
await memoryServer.start();
await startApp();
await populateDatabase();
const tok = await request(app)
.post("/login")
.send({ app: "tester", secret: "test" });
token = tok.body.token;
});
afterAll(async () => {
if (!process.env.DEDICATED_MONGODB_SERVER)
await memoryServer.stop();
})
afterEach(() => { afterEach(() => {
mock.restore(); mock.restore();
mock.module("../src/services/ImageService", () => ({ mock.module("../src/services/ImageService", () => ({
default: imageServiceOriginal, default: imageServiceOriginal,
})); }));
}); });
describe("GET / shows all of the endpoints", async () => { describe("GET / shows all of the endpoints", async () => {
const res = await request(app).get("/"); const res = await request(app).get("/");
it("should be", async () => { it("should be", async () => {
expect(res.body).toHaveProperty("endpoints"); expect(res.body).toHaveProperty("endpoints");
}); });
it("should be an array", () => { it("should be an array", () => {
expect(Array.isArray(res.body.endpoints)).toBeTrue(); expect(Array.isArray(res.body.endpoints)).toBeTrue();
}); });
}); });
describe("GET /images works properly", async () => { describe("GET /images works properly", async () => {
const res = await request(app).get("/images"); const res = await request(app).get("/images");
it("should be an array", () => { it("should be an array", () => {
expect(Array.isArray(res.body.images)).toBeTrue(); expect(Array.isArray(res.body.images)).toBeTrue();
}); });
it("should return a 200", async () => { it("should return a 200", async () => {
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
}); });
}); });
describe("POST /images works properly", () => { describe("POST /images works properly", () => {
it("should return 401 for unauthenticated requests", async () => { it("should return 401 for unauthenticated requests", async () => {
const res = await request(app) const res = await request(app)
.post("/images") .post("/images")
.send({ .send({
url: "https://test.url.com/0", url: "https://test.url.com/0",
status: "available", status: "available",
tags: ["2girls", "touhou"], tags: ["2girls", "touhou"],
}); });
expect(res.status).toBe(401); expect(res.status).toBe(401);
}); });
it("should return 403 for invalid tokens", async () => { it("should return 403 for invalid tokens", async () => {
const res = await request(app) const res = await request(app)
.post("/images") .post("/images")
.set("authorization", `Bearer token`) .set("authorization", `Bearer token`)
.send({ .send({
url: "https://test.url.com/0", url: "https://test.url.com/0",
status: "available", status: "available",
tags: ["2girls", "touhou"], tags: ["2girls", "touhou"],
}); });
expect(res.status).toBe(403); expect(res.status).toBe(403);
}); });
it("should return 201 for new image", async () => { it("should return 201 for new image", async () => {
const res = await request(app) const res = await request(app)
.post("/images") .post("/images")
.set("authorization", `Bearer ${token}`) .set("authorization", `Bearer ${token}`)
.send({ .send({
url: "https://test.url.com/1", url: "https://test.url.com/1",
status: "available", status: "available",
tags: ["2girls", "touhou"], tags: ["2girls", "touhou"],
}); });
expect(res.status).toBe(201); expect(res.status).toBe(201);
}); });
it("should return 409 for a repeated images", async () => { it("should return 409 for a repeated images", async () => {
await request(app) await request(app)
.post("/images") .post("/images")
.set("authorization", `Bearer ${token}`) .set("authorization", `Bearer ${token}`)
.send({ .send({
url: "https://test.url.com/2", url: "https://test.url.com/2",
status: "available", status: "available",
tags: ["2girls", "touhou"], tags: ["2girls", "touhou"],
}); });
const res = await request(app) const res = await request(app)
.post("/images") .post("/images")
.set("authorization", `Bearer ${token}`) .set("authorization", `Bearer ${token}`)
.send({ .send({
url: "https://test.url.com/2", url: "https://test.url.com/2",
status: "available", status: "available",
tags: ["2girls", "touhou"], tags: ["2girls", "touhou"],
}); });
expect(res.status).toBe(409); expect(res.status).toBe(409);
}); });
it("should return 500 for an error on the service", async () => { it("should return 500 for an error on the service", async () => {
mock.module("../src/services/ImageService", () => ({ mock.module("../src/services/ImageService", () => ({
default: { default: {
add: () => { add: () => {
throw new Error("This is an expected testing error"); throw new Error("This is an expected testing error");
}, },
}, },
})); }));
const res = await request(app) const res = await request(app)
.post("/images") .post("/images")
.set("authorization", `Bearer ${token}`) .set("authorization", `Bearer ${token}`)
.send({ .send({
url: "https://test.url.com/3", url: "https://test.url.com/3",
status: "available", status: "available",
tags: ["2girls", "touhou"], tags: ["2girls", "touhou"],
}); });
expect(res.status).toBe(500); expect(res.status).toBe(500);
}); });
it("should return 400 for malformed requests", async () => { it("should return 400 for malformed requests", async () => {
mock.restore(); mock.restore();
const res = await request(app) const res = await request(app)
.post("/images") .post("/images")
.set("authorization", `Bearer ${token}`) .set("authorization", `Bearer ${token}`)
.send({ .send({
url: "https://test.url.com/4", url: "https://test.url.com/4",
status: "wrong", status: "wrong",
tags: ["2girls", "touhou"], tags: ["2girls", "touhou"],
}); });
expect(res.status).toBe(400); expect(res.status).toBe(400);
}); });
}); });

View File

@ -1,12 +1,25 @@
import { describe, expect, it, mock } from "bun:test"; import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import request from "supertest"; import request from "supertest";
import { app } from "../src"; import app, { startApp } from "../src/app";
import memoryServer from "./memoryServer";
import populateDatabase from "./populateDatabase";
beforeAll(async () => {
if (!process.env.DEDICATED_MONGODB_SERVER)
await memoryServer.start();
await startApp();
await populateDatabase();
});
afterAll(async () => {
if (!process.env.DEDICATED_MONGODB_SERVER)
await memoryServer.stop();
});
describe("/login", async () => { describe("/login", async () => {
const correctRespose = await request(app).post("/login").send({ const correctRespose = await request(app)
app: "tester", .post("/login")
secret: "test", .send({ app: "tester", secret: "test" });
});
it("should return 200 for correct login", () => { it("should return 200 for correct login", () => {
expect(correctRespose.status).toBe(200); expect(correctRespose.status).toBe(200);
}); });

19
tests/memoryServer.ts Normal file
View File

@ -0,0 +1,19 @@
import { MongoMemoryServer } from "mongodb-memory-server";
class MemoryServer {
mongod: MongoMemoryServer | undefined;
async start() {
this.mongod = await MongoMemoryServer.create();
const uri = this.mongod.getUri("bot");
process.env.MONGODB_URI = uri;
}
async stop() {
if (this.mongod) {
await this.mongod.stop();
}
}
}
export default new MemoryServer();

15
tests/populateDatabase.ts Normal file
View File

@ -0,0 +1,15 @@
import authModel from "../src/models/AuthModel";
import imageModel from "../src/models/ImageModel";
export default async function () {
await imageModel.create({
url: "https://example.com",
status: "consumed",
tags: ["2girls", "sleeping"],
});
await authModel.create({
app: "tester",
secret: "test",
});
}