Skip to content

Commit

Permalink
Implement last time online for users (#947)
Browse files Browse the repository at this point in the history
* Implement last time online for users

* Fix import

* Increase coverage
  • Loading branch information
ipasic-softserve authored Dec 2, 2024
1 parent 41132c2 commit 0b3a51e
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 9 deletions.
6 changes: 6 additions & 0 deletions docs/user/user-schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ definitions:
lastLogin:
type: string
format: date-time
lastSeen:
type: string
format: date-time
status:
type: object
properties:
Expand Down Expand Up @@ -250,6 +253,9 @@ definitions:
lastLogin:
type: string
format: date-time
lastSeen:
type: string
format: date-time
status:
type: object
properties:
Expand Down
4 changes: 4 additions & 0 deletions docs/user/user.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ paths:
photo: john-doe-photo.jpg
status: { student: active, tutor: active, admin: active }
lastLogin: null
lastSeen: null
FAQ:
{
student: [{ question: tutor question, _id: 63525e23bf163f5ea609ff2b, answer: tutor answer }],
Expand Down Expand Up @@ -106,6 +107,7 @@ paths:
}
videoLink: { tutor: www.youtube.com/watch?v=ebTnuLRnIOY }
lastLogin: 2022-09-02T11:59:53.243+00:00
lastSeen: 2022-09-02T11:59:53.243+00:00
createdAt: 2021-04-09T11:34:53.243+00:00
updatedAt: 2022-09-02T11:59:53.243+00:00
401:
Expand Down Expand Up @@ -255,6 +257,7 @@ paths:
isEmailConfirmed: true
isFirstLogin: true
lastLogin: 2022-09-02T11:59:53.243+00:00
lastSeen: 2022-09-02T11:59:53.243+00:00
bookmarkedOffers: []
FAQ:
{
Expand Down Expand Up @@ -301,6 +304,7 @@ paths:
isEmailConfirmed: true
isFirstLogin: true
lastLogin: 2022-09-02T11:59:53.243+00:00
lastSeen: 2022-09-02T11:59:53.243+00:00
bookmarkedOffers: []
FAQ:
{
Expand Down
5 changes: 4 additions & 1 deletion src/event-handlers/activityHandler.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
const { updateLastSeen } = require('~/services/user')

module.exports = (io, socket, usersOnline) => {
const connectUser = () => {
socket.join(socket.user.id)
usersOnline.add(socket.user.id)
io.emit('usersOnline', Array.from(usersOnline))
}

const disconnect = () => {
const disconnect = async () => {
if (socket.user && !io.sockets.adapter.rooms.has(socket.user.id)) {
usersOnline.delete(socket.user.id)
io.emit('usersOnline', Array.from(usersOnline))
await updateLastSeen(socket.user.id)
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ const userSchema = new Schema(
type: Date,
default: null
},
lastSeen: {
type: Date,
default: null
},
appLanguage: {
type: String,
enum: {
Expand Down
18 changes: 16 additions & 2 deletions src/services/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ const userService = {
const user = await User.findOne({ _id: id, ...(role && { role }) })
.populate(populateOptions('tutor'))
.populate(populateOptions('student'))
.select('+lastLoginAs +isEmailConfirmed +isFirstLogin +bookmarkedOffers +videoLink +notificationSettings')
.select(
'+lastLoginAs +isEmailConfirmed +isFirstLogin +bookmarkedOffers +videoLink +notificationSettings +lastSeen'
)
.lean()
.exec()
if (isEdit) {
Expand All @@ -82,7 +84,7 @@ const userService = {

getUserByEmail: async (email) => {
const user = await User.findOne({ email })
.select('+password +lastLoginAs +isEmailConfirmed +isFirstLogin +appLanguage +notificationSettings')
.select('+password +lastLoginAs +isEmailConfirmed +isFirstLogin +appLanguage +notificationSettings +lastSeen')
.lean()
.exec()

Expand Down Expand Up @@ -177,6 +179,18 @@ const userService = {
await User.findByIdAndUpdate(id, filteredUpdateData, { new: true, runValidators: true }).lean().exec()
},

updateLastSeen: async (id) => {
const user = await User.findById(id).lean().exec()

if (!user) {
throw createError(404, DOCUMENT_NOT_FOUND([User.modelName]))
}

await User.findByIdAndUpdate(id, { $set: { lastSeen: Date.now() } }, { new: true })
.lean()
.exec()
},

_updateMainSubjects: async (mainSubjects, userSubjects, role, userId) => {
const oldSubjects = userSubjects[role]
let newSubjects = { ...userSubjects }
Expand Down
31 changes: 25 additions & 6 deletions src/test/unit/event-handlers/activityHandler.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
const activityHandler = require('~/event-handlers/activityHandler')
const userSchema = require('~/models/user')

const mockFindById = (socketUserId) => (id) => {
const isUserIdMatch = id.toString() === socketUserId.toString()
return {
lean: () => ({
exec: () => Promise.resolve(isUserIdMatch ? { _id: id } : null)
})
}
}

const mockFindByIdAndUpdate = () => (id, update) => ({
lean: () => ({
exec: () => Promise.resolve({ _id: id, ...update.$set })
})
})

describe('activityHandler', () => {
let io, socket, usersOnline
Expand All @@ -12,6 +28,9 @@ describe('activityHandler', () => {
}
usersOnline = new Set()

jest.spyOn(userSchema, 'findById').mockImplementation(mockFindById(socket.user.id))
jest.spyOn(userSchema, 'findByIdAndUpdate').mockImplementation(mockFindByIdAndUpdate())

activityHandler(io, socket, usersOnline)
})

Expand All @@ -25,35 +44,35 @@ describe('activityHandler', () => {
expect(io.emit).toHaveBeenCalledWith('usersOnline', Array.from(usersOnline))
})

test('should call disconnect and remove user from usersOnline set and emit usersOnline event', () => {
test('should call disconnect and remove user from usersOnline set and emit usersOnline event', async () => {
usersOnline.add('user1')

const disconnectCallback = socket.on.mock.calls.find(([event]) => event === 'disconnect')[1]

disconnectCallback()
await disconnectCallback()

expect(usersOnline.has('user1')).toBe(false)
expect(io.emit).toHaveBeenCalledWith('usersOnline', Array.from(usersOnline))
})

test('should not delete the user from usersOnline when the user has at least one active session', () => {
test('should not delete the user from usersOnline when the user has at least one active session', async () => {
usersOnline.add('user1')
io.sockets.adapter.rooms.set('user1', 'socketId')

const disconnectCallback = socket.on.mock.calls.find(([event]) => event === 'disconnect')[1]

disconnectCallback()
await disconnectCallback()

expect(usersOnline.has('user1')).toBe(true)
expect(io.emit).not.toHaveBeenCalled()
})

test('should call disconnect and do nothing if socket.user is undefined', () => {
test('should call disconnect and do nothing if socket.user is undefined', async () => {
socket.user = undefined

const disconnectCallback = socket.on.mock.calls.find(([event]) => event === 'disconnect')[1]

disconnectCallback()
await disconnectCallback()

expect(io.emit).not.toHaveBeenCalled()
})
Expand Down
39 changes: 39 additions & 0 deletions src/test/unit/services/user.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
jest.mock('~/models/offer')
jest.mock('~/services/offer')
jest.mock('~/services/cooperation')
const { createError } = require('~/utils/errorsHelper')

describe('User service', () => {
afterEach(() => {
Expand Down Expand Up @@ -328,4 +329,42 @@ describe('User service', () => {
await expect(userService.updateStatus(id, updateStatus)).rejects.toThrow(DOCUMENT_NOT_FOUND([User.modelName]))
})
})
describe('updateLastSeen', () => {
it('should update lastSeen field', async () => {
const id = '123'
const userMock = {
_id: '123'
}

jest.spyOn(User, 'findById').mockReturnValue({
lean: jest.fn().mockReturnThis(),
exec: jest.fn().mockResolvedValue(userMock)
})

jest.spyOn(User, 'findByIdAndUpdate').mockReturnValue({
lean: jest.fn().mockReturnThis(),
exec: jest.fn().mockResolvedValue(userMock)
})

await userService.updateLastSeen(id)

expect(User.findById).toHaveBeenCalledWith(id)
expect(User.findByIdAndUpdate).toHaveBeenCalledWith(id, { $set: { lastSeen: expect.any(Number) } }, { new: true })
})

it('should throw a 404 error if user is not found', async () => {
const id = 'non-existent id'

jest.spyOn(User, 'findById').mockReturnValue({
lean: jest.fn().mockReturnThis(),
exec: jest.fn().mockResolvedValue(null)
})

await expect(userService.updateLastSeen(id)).rejects.toThrow(
createError(404, DOCUMENT_NOT_FOUND([User.modelName]))
)

expect(User.findById).toHaveBeenCalledWith(id)
})
})
})

0 comments on commit 0b3a51e

Please sign in to comment.