refactor(realtime): separate test case to individually file

Signed-off-by: BoHong Li <a60814billy@gmail.com>
This commit is contained in:
BoHong Li 2019-05-27 14:54:39 +08:00
parent b17f417af4
commit 79666aeec3
No known key found for this signature in database
GPG Key ID: 9696D5590D58290F
6 changed files with 770 additions and 709 deletions

View File

@ -1,709 +0,0 @@
'use strict'
/* eslint-env node, mocha */
const mock = require('mock-require')
const assert = require('assert')
const sinon = require('sinon')
function makeMockSocket (headers, query) {
const broadCastChannelCache = {}
return {
id: Math.round(Math.random() * 10000),
handshake: {
headers: Object.assign({}, headers),
query: Object.assign({}, query)
},
on: sinon.fake(),
emit: sinon.fake(),
broadCastChannelCache: {},
broadcast: {
to: (channel) => {
if (!broadCastChannelCache[channel]) {
broadCastChannelCache[channel] = {
channel: channel,
emit: sinon.fake()
}
}
return broadCastChannelCache[channel]
}
}
}
}
function removeModuleFromRequireCache (modulePath) {
delete require.cache[require.resolve(modulePath)]
}
describe('realtime', function () {
describe('extractNoteIdFromSocket', function () {
beforeEach(() => {
mock('../lib/logger', {})
mock('../lib/history', {})
mock('../lib/models', {})
})
afterEach(() => {
delete require.cache[require.resolve('../lib/realtime')]
mock.stopAll()
})
describe('urlPath not set', function () {
beforeEach(() => {
mock('../lib/config', {})
realtime = require('../lib/realtime')
})
let realtime
it('return false if socket or socket.handshake not exists', function () {
let noteId = realtime.extractNoteIdFromSocket()
assert.strictEqual(false, noteId)
noteId = realtime.extractNoteIdFromSocket({})
assert.strictEqual(false, noteId)
})
it('return false if query not set and referer not set', function () {
let noteId = realtime.extractNoteIdFromSocket(makeMockSocket({
otherHeader: 1
}, {
otherQuery: 1
}))
assert.strictEqual(false, noteId)
})
it('return noteId from query', function () {
// Arrange
const incomingNoteId = 'myNoteId'
const incomingSocket = makeMockSocket(undefined, { noteId: incomingNoteId })
// Act
const noteId = realtime.extractNoteIdFromSocket(incomingSocket)
// Assert
assert.strictEqual(noteId, incomingNoteId)
})
it('return noteId from old method (referer)', function () {
// Arrange
const incomingNoteId = 'myNoteId'
const incomingSocket = makeMockSocket({
referer: `https://localhost:3000/${incomingNoteId}`
})
// Act
const noteId = realtime.extractNoteIdFromSocket(incomingSocket)
// Assert
assert.strictEqual(noteId, incomingNoteId)
})
})
describe('urlPath is set', function () {
let realtime
it('return noteId from old method (referer) and urlPath set', function () {
// Arrange
const urlPath = 'hello'
mock('../lib/config', {
urlPath: urlPath
})
realtime = require('../lib/realtime')
const incomingNoteId = 'myNoteId'
const incomingSocket = makeMockSocket({
referer: `https://localhost:3000/${urlPath}/${incomingNoteId}`
})
// Act
const noteId = realtime.extractNoteIdFromSocket(incomingSocket)
// Assert
assert.strictEqual(noteId, incomingNoteId)
})
})
})
describe('parseNoteIdFromSocket', function () {
let realtime
beforeEach(() => {
mock('../lib/logger', {})
mock('../lib/history', {})
mock('../lib/models', {
Note: {
parseNoteId: function (noteId, callback) {
callback(null, noteId)
}
}
})
mock('../lib/config', {})
})
afterEach(() => {
removeModuleFromRequireCache('../lib/realtime')
mock.stopAll()
})
it('should return null when socket not send noteId', function () {
realtime = require('../lib/realtime')
const mockSocket = makeMockSocket()
const fakeCallback = sinon.fake()
realtime.parseNoteIdFromSocket(mockSocket, fakeCallback)
assert(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, [null, null])
})
describe('noteId exists', function () {
beforeEach(() => {
mock('../lib/models', {
Note: {
parseNoteId: function (noteId, callback) {
callback(null, noteId)
}
}
})
})
it('should return noteId when noteId exists', function () {
realtime = require('../lib/realtime')
const noteId = '123456'
const mockSocket = makeMockSocket(undefined, {
noteId: noteId
})
realtime = require('../lib/realtime')
const fakeCallback = sinon.fake()
realtime.parseNoteIdFromSocket(mockSocket, fakeCallback)
assert(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, [null, noteId])
})
})
describe('noteId not exists', function () {
beforeEach(() => {
mock('../lib/models', {
Note: {
parseNoteId: function (noteId, callback) {
callback(null, null)
}
}
})
})
it('should return null when noteId not exists', function () {
realtime = require('../lib/realtime')
const noteId = '123456'
const mockSocket = makeMockSocket(undefined, {
noteId: noteId
})
realtime = require('../lib/realtime')
const fakeCallback = sinon.fake()
realtime.parseNoteIdFromSocket(mockSocket, fakeCallback)
assert(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, [null, null])
})
})
describe('parse note error', function () {
beforeEach(() => {
mock('../lib/models', {
Note: {
parseNoteId: function (noteId, callback) {
/* eslint-disable-next-line */
callback('error', null)
}
}
})
})
it('should return error when noteId parse error', function () {
realtime = require('../lib/realtime')
const noteId = '123456'
const mockSocket = makeMockSocket(undefined, {
noteId: noteId
})
realtime = require('../lib/realtime')
const fakeCallback = sinon.fake()
realtime.parseNoteIdFromSocket(mockSocket, fakeCallback)
assert(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, ['error', null])
})
})
})
describe('update note is dirty timer', function () {
let realtime
beforeEach(() => {
mock('../lib/logger', {
error: () => {
}
})
mock('../lib/history', {})
mock('../lib/models', {
Revision: {
saveAllNotesRevision: () => {
}
}
})
mock('../lib/config', {})
})
afterEach(() => {
removeModuleFromRequireCache('../lib/realtime')
mock.stopAll()
})
it('should update note when note is dirty', (done) => {
const clock = sinon.useFakeTimers()
realtime = require('../lib/realtime')
sinon.stub(realtime, 'updateNote').callsFake(function (note, callback) {
callback(null, null)
})
const socketIoEmitFake = sinon.fake()
realtime.io = {
to: sinon.stub().callsFake(function () {
return {
emit: socketIoEmitFake
}
})
}
realtime.notes['note1'] = {
server: {
isDirty: false
},
socks: []
}
let note2 = {
server: {
isDirty: true
},
socks: []
}
realtime.notes['note2'] = note2
clock.tick(1000)
clock.restore()
setTimeout(() => {
assert(note2.server.isDirty === false)
done()
}, 50)
})
})
describe('updateNote', function () {
let realtime, fakeNote
beforeEach(() => {
mock('../lib/logger', {
error: () => {
}
})
mock('../lib/history', {})
mock('../lib/models', {
Note: {
findOne: async function () {
return fakeNote
}
}
})
mock('../lib/config', {})
})
afterEach(() => {
mock.stopAll()
})
it('should return null when note not found', function (done) {
fakeNote = null
realtime = require('../lib/realtime')
sinon.stub(realtime, 'finishUpdateNote').callsFake(function (a, b, callback) {
callback(null, b)
})
const fakeCallback = sinon.fake()
realtime.updateNote({ id: '123' }, fakeCallback)
setTimeout(() => {
assert.ok(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, [null, null])
sinon.restore()
done()
}, 50)
})
})
describe('finishUpdateNote', function () {
let realtime
beforeEach(() => {
mock('../lib/logger', {})
mock('../lib/history', {})
mock('../lib/models', {
Note: {
parseNoteTitle: (data) => (data)
}
})
mock('../lib/config', {})
realtime = require('../lib/realtime')
})
afterEach(() => {
removeModuleFromRequireCache('../lib/realtime')
mock.stopAll()
})
it('return null when note is null', () => {
const fakeCallback = sinon.fake()
realtime.finishUpdateNote(null, {}, fakeCallback)
assert.ok(fakeCallback.calledOnce)
assert.deepStrictEqual(fakeCallback.lastCall.args, [null, null])
})
})
describe('connection', function () {
let realtime
beforeEach(() => {
mock('../lib/logger', {
error: () => {
}
})
mock('../lib/history', {})
mock('../lib/models', {
Note: {
parseNoteTitle: (data) => (data)
}
})
mock('../lib/config', {})
realtime = require('../lib/realtime')
})
afterEach(() => {
removeModuleFromRequireCache('../lib/realtime')
mock.stopAll()
sinon.restore()
})
describe('fail', function () {
it('should fast return when server not start', () => {
const mockSocket = makeMockSocket()
realtime.maintenance = true
const spy = sinon.spy(realtime, 'parseNoteIdFromSocket')
realtime.connection(mockSocket)
assert(!spy.called)
})
it('should failed when parse noteId occur error', () => {
const mockSocket = makeMockSocket()
realtime.maintenance = false
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocket').callsFake((socket, callback) => {
/* eslint-disable-next-line */
callback('error', null)
})
const failConnectionSpy = sinon.stub(realtime, 'failConnection')
realtime.connection(mockSocket)
assert(parseNoteIdFromSocketSpy.called)
assert(failConnectionSpy.calledOnce)
assert.deepStrictEqual(failConnectionSpy.lastCall.args, [500, 'error', mockSocket])
})
it('should failed when noteId not exists', () => {
const mockSocket = makeMockSocket()
realtime.maintenance = false
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocket').callsFake((socket, callback) => {
/* eslint-disable-next-line */
callback(null, null)
})
const failConnectionSpy = sinon.stub(realtime, 'failConnection')
realtime.connection(mockSocket)
assert(parseNoteIdFromSocketSpy.called)
assert(failConnectionSpy.calledOnce)
assert.deepStrictEqual(failConnectionSpy.lastCall.args, [404, 'note id not found', mockSocket])
})
})
it('should success connect', function () {
const mockSocket = makeMockSocket()
const noteId = 'note123'
realtime.maintenance = false
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocket').callsFake((socket, callback) => {
/* eslint-disable-next-line */
callback(null, noteId)
})
const failConnectionStub = sinon.stub(realtime, 'failConnection')
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
const startConnectionStub = sinon.stub(realtime, 'startConnection')
realtime.connection(mockSocket)
assert.ok(parseNoteIdFromSocketSpy.calledOnce)
assert(failConnectionStub.called === false)
assert(updateUserDataStub.calledOnce)
assert(startConnectionStub.calledOnce)
assert(mockSocket.on.callCount === 11)
})
})
describe('socket event', function () {
let realtime
const noteId = 'note123'
let clientSocket
const eventFuncMap = new Map()
beforeEach(() => {
mock('../lib/logger', {
error: () => {
},
info: () => {}
})
mock('../lib/history', {})
mock('../lib/models', {
Note: {
parseNoteTitle: (data) => (data)
}
})
mock('../lib/config', {
fullversion: '1.5.0',
minimumCompatibleVersion: '1.0.0'
})
realtime = require('../lib/realtime')
// get all socket event handler
clientSocket = makeMockSocket()
clientSocket.on = function (event, func) {
eventFuncMap.set(event, func)
}
realtime.maintenance = false
sinon.stub(realtime, 'parseNoteIdFromSocket').callsFake((socket, callback) => {
/* eslint-disable-next-line */
callback(null, noteId)
})
const wrappedFuncs = []
wrappedFuncs.push(sinon.stub(realtime, 'failConnection'))
wrappedFuncs.push(sinon.stub(realtime, 'updateUserData'))
wrappedFuncs.push(sinon.stub(realtime, 'startConnection'))
realtime.connection(clientSocket)
wrappedFuncs.forEach((wrappedFunc) => {
wrappedFunc.restore()
})
})
afterEach(() => {
removeModuleFromRequireCache('../lib/realtime')
mock.stopAll()
sinon.restore()
})
describe('refresh', function () {
it('should call refresh', () => {
const refreshFunc = eventFuncMap.get('refresh')
const emitRefreshStub = sinon.stub(realtime, 'emitRefresh')
refreshFunc()
assert(emitRefreshStub.calledOnce)
assert.deepStrictEqual(emitRefreshStub.lastCall.args[0], clientSocket)
})
})
describe('user status', function () {
it('should call emitUserStatus and update user data', () => {
const userStatusFunc = eventFuncMap.get('user status')
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
realtime.notes[noteId] = {}
const userData = {
idle: true,
type: 'xs'
}
userStatusFunc(userData)
assert(emitUserStatusStub.calledOnce)
assert.deepStrictEqual(emitUserStatusStub.lastCall.args[0], clientSocket)
assert(realtime.users[clientSocket.id].idle === true)
assert(realtime.users[clientSocket.id].type === 'xs')
})
it('should call emitUserStatus without userdata', () => {
const userStatusFunc = eventFuncMap.get('user status')
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
realtime.notes[noteId] = {}
userStatusFunc()
assert(emitUserStatusStub.calledOnce)
assert.deepStrictEqual(emitUserStatusStub.lastCall.args[0], clientSocket)
assert(realtime.users[clientSocket.id].idle === false)
assert(realtime.users[clientSocket.id].type === null)
})
it('should not call emitUserStatus when user not exists', () => {
const userStatusFunc = eventFuncMap.get('user status')
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
realtime.notes[noteId] = {}
delete realtime.users[clientSocket.id]
const userData = {
idle: true,
type: 'xs'
}
userStatusFunc(userData)
assert(emitUserStatusStub.called === false)
})
it('should not call emitUserStatus when note not exists', () => {
const userStatusFunc = eventFuncMap.get('user status')
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
realtime.notes = {}
realtime.users[clientSocket.id] = {}
const userData = {
idle: true,
type: 'xs'
}
userStatusFunc(userData)
assert(emitUserStatusStub.called === false)
})
})
describe('disconnect', function () {
it('should push socket to disconnect queue and call disconnect function', () => {
const disconnectFunc = eventFuncMap.get('disconnect')
const disconnectStub = sinon.stub(realtime, 'disconnect')
disconnectFunc()
assert(realtime.disconnectSocketQueue.length === 1)
assert(disconnectStub.calledOnce)
})
it('should quick return when socket is in disconnect queue', () => {
const disconnectFunc = eventFuncMap.get('disconnect')
const disconnectStub = sinon.stub(realtime, 'disconnect')
realtime.disconnectSocketQueue.push(clientSocket)
disconnectFunc()
assert(disconnectStub.called === false)
})
})
;['cursor focus', 'cursor activity', 'cursor blur'].forEach((event) => {
describe(event, function () {
let cursorFocusFunc
const cursorData = {
cursor: 10
}
beforeEach(() => {
cursorFocusFunc = eventFuncMap.get(event)
realtime.notes[noteId] = {}
})
it('should broadcast to all client', () => {
cursorFocusFunc(cursorData)
const broadChannelEmitFake = clientSocket.broadcast.to(noteId).emit
assert(broadChannelEmitFake.calledOnce)
assert(broadChannelEmitFake.lastCall.args[0] === event)
if (event === 'cursor blur') {
assert(broadChannelEmitFake.lastCall.args[1].id === clientSocket.id)
} else {
assert.deepStrictEqual(broadChannelEmitFake.lastCall.args[1].cursor, cursorData)
}
})
it('should not broadcast when note not exists', () => {
delete realtime.notes[noteId]
cursorFocusFunc(cursorData)
const broadChannelEmitFake = clientSocket.broadcast.to(noteId).emit
assert(broadChannelEmitFake.called === false)
})
it('should not broadcast when user not exists', () => {
delete realtime.users[clientSocket.id]
cursorFocusFunc(cursorData)
const broadChannelEmitFake = clientSocket.broadcast.to(noteId).emit
assert(broadChannelEmitFake.called === false)
})
})
})
describe('version', function () {
it('should emit server version ', () => {
const versionFunc = eventFuncMap.get('version')
versionFunc()
assert(clientSocket.emit.called)
assert(clientSocket.emit.lastCall.args[0], 'version')
assert.deepStrictEqual(clientSocket.emit.lastCall.args[1], {
version: '1.5.0',
minimumCompatibleVersion: '1.0.0'
})
})
})
describe('online users', function () {
it('should return online user list', () => {
const onlineUsersFunc = eventFuncMap.get('online users')
realtime.notes[noteId] = {
users: {
10: {
id: 10
},
20: {
id: 20
}
}
}
onlineUsersFunc()
assert(clientSocket.emit.called)
assert(clientSocket.emit.lastCall.args[0] === 'online users')
let returnUserList = clientSocket.emit.lastCall.args[1].users
assert(returnUserList.length === 2)
assert(returnUserList[0].id === 10)
assert(returnUserList[1].id === 20)
})
it('should not return user list when note not exists', () => {
const onlineUsersFunc = eventFuncMap.get('online users')
onlineUsersFunc()
assert(clientSocket.emit.called === false)
})
})
describe('user changed', function () {
it('should call updateUserData', () => {
const userChangedFunc = eventFuncMap.get('user changed')
realtime.notes[noteId] = {
users: {
[clientSocket.id]: {}
}
}
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
userChangedFunc()
assert(updateUserDataStub.calledOnce)
assert(emitOnlineUsersStub.calledOnce)
})
it('should direct return when note not exists', () => {
const userChangedFunc = eventFuncMap.get('user changed')
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
userChangedFunc()
assert(updateUserDataStub.called === false)
assert(emitOnlineUsersStub.called === false)
})
it('should direct return when note not exists', () => {
const userChangedFunc = eventFuncMap.get('user changed')
realtime.notes[noteId] = {
users: {
}
}
delete realtime.users[clientSocket.id]
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
userChangedFunc()
assert(updateUserDataStub.called === false)
assert(emitOnlineUsersStub.called === false)
})
})
describe('permission', function () {
})
})
})

