Skip to content

Commit

Permalink
Improve server tests (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonfelixrico authored Apr 6, 2024
1 parent 2b8735e commit e423af8
Show file tree
Hide file tree
Showing 19 changed files with 617 additions and 113 deletions.
27 changes: 24 additions & 3 deletions .github/workflows/verify-server-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
working-directory: ./server
shell: bash

test:
test-unit:
needs:
- build
- lint
Expand All @@ -57,6 +57,27 @@ jobs:
working-directory: ./server
shell: bash
- name: Run Jest tests
run: yarn test
run: yarn test:unit
working-directory: ./server
shell: bash
shell: bash

test-e2e:
needs:
- build
- lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: yarn
- name: Install dependencies
run: yarn install
working-directory: ./server
shell: bash
- name: Run Jest tests
run: yarn test:e2e
working-directory: ./server
shell: bash
1 change: 1 addition & 0 deletions Dockerfile.server
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ RUN yarn install

COPY server/src ./src
COPY server/tsconfig.json .
COPY server/tsconfig.production.json .
RUN yarn build

# Run stage
Expand Down
3 changes: 2 additions & 1 deletion server/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ const config = {
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
'@test/(.*)': '<rootDir>/test/$1',
'@test/(.*)': '<rootDir>/jest/$1',
'@manifest': '<rootDir>/package.json',
},

// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
Expand Down
94 changes: 94 additions & 0 deletions server/jest/__tests__/socket-join.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { io } from 'socket.io-client'
import { createAppInstance } from '@test/utils/server.test-utils'
import { connectWrapperManager } from '@test/utils/socket.test-utils'
import Bluebird from 'bluebird'
import { createTestAxios } from '@test/utils/axios.test-utils'

describe('socket-join', () => {
const testAxios = createTestAxios(3001)
const { connectWrapper, disconnectAll } = connectWrapperManager()
afterEach(() => {
disconnectAll()
})

let serverCleanup: () => Promise<void>
beforeAll(async () => {
const { close } = await createAppInstance(3001)
serverCleanup = close
})
afterAll(async () => {
await serverCleanup!()
})

it('allows connecting', async () => {
const { data } = await testAxios.post<{ id: string }>('/room')
const roomId = data.id

const client = io('http://localhost:3001', {
query: {
roomId,
name: 'User 1',
clientId: 'user-1',
},
})

await expect(connectWrapper(client)).resolves.toBeTruthy()
})

it('broadcasts joins/leaves to other people', async () => {
const { data } = await testAxios.post<{ id: string }>('/room')
const roomId = data.id

const clientA = await connectWrapper(
io('http://localhost:3001', {
query: {
roomId,
name: 'User 1',
clientId: 'user-1',
},
})
)

const serverEventSpy = jest.fn()
clientA.on('SERVER', serverEventSpy)

// role: joiner
const clientB = await connectWrapper(
io('http://localhost:3001', {
query: {
roomId,
name: 'User 2',
clientId: 'user-2',
},
forceNew: true,
})
)

expect(serverEventSpy).toHaveBeenNthCalledWith(1, {
CONN_ACTIVITY: expect.objectContaining({
id: 'user-2',
name: 'User 2',
action: 'join',
}),
})

clientB.disconnect()
/*
* Arbitray delay, we just want to wait for the server broadcast for disconnect
* TODO find a better method than doing a delay. this is not going to be very consistent if the server takes longer than the delay to broadcast.
*
* Things tried before falling back to this:
* - Listening for the disconnect event on clientB (`clientB.once('disconnect', ...)`). The event firing doesn't mean that the message has been
* broadcasted.
* - // add more here
*/
await Bluebird.delay(1_000)
expect(serverEventSpy).toHaveBeenNthCalledWith(2, {
CONN_ACTIVITY: expect.objectContaining({
id: 'user-2',
name: 'User 2',
action: 'leave',
}),
})
})
})
99 changes: 99 additions & 0 deletions server/jest/__tests__/socket-pad.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Socket, io } from 'socket.io-client'
import { createAppInstance } from '@test/utils/server.test-utils'
import { connectWrapperManager } from '@test/utils/socket.test-utils'
import Bluebird from 'bluebird'
import { createTestAxios } from '@test/utils/axios.test-utils'

