Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Heatlh) add ep and tests #8

Merged
merged 1 commit into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ jobs:
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis

- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '18'

- name: Install dependencies
run: npm install # Adjust for your specific setup

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,4 @@ dist

test-report.xml
debug.json
config/*.env
File renamed without changes.
8 changes: 7 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@ module.exports = {
reporters: ['default'],
testResultsProcessor: 'jest-sonar-reporter',
coverageReporters: ['text', 'html', 'lcov', 'clover'],
coveragePathIgnorePatterns: ['/node_modules/', '/test/']
coveragePathIgnorePatterns: [
'/interfaces/',
'/node_modules/',
'/test/',
'/start.ts',
'/server.ts',
]
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"scripts": {
"check-dependencies": "madge --circular ./src",
"dev:api": "cross-env NODE_ENV=dev ts-node-dev --ignore-watch node_modules ./src/apps/apiApp/backend/start.ts",
"start:api": "cross-env NODE_ENV=production ts-node ./src/apps/apiApp/backend/start.ts",
"lint": "eslint . --ext .ts --max-warnings=0",
"lint:fix": "eslint . --ext .ts --fix",
"pre-commit": "lint-staged",
Expand Down
1 change: 1 addition & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
sonar.projectKey=vinjatovix_ts-api
sonar.organization=vinjatovix
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=**/tests/**/*, **/node_modules/**/*, **/start.ts


# This is the name and version displayed in the SonarCloud UI.
Expand Down
20 changes: 20 additions & 0 deletions src/apps/apiApp/backend/ApiApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Server } from './server';

export class ApiApp {
server?: Server;

async start() {
const port: string = process.env.PORT ?? '0';
this.server = new Server(port);

return this.server.listen();
}

async stop() {
return this.server?.stop();
}

get httpServer() {
return this.server?.getHTTPServer();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Request, Response } from 'express';
import { Controller } from '../../../shared/interfaces/Controller';

export class GetStatusController implements Controller {
async run(_req: Request, res: Response): Promise<void> {
res.status(200).json({ status: 'OK' });
}
}
2 changes: 2 additions & 0 deletions src/apps/apiApp/backend/dependency-injection/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
imports:
- { resource: ./apps/application.yml }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
imports:
- { resource: ./application.yml }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
imports:
- { resource: ./application.yml }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
imports:
- { resource: ./application.yml }
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
services:
Apps.apiApp.controllers.health.GetStatusController:
class: ../../controllers/health/GetStatusController
arguments: []
9 changes: 9 additions & 0 deletions src/apps/apiApp/backend/dependency-injection/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ContainerBuilder, YamlFileLoader } from 'node-dependency-injection';

const container = new ContainerBuilder();
const loader = new YamlFileLoader(container);
const env = process.env.NODE_ENV ?? 'dev';

loader.load(`${__dirname}/application_${env}.yml`);

export default container;
14 changes: 14 additions & 0 deletions src/apps/apiApp/backend/routes/health/health.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Request, Response, Router } from 'express';
import { GetStatusController } from '../../controllers/health/GetStatusController';
import container from '../../dependency-injection';

const prefix = '/api/v1/health';

export const register = (router: Router) => {
const controller: GetStatusController = container.get(
'Apps.apiApp.controllers.health.GetStatusController'
);
router.get(`${prefix}/http`, (req: Request, res: Response) => {
controller.run(req, res);
});
};
18 changes: 18 additions & 0 deletions src/apps/apiApp/backend/routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Router } from 'express';
import { globSync } from 'glob';

export function registerRoutes(router: Router) {
const routes = globSync('**/*.routes.*', {
cwd: __dirname,
ignore: '**/index.ts'
});
routes.forEach((route) => {
_register(route, router);
});
}

const _register = (routePath: string, router: Router) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const route = require(__dirname + '/' + routePath);
route.register(router);
};
80 changes: 80 additions & 0 deletions src/apps/apiApp/backend/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import dotenv from 'dotenv';
import bodyParser from 'body-parser';
import errorHandler from 'errorhandler';
import express, { Request, Response, NextFunction } from 'express';
import Router from 'express-promise-router';
import helmet from 'helmet';
import * as http from 'http';
import httpStatus from 'http-status';
import cors from 'cors';
import { registerRoutes } from './routes';

dotenv.config();
const corsOptions = {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true
};

export class Server {
private express: express.Express;
private port: string;
private httpServer?: http.Server;

constructor(port: string) {
this.port = port;
this.express = express();
this.express.use(cors(corsOptions));
this.express.use(bodyParser.json());
this.express.use(bodyParser.urlencoded({ extended: true }));
this.express.use(helmet.xssFilter());
this.express.use(helmet.noSniff());
this.express.use(helmet.hidePoweredBy());
this.express.use(helmet.frameguard({ action: 'deny' }));
const router = Router();
router.use(errorHandler());
this.express.use(router);

registerRoutes(router);

router.use(
(err: Error, _req: Request, res: Response, _next: NextFunction): void => {
console.log(err);
res.status(httpStatus.INTERNAL_SERVER_ERROR).send(err.message);
}
);
}

async listen(): Promise<void> {
return new Promise((resolve) => {
const env = this.express.get('env') as string;
this.httpServer = this.express.listen(this.port, () => {
console.log(
` Mock Backend App is running at http://localhost:${this.port} in ${env} mode`
);
console.log(' Press CTRL-C to stop\n');
resolve();
});
});
}

getHTTPServer() {
return this.httpServer;
}

async stop(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.httpServer) {
this.httpServer.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
} else {
resolve();
}
});
}
}
22 changes: 22 additions & 0 deletions src/apps/apiApp/backend/start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import dotenv from 'dotenv';
import path from 'path';
import { ApiApp } from './ApiApp';