View File

@ -0,0 +1,91 @@
/* eslint-env node, mocha */
'use strict'
const mock = require('mock-require')
const assert = require('assert')
const { makeMockSocket } = require('./utils')
describe('realtime#extractNoteIdFromSocket', function () {
beforeEach(() => {
mock('../../lib/logger', {})
mock('../../lib/history', {})
mock('../../lib/models', {})
})
afterEach(() => {
delete require.cache[require.resolve('../../lib/realtime')]
mock.stopAll()
})
describe('urlPath not set', function () {
beforeEach(() => {
mock('../../lib/config', {})
realtime = require('../../lib/realtime')
})
let realtime
it('return false if socket or socket.handshake not exists', function () {
let noteId = realtime.extractNoteIdFromSocket()
assert.strictEqual(false, noteId)
noteId = realtime.extractNoteIdFromSocket({})
assert.strictEqual(false, noteId)
})
it('return false if query not set and referer not set', function () {
let noteId = realtime.extractNoteIdFromSocket(makeMockSocket({
otherHeader: 1
}, {
otherQuery: 1
}))
assert.strictEqual(false, noteId)
})
it('return noteId from query', function () {
// Arrange
const incomingNoteId = 'myNoteId'
const incomingSocket = makeMockSocket(undefined, { noteId: incomingNoteId })
// Act
const noteId = realtime.extractNoteIdFromSocket(incomingSocket)
// Assert
assert.strictEqual(noteId, incomingNoteId)
})
it('return noteId from old method (referer)', function () {
// Arrange
const incomingNoteId = 'myNoteId'
const incomingSocket = makeMockSocket({
referer: `https://localhost:3000/${incomingNoteId}`
})
// Act
const noteId = realtime.extractNoteIdFromSocket(incomingSocket)
// Assert
assert.strictEqual(noteId, incomingNoteId)
})
})
describe('urlPath is set', function () {
let realtime
it('return noteId from old method (referer) and urlPath set', function () {
// Arrange
const urlPath = 'hello'
mock('../../lib/config', {
urlPath: urlPath
})
realtime = require('../../lib/realtime')
const incomingNoteId = 'myNoteId'
const incomingSocket = makeMockSocket({
referer: `https://localhost:3000/${urlPath}/${incomingNoteId}`
})
// Act
const noteId = realtime.extractNoteIdFromSocket(incomingSocket)
// Assert
assert.strictEqual(noteId, incomingNoteId)
})
})
})

