Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement last time online for users #947

Merged
merged 3 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -241,6 +244,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 @@ -59,7 +59,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 @@ -81,7 +83,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 @@ -176,6 +178,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)
})
})
})
Loading