diff --git a/apps/meteor/tests/e2e/presence-stale.spec.ts b/apps/meteor/tests/e2e/presence-stale.spec.ts new file mode 100644 index 0000000000000..fc639f0610366 --- /dev/null +++ b/apps/meteor/tests/e2e/presence-stale.spec.ts @@ -0,0 +1,73 @@ +import { UserStatus, type IUserSession } from '@rocket.chat/core-typings'; + +import { Users } from './fixtures/userStates'; +import { HomeChannel } from './page-objects'; +import { test, expect } from './utils/test'; + +test.describe('Should remove stale sessions', () => { + test('user1 sees user2 offline after inactivity', async ({ browser, db }) => { + test.slow(); + + const [user1Context, user2Context] = await Promise.all([ + browser.newContext({ storageState: Users.user1.state }), + browser.newContext({ storageState: Users.user2.state }), + ]); + + const [user1Page, user2Page] = await Promise.all([user1Context.newPage(), user2Context.newPage()]); + + await Promise.all([user1Page.goto('/home'), user2Page.goto('/home')]); + + const user1Home = new HomeChannel(user1Page); + + await user1Home.sidenav.openChat('user2'); + + await Promise.all([ + expect(user1Home.content.channelHeader).toContainText('user2'), + expect(user1Home.content.channelHeader.locator('.rcx-status-bullet--online')).toBeVisible(), + ]); + + await test.step('Simulate inactivity by updating user2 session and status', async () => { + await test.step('Close user2 page to simulate disconnection', async () => { + await user2Context.setOffline(true); + }); + await db.users.updateStatusById(Users.user2.data._id, { + status: UserStatus.ONLINE, + statusConnection: UserStatus.ONLINE, + }); + + const user2Session: IUserSession = (await db.usersSessions.findOneById(Users.user2.data._id)) ?? { + _id: Users.user2.data._id, + connections: [], + }; + + expect(user2Session.connections.length).toBeGreaterThan(0); + + await db.usersSessions.col.updateOne( + { _id: Users.user2.data._id }, + { + $set: { + connections: user2Session.connections.map((connection) => ({ + ...connection, + _createdAt: new Date(connection._createdAt.getTime() - 6 * 60 * 1000), + _updatedAt: new Date(connection._updatedAt.getTime() - 6 * 60 * 1000), + status: UserStatus.ONLINE, + })), + }, + }, + ); + }); + + await expect(user1Home.content.channelHeader.getByRole('button', { name: 'user2', exact: true }).getByRole('img')).toContainClass( + 'rcx-status-bullet--online', + { timeout: 120_000 }, + ); + + await expect(user1Home.content.channelHeader.getByRole('button', { name: 'user2', exact: true }).getByRole('img')).toContainClass( + 'rcx-status-bullet--offline', + { timeout: 120_000 }, + ); + + await user1Context.close(); + await user2Context.close(); + }); +}); diff --git a/apps/meteor/tests/e2e/utils/db.ts b/apps/meteor/tests/e2e/utils/db.ts new file mode 100644 index 0000000000000..b509d97152a1c --- /dev/null +++ b/apps/meteor/tests/e2e/utils/db.ts @@ -0,0 +1,26 @@ +import { UsersSessionsRaw, UsersRaw } from '@rocket.chat/models'; +import { MongoClient } from 'mongodb'; + +import { URL_MONGODB } from '../config/constants'; + +export class DatabaseClient { + client: MongoClient; + + users: UsersRaw; + + usersSessions: UsersSessionsRaw; + + private constructor(client: MongoClient) { + this.client = client; + this.users = new UsersRaw(client.db()); + this.usersSessions = new UsersSessionsRaw(client.db()); + } + + static async connect(): Promise { + return new DatabaseClient(await MongoClient.connect(URL_MONGODB)); + } + + async close(): Promise { + await this.client.close(); + } +} diff --git a/apps/meteor/tests/e2e/utils/test.ts b/apps/meteor/tests/e2e/utils/test.ts index 716c91d25aaf9..48cee45b1127c 100644 --- a/apps/meteor/tests/e2e/utils/test.ts +++ b/apps/meteor/tests/e2e/utils/test.ts @@ -7,6 +7,7 @@ import type { Locator, APIResponse, APIRequestContext } from '@playwright/test'; import { test as baseTest, request as baseRequest } from '@playwright/test'; import { v4 as uuid } from 'uuid'; +import { DatabaseClient } from './db'; import { BASE_API_URL, API_PREFIX, ADMIN_CREDENTIALS } from '../config/constants'; import { Users } from '../fixtures/userStates'; @@ -37,7 +38,17 @@ let apiContext: APIRequestContext; const cacheFromCredentials = new Map(); -export const test = baseTest.extend({ +export const test = baseTest.extend({ + db: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use) => { + const db = await DatabaseClient.connect(); + await use(db); + await db.close(); + }, + { scope: 'worker' }, + ], + context: async ({ context }, use) => { if (!process.env.E2E_COVERAGE) { await use(context);