describe('socket-join', () => {
const testAxios = createTestAxios(3002)

let serverCleanup: () => Promise<void>
let roomId: string
beforeAll(async () => {
const { close } = await createAppInstance(3002)
serverCleanup = close

const { data } = await testAxios.post<{
id: string
}>('/room')

roomId = data.id
}, 10_000)
afterAll(async () => {
await serverCleanup!()
})

const { connectWrapper, disconnectAll } = connectWrapperManager()
let clientA: Socket
let clientB: Socket

beforeEach(async () => {
clientA = await connectWrapper(
io('http://localhost:3002', {
query: {
roomId,
clientId: 'user-a',
nanme: 'User A',
},
})
)

clientB = await connectWrapper(
io('http://localhost:3002', {
query: {
roomId,
clientId: 'user-a',
nanme: 'User A',
},
})
)
})

afterEach(() => {
disconnectAll()
})

it('broadcasts PAD events', async () => {
const clientAHandler = jest.fn()
const clientBHandler = jest.fn()

clientA.on('PAD', clientAHandler)
clientB.on('PAD', clientBHandler)

clientA.emit('PAD', {
DUMMY_MESSAGE: {
foo: 'bar',
},
})

await Bluebird.delay(1_000)
expect(clientAHandler).not.toHaveBeenCalled()
expect(clientBHandler).toHaveBeenCalledWith({
DUMMY_MESSAGE: {
foo: 'bar',
},
})
})

it('broadcasts PAD_TRANSIENT events', async () => {
const clientAHandler = jest.fn()
const clientBHandler = jest.fn()

clientA.on('PAD_TRANSIENT', clientAHandler)
clientB.on('PAD_TRANSIENT', clientBHandler)

clientA.emit('PAD_TRANSIENT', {
DUMMY_MESSAGE: {
foo: 'bar',
},
})

await Bluebird.delay(1_000)
expect(clientAHandler).not.toHaveBeenCalled()
expect(clientBHandler).toHaveBeenCalledWith({
DUMMY_MESSAGE: {
foo: 'bar',
},
})
})
})
25 changes: 0 additions & 25 deletions server/jest/tsconfig.json

This file was deleted.

7 changes: 7 additions & 0 deletions server/jest/utils/axios.test-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import axios from 'axios'

export function createTestAxios(port = 3000) {
return axios.create({
baseURL: `http://localhost:${port}`,
})
}
29 changes: 29 additions & 0 deletions server/jest/utils/server.test-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { initServer } from '@/server'
import { createHttpTerminator } from 'http-terminator'

export async function createAppInstance(port = 3000) {
const httpServer = initServer()

const terminator = createHttpTerminator({
server: httpServer,
})

async function close() {
terminator.terminate()
}

await new Promise<void>((resolve, reject) => {
try {
httpServer.listen(port, () => {
resolve()
})
} catch (e) {
// calling server.listen can throw, hence we have a try-catch here
reject(e)
}
})

return {
close,
}
}
33 changes: 33 additions & 0 deletions server/jest/utils/socket.test-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Socket } from 'socket.io-client'

export function connectWrapper(socket: Socket) {
const waiter = new Promise<Socket>((resolve, reject) => {
socket.once('connect', () => {
resolve(socket)
})
socket.once('connect_error', reject)
})
socket.connect()

return waiter
}

export function connectWrapperManager() {
let clients: Socket[] = []

async function connect(socket: Socket) {
const client = await connectWrapper(socket)
clients.push(client)
return client
}

function disconnectAll() {
clients.forEach((client) => client.disconnect())
clients = []
}

return {
connectWrapper: connect,
disconnectAll,
}
}
3 changes: 0 additions & 3 deletions server/nodemon.json

This file was deleted.

14 changes: 12 additions & 2 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
"scripts": {
"lint": "eslint ./**/*.ts",
"format": "prettier ./**/*.{ts,json,js} --w",
"build": "tsc && tsc-alias",
"build": "tsc -p ./tsconfig.production.json && tsc-alias -p ./tsconfig.production.json",
"build:clean": "rm -rf ./dist",
"start": "node dist/src/index.js",
"dev": "tsx src/index.ts",
"test": "jest"
"test:unit": "jest --passWithNoTests ./src",
"test:e2e": "jest --passWithNoTests --forceExit ./jest"
},
"dependencies": {
"@faker-js/faker": "^8.4.1",
Expand All @@ -21,17 +23,25 @@
"socket.io": "^4.7.4"
},
"devDependencies": {
"@types/bluebird": "^3.5.42",
"@types/express": "^4.17.21",
"@types/http-terminator": "^2.0.5",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.25",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"axios": "^1.6.8",
"bluebird": "^3.7.2",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"http-terminator": "^3.2.0",
"jest": "^29.7.0",
"prettier": "^3.2.5",
"socket.io-client": "^4.7.5",
"supertest": "^6.3.4",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"tsc-alias": "^1.8.8",
Expand Down
Loading

0 comments on commit e423af8

Please sign in to comment.