Skip to content

Commit 1a5b839

Browse files
committed
First commit
0 parents  commit 1a5b839

11 files changed

+3909
-0
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
node_modules/
2+
dist
3+
coverage/
4+
.awcache/
5+
6+
*.log
7+
*.env

__mocks__/net.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
const server = {
4+
on: jest.fn()
5+
}
6+
7+
interface FakeNet {
8+
createServer: () => typeof server
9+
}
10+
11+
const net = jest.genMockFromModule('net');
12+
13+
(net as FakeNet).createServer = () => server
14+
15+
module.exports = net

jest.config.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
}

package.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "chat-server",
3+
"version": "0.0.1",
4+
"description": "A very simple chat server",
5+
"author": "Elia Mazzuoli <[email protected]>",
6+
"license": "none",
7+
"scripts": {
8+
"start": "DEBUG=* ts-node --project tsconfig.json src/index.ts",
9+
"test": "jest --coverage=true"
10+
},
11+
"devDependencies": {
12+
"@types/debug": "^4.1.5",
13+
"@types/jest": "^24.9.1",
14+
"@types/node": "^13.1.8",
15+
"jest": "^25.1.0",
16+
"ts-jest": "^25.0.0",
17+
"ts-node": "^8.6.2",
18+
"typescript": "^3.5.3"
19+
},
20+
"dependencies": {
21+
"debug": "^4.1.1"
22+
}
23+
}

src/Application.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import D from 'debug'
2+
3+
const debug = D('application')
4+
5+
export interface User {
6+
address: string
7+
port: number
8+
send: (data: string | Uint8Array) => boolean
9+
}
10+
11+
export const createNewApplication = () => {
12+
13+
// This is the unique mutable point for simulate memory
14+
let users: User[] = []
15+
16+
return {
17+
addUser: (user: User) => {
18+
users = [...users, user]
19+
debug(`Added user from ${user.address}:${user.port}, ${users.length} in the chat`)
20+
},
21+
removeUser: (user: User) => {
22+
users = users.filter( u => u.address !== user.address || u.port !== user.port )
23+
debug(`Removed user from ${user.address}:${user.port}, ${users.length} in the chat`)
24+
},
25+
broadcastMessageByUser: (user: User, message: Buffer) => {
26+
debug(`User from ${user.address}:${user.port} sent message`)
27+
users
28+
.filter( u => u.address !== user.address || u.port !== user.port )
29+
.forEach( dest => {
30+
dest.send(message)
31+
debug(` -> ${dest.address}:${dest.port}`)
32+
});
33+
}
34+
}
35+
36+
}
37+
38+
export type Application = ReturnType<typeof createNewApplication>

src/TcpServer.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import tcp, { Server } from 'net'
2+
import { Application } from './Application';
3+
import D from 'debug'
4+
5+
const debug = D('TcpServer')
6+
7+
export const createNewTcpServer = (application: Application): Server => {
8+
9+
const server = tcp.createServer()
10+
11+
server.on('connection', (conn) => {
12+
if ( !conn.remoteAddress || !conn.remotePort ) {
13+
debug(`Connection not valid refused`)
14+
return
15+
}
16+
17+
conn.setEncoding('utf-8');
18+
19+
const send = (data: string | Uint8Array): boolean => conn.write(data)
20+
21+
const user = { address: conn.remoteAddress, port: conn.remotePort, send }
22+
application.addUser( user )
23+
24+
conn.on('data', data => application.broadcastMessageByUser(user, data))
25+
conn.on('end', () => application.removeUser(user))
26+
})
27+
28+
return server
29+
}

