diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml index 0cca8ee..7c64ca0 100644 --- a/.gitea/workflows/test.yaml +++ b/.gitea/workflows/test.yaml @@ -1,19 +1,19 @@ -name: Gitea Actions Demo +name: Unit Tests with docker compose run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀 on: [pull_request] jobs: - Explore-Gitea-Actions: + unit-test: container: - image: oven/bun:1 + image: docker:dind steps: - - run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event." - - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!" - - run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}." + - name: Starting docker daemon + run: docker-init -- dockerd --host=unix:///var/run/docker.sock & + - name: Installing necessary packages + run: apk add npm git curl bash - name: Check out repository code uses: actions/checkout@v3 - - run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner." - - run: echo "🖥️ The workflow is now ready to test your code on the runner." - - name: List files in the repository - run: | - ls ${{ gitea.workspace }} \ No newline at end of file + - name: Install project dependencies + run: npm install + - name: Run docker-compose + run: docker compose down -v && docker compose run bot-api bun test \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index 964794b..30088de 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,7 @@ version: "3" services: mongodb: - image: mongo + image: mongo:bionic container_name: mongodb ports: - "27017:27017" @@ -12,7 +12,6 @@ services: MONGO_INITDB_DATABASE: bot volumes: - mongodb_data:/data/db - - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro bot-api: image: oven/bun:1 diff --git a/mongo-init.js b/mongo-init.js deleted file mode 100644 index 67b53c9..0000000 --- a/mongo-init.js +++ /dev/null @@ -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", -}); diff --git a/package.json b/package.json index 2080221..e11f4b4 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,10 @@ "module": "index.ts", "type": "module", "devDependencies": { + "@types/express": "^4.17.21", + "@types/express-list-endpoints": "^6.0.3", "@types/jest": "^29.5.11", + "@types/jsonwebtoken": "^9.0.5", "@types/supertest": "^6.0.1", "bun-types": "latest", "jest": "^29.7.0", @@ -20,10 +23,6 @@ "test": "docker compose down -v && docker compose run bot-api bun test" }, "dependencies": { - "@types/express": "^4.17.21", - "@types/express-list-endpoints": "^6.0.3", - "@types/jsonwebtoken": "^9.0.5", - "@types/mongoose": "^5.11.97", "express": "^4.18.2", "express-list-endpoints": "^6.0.0", "jsonwebtoken": "^9.0.2", diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..43a3f6f --- /dev/null +++ b/src/app.ts @@ -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; diff --git a/src/controllers/AuthControler.ts b/src/controllers/AuthControler.ts index 4f0649c..335e11a 100644 --- a/src/controllers/AuthControler.ts +++ b/src/controllers/AuthControler.ts @@ -13,7 +13,7 @@ class AuthControler { const authenticated = await AuthService.find(app, secret); if (authenticated) { - console.log("Authenticated app ", authenticated.app); + console.log("Authenticated app", authenticated.app); // Generate an access token const accessToken = jwt.sign( { app: authenticated.app }, diff --git a/src/controllers/ImageController.ts b/src/controllers/ImageController.ts index 1b86aea..75fca37 100644 --- a/src/controllers/ImageController.ts +++ b/src/controllers/ImageController.ts @@ -3,12 +3,11 @@ import imageService from "../services/ImageService"; import mongoose, { mongo } from "mongoose"; class ImageController { - async getAllImages(req: Request, res: Response): Promise { + async getAllImages(_: Request, res: Response): Promise { try { const images = await imageService.findAll(); res.json({ images }); } catch (error) { - console.error(error); res.status(500).json({ error: "Internal Server Error" }); } } @@ -23,11 +22,9 @@ class ImageController { } catch (error: any) { if (error instanceof mongo.MongoServerError && error.code === 11000) { // Should return 409 Conflict for existing urls - res - .status(409) - .json({ - error: `the image with URL ${error.keyValue.url} already exists`, - }); + res.status(409).json({ + error: `the image with URL ${error.keyValue.url} already exists`, + }); } else if (error instanceof mongoose.Error.ValidationError) { // Should return 400 Bad request for invalid requests res.status(400).json({ error: error.message }); diff --git a/src/index.ts b/src/index.ts index d74231b..4f8ee9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,42 +1,8 @@ -import express from "express"; -import mongoose from "mongoose"; -import listEndpoints from "express-list-endpoints"; -import imageController from "./controllers/ImageController"; -import authControler from "./controllers/AuthControler"; +import populateDatabase from "../tests/populateDatabase"; +import { startApp } from "./app"; -export const app = express(); +await 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(); +try { + await populateDatabase(); +} catch {} diff --git a/src/models/AuthModel.ts b/src/models/AuthModel.ts index df71afe..8c4a94f 100644 --- a/src/models/AuthModel.ts +++ b/src/models/AuthModel.ts @@ -5,15 +5,19 @@ export interface Auth extends Document { secret: String; } -const AuthSchema = new mongoose.Schema({ - app: { - type: String, - required: true, +const AuthSchema = new mongoose.Schema( + { + app: { + type: String, + required: true, + index: true, + }, + secret: { + type: String, + required: true, + }, }, - secret: { - type: String, - required: true, - }, -}); + { collection: "authorizations" } +); export default mongoose.model("authorizations", AuthSchema); diff --git a/src/models/ImageModel.ts b/src/models/ImageModel.ts index d91ab18..f2baa49 100644 --- a/src/models/ImageModel.ts +++ b/src/models/ImageModel.ts @@ -6,21 +6,27 @@ export interface Image extends Document { tags?: String[]; } -const ImageSchema = new mongoose.Schema({ - url: { - type: String, - required: true, - }, - status: { - type: String, - enum: { - values: ["consumed", "unavailable", "available"], +const ImageSchema = new mongoose.Schema( + { + url: { + type: String, + required: true, + index: true, + unique: true, + }, + status: { + type: String, + enum: { + values: ["consumed", "unavailable", "available"], + }, + required: true, + index: true, + }, + tags: { + type: [String], }, - required: true, }, - tags: { - type: [String], - }, -}); + { collection: "images" } +); export default mongoose.model("images", ImageSchema); diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index aed8d12..ac32127 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -2,7 +2,7 @@ import AuthModel, { Auth } from "../models/AuthModel"; class AuthService { async find(app: String, secret: String): Promise { - const auth = await AuthModel.findOne({ app: app, secret: secret }).exec(); + const auth = await AuthModel.findOne({ app: app, secret: secret }); return auth; } } diff --git a/tests/app.test.ts b/tests/app.test.ts index ac40ace..f8f7652 100644 --- a/tests/app.test.ts +++ b/tests/app.test.ts @@ -1,13 +1,29 @@ -import { afterEach, beforeAll, describe, expect, it, mock } from "bun:test"; -import request from "supertest"; -import { app } from "../src"; +import { + afterEach, + beforeAll, + describe, + expect, + it, + mock, +} from "bun:test"; +import request, { Response } from "supertest"; +import app, { startApp } from "../src/app"; import imageService from "../src/services/ImageService"; +import populateDatabase from "./populateDatabase"; const imageServiceOriginal = imageService; -const tok = await request(app) - .post("/login") - .send({ app: "tester", secret: "test" }); -const token = tok.body.token; + +let token: string; + +beforeAll(async () => { + await startApp(); + await populateDatabase(); + + const tok = await request(app) + .post("/login") + .send({ app: "tester", secret: "test" }); + token = tok.body.token; +}); afterEach(() => { mock.restore(); @@ -16,6 +32,28 @@ afterEach(() => { })); }); +describe("/login works as instended", async () => { + let correctRespose: Response; + beforeAll(async () => { + correctRespose = await request(app) + .post("/login") + .send({ app: "tester", secret: "test" }); + }); + + it("should return 200 for correct login", async () => { + expect(correctRespose.status).toBe(200); + }); + + it("should contain a token", () => { + expect(correctRespose.body).toHaveProperty("token"); + }); + + it("should return 403 for invalid credentials", async () => { + const res = await request(app).post("/login").send({}); + expect(res.status).toBe(403); + }); +}); + describe("GET / shows all of the endpoints", async () => { const res = await request(app).get("/"); @@ -38,6 +76,21 @@ describe("GET /images works properly", async () => { it("should return a 200", async () => { expect(res.statusCode).toBe(200); }); + + it("should return 500 for an error on the service", async () => { + mock.module("../src/services/ImageService", () => ({ + default: { + add: () => { + throw new Error("This is an expected testing error"); + }, + }, + })); + + const res = await request(app) + .get("/images"); + + expect(res.status).toBe(500); + }); }); describe("POST /images works properly", () => { diff --git a/tests/auth.test.ts b/tests/auth.test.ts deleted file mode 100644 index bd20cd2..0000000 --- a/tests/auth.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it, mock } from "bun:test"; -import request from "supertest"; -import { app } from "../src"; - -describe("/login", async () => { - const correctRespose = await request(app).post("/login").send({ - app: "tester", - secret: "test", - }); - it("should return 200 for correct login", () => { - expect(correctRespose.status).toBe(200); - }); - it("should contain a token", () => { - expect(correctRespose.body).toHaveProperty("token"); - }); - - it("should return 403 for invalid credentials", async () => { - const res = await request(app).post("/login").send({}); - expect(res.status).toBe(403); - }); -}); diff --git a/tests/populateDatabase.ts b/tests/populateDatabase.ts new file mode 100644 index 0000000..f60c9c4 --- /dev/null +++ b/tests/populateDatabase.ts @@ -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", + }); +}