diff --git a/backend/package-lock.json b/backend/package-lock.json index d151668..714ef76 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -26,6 +26,7 @@ "eslint-plugin-react": "^7.37.4", "globals": "^15.14.0", "jest": "^29.7.0", + "socket.io-client": "^4.8.1", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "^5.7.3", @@ -3018,6 +3019,38 @@ "node": ">=10.2.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", @@ -7159,6 +7192,40 @@ } } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -8161,6 +8228,15 @@ } } }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/package.json b/backend/package.json index c858fa3..d25555d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -32,6 +32,7 @@ "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "^5.7.3", - "typescript-eslint": "^8.21.0" + "typescript-eslint": "^8.21.0", + "socket.io-client": "^4.8.1" } } diff --git a/backend/src/app.spec.ts b/backend/src/app.spec.ts new file mode 100644 index 0000000..d886fe0 --- /dev/null +++ b/backend/src/app.spec.ts @@ -0,0 +1,73 @@ +import { ChatServer } from "./chat-server"; +import CommandService from "./command-service"; +import ChatRepo, { ChatSession } from "./repositories/chat-repo"; +import { pino } from "pino"; +import { io as ioc, type Socket as ClientSocket } from "socket.io-client"; +import { MongoClient } from "mongodb"; + +describe("App Integration Test", () => { + let chatServer: ChatServer; + let mongoClient: MongoClient; + let clientSocket: ClientSocket; + let close: () => void; + const mongoUri = process.env.MONGODB_URI || "mongodb://localhost:27017"; + const serverPort = 3001; + const serverUri = `http://localhost:${serverPort}`; + + beforeAll(async () => { + const dbName = "test" + Date.now(); + const collectionName = "chat-session"; + const logger = pino(); + mongoClient = await new MongoClient(mongoUri).connect(); + logger.info("Connected to MongoDB"); + const db = mongoClient.db(dbName); + const collection = db.collection(collectionName); + const commandService = new CommandService(logger, new ChatRepo(collection)); + chatServer = new ChatServer(logger, commandService, serverPort); + close = chatServer.listen(); + }); + + beforeEach(async () => { + clientSocket = await ioc(serverUri).connect(); + }); + + afterEach(async () => { + await clientSocket.close(); + }); + + afterAll(async () => { + await close(); + await mongoClient.close(); + }); + + it("should return result", (done: jest.DoneCallback) => { + clientSocket.on("result", (arg) => { + expect(arg).toBe("2"); + done(); + }); + clientSocket.emit("operation", "1+1"); + }); + it("should return error", (done: jest.DoneCallback) => { + clientSocket.on("error", (arg) => { + expect(arg).toBe("Invalid command"); + done(); + }); + clientSocket.emit("operation", "1--2"); + }); + it("should return history", (done: jest.DoneCallback) => { + const expected = [{ expression: "1+1", result: "2" }, { expression: "2+3", result: "5" }]; + + clientSocket.on("result", (arg) => { + if (arg===expected[expected.length-1].result) { + clientSocket.emit("history"); + } + }); + clientSocket.on("history", (arg) => { + expect(arg).toEqual(expected); + done(); + }); + + clientSocket.emit("operation", "1+1"); // step 1 + clientSocket.emit("operation", "2+3"); // step 2 + }); +}); diff --git a/backend/src/app.ts b/backend/src/app.ts index 9d9e72d..0f45d37 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -9,20 +9,30 @@ const uri = process.env.MONGODB_URI || "mongodb://localhost:27017"; const dbName = "math-chat"; const collectionName = "chat-session"; -async function getMongoCollection(uri: string, dbName: string, collectionName: string): Promise> { - const mongoClient = new MongoClient(uri); - await mongoClient.connect(); +async function getMongoCollection(mongoClient: MongoClient, dbName: string, collectionName: string): Promise> { const db = mongoClient.db(dbName); const collection = db.collection(collectionName); return collection; } +function connectMongo(uri: string): Promise { + return new MongoClient(uri).connect(); +} async function main() { const logger = pino(); - const collection = await getMongoCollection(uri, dbName, collectionName); + const mongoClient = await connectMongo(uri); + const collection = await getMongoCollection(mongoClient, dbName, collectionName); const commandService = new CommandService(logger, new ChatRepo(collection)); const chatServer = new ChatServer(logger, commandService, 3000); - chatServer.listen(); + const serverClose = await chatServer.listen(); + + // graceful shutdown + const shutdown = () => { + serverClose(); + mongoClient.close(); + } + process.on('SIGTERM', shutdown); + process.on('SIGINT', shutdown); } main(); \ No newline at end of file diff --git a/backend/src/chat-server.ts b/backend/src/chat-server.ts index ee74c13..e181236 100644 --- a/backend/src/chat-server.ts +++ b/backend/src/chat-server.ts @@ -60,7 +60,7 @@ export class ChatServer { public listen() { const httpServer = this.app.listen(this.port, - () => { console.log(`Server listening on port ${this.port}`) } + () => { this.logger.info(`Server listening on port ${this.port}`) } ); // Express server and socket.io server is sharing the same http server @@ -70,5 +70,10 @@ export class ChatServer { } }); this.setupSocketHandler(io); + return async function close() { + httpServer.close(); + await io.close(); + } } } +