diff --git a/.gitignore b/.gitignore index a86b657..dd4b88e 100755 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,6 @@ typings/ .elasticbeanstalk/* !.elasticbeanstalk/*.cfg.yml !.elasticbeanstalk/*.global.yml + +# Test Coverage +coverage \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 7c534e1..4a524fd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,19 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} **/ -export default { +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { testEnvironment: 'node', transform: { - '^.+\.tsx?$': ['ts-jest', {}], + '^.+\\.tsx?$': ['ts-jest', {}], }, + coverageDirectory: 'coverage', + collectCoverage: true, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/*.test.{ts,tsx}', + '!src/**/index.ts', + '!src/**/types.ts', + ], + coverageReporters: ['text', 'lcov'], + testPathIgnorePatterns: ['/node_modules/', '/dist/'], + modulePathIgnorePatterns: ['/dist/'], }; diff --git a/package.json b/package.json index 80f4610..33038c6 100755 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "description": "", "main": "./src/index.ts", "private": true, - "type": "module", "scripts": { "prebuild": "rm -rf dist", "build": "tsc", @@ -12,6 +11,7 @@ "dev": "env-cmd ts-node src/index.ts", "test": "jest", "test:watch": "jest --watch", + "test:coverage": "jest --coverage", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", "lint:check": "eslint . --ext .ts --no-ignore", diff --git a/src/http_tests/app.http b/src/__tests__/http/app.http similarity index 100% rename from src/http_tests/app.http rename to src/__tests__/http/app.http diff --git a/src/http_tests/app.test.ts b/src/__tests__/http/app.test.ts similarity index 96% rename from src/http_tests/app.test.ts rename to src/__tests__/http/app.test.ts index f148cfc..a0ccd0a 100644 --- a/src/http_tests/app.test.ts +++ b/src/__tests__/http/app.test.ts @@ -1,7 +1,7 @@ import 'dotenv/config'; import request, { Response } from 'supertest'; -import app from '../app'; +import app from '../../app'; interface ResponseBody { message?: string; diff --git a/src/http_tests/authentication.test.ts b/src/__tests__/http/authentication.test.ts similarity index 95% rename from src/http_tests/authentication.test.ts rename to src/__tests__/http/authentication.test.ts index a0bbacd..ed5bd33 100644 --- a/src/http_tests/authentication.test.ts +++ b/src/__tests__/http/authentication.test.ts @@ -1,8 +1,8 @@ import 'dotenv/config'; import request, { Response } from 'supertest'; -import app from '../app'; -import { stopDatabase } from '../database/mongo-common'; +import app from '../../app'; +import { stopDatabase } from '../../database/mongo-common'; // Define expected response shapes interface AuthResponse { diff --git a/src/http_tests/stores.http b/src/__tests__/http/stores.http old mode 100755 new mode 100644 similarity index 100% rename from src/http_tests/stores.http rename to src/__tests__/http/stores.http diff --git a/src/http_tests/stores.test.ts b/src/__tests__/http/stores.test.ts similarity index 95% rename from src/http_tests/stores.test.ts rename to src/__tests__/http/stores.test.ts index 9de28b4..91133f2 100644 --- a/src/http_tests/stores.test.ts +++ b/src/__tests__/http/stores.test.ts @@ -1,8 +1,8 @@ import 'dotenv/config'; import request, { Response } from 'supertest'; -import app from '../app'; // Adjust the path as necessary -import { stopDatabase } from '../database/mongo-common'; +import app from '../../app'; // Adjust the path as necessary +import { stopDatabase } from '../../database/mongo-common'; interface Store { _id?: string; // Optionally include the ID in responses diff --git a/src/__tests__/unit/errorHandler.test.ts b/src/__tests__/unit/errorHandler.test.ts new file mode 100644 index 0000000..1b1d34b --- /dev/null +++ b/src/__tests__/unit/errorHandler.test.ts @@ -0,0 +1,50 @@ +import { Request, Response } from 'express'; + +import errorHandler from '../../midleware/errorHandler'; + +describe('errorHandler middleware', () => { + const mockReq = {} as Request; + + let mockStatus: jest.Mock; + let mockJson: jest.Mock; + let mockRes: Response; + + beforeEach(() => { + mockStatus = jest.fn().mockReturnThis(); + mockJson = jest.fn(); + + mockRes = { + json: mockJson, + status: mockStatus, + } as unknown as Response; + }); + + const mockNext = jest.fn(); + + it('should respond with 500 and error message in non-production', () => { + process.env.NODE_ENV = 'development'; // or 'test' + const err = new Error('Something went wrong'); + + errorHandler(err, mockReq, mockRes, mockNext); + + expect(mockStatus).toHaveBeenCalledWith(500); + expect(mockJson).toHaveBeenCalledWith( + expect.objectContaining({ + error: 'Something went wrong', + message: 'Internal Server Error', + }) + ); + }); + + it('should not include error details in production', () => { + process.env.NODE_ENV = 'production'; + const err = new Error('Production error'); + + errorHandler(err, mockReq, mockRes, mockNext); + + expect(mockStatus).toHaveBeenCalledWith(500); + expect(mockJson).toHaveBeenCalledWith({ + message: 'Internal Server Error', + }); + }); +}); diff --git a/src/__tests__/unit/git-user-name.test.ts b/src/__tests__/unit/git-user-name.test.ts new file mode 100644 index 0000000..e1c97bb --- /dev/null +++ b/src/__tests__/unit/git-user-name.test.ts @@ -0,0 +1,38 @@ +import { execSync } from 'child_process'; + +import getGitUserName from '../../utils/git-user-name'; + +jest.mock('child_process'); + +describe('getGitUserName', () => { + afterEach(() => { + jest.resetAllMocks(); + delete process.env.GITHUB_ACTIONS; + delete process.env.GITHUB_ACTOR; + }); + + it('returns GITHUB_ACTOR when running in GitHub Actions', () => { + process.env.GITHUB_ACTIONS = 'true'; + process.env.GITHUB_ACTOR = 'github-test-user'; + expect(getGitUserName()).toBe('github-test-user'); + }); + + it('returns "github-actions" if GITHUB_ACTOR is missing in GitHub Actions', () => { + process.env.GITHUB_ACTIONS = 'true'; + delete process.env.GITHUB_ACTOR; + expect(getGitUserName()).toBe('github-actions'); + }); + + it('returns git config user.name when not in GitHub Actions', () => { + (execSync as jest.Mock).mockReturnValue('Test User\n'); + expect(getGitUserName()).toBe('Test User'); + expect(execSync).toHaveBeenCalledWith('git config --get user.name', { encoding: 'utf8' }); + }); + + it('returns "unknown" if execSync throws', () => { + (execSync as jest.Mock).mockImplementation(() => { + throw new Error('fail'); + }); + expect(getGitUserName()).toBe('unknown'); + }); +}); diff --git a/src/__tests__/unit/stores.test.ts b/src/__tests__/unit/stores.test.ts new file mode 100644 index 0000000..e34ade2 --- /dev/null +++ b/src/__tests__/unit/stores.test.ts @@ -0,0 +1,37 @@ +// tests/database/stores.test.ts +import { ObjectId } from 'mongodb'; + +import { getDatabase } from '../../database/mongo-common'; +import { deleteStore, updateStore } from '../../database/stores'; + +jest.mock('../../database/mongo-common'); + +const mockCollection = { + deleteOne: jest.fn(), + findOne: jest.fn(), + updateOne: jest.fn(), +}; + +(getDatabase as jest.Mock).mockResolvedValue({ + collection: () => mockCollection, +}); + +describe('stores.ts unit tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return "No store found with that id" if delete count is 0', async () => { + mockCollection.deleteOne.mockResolvedValueOnce({ deletedCount: 0 }); + const response = await deleteStore(new ObjectId().toHexString()); + expect(response).toEqual({ message: 'No store found with that id' }); + }); + + it('should return null if store not found after update', async () => { + mockCollection.updateOne.mockResolvedValueOnce({}); + mockCollection.findOne.mockResolvedValueOnce(null); + + const result = await updateStore(new ObjectId().toHexString(), { name: 'Updated' }); + expect(result).toBeNull(); + }); +}); diff --git a/src/midleware/errorHandler.ts b/src/midleware/errorHandler.ts index c2f4b60..71c76cc 100644 --- a/src/midleware/errorHandler.ts +++ b/src/midleware/errorHandler.ts @@ -3,8 +3,6 @@ import { NextFunction, Request, Response } from 'express'; // using _ to indicate that the parameter is not used // eslint-disable-next-line @typescript-eslint/no-unused-vars const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction): void => { - console.error(`[ERROR] ${err.stack ?? 'No stack trace available'}`); - const response = { message: 'Internal Server Error', ...(process.env.NODE_ENV !== 'production' && { error: err.message }), diff --git a/src/utils/git-user-name.ts b/src/utils/git-user-name.ts index a9b9247..7dc31b0 100644 --- a/src/utils/git-user-name.ts +++ b/src/utils/git-user-name.ts @@ -9,7 +9,7 @@ function getGitUserName(): string { const name = execSync('git config --get user.name', { encoding: 'utf8' }).trim(); return name || 'unknown'; } catch (err) { - console.info('Git user name not found, returning "unknown"', err); + console.error('Error getting git user name:', err); return 'unknown'; } }