src/__tests__/Application.test.ts

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { createNewApplication } from '../Application'
2+
3+
describe('Send broadcast message', () => {
4+
it('should send any message if there is only one user', () => {
5+
const user1 = {
6+
address: '127.0.0.1',
7+
port: 3001,
8+
send: jest.fn()
9+
}
10+
11+
const app = createNewApplication()
12+
app.addUser(user1)
13+
14+
app.broadcastMessageByUser(user1, new Buffer('Hello'))
15+
16+
expect(user1.send.mock.calls.length).toBe(0)
17+
}),
18+
19+
it('should send message to all users but the sender if there are more than two users', () => {
20+
const user1 = {
21+
address: '127.0.0.1',
22+
port: 3001,
23+
send: jest.fn()
24+
}
25+
26+
const user2 = {
27+
address: '127.0.0.2',
28+
port: 3002,
29+
send: jest.fn()
30+
}
31+
32+
const user3 = {
33+
address: '127.0.0.3',
34+
port: 3003,
35+
send: jest.fn()
36+
}
37+
38+
const app = createNewApplication()
39+
app.addUser(user1)
40+
app.addUser(user2)
41+
app.addUser(user3)
42+
43+
app.broadcastMessageByUser(user1, new Buffer('Hello'))
44+
45+
expect(user1.send.mock.calls.length).toBe(0)
46+
expect(user2.send.mock.calls.length).toBe(1)
47+
expect(user3.send.mock.calls.length).toBe(1)
48+
})
49+
50+
it('should not send message to deleted user', () => {
51+
const user1 = {
52+
address: '127.0.0.1',
53+
port: 3001,
54+
send: jest.fn()
55+
}
56+
57+
const user2 = {
58+
address: '127.0.0.2',
59+
port: 3002,
60+
send: jest.fn()
61+
}
62+
63+
const user3 = {
64+
address: '127.0.0.3',
65+
port: 3003,
66+
send: jest.fn()
67+
}
68+
69+
const app = createNewApplication()
70+
app.addUser(user1)
71+
app.addUser(user2)
72+
app.addUser(user3)
73+
74+
app.broadcastMessageByUser(user1, new Buffer('Hello'))
75+
76+
expect(user1.send.mock.calls.length).toBe(0)
77+
expect(user2.send.mock.calls.length).toBe(1)
78+
expect(user3.send.mock.calls.length).toBe(1)
79+
80+
app.removeUser(user3)
81+
app.broadcastMessageByUser(user1, new Buffer('Hello'))
82+
83+
expect(user1.send.mock.calls.length).toBe(0)
84+
expect(user2.send.mock.calls.length).toBe(2)
85+
expect(user3.send.mock.calls.length).toBe(1)
86+
})
87+
})

