mirror of https://github.com/status-im/codimd.git
591 lines
18 KiB
JavaScript
591 lines
18 KiB
JavaScript
/* 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'
|
|
const note = {
|
|
id: noteId,
|
|
authors: [
|
|
{
|
|
userId: 'user1',
|
|
color: 'red',
|
|
user: {
|
|
id: 'user1',
|
|
name: 'Alice'
|
|
}
|
|
},
|
|
{
|
|
userId: 'user2',
|
|
color: 'blue',
|
|
user: {
|
|
id: 'user2',
|
|
name: 'Bob'
|
|
}
|
|
}
|
|
]
|
|
}
|
|
let realtime
|
|
let clientSocket
|
|
let modelsMock
|
|
let eventFuncMap
|
|
let configMock
|
|
let clock
|
|
|
|
beforeEach(function (done) {
|
|
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])),
|
|
findOne: sinon.stub().returns(Promise.resolve(note))
|
|
},
|
|
User: {}
|
|
}
|
|
modelsMock.User.getProfile = sinon.stub().callsFake((user) => {
|
|
return user
|
|
})
|
|
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)
|
|
mock('../../lib/ot', require('../testDoubles/otFake'))
|
|
realtime = require('../../lib/realtime')
|
|
|
|
// get all socket event handler
|
|
clientSocket = makeMockSocket(null, {
|
|
noteId: noteId
|
|
})
|
|
clientSocket.request.user.logged_in = true
|
|
clientSocket.request.user.id = 'user1'
|
|
// clientSocket.noteId = noteId
|
|
clientSocket.on = function (event, func) {
|
|
eventFuncMap.set(event, func)
|
|
}
|
|
realtime.maintenance = false
|
|
|
|
realtime.io = (function () {
|
|
const roomMap = new Map()
|
|
return {
|
|
to: function (roomId) {
|
|
if (!roomMap.has(roomId)) {
|
|
roomMap.set(roomId, {
|
|
emit: sinon.stub()
|
|
})
|
|
}
|
|
return roomMap.get(roomId)
|
|
}
|
|
}
|
|
}())
|
|
|
|
const wrappedFuncs = []
|
|
wrappedFuncs.push(sinon.stub(realtime, 'updateUserData'))
|
|
wrappedFuncs.push(sinon.stub(realtime, 'emitOnlineUsers'))
|
|
wrappedFuncs.push(sinon.stub(realtime, 'parseNoteIdFromSocketAsync').returns(Promise.resolve(noteId)))
|
|
wrappedFuncs.push(sinon.stub(realtime, 'updateHistory'))
|
|
wrappedFuncs.push(sinon.stub(realtime, 'emitRefresh'))
|
|
|
|
realtime.connection(clientSocket)
|
|
|
|
setTimeout(() => {
|
|
wrappedFuncs.forEach((wrappedFunc) => {
|
|
wrappedFunc.restore()
|
|
})
|
|
done()
|
|
}, 50)
|
|
})
|
|
|
|
afterEach(function () {
|
|
removeModuleFromRequireCache('../../lib/realtime')
|
|
removeModuleFromRequireCache('../../lib/realtimeClientConnection')
|
|
mock.stopAll()
|
|
sinon.restore()
|
|
clock.restore()
|
|
clientSocket = null
|
|
})
|
|
|
|
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.deleteAllNoteFromPool()
|
|
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 queueForDisconnectStub = sinon.stub(realtime, 'queueForDisconnect')
|
|
disconnectFunc()
|
|
assert(queueForDisconnectStub.calledOnce)
|
|
})
|
|
|
|
it('should quick return when socket is in disconnect queue', () => {
|
|
const disconnectFunc = eventFuncMap.get('disconnect')
|
|
const queueForDisconnectStub = sinon.stub(realtime, 'queueForDisconnect')
|
|
realtime.disconnectProcessQueue.push(clientSocket.id, async () => {})
|
|
disconnectFunc()
|
|
assert(queueForDisconnectStub.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')
|
|
const 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')
|
|
realtime.deleteAllNoteFromPool()
|
|
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')
|
|
realtime.deleteAllNoteFromPool()
|
|
userChangedFunc()
|
|
assert(updateUserDataStub.called === false)
|
|
assert(emitOnlineUsersStub.called === false)
|
|
})
|
|
|
|
it('should direct return when note\'s users 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 () {
|
|
const ownerId = 'user1_id'
|
|
const 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.deleteAllNoteFromPool()
|
|
realtime.addNote({
|
|
id: noteId,
|
|
owner: ownerId
|
|
})
|
|
|
|
checkViewPermissionSpy = sinon.spy(realtime, 'checkViewPermission')
|
|
permissionFunc = eventFuncMap.get('permission')
|
|
})
|
|
|
|
it('should disconnect when lose view permission', function (done) {
|
|
realtime.getNoteFromNotePool(noteId).permission = 'editable'
|
|
realtime.getNoteFromNotePool(noteId).socks = [clientSocket, undefined, otherClient]
|
|
|
|
permissionFunc('private')
|
|
|
|
setTimeout(() => {
|
|
// should change note permission to private
|
|
assert(modelsMock.Note.update.calledOnce)
|
|
assert(modelsMock.Note.update.lastCall.args[0].permission === 'private')
|
|
assert(modelsMock.Note.update.lastCall.args[1].where.id === noteId)
|
|
// should check all connected client
|
|
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)
|
|
})
|
|
})
|
|
})
|