dotenv.config({
path: path.resolve(
__dirname,
`../../../../config/${process.env.NODE_ENV}.env`
)
});

try {
new ApiApp().start();
} catch (e) {
console.log(e);
process.exit(1);
}

process.on('uncaughtException', (err) => {
console.log('uncaughtException', err);
process.exit(1);
});
5 changes: 5 additions & 0 deletions src/apps/apiApp/shared/interfaces/Controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Request, Response } from 'express';

export interface Controller {
run(req: Request, res: Response): Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import request from 'supertest';
import { describe, beforeAll, afterAll, it, expect } from '@jest/globals';
import { ApiApp } from '../../../../../../src/apps/apiApp/backend/ApiApp';

let app: ApiApp;

describe('Health Check Endpoint', () => {
beforeAll(async () => {
app = new ApiApp();
await app.start();
});

afterAll(async () => {
await app.stop();
});

it('should return status 200 and "OK"', async () => {
const { status, body } = await request(app.httpServer).get(
'/api/v1/health/http'
);

expect(status).toBe(200);
expect(body).toMatchObject({ status: 'OK' });
});
});
21 changes: 21 additions & 0 deletions tests/Contexts/api/backEnd/health/unit/getStatusController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Request, Response } from 'express';
import { GetStatusController } from '../../../../../../src/apps/apiApp/backend/controllers/health/GetStatusController';

describe('GetStatusController', () => {
let controller: GetStatusController;
let mockResponse: Partial<Response>;

beforeEach(() => {
controller = new GetStatusController();
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
});

it('should send a status of 200 with the message "OK"', async () => {
await controller.run({} as Request, mockResponse as Response);
expect(mockResponse.status).toHaveBeenCalledWith(200);
expect(mockResponse.json).toHaveBeenCalledWith({ status: 'OK' });
});
});
40 changes: 40 additions & 0 deletions tests/Contexts/api/backEnd/unit/apiApp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ApiApp } from '../../../../../src/apps/apiApp/backend/ApiApp';
import { Server } from '../../../../../src/apps/apiApp/backend/server';

describe('ApiApp', () => {
let apiApp: ApiApp;
const listenSpy = jest.spyOn(Server.prototype, 'listen');
const stopSpy = jest.spyOn(Server.prototype, 'stop');

beforeAll(() => {
apiApp = new ApiApp();
});

afterEach(async () => {
if (apiApp.httpServer?.listening) {
await apiApp.stop();
}
});

it('should start the server', async () => {
await apiApp.start();

expect(listenSpy).toHaveBeenCalled();
});

it('should stop the server', async () => {
await apiApp.start();

await apiApp.stop();

expect(stopSpy).toHaveBeenCalled();
});

it('should return the HTTP server', async () => {
await apiApp.start();

const httpServer = apiApp.httpServer;

expect(httpServer).toBeDefined();
});
});
15 changes: 15 additions & 0 deletions tests/apps/apiApp/backend/features/health/getStatus.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Feature: Api Health Check
In order to verify the application's availability
As a health check client
I want check the API status

Scenario: Performing a health check
Given a GET request to "/api/v1/health/http"
Then the response status code should be 200
Then the response body should be
"""
{
"status": "OK"
}
"""

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { AfterAll, BeforeAll, Given, Then } from '@cucumber/cucumber';
import request from 'supertest';
import { ApiApp } from '../../../../../../src/apps/apiApp/backend/ApiApp';
import chai from 'chai';

const expect = chai.expect;
let _request: request.Test;
let app: ApiApp;

BeforeAll(async () => {
app = new ApiApp();
await app.start();
});

AfterAll(async () => {
await app.stop();
});

Given('a GET request to {string}', (route: string) => {
_request = request(app.httpServer).get(route);
});

Then('the response status code should be {int}', async (status: number) => {
const response = await _request;
expect(response.status).to.be.equal(status);
});

Then('the response body should be', async (docString: string) => {
const response = await _request;
const expectedResponseBody = JSON.parse(docString);
expect(response.body).to.deep.equal(expectedResponseBody);
});

Then('the response body should not be', async (docString: string) => {
const response = await _request;
const expectedResponseBody = JSON.parse(docString);
expect(response.body).to.not.deep.equal(expectedResponseBody);
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
"noUnusedLocals": true,
"outDir": "./dist"
},
"include": ["src/**/**.ts"],
"include": ["src/**/*.ts", "tests/**/*.ts"],
"exclude": ["node_modules"]
}
Loading