diff --git a/test/realtime.test.js b/test/realtime.test.js deleted file mode 100644 index 718b4a86..00000000 --- a/test/realtime.test.js +++ /dev/null @@ -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 () { - - }) - - }) -}) diff --git a/test/realtime/extractNoteIdFromSocket.test.js b/test/realtime/extractNoteIdFromSocket.test.js new file mode 100644 index 00000000..eff6b7aa --- /dev/null +++ b/test/realtime/extractNoteIdFromSocket.test.js @@ -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) + }) + }) +}) diff --git a/test/realtime/parseNoteIdFromSocket.test.js b/test/realtime/parseNoteIdFromSocket.test.js new file mode 100644 index 00000000..1eb6d438 --- /dev/null +++ b/test/realtime/parseNoteIdFromSocket.test.js @@ -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]) + }) + }) +}) diff --git a/test/realtime/realtime.test.js b/test/realtime/realtime.test.js new file mode 100644 index 00000000..c6f4276d --- /dev/null +++ b/test/realtime/realtime.test.js @@ -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) + }) + }) + +}) diff --git a/test/realtime/socket-events.test.js b/test/realtime/socket-events.test.js new file mode 100644 index 00000000..462e3ad8 --- /dev/null +++ b/test/realtime/socket-events.test.js @@ -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) + }) + }) +}) diff --git a/test/realtime/utils.js b/test/realtime/utils.js new file mode 100644 index 00000000..55f4f9b5 --- /dev/null +++ b/test/realtime/utils.js @@ -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