src/__tests__/TcpServer.test.ts

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
jest.mock('net')
2+
3+
describe('Create new TcpServer', () => {
4+
5+
beforeEach(() => {
6+
jest.resetModules();
7+
})
8+
9+
it('gived a correct connection should set connection and call addUser', () => {
10+
const TcpServer = require('../TcpServer')
11+
12+
const applicationAddUser = jest.fn()
13+
14+
const server = TcpServer.createNewTcpServer({
15+
addUser: applicationAddUser
16+
})
17+
expect(server.on.mock.calls[0][0]).toBe('connection')
18+
19+
const tcpServerConnectionCb = server.on.mock.calls[0][1]
20+
const connectionMock = {
21+
setEncoding: jest.fn(),
22+
on: jest.fn(),
23+
remoteAddress: '127.0.0.1',
24+
remotePort: 45664,
25+
}
26+
27+
tcpServerConnectionCb(connectionMock)
28+
29+
expect(connectionMock.setEncoding.mock.calls[0][0]).toBe('utf-8')
30+
expect(applicationAddUser.mock.calls.length).toBe(1)
31+
expect(connectionMock.on.mock.calls[0][0]).toBe('data')
32+
expect(connectionMock.on.mock.calls[1][0]).toBe('end')
33+
}),
34+
35+
it('gived an incorrect connection should not set connection and call addUser', () => {
36+
const TcpServer = require('../TcpServer')
37+
38+
const applicationAddUser = jest.fn()
39+
40+
const server = TcpServer.createNewTcpServer({
41+
addUser: applicationAddUser
42+
})
43+
expect(server.on.mock.calls[0][0]).toBe('connection')
44+
45+
const tcpServerConnectionCb = server.on.mock.calls[0][1]
46+
const connectionMock = {
47+
setEncoding: jest.fn(),
48+
on: jest.fn(),
49+
remoteAddress: '127.0.0.1',
50+
} // remotePort is missing
51+
52+
tcpServerConnectionCb(connectionMock)
53+
54+
expect(connectionMock.setEncoding.mock.calls.length).toBe(0)
55+
expect(applicationAddUser.mock.calls.length).toBe(0)
56+
expect(connectionMock.on.mock.calls.length).toBe(0)
57+
})
58+
59+
it('shoudl call the write func when an user send a message', () => {
60+
const TcpServer = require('../TcpServer')
61+
62+
const applicationAddUser = jest.fn()
63+
const applicationBroadcastMessageByUser = jest.fn()
64+
65+
const server = TcpServer.createNewTcpServer({
66+
addUser: applicationAddUser,
67+
broadcastMessageByUser: applicationBroadcastMessageByUser
68+
})
69+
expect(server.on.mock.calls[0][0]).toBe('connection')
70+
71+
const tcpServerConnectionCb = server.on.mock.calls[0][1]
72+
const connectionMock = {
73+
setEncoding: jest.fn(),
74+
on: jest.fn(),
75+
write: jest.fn(),
76+
remoteAddress: '127.0.0.1',
77+
remotePort: 3001
78+
}
79+
80+
tcpServerConnectionCb(connectionMock)
81+
expect(applicationAddUser.mock.calls.length).toBe(1)
82+
83+
const connectionOnDataCb = connectionMock.on.mock.calls[0][1]
84+
connectionOnDataCb(new Buffer('Hello'))
85+
86+
expect(applicationBroadcastMessageByUser.mock.calls.length).toBe(1)
87+
88+
const userSendFunction = applicationBroadcastMessageByUser.mock.calls[0][0].send
89+
userSendFunction('Hello')
90+
91+
expect(connectionMock.write.mock.calls.length).toBe(1)
92+
})
93+
94+
it('shoudl call removeUser when a connection end', () => {
95+
const TcpServer = require('../TcpServer')
96+
97+
const applicationAddUser = jest.fn()
98+
const applicationremoveUser = jest.fn()
99+
100+
const server = TcpServer.createNewTcpServer({
101+
addUser: applicationAddUser,
102+
removeUser: applicationremoveUser
103+
})
104+
expect(server.on.mock.calls[0][0]).toBe('connection')
105+
106+
const tcpServerConnectionCb = server.on.mock.calls[0][1]
107+
const connectionMock = {
108+
setEncoding: jest.fn(),
109+
on: jest.fn(),
110+
remoteAddress: '127.0.0.1',
111+
remotePort: 3001
112+
}
113+
114+
tcpServerConnectionCb(connectionMock)
115+
expect(applicationAddUser.mock.calls.length).toBe(1)
116+
117+
const endConnectionCb = connectionMock.on.mock.calls[1][1]
118+
endConnectionCb({})
119+
120+
expect(applicationremoveUser.mock.calls.length).toBe(1)
121+
})
122+
})

src/index.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { createNewApplication } from './Application';
2+
import { createNewTcpServer } from './TcpServer';
3+
import D from 'debug'
4+
5+
const debug = D('boot')
6+
const app = createNewApplication()
7+
const server = createNewTcpServer(app)
8+
9+
debug('Server listening on port 10000')
10+
server.listen(10000)

tsconfig.json

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"compilerOptions": {
3+
"moduleResolution": "node",
4+
"module": "commonjs",
5+
"target": "es2019",
6+
"outDir": "dist",
7+
"rootDir": "src",
8+
"lib": ["es2019"],
9+
"types": ["node", "jest"],
10+
"resolveJsonModule": true,
11+
"allowSyntheticDefaultImports": true,
12+
13+
"inlineSourceMap": true,
14+
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
15+
16+
"strict": true /* Enable all strict type-checking options. */,
17+
18+
/* Strict Type-Checking Options */
19+
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
20+
"strictNullChecks": true /* Enable strict null checks. */,
21+
// "strictFunctionTypes": true /* Enable strict checking of function types. */,
22+
// "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
23+
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
24+
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
25+
26+
/* Additional Checks */
27+
"noUnusedLocals": true /* Report errors on unused locals. */,
28+
"noUnusedParameters": true /* Report errors on unused parameters. */,
29+
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
30+
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
31+
32+
/* Debugging Options */
33+
"traceResolution": false /* Report module resolution log messages. */,
34+
"listEmittedFiles": false /* Print names of generated files part of the compilation. */,
35+
"listFiles": false /* Print names of files part of the compilation. */,
36+
"pretty": true /* Stylize errors and messages using color and context. */,
37+
38+
/* Experimental Options */
39+
// "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
40+
// "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
41+
42+
"skipLibCheck": true
43+
},
44+
"compileOnSave": false
45+
}

0 commit comments

Comments
 (0)