/* 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 let configMock let clock beforeEach(function () { clock = sinon.useFakeTimers({ toFake: ['setInterval'] }) eventFuncMap = new Map() modelsMock = { Note: { parseNoteTitle: (data) => (data), destroy: sinon.stub().returns(Promise.resolve(1)), update: sinon.stub().returns(Promise.resolve(1)) } } configMock = { fullversion: '1.5.0', minimumCompatibleVersion: '1.0.0' } mock('../../lib/logger', { error: () => { }, info: () => { } }) mock('../../lib/history', {}) mock('../../lib/models', modelsMock) mock('../../lib/config', configMock) 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') removeModuleFromRequireCache('../../lib/realtimeClientConnection') mock.stopAll() sinon.restore() clock.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) }) }) describe('delete', function () { it('should delete note when owner request', function (done) { const currentUserId = 'user1_id' const noteOwnerId = 'user1_id' const otherClient = makeMockSocket() clientSocket.request = { user: { logged_in: true, id: currentUserId } } realtime.notes[noteId] = { owner: noteOwnerId, socks: [clientSocket, undefined, otherClient] } const deleteFunc = eventFuncMap.get('delete') deleteFunc() setTimeout(() => { assert(otherClient.disconnect.calledOnce) assert(otherClient.emit.calledOnce) assert(otherClient.emit.lastCall.args[0] === 'delete') assert(clientSocket.disconnect.calledOnce) assert(clientSocket.emit.calledOnce) assert(clientSocket.emit.lastCall.args[0] === 'delete') assert(modelsMock.Note.destroy.calledOnce) done() }, 10) }) it('should not do anything when user not login', function (done) { const noteOwnerId = 'user1_id' clientSocket.request = {} realtime.notes[noteId] = { owner: noteOwnerId, socks: [clientSocket] } const deleteFunc = eventFuncMap.get('delete') deleteFunc() setTimeout(() => { assert(modelsMock.Note.destroy.called === false) assert(clientSocket.disconnect.called === false) done() }, 10) }) it('should not do anything when note not exists', function (done) { const currentUserId = 'user1_id' clientSocket.request = { user: { logged_in: true, id: currentUserId } } const deleteFunc = eventFuncMap.get('delete') deleteFunc() setTimeout(() => { assert(modelsMock.Note.destroy.called === false) assert(clientSocket.disconnect.called === false) done() }, 10) }) it('should not do anything when note owner is not me', function (done) { const currentUserId = 'user1_id' const noteOwnerId = 'user2_id' const otherClient = makeMockSocket() clientSocket.request = { user: { logged_in: true, id: currentUserId } } realtime.notes[noteId] = { owner: noteOwnerId, socks: [clientSocket, otherClient] } const deleteFunc = eventFuncMap.get('delete') deleteFunc() setTimeout(() => { assert(clientSocket.disconnect.called === false) assert(modelsMock.Note.destroy.called === false) done() }, 10) }) it('should not do anything when note destroy fail', function (done) { const currentUserId = 'user1_id' const noteOwnerId = 'user1_id' modelsMock.Note.destroy.withArgs({ where: { id: noteId } }).returns(Promise.resolve(0)) const otherClient = makeMockSocket() clientSocket.request = { user: { logged_in: true, id: currentUserId } } realtime.notes[noteId] = { id: noteId, owner: noteOwnerId, socks: [clientSocket, otherClient] } const deleteFunc = eventFuncMap.get('delete') deleteFunc() setTimeout(() => { assert(modelsMock.Note.destroy.calledOnce) assert(clientSocket.disconnect.called === false) done() }, 10) }) }) describe('permission', function () { let ownerId = 'user1_id' let otherSignInUserId = 'user2_id' let otherClient let checkViewPermissionSpy let permissionFunc beforeEach(function () { otherClient = makeMockSocket() clientSocket.request = { user: { id: ownerId, logged_in: true } } otherClient.request = { user: { id: otherSignInUserId, logged_in: true } } realtime.notes[noteId] = { owner: ownerId } realtime.io = { to: function () { return { emit: sinon.stub() } } } checkViewPermissionSpy = sinon.spy(realtime, 'checkViewPermission') permissionFunc = eventFuncMap.get('permission') }) it('should disconnect when lose view permission', function (done) { realtime.notes[noteId].permission = 'editable' realtime.notes[noteId].socks = [clientSocket, undefined, otherClient] permissionFunc('private') setTimeout(() => { assert(checkViewPermissionSpy.callCount === 2) assert(otherClient.emit.calledOnce) assert(otherClient.disconnect.calledOnce) done() }, 5) }) it('should not do anything when user not logged in', function (done) { clientSocket.request = {} permissionFunc('private') setTimeout(() => { assert(modelsMock.Note.update.called === false) done() }, 5) }) it('should not do anything when note not exists', function (done) { delete realtime.notes[noteId] permissionFunc('private') setTimeout(() => { assert(modelsMock.Note.update.called === false) done() }, 5) }) it('should not do anything when not note owner', function (done) { clientSocket.request.user.id = 'other_user_id' permissionFunc('private') setTimeout(() => { assert(modelsMock.Note.update.called === false) done() }, 5) }) it('should change permission to freely when config allowAnonymous and allowAnonymousEdits are true', function (done) { configMock.allowAnonymous = true configMock.allowAnonymousEdits = true realtime.notes[noteId].socks = [clientSocket, undefined, otherClient] permissionFunc('freely') setTimeout(() => { assert(checkViewPermissionSpy.callCount === 2) assert(otherClient.emit.called === false) assert(otherClient.disconnect.called === false) assert(clientSocket.emit.called === false) assert(clientSocket.disconnect.called === false) done() }, 5) }) it('should not change permission to freely when config allowAnonymous and allowAnonymousEdits are false', function (done) { configMock.allowAnonymous = false configMock.allowAnonymousEdits = false realtime.notes[noteId].socks = [clientSocket, undefined, otherClient] permissionFunc('freely') setTimeout(() => { assert(modelsMock.Note.update.called === false) assert(checkViewPermissionSpy.called === false) done() }, 5) }) it('should change permission to freely when config allowAnonymous is true', function (done) { configMock.allowAnonymous = true configMock.allowAnonymousEdits = false realtime.notes[noteId].socks = [clientSocket, undefined, otherClient] permissionFunc('freely') setTimeout(() => { assert(checkViewPermissionSpy.callCount === 2) assert(otherClient.emit.called === false) assert(otherClient.disconnect.called === false) assert(clientSocket.emit.called === false) assert(clientSocket.disconnect.called === false) done() }, 5) }) it('should change permission to freely when config allowAnonymousEdits is true', function (done) { configMock.allowAnonymous = false configMock.allowAnonymousEdits = true realtime.notes[noteId].socks = [clientSocket, undefined, otherClient] permissionFunc('freely') setTimeout(() => { assert(checkViewPermissionSpy.callCount === 2) assert(otherClient.emit.called === false) assert(otherClient.disconnect.called === false) assert(clientSocket.emit.called === false) assert(clientSocket.disconnect.called === false) done() }, 5) }) }) })