View File

@ -0,0 +1,112 @@
/* eslint-env node, mocha */
'use strict'
const assert = require('assert')
const mock = require('mock-require')
const sinon = require('sinon')
const { makeMockSocket, removeModuleFromRequireCache } = require('./utils')
describe('realtime#parseNoteIdFromSocket', function () {
let realtime
beforeEach(() => {
mock('../../lib/logger', {})
mock('../../lib/history', {})
mock('../../lib/models', {
Note: {
parseNoteId: function (noteId, callback) {
callback(null, noteId)
}
}
})
mock('../../lib/config', {})
})
afterEach(() => {
removeModuleFromRequireCache('../../lib/realtime')
mock.stopAll()
})
it('should return null when socket not send noteId', function () {
realtime = require('../../lib/realtime')
const mockSocket = makeMockSocket()
const fakeCallback = sinon.fake()
realtime.parseNoteIdFromSocket(mockSocket, fakeCallback)
assert(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, [null, null])
})
describe('noteId exists', function () {
beforeEach(() => {
mock('../../lib/models', {
Note: {
parseNoteId: function (noteId, callback) {
callback(null, noteId)
}
}
})
})
it('should return noteId when noteId exists', function () {
realtime = require('../../lib/realtime')
const noteId = '123456'
const mockSocket = makeMockSocket(undefined, {
noteId: noteId
})
realtime = require('../../lib/realtime')
const fakeCallback = sinon.fake()
realtime.parseNoteIdFromSocket(mockSocket, fakeCallback)
assert(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, [null, noteId])
})
})
describe('noteId not exists', function () {
beforeEach(() => {
mock('../../lib/models', {
Note: {
parseNoteId: function (noteId, callback) {
callback(null, null)
}
}
})
})
it('should return null when noteId not exists', function () {
realtime = require('../../lib/realtime')
const noteId = '123456'
const mockSocket = makeMockSocket(undefined, {
noteId: noteId
})
realtime = require('../../lib/realtime')
const fakeCallback = sinon.fake()
realtime.parseNoteIdFromSocket(mockSocket, fakeCallback)
assert(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, [null, null])
})
})
describe('parse note error', function () {
beforeEach(() => {
mock('../../lib/models', {
Note: {
parseNoteId: function (noteId, callback) {
/* eslint-disable-next-line */
callback('error', null)
}
}
})
})
it('should return error when noteId parse error', function () {
realtime = require('../../lib/realtime')
const noteId = '123456'
const mockSocket = makeMockSocket(undefined, {
noteId: noteId
})
realtime = require('../../lib/realtime')
const fakeCallback = sinon.fake()
realtime.parseNoteIdFromSocket(mockSocket, fakeCallback)
assert(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, ['error', null])
})
})
})

