Skip to content

Commit

Permalink
Socket.io server code to TS and adds unit testing (#418)
Browse files Browse the repository at this point in the history
* feat: initial typescript setup

* feat: converts to ts `api-config` file

* feat: converts to ts `socket-config` file

* feat: converts to ts `rate-limiter` file

* feat: converts to ts `utils` file

* feat: updates the `Dockerfile` to support prebuilding from typescript

* fix: fixes docker image running

* feat: adds package.json start:docker script, setup unit testing and adds it to `api-config` file

* feat: adds unit tests for `rate-limiter` and `socket-config` files

* chore: add jest config files

* fix: fixes socket-config tests and refactors utils.ts for testability

---------

Co-authored-by: Omri <[email protected]>
  • Loading branch information
andreahaku and omridan159 authored Oct 30, 2023
1 parent 3c1a617 commit dc7e024
Show file tree
Hide file tree
Showing 19 changed files with 1,218 additions and 359 deletions.
26 changes: 19 additions & 7 deletions packages/sdk-socket-server/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
FROM node:18-alpine

RUN apk update && apk upgrade && apk add --no-cache zlib
# Build stage
FROM node:18-alpine AS builder

# Install build dependencies and build the project
WORKDIR /app
COPY package.json ./
RUN yarn install
COPY . .
RUN yarn build

COPY package.json .
# Runtime stage
FROM node:18-alpine

RUN yarn install
# Install runtime dependencies
WORKDIR /app
COPY --from=builder /app/package.json ./
RUN yarn install --production

COPY . .
# Copy built project and .env file from the build stage
COPY --from=builder /app/dist ./dist
COPY .env ./

# Expose the server port
EXPOSE 4000

CMD ["yarn","start"]
# Start the server
CMD ["node", "dist/index.js"]
14 changes: 14 additions & 0 deletions packages/sdk-socket-server/__mocks__/analytics-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class Analytics {
constructor(writeKey: string, options?: object) {
// Mock constructor
}

track(event: object, callback?: (err: Error | null) => void): void {
// Mock track method
if (callback) {
callback(null);
}
}
}

export default Analytics;
38 changes: 38 additions & 0 deletions packages/sdk-socket-server/api-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import request from 'supertest';
import { app } from './api-config';

jest.mock('analytics-node');

describe('API Config', () => {
describe('GET /', () => {
it('should respond with success', async () => {
const response = await request(app).get('/');
expect(response.status).toBe(200);
expect(response.body).toStrictEqual({ success: true });
});
});

describe('POST /debug', () => {
it('should respond with error when event is missing', async () => {
const response = await request(app).post('/debug').send({});
expect(response.status).toBe(400);
expect(response.body).toStrictEqual({ error: 'event is required' });
});

it('should respond with error when event name is wrong', async () => {
const response = await request(app)
.post('/debug')
.send({ event: 'wrong_event' });
expect(response.status).toBe(400);
expect(response.body).toStrictEqual({ error: 'wrong event name' });
});

it('should respond with success when event is correct', async () => {
const response = await request(app)
.post('/debug')
.send({ event: 'sdk_test' });
expect(response.status).toBe(200);
expect(response.body).toStrictEqual({ success: true });
});
});
});
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
/* eslint-disable node/no-process-env */
const crypto = require('crypto');
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const helmet = require('helmet');
const { LRUCache } = require('lru-cache');
const Analytics = require('analytics-node');
import crypto from 'crypto';
import express from 'express';
import bodyParser from 'body-parser';
import cors from 'cors';
import helmet from 'helmet';
import { LRUCache } from 'lru-cache';
import Analytics from 'analytics-node';

const isDevelopment = process.env.NODE_ENV === 'development';
const isDevelopment: boolean = process.env.NODE_ENV === 'development';

const userIdHashCache = new LRUCache({
const userIdHashCache = new LRUCache<string, string>({
max: 5000,
maxAge: 1000 * 60 * 60 * 24,
ttl: 1000 * 60 * 60 * 24,
});

const app = express();
Expand All @@ -25,11 +25,11 @@ app.disable('x-powered-by');

const analytics = new Analytics(
isDevelopment
? process.env.SEGMENT_API_KEY_DEBUG
: process.env.SEGMENT_API_KEY_PRODUCTION,
? process.env.SEGMENT_API_KEY_DEBUG || ''
: process.env.SEGMENT_API_KEY_PRODUCTION || '',
{
flushInterval: isDevelopment ? 1000 : 10000,
errorHandler: (err) => {
errorHandler: (err: Error) => {
console.error(`ERROR> Analytics-node flush failed: ${err}`);
},
},
Expand All @@ -51,15 +51,22 @@ app.post('/debug', (_req, res) => {
return res.status(400).json({ error: 'wrong event name' });
}

const id = body.id || 'socket.io-server';
let userIdHash = userIdHashCache.get(id);
const id: string = body.id || 'socket.io-server';
let userIdHash: string | undefined = userIdHashCache.get(id);

if (!userIdHash) {
userIdHash = crypto.createHash('sha1').update(id).digest('hex');
userIdHashCache.set(id, userIdHash);
}

const event = {
const event: {
userId: string;
event: string;
properties: {
userId: string;
[key: string]: string | undefined; // This line adds an index signature
};
} = {
userId: userIdHash,
event: body.event,
properties: {
Expand All @@ -68,7 +75,7 @@ app.post('/debug', (_req, res) => {
};

// Define properties to be excluded
const propertiesToExclude = ['icon'];
const propertiesToExclude: string[] = ['icon'];

for (const property in body) {
if (
Expand All @@ -84,9 +91,9 @@ app.post('/debug', (_req, res) => {
console.log('DEBUG> Event object:', event);
}

analytics.track(event, function (err, batch) {
analytics.track(event, function (err: Error) {
if (isDevelopment) {
console.log('DEBUG> Segment batch', batch);
console.log('DEBUG> Segment batch');
}

if (err) {
Expand All @@ -100,4 +107,4 @@ app.post('/debug', (_req, res) => {
}
});

module.exports = { app, analytics };
export { app, analytics };
23 changes: 0 additions & 23 deletions packages/sdk-socket-server/index.js

This file was deleted.

27 changes: 27 additions & 0 deletions packages/sdk-socket-server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import dotenv from 'dotenv';
dotenv.config();

import http from 'http';
import { app, analytics } from './api-config';
import configureSocketIO from './socket-config';
import { cleanupAndExit } from './utils';

const isDevelopment: boolean = process.env.NODE_ENV === 'development';

const server = http.createServer(app);
configureSocketIO(server); // configure socket.io server

console.log('INFO> isDevelopment?', isDevelopment);

// Register event listeners for process termination events
process.on('SIGINT', async () => {
await cleanupAndExit(server, analytics);
});
process.on('SIGTERM', async () => {
await cleanupAndExit(server, analytics);
});

const port: number = Number(process.env.PORT) || 4000;
server.listen(port, () => {
console.log(`INFO> listening on *:${port}`);
});
19 changes: 19 additions & 0 deletions packages/sdk-socket-server/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import baseConfig from '../../jest.config.base';

module.exports = {
...baseConfig,
testEnvironment: 'node',
coveragePathIgnorePatterns: ['./types', './index.ts'],
collectCoverageFrom: ['*.ts'],
coverageThreshold: {
global: {
branches: 28,
functions: 44,
lines: 49,
statements: 49,
},
},
clearMocks: true,
resetMocks: false,
restoreMocks: false,
};
5 changes: 5 additions & 0 deletions packages/sdk-socket-server/nodemon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"watch": ["."],
"ext": "ts",
"exec": "ts-node"
}
36 changes: 28 additions & 8 deletions packages/sdk-socket-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@metamask/sdk-socket-server",
"version": "1.0.0",
"version": "2.0.0",
"private": true,
"description": "",
"homepage": "https://github.com/MetaMask/metamask-sdk#readme",
Expand All @@ -12,21 +12,22 @@
"url": "https://github.com/MetaMask/metamask-sdk.git"
},
"author": "",
"main": "index.js",
"main": "dist/index.js",
"scripts": {
"build": "echo 'N/A'",
"build": "tsc",
"build:post-tsc": "echo 'N/A'",
"build:pre-tsc": "echo 'N/A'",
"clean": "echo 'N/A'",
"debug": "NODE_ENV=development nodemon index.js --trace-warnings",
"clean": "rimraf dist",
"debug": "NODE_ENV=development ts-node-dev --respawn --transpile-only --inspect -- index.ts",
"lint": "yarn lint:eslint && yarn lint:misc --check",
"lint:changelog": "yarn auto-changelog validate",
"lint:eslint": "eslint . --cache --ext js,ts",
"lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write",
"lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' --ignore-path ../../.gitignore",
"reset": "yarn clean && rimraf ./node_modules/",
"start": "node index.js",
"test": "echo 'N/A'",
"start": "yarn build && node dist/index.js",
"start:docker": "docker run -p 4000:4000 -d socket-test",
"test": "jest",
"test:ci": "jest --coverage --passWithNoTests"
},
"dependencies": {
Expand All @@ -45,6 +46,19 @@
"devDependencies": {
"@lavamoat/allow-scripts": "^2.3.1",
"@metamask/auto-changelog": "^2.3.0",
"@types/analytics-node": "^3.1.13",
"@types/body-parser": "^1.19.4",
"@types/cors": "^2.8.15",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.20",
"@types/helmet": "^4.0.0",
"@types/jest": "^29.5.6",
"@types/node": "^20.8.7",
"@types/socket.io": "^3.0.2",
"@types/supertest": "^2.0.15",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^6.8.0",
"@typescript-eslint/parser": "^6.8.0",
"eslint": "^7.30.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.4",
Expand All @@ -54,7 +68,13 @@
"eslint-plugin-prettier": "^3.4.0",
"jest": "^29.6.4",
"nodemon": "^2.0.20",
"prettier": "^2.8.8"
"prettier": "^2.8.8",
"socket.io-client": "^4.7.2",
"supertest": "^6.3.3",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0",
"typescript": "^5.2.2"
},
"lavamoat": {
"allowScripts": {
Expand Down
43 changes: 43 additions & 0 deletions packages/sdk-socket-server/rate-limiter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
resetRateLimits,
increaseRateLimits,
setLastConnectionErrorTimestamp,
} from './rate-limiter';

import os from 'os';

jest.mock('os');

describe('rate-limiter', () => {
let consoleLogSpy: jest.SpyInstance;

beforeEach(() => {
jest.resetModules(); // Clear any cached modules, which includes the rateLimiter instances.
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
});

afterEach(() => {
consoleLogSpy.mockRestore();
});

it('resetRateLimits should reset rate limits', () => {
setLastConnectionErrorTimestamp(Date.now() - 10001);
resetRateLimits();

expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('INFO> RL points:'),
);
});

it('increaseRateLimits should adjust rate limits based on system load', () => {
(os.loadavg as jest.Mock).mockReturnValue([0.5, 0.5, 0.5]);
(os.cpus as jest.Mock).mockReturnValue(new Array(4));
(os.totalmem as jest.Mock).mockReturnValue(10000000);
(os.freemem as jest.Mock).mockReturnValue(5000000);
increaseRateLimits(50);

expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('INFO> RL points:'),
);
});
});
Loading

0 comments on commit dc7e024

Please sign in to comment.