diff --git a/README.md b/README.md index ef92c8f..b0b773a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This repository hosts the frontend service implementation of the Playground. To start the playground using Docker, run: ```bash -docker-compose up +docker-compose upg ``` ## Prerequisites @@ -68,13 +68,13 @@ services: backend: build: context: . - dockerfile: Dockerfile_Backend + dockerfile: PlaygroundMonoDockerfile ports: - '8080:8080' depends_on: - dicedb environment: - - DICE_ADDR=dicedb:7379 + - DICEDB_ADDR=dicedb:7379 ``` 2. Run the following command to start the backend server and DiceDB: @@ -123,8 +123,9 @@ To generate a static production build of your Next.js application, follow these Ensure that you have the following line in your `next.config.mjs` file: ```javascript - output: 'export' + output: 'export'; ``` + 2. **Build the Project:** Run the following command in your terminal: @@ -132,6 +133,7 @@ To generate a static production build of your Next.js application, follow these ```bash npm run build ``` + 3. **Testing static build locally:** ```bash npx serve@latest out @@ -188,6 +190,6 @@ Contributors can join the [Discord Server](https://discord.gg/6r8uXWtXh7) for qu ## Contributors - - + + diff --git a/package-lock.json b/package-lock.json index bed3c30..e54735e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "postcss": "^8", "prettier": "^3.3.3", "tailwindcss": "^3.4.1", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "^5" } @@ -2899,6 +2900,12 @@ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3178,6 +3185,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -3973,6 +3992,21 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.31", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.31.tgz", @@ -4853,6 +4887,36 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -6105,6 +6169,24 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -7217,6 +7299,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9756,6 +9844,66 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", diff --git a/package.json b/package.json index daff1dc..b55f7de 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "postcss": "^8", "prettier": "^3.3.3", "tailwindcss": "^3.4.1", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "^5" }, diff --git a/src/lib/__tests__/api.test.ts b/src/lib/__tests__/api.test.ts new file mode 100644 index 0000000..deae385 --- /dev/null +++ b/src/lib/__tests__/api.test.ts @@ -0,0 +1,93 @@ +import { executeShellCommandOnServer } from '../api'; +import { WebService } from '@/services/webServices'; + +// Mock WebService +jest.mock('@/services/webServices', () => ({ + WebService: { + post: jest.fn(), + }, +})); + +describe('executeShellCommandOnServer', () => { + const mockCmd = 'testCommand'; + const mockCmdOptions = { option1: 'value1' }; + const mockCmdExecURL = `/shell/exec/${mockCmd}`; + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + jest.clearAllMocks(); + }); + + it('should return data when WebService.post is successful', async () => { + const mockResponse = { data: 'Success Response' }; + (WebService.post as jest.Mock).mockResolvedValueOnce(mockResponse); + + const result = await executeShellCommandOnServer(mockCmd, mockCmdOptions); + + expect(WebService.post).toHaveBeenCalledWith( + mockCmdExecURL, + mockCmdOptions, + ); + expect(result).toEqual('Success Response'); + }); + + it('should return error message when response structure is unexpected', async () => { + const mockResponse = {}; // Simulate unexpected response structure + (WebService.post as jest.Mock).mockResolvedValueOnce(mockResponse); + + const result = await executeShellCommandOnServer(mockCmd, mockCmdOptions); + + expect(result).toBe('Error: Error: Unexpected response structure'); + expect(WebService.post).toHaveBeenCalledWith( + mockCmdExecURL, + mockCmdOptions, + ); + }); + + it('should return error message when WebService.post throws an error', async () => { + const mockError = new Error('Network Error'); + + (WebService.post as jest.Mock).mockRejectedValueOnce(mockError); + const result = await executeShellCommandOnServer(mockCmd, mockCmdOptions); + expect(WebService.post).toHaveBeenCalledWith( + mockCmdExecURL, + mockCmdOptions, + ); + expect(result).toBe(`Error: ${mockError}`); + consoleErrorSpy.mockRestore(); + }); + + it('should log error to the console when WebService.post throws an error', async () => { + const mockError = new Error('Request Failed'); + const consoleSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + (WebService.post as jest.Mock).mockRejectedValueOnce(mockError); + + await executeShellCommandOnServer(mockCmd, mockCmdOptions); + + expect(consoleSpy).toHaveBeenCalledWith( + 'Error executing command:', + mockError, + ); + consoleSpy.mockRestore(); + }); + + it('should always include the cmd parameter in the URL', async () => { + const mockResponse = { data: 'Some Response' }; + (WebService.post as jest.Mock).mockResolvedValueOnce(mockResponse); + + const result = await executeShellCommandOnServer(mockCmd, mockCmdOptions); + + expect(WebService.post).toHaveBeenCalledWith( + expect.stringContaining(`/shell/exec/${mockCmd}`), + mockCmdOptions, + ); + expect(result).toEqual('Some Response'); + }); +}); diff --git a/src/lib/api.ts b/src/lib/api.ts index 1bea792..00ab6c0 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -5,8 +5,10 @@ export const executeShellCommandOnServer = async ( cmd: string, cmdOptions: object, ) => { + const cmdExecURL = `/shell/exec/${cmd}`; + try { - const response = await WebService.post(`/shell/exec`, cmdOptions); + const response = await WebService.post(cmdExecURL, cmdOptions); if (response?.data) { return response.data; } else { diff --git a/src/services/webServices.ts b/src/services/webServices.ts index f721a73..436c323 100644 --- a/src/services/webServices.ts +++ b/src/services/webServices.ts @@ -2,7 +2,7 @@ let PLAYGROUND_MONO_URL = process.env.NEXT_PUBLIC_PLAYGROUND_MONO_URL; if (!PLAYGROUND_MONO_URL) { console.warn( - 'Warning: NEXT_PUBLIC_BACKEND_URL is not defined. Defaulting to http://localhost:3000', + 'Warning: NEXT_PUBLIC_PLAYGROUND_MONO_URL is not defined. Defaulting to http://localhost:8080', ); PLAYGROUND_MONO_URL = 'http://localhost:8080'; } @@ -41,10 +41,16 @@ export const WebService = { try { const response = await fetch(`${PLAYGROUND_MONO_URL}${url}`, options); + if (!response) { + throw new Error('No response received from the server.'); + } + if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } + const result = await response.json(); + return result; } catch (error) { if (error instanceof Error)