View File

@ -0,0 +1,261 @@
'use strict'
/* eslint-env node, mocha */
const mock = require('mock-require')
const assert = require('assert')
const sinon = require('sinon')
function makeMockSocket (headers, query) {
const broadCastChannelCache = {}
return {
id: Math.round(Math.random() * 10000),
handshake: {
headers: Object.assign({}, headers),
query: Object.assign({}, query)
},
on: sinon.fake(),
emit: sinon.fake(),
broadCastChannelCache: {},
broadcast: {
to: (channel) => {
if (!broadCastChannelCache[channel]) {
broadCastChannelCache[channel] = {
channel: channel,
emit: sinon.fake()
}
}
return broadCastChannelCache[channel]
}
},
disconnect: sinon.fake()
}
}
function removeModuleFromRequireCache (modulePath) {
delete require.cache[require.resolve(modulePath)]
}
describe('realtime', function () {
describe('update note is dirty timer', function () {
let realtime
beforeEach(() => {
mock('../../lib/logger', {
error: () => {
}
})
mock('../../lib/history', {})
mock('../../lib/models', {
Revision: {
saveAllNotesRevision: () => {
}
}
})
mock('../../lib/config', {})
})
afterEach(() => {
removeModuleFromRequireCache('../../lib/realtime')
mock.stopAll()
})
it('should update note when note is dirty', (done) => {
const clock = sinon.useFakeTimers()
realtime = require('../../lib/realtime')
sinon.stub(realtime, 'updateNote').callsFake(function (note, callback) {
callback(null, null)
})
const socketIoEmitFake = sinon.fake()
realtime.io = {
to: sinon.stub().callsFake(function () {
return {
emit: socketIoEmitFake
}
})
}
realtime.notes['note1'] = {
server: {
isDirty: false
},
socks: []
}
let note2 = {
server: {
isDirty: true
},
socks: []
}
realtime.notes['note2'] = note2
clock.tick(1000)
clock.restore()
setTimeout(() => {
assert(note2.server.isDirty === false)
done()
}, 50)
})
})
describe('updateNote', function () {
let realtime, fakeNote
beforeEach(() => {
mock('../../lib/logger', {
error: () => {
}
})
mock('../../lib/history', {})
mock('../../lib/models', {
Note: {
findOne: async function () {
return fakeNote
}
}
})
mock('../../lib/config', {})
})
afterEach(() => {
mock.stopAll()
})
it('should return null when note not found', function (done) {
fakeNote = null
realtime = require('../../lib/realtime')
sinon.stub(realtime, 'finishUpdateNote').callsFake(function (a, b, callback) {
callback(null, b)
})
const fakeCallback = sinon.fake()
realtime.updateNote({ id: '123' }, fakeCallback)
setTimeout(() => {
assert.ok(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, [null, null])
sinon.restore()
done()
}, 50)
})
})
describe('finishUpdateNote', function () {
let realtime
beforeEach(() => {
mock('../../lib/logger', {})
mock('../../lib/history', {})
mock('../../lib/models', {
Note: {
parseNoteTitle: (data) => (data)
}
})
mock('../../lib/config', {})
realtime = require('../../lib/realtime')
})
afterEach(() => {
removeModuleFromRequireCache('../../lib/realtime')
mock.stopAll()
})
it('return null when note is null', () => {
const fakeCallback = sinon.fake()
realtime.finishUpdateNote(null, {}, fakeCallback)
assert.ok(fakeCallback.calledOnce)
assert.deepStrictEqual(fakeCallback.lastCall.args, [null, null])
})
})
describe('connection', function () {
let realtime
beforeEach(() => {
mock('../../lib/logger', {
error: () => {
}
})
mock('../../lib/history', {})
mock('../../lib/models', {
Note: {
parseNoteTitle: (data) => (data)
}
})
mock('../../lib/config', {})
realtime = require('../../lib/realtime')
})
afterEach(() => {
removeModuleFromRequireCache('../../lib/realtime')
mock.stopAll()
sinon.restore()
})
describe('fail', function () {
it('should fast return when server not start', () => {
const mockSocket = makeMockSocket()
realtime.maintenance = true
const spy = sinon.spy(realtime, 'parseNoteIdFromSocket')
realtime.connection(mockSocket)
assert(!spy.called)
})
it('should failed when parse noteId occur error', () => {
const mockSocket = makeMockSocket()
realtime.maintenance = false
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocket').callsFake((socket, callback) => {
/* eslint-disable-next-line */
callback('error', null)
})
const failConnectionSpy = sinon.stub(realtime, 'failConnection')
realtime.connection(mockSocket)
assert(parseNoteIdFromSocketSpy.called)
assert(failConnectionSpy.calledOnce)
assert.deepStrictEqual(failConnectionSpy.lastCall.args, [500, 'error', mockSocket])
})
it('should failed when noteId not exists', () => {
const mockSocket = makeMockSocket()
realtime.maintenance = false
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocket').callsFake((socket, callback) => {
/* eslint-disable-next-line */
callback(null, null)
})
const failConnectionSpy = sinon.stub(realtime, 'failConnection')
realtime.connection(mockSocket)
assert(parseNoteIdFromSocketSpy.called)
assert(failConnectionSpy.calledOnce)
assert.deepStrictEqual(failConnectionSpy.lastCall.args, [404, 'note id not found', mockSocket])
})
})
it('should success connect', function () {
const mockSocket = makeMockSocket()
const noteId = 'note123'
realtime.maintenance = false
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocket').callsFake((socket, callback) => {
/* eslint-disable-next-line */
callback(null, noteId)
})
const failConnectionStub = sinon.stub(realtime, 'failConnection')
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
const startConnectionStub = sinon.stub(realtime, 'startConnection')
realtime.connection(mockSocket)
assert.ok(parseNoteIdFromSocketSpy.calledOnce)
assert(failConnectionStub.called === false)
assert(updateUserDataStub.calledOnce)
assert(startConnectionStub.calledOnce)
assert(mockSocket.on.callCount === 11)
})
})
})

View File

@ -0,0 +1,270 @@
/* eslint-env node, mocha */
'use strict'
const assert = require('assert')
const mock = require('mock-require')
const sinon = require('sinon')
const { makeMockSocket, removeModuleFromRequireCache } = require('./utils')
describe('realtime#socket event', function () {
const noteId = 'note123'
let realtime
let clientSocket
let modelsMock
let eventFuncMap
beforeEach(function () {
eventFuncMap = new Map()
modelsMock = {
Note: {
parseNoteTitle: (data) => (data),
destroy: sinon.stub().returns(Promise.resolve(1))
}
}
mock('../../lib/logger', {
error: () => {
},
info: () => {
}
})
mock('../../lib/history', {})
mock('../../lib/models', modelsMock)
mock('../../lib/config', {
fullversion: '1.5.0',
minimumCompatibleVersion: '1.0.0'
})
realtime = require('../../lib/realtime')
// get all socket event handler
clientSocket = makeMockSocket()
clientSocket.on = function (event, func) {
eventFuncMap.set(event, func)
}
realtime.maintenance = false
sinon.stub(realtime, 'parseNoteIdFromSocket').callsFake((socket, callback) => {
/* eslint-disable-next-line */
callback(null, noteId)
})
const wrappedFuncs = []
wrappedFuncs.push(sinon.stub(realtime, 'failConnection'))
wrappedFuncs.push(sinon.stub(realtime, 'updateUserData'))
wrappedFuncs.push(sinon.stub(realtime, 'startConnection'))
realtime.connection(clientSocket)
wrappedFuncs.forEach((wrappedFunc) => {
wrappedFunc.restore()
})
})
afterEach(function () {
removeModuleFromRequireCache('../../lib/realtime')
mock.stopAll()
sinon.restore()
})
describe('refresh', function () {
it('should call refresh', () => {
const refreshFunc = eventFuncMap.get('refresh')
const emitRefreshStub = sinon.stub(realtime, 'emitRefresh')
refreshFunc()
assert(emitRefreshStub.calledOnce)
assert.deepStrictEqual(emitRefreshStub.lastCall.args[0], clientSocket)
})
})
describe('user status', function () {
it('should call emitUserStatus and update user data', () => {
const userStatusFunc = eventFuncMap.get('user status')
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
realtime.notes[noteId] = {}
const userData = {
idle: true,
type: 'xs'
}
userStatusFunc(userData)
assert(emitUserStatusStub.calledOnce)
assert.deepStrictEqual(emitUserStatusStub.lastCall.args[0], clientSocket)
assert(realtime.users[clientSocket.id].idle === true)
assert(realtime.users[clientSocket.id].type === 'xs')
})
it('should call emitUserStatus without userdata', () => {
const userStatusFunc = eventFuncMap.get('user status')
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
realtime.notes[noteId] = {}
userStatusFunc()
assert(emitUserStatusStub.calledOnce)
assert.deepStrictEqual(emitUserStatusStub.lastCall.args[0], clientSocket)
assert(realtime.users[clientSocket.id].idle === false)
assert(realtime.users[clientSocket.id].type === null)
})
it('should not call emitUserStatus when user not exists', () => {
const userStatusFunc = eventFuncMap.get('user status')
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
realtime.notes[noteId] = {}
delete realtime.users[clientSocket.id]
const userData = {
idle: true,
type: 'xs'
}
userStatusFunc(userData)
assert(emitUserStatusStub.called === false)
})
it('should not call emitUserStatus when note not exists', () => {
const userStatusFunc = eventFuncMap.get('user status')
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
realtime.notes = {}
realtime.users[clientSocket.id] = {}
const userData = {
idle: true,
type: 'xs'
}
userStatusFunc(userData)
assert(emitUserStatusStub.called === false)
})
})
describe('disconnect', function () {
it('should push socket to disconnect queue and call disconnect function', () => {
const disconnectFunc = eventFuncMap.get('disconnect')
const disconnectStub = sinon.stub(realtime, 'disconnect')
disconnectFunc()
assert(realtime.disconnectSocketQueue.length === 1)
assert(disconnectStub.calledOnce)
})
it('should quick return when socket is in disconnect queue', () => {
const disconnectFunc = eventFuncMap.get('disconnect')
const disconnectStub = sinon.stub(realtime, 'disconnect')
realtime.disconnectSocketQueue.push(clientSocket)
disconnectFunc()
assert(disconnectStub.called === false)
})
})
;['cursor focus', 'cursor activity', 'cursor blur'].forEach((event) => {
describe(event, function () {
let cursorFocusFunc
const cursorData = {
cursor: 10
}
beforeEach(() => {
cursorFocusFunc = eventFuncMap.get(event)
realtime.notes[noteId] = {}
})
it('should broadcast to all client', () => {
cursorFocusFunc(cursorData)
const broadChannelEmitFake = clientSocket.broadcast.to(noteId).emit
assert(broadChannelEmitFake.calledOnce)
assert(broadChannelEmitFake.lastCall.args[0] === event)
if (event === 'cursor blur') {
assert(broadChannelEmitFake.lastCall.args[1].id === clientSocket.id)
} else {
assert.deepStrictEqual(broadChannelEmitFake.lastCall.args[1].cursor, cursorData)
}
})
it('should not broadcast when note not exists', () => {
delete realtime.notes[noteId]
cursorFocusFunc(cursorData)
const broadChannelEmitFake = clientSocket.broadcast.to(noteId).emit
assert(broadChannelEmitFake.called === false)
})
it('should not broadcast when user not exists', () => {
delete realtime.users[clientSocket.id]
cursorFocusFunc(cursorData)
const broadChannelEmitFake = clientSocket.broadcast.to(noteId).emit
assert(broadChannelEmitFake.called === false)
})
})
})
describe('version', function () {
it('should emit server version ', () => {
const versionFunc = eventFuncMap.get('version')
versionFunc()
assert(clientSocket.emit.called)
assert(clientSocket.emit.lastCall.args[0], 'version')
assert.deepStrictEqual(clientSocket.emit.lastCall.args[1], {
version: '1.5.0',
minimumCompatibleVersion: '1.0.0'
})
})
})
describe('online users', function () {
it('should return online user list', function () {
const onlineUsersFunc = eventFuncMap.get('online users')
realtime.notes[noteId] = {
users: {
10: {
id: 10
},
20: {
id: 20
}
}
}
onlineUsersFunc()
assert(clientSocket.emit.called)
assert(clientSocket.emit.lastCall.args[0] === 'online users')
let returnUserList = clientSocket.emit.lastCall.args[1].users
assert(returnUserList.length === 2)
assert(returnUserList[0].id === 10)
assert(returnUserList[1].id === 20)
})
it('should not return user list when note not exists', function () {
const onlineUsersFunc = eventFuncMap.get('online users')
onlineUsersFunc()
assert(clientSocket.emit.called === false)
})
})
describe('user changed', function () {
it('should call updateUserData', () => {
const userChangedFunc = eventFuncMap.get('user changed')
realtime.notes[noteId] = {
users: {
[clientSocket.id]: {}
}
}
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
userChangedFunc()
assert(updateUserDataStub.calledOnce)
assert(emitOnlineUsersStub.calledOnce)
})
it('should direct return when note not exists', function () {
const userChangedFunc = eventFuncMap.get('user changed')
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
userChangedFunc()
assert(updateUserDataStub.called === false)
assert(emitOnlineUsersStub.called === false)
})
it('should direct return when note not exists', function () {
const userChangedFunc = eventFuncMap.get('user changed')
realtime.notes[noteId] = {
users: {}
}
delete realtime.users[clientSocket.id]
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
userChangedFunc()
assert(updateUserDataStub.called === false)
assert(emitOnlineUsersStub.called === false)
})
})
})

36
test/realtime/utils.js Normal file
View File

@ -0,0 +1,36 @@
'use strict'
const sinon = require('sinon')
function makeMockSocket (headers, query) {
const broadCastChannelCache = {}
return {
id: Math.round(Math.random() * 10000),
handshake: {
headers: Object.assign({}, headers),
query: Object.assign({}, query)
},
on: sinon.fake(),
emit: sinon.fake(),
broadCastChannelCache: {},
broadcast: {
to: (channel) => {
if (!broadCastChannelCache[channel]) {
broadCastChannelCache[channel] = {
channel: channel,
emit: sinon.fake()
}
}
return broadCastChannelCache[channel]
}
},
disconnect: sinon.fake()
}
}
function removeModuleFromRequireCache (modulePath) {
delete require.cache[require.resolve(modulePath)]
}
exports.makeMockSocket = makeMockSocket
exports.removeModuleFromRequireCache = removeModuleFromRequireCache