From bdb222d5f4c9227ca0efb70d00cd9ee44867ba6c Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:12:07 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20fix:=20resolve=20session=20persi?= =?UTF-8?q?stence=20post=20password=20reset=20(#5077)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: Implement session management with CRUD operations and integrate into user workflows * ✨ refactor: Update session model import paths and enhance session creation logic in AuthService * ✨ refactor: Validate session and user ID formats in session management functions * ✨ style: Enhance UI components with improved styling and accessibility features * chore: Update login form tests to use getByTestId instead of getByRole, remove console.log() * chore: Update login form tests to use getByTestId instead of getByRole --------- Co-authored-by: Danny Avila --- api/cache/banViolation.js | 4 +- api/models/Session.js | 292 +++++++++++++++--- api/models/index.js | 19 +- api/models/schema/session.js | 20 ++ api/server/controllers/AuthController.js | 5 +- api/server/controllers/UserController.js | 4 +- api/server/services/AuthService.js | 36 ++- client/src/components/Auth/ErrorMessage.tsx | 2 +- client/src/components/Auth/Login.tsx | 5 +- client/src/components/Auth/LoginForm.tsx | 14 +- client/src/components/Auth/Registration.tsx | 13 +- .../components/Auth/RequestPasswordReset.tsx | 62 ++-- client/src/components/Auth/ResetPassword.tsx | 11 +- .../components/Auth/__tests__/Login.spec.tsx | 12 +- .../Auth/__tests__/LoginForm.spec.tsx | 6 +- client/src/utils/convos.spec.ts | 2 - e2e/setup/cleanupUser.ts | 11 +- 17 files changed, 402 insertions(+), 116 deletions(-) create mode 100644 api/models/schema/session.js diff --git a/api/cache/banViolation.js b/api/cache/banViolation.js index 1d86007638..cdbff85c54 100644 --- a/api/cache/banViolation.js +++ b/api/cache/banViolation.js @@ -1,7 +1,7 @@ const { ViolationTypes } = require('librechat-data-provider'); const { isEnabled, math, removePorts } = require('~/server/utils'); +const { deleteAllUserSessions } = require('~/models'); const getLogStores = require('./getLogStores'); -const Session = require('~/models/Session'); const { logger } = require('~/config'); const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {}; @@ -46,7 +46,7 @@ const banViolation = async (req, res, errorMessage) => { return; } - await Session.deleteAllUserSessions(user_id); + await deleteAllUserSessions({ userId: user_id }); res.clearCookie('refreshToken'); const banLogs = getLogStores(ViolationTypes.BAN); diff --git a/api/models/Session.js b/api/models/Session.js index 77cc30118b..dbb66ed8ff 100644 --- a/api/models/Session.js +++ b/api/models/Session.js @@ -1,75 +1,275 @@ const mongoose = require('mongoose'); const signPayload = require('~/server/services/signPayload'); const { hashToken } = require('~/server/utils/crypto'); +const sessionSchema = require('./schema/session'); const { logger } = require('~/config'); +const Session = mongoose.model('Session', sessionSchema); + const { REFRESH_TOKEN_EXPIRY } = process.env ?? {}; -const expires = eval(REFRESH_TOKEN_EXPIRY) ?? 1000 * 60 * 60 * 24 * 7; +const expires = eval(REFRESH_TOKEN_EXPIRY) ?? 1000 * 60 * 60 * 24 * 7; // 7 days default -const sessionSchema = mongoose.Schema({ - refreshTokenHash: { - type: String, - required: true, - }, - expiration: { - type: Date, - required: true, - expires: 0, - }, - user: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true, - }, -}); +/** + * Error class for Session-related errors + */ +class SessionError extends Error { + constructor(message, code = 'SESSION_ERROR') { + super(message); + this.name = 'SessionError'; + this.code = code; + } +} + +/** + * Creates a new session for a user + * @param {string} userId - The ID of the user + * @param {Object} options - Additional options for session creation + * @param {Date} options.expiration - Custom expiration date + * @returns {Promise<{session: Session, refreshToken: string}>} + * @throws {SessionError} + */ +const createSession = async (userId, options = {}) => { + if (!userId) { + throw new SessionError('User ID is required', 'INVALID_USER_ID'); + } -sessionSchema.methods.generateRefreshToken = async function () { try { - let expiresIn; - if (this.expiration) { - expiresIn = this.expiration.getTime(); - } else { - expiresIn = Date.now() + expires; - this.expiration = new Date(expiresIn); + const session = new Session({ + user: userId, + expiration: options.expiration || new Date(Date.now() + expires), + }); + const refreshToken = await generateRefreshToken(session); + return { session, refreshToken }; + } catch (error) { + logger.error('[createSession] Error creating session:', error); + throw new SessionError('Failed to create session', 'CREATE_SESSION_FAILED'); + } +}; + +/** + * Finds a session by various parameters + * @param {Object} params - Search parameters + * @param {string} [params.refreshToken] - The refresh token to search by + * @param {string} [params.userId] - The user ID to search by + * @param {string} [params.sessionId] - The session ID to search by + * @param {Object} [options] - Additional options + * @param {boolean} [options.lean=true] - Whether to return plain objects instead of documents + * @returns {Promise} + * @throws {SessionError} + */ +const findSession = async (params, options = { lean: true }) => { + try { + const query = {}; + + if (!params.refreshToken && !params.userId && !params.sessionId) { + throw new SessionError('At least one search parameter is required', 'INVALID_SEARCH_PARAMS'); + } + + if (params.refreshToken) { + const tokenHash = await hashToken(params.refreshToken); + query.refreshTokenHash = tokenHash; + } + + if (params.userId) { + query.user = params.userId; + } + + if (params.sessionId) { + const sessionId = params.sessionId.sessionId || params.sessionId; + if (!mongoose.Types.ObjectId.isValid(sessionId)) { + throw new SessionError('Invalid session ID format', 'INVALID_SESSION_ID'); + } + query._id = sessionId; + } + + // Add expiration check to only return valid sessions + query.expiration = { $gt: new Date() }; + + const sessionQuery = Session.findOne(query); + + if (options.lean) { + return await sessionQuery.lean(); + } + + return await sessionQuery.exec(); + } catch (error) { + logger.error('[findSession] Error finding session:', error); + throw new SessionError('Failed to find session', 'FIND_SESSION_FAILED'); + } +}; + +/** + * Updates session expiration + * @param {Session|string} session - The session or session ID to update + * @param {Date} [newExpiration] - Optional new expiration date + * @returns {Promise} + * @throws {SessionError} + */ +const updateExpiration = async (session, newExpiration) => { + try { + const sessionDoc = typeof session === 'string' ? await Session.findById(session) : session; + + if (!sessionDoc) { + throw new SessionError('Session not found', 'SESSION_NOT_FOUND'); + } + + sessionDoc.expiration = newExpiration || new Date(Date.now() + expires); + return await sessionDoc.save(); + } catch (error) { + logger.error('[updateExpiration] Error updating session:', error); + throw new SessionError('Failed to update session expiration', 'UPDATE_EXPIRATION_FAILED'); + } +}; + +/** + * Deletes a session by refresh token or session ID + * @param {Object} params - Delete parameters + * @param {string} [params.refreshToken] - The refresh token of the session to delete + * @param {string} [params.sessionId] - The ID of the session to delete + * @returns {Promise} + * @throws {SessionError} + */ +const deleteSession = async (params) => { + try { + if (!params.refreshToken && !params.sessionId) { + throw new SessionError( + 'Either refreshToken or sessionId is required', + 'INVALID_DELETE_PARAMS', + ); + } + + const query = {}; + + if (params.refreshToken) { + query.refreshTokenHash = await hashToken(params.refreshToken); + } + + if (params.sessionId) { + query._id = params.sessionId; + } + + const result = await Session.deleteOne(query); + + if (result.deletedCount === 0) { + logger.warn('[deleteSession] No session found to delete'); + } + + return result; + } catch (error) { + logger.error('[deleteSession] Error deleting session:', error); + throw new SessionError('Failed to delete session', 'DELETE_SESSION_FAILED'); + } +}; + +/** + * Deletes all sessions for a user + * @param {string} userId - The ID of the user + * @param {Object} [options] - Additional options + * @param {boolean} [options.excludeCurrentSession] - Whether to exclude the current session + * @param {string} [options.currentSessionId] - The ID of the current session to exclude + * @returns {Promise} + * @throws {SessionError} + */ +const deleteAllUserSessions = async (userId, options = {}) => { + try { + if (!userId) { + throw new SessionError('User ID is required', 'INVALID_USER_ID'); + } + + // Extract userId if it's passed as an object + const userIdString = userId.userId || userId; + + if (!mongoose.Types.ObjectId.isValid(userIdString)) { + throw new SessionError('Invalid user ID format', 'INVALID_USER_ID_FORMAT'); + } + + const query = { user: userIdString }; + + if (options.excludeCurrentSession && options.currentSessionId) { + query._id = { $ne: options.currentSessionId }; + } + + const result = await Session.deleteMany(query); + + if (result.deletedCount > 0) { + logger.debug( + `[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userIdString}.`, + ); + } + + return result; + } catch (error) { + logger.error('[deleteAllUserSessions] Error deleting user sessions:', error); + throw new SessionError('Failed to delete user sessions', 'DELETE_ALL_SESSIONS_FAILED'); + } +}; + +/** + * Generates a refresh token for a session + * @param {Session} session - The session to generate a token for + * @returns {Promise} + * @throws {SessionError} + */ +const generateRefreshToken = async (session) => { + if (!session || !session.user) { + throw new SessionError('Invalid session object', 'INVALID_SESSION'); + } + + try { + const expiresIn = session.expiration ? session.expiration.getTime() : Date.now() + expires; + + if (!session.expiration) { + session.expiration = new Date(expiresIn); } const refreshToken = await signPayload({ - payload: { id: this.user }, + payload: { + id: session.user, + sessionId: session._id, + }, secret: process.env.JWT_REFRESH_SECRET, expirationTime: Math.floor((expiresIn - Date.now()) / 1000), }); - this.refreshTokenHash = await hashToken(refreshToken); - - await this.save(); + session.refreshTokenHash = await hashToken(refreshToken); + await session.save(); return refreshToken; } catch (error) { - logger.error( - 'Error generating refresh token. Is a `JWT_REFRESH_SECRET` set in the .env file?\n\n', - error, - ); - throw error; + logger.error('[generateRefreshToken] Error generating refresh token:', error); + throw new SessionError('Failed to generate refresh token', 'GENERATE_TOKEN_FAILED'); } }; -sessionSchema.statics.deleteAllUserSessions = async function (userId) { +/** + * Counts active sessions for a user + * @param {string} userId - The ID of the user + * @returns {Promise} + * @throws {SessionError} + */ +const countActiveSessions = async (userId) => { try { if (!userId) { - return; - } - const result = await this.deleteMany({ user: userId }); - if (result && result?.deletedCount > 0) { - logger.debug( - `[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userId}.`, - ); + throw new SessionError('User ID is required', 'INVALID_USER_ID'); } + + return await Session.countDocuments({ + user: userId, + expiration: { $gt: new Date() }, + }); } catch (error) { - logger.error('[deleteAllUserSessions] Error in deleting user sessions:', error); - throw error; + logger.error('[countActiveSessions] Error counting active sessions:', error); + throw new SessionError('Failed to count active sessions', 'COUNT_SESSIONS_FAILED'); } }; -const Session = mongoose.model('Session', sessionSchema); - -module.exports = Session; +module.exports = { + createSession, + findSession, + updateExpiration, + deleteSession, + deleteAllUserSessions, + generateRefreshToken, + countActiveSessions, + SessionError, +}; diff --git a/api/models/index.js b/api/models/index.js index 73fc2f4ab9..73cfa1c96c 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -26,10 +26,18 @@ const { deleteMessagesSince, deleteMessages, } = require('./Message'); +const { + createSession, + findSession, + updateExpiration, + deleteSession, + deleteAllUserSessions, + generateRefreshToken, + countActiveSessions, +} = require('./Session'); const { getConvoTitle, getConvo, saveConvo, deleteConvos } = require('./Conversation'); const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset'); const { createToken, findToken, updateToken, deleteTokens } = require('./Token'); -const Session = require('./Session'); const Balance = require('./Balance'); const User = require('./User'); const Key = require('./Key'); @@ -75,8 +83,15 @@ module.exports = { updateToken, deleteTokens, + createSession, + findSession, + updateExpiration, + deleteSession, + deleteAllUserSessions, + generateRefreshToken, + countActiveSessions, + User, Key, - Session, Balance, }; diff --git a/api/models/schema/session.js b/api/models/schema/session.js new file mode 100644 index 0000000000..ccda43573d --- /dev/null +++ b/api/models/schema/session.js @@ -0,0 +1,20 @@ +const mongoose = require('mongoose'); + +const sessionSchema = mongoose.Schema({ + refreshTokenHash: { + type: String, + required: true, + }, + expiration: { + type: Date, + required: true, + expires: 0, + }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true, + }, +}); + +module.exports = sessionSchema; diff --git a/api/server/controllers/AuthController.js b/api/server/controllers/AuthController.js index 0225798535..4f83c4e8a3 100644 --- a/api/server/controllers/AuthController.js +++ b/api/server/controllers/AuthController.js @@ -6,8 +6,8 @@ const { setAuthTokens, requestPasswordReset, } = require('~/server/services/AuthService'); +const { findSession, getUserById, deleteAllUserSessions } = require('~/models'); const { hashToken } = require('~/server/utils/crypto'); -const { Session, getUserById } = require('~/models'); const { logger } = require('~/config'); const registrationController = async (req, res) => { @@ -45,6 +45,7 @@ const resetPasswordController = async (req, res) => { if (resetPasswordService instanceof Error) { return res.status(400).json(resetPasswordService); } else { + await deleteAllUserSessions({ userId: req.body.userId }); return res.status(200).json(resetPasswordService); } } catch (e) { @@ -77,7 +78,7 @@ const refreshController = async (req, res) => { const hashedToken = await hashToken(refreshToken); // Find the session with the hashed refresh token - const session = await Session.findOne({ user: userId, refreshTokenHash: hashedToken }); + const session = await findSession({ userId: userId, refreshToken: hashedToken }); if (session && session.expiration > new Date()) { const token = await setAuthTokens(userId, res, session._id); res.status(200).send({ token, user }); diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 9e01da38e5..17089e8fdc 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -1,5 +1,4 @@ const { - Session, Balance, getFiles, deleteFiles, @@ -7,6 +6,7 @@ const { deletePresets, deleteMessages, deleteUserById, + deleteAllUserSessions, } = require('~/models'); const User = require('~/models/User'); const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService'); @@ -112,7 +112,7 @@ const deleteUserController = async (req, res) => { try { await deleteMessages({ user: user.id }); // delete user messages - await Session.deleteMany({ user: user.id }); // delete user sessions + await deleteAllUserSessions({ userId: user.id }); // delete user sessions await Transaction.deleteMany({ user: user.id }); // delete user transactions await deleteUserKey({ userId: user.id, all: true }); // delete user keys await Balance.deleteMany({ user: user._id }); // delete user balances diff --git a/api/server/services/AuthService.js b/api/server/services/AuthService.js index 383f00cde7..624f8af0c2 100644 --- a/api/server/services/AuthService.js +++ b/api/server/services/AuthService.js @@ -10,7 +10,15 @@ const { generateToken, deleteUserById, } = require('~/models/userMethods'); -const { createToken, findToken, deleteTokens, Session } = require('~/models'); +const { + createToken, + findToken, + deleteTokens, + findSession, + deleteSession, + createSession, + generateRefreshToken, +} = require('~/models'); const { isEnabled, checkEmailConfig, sendEmail } = require('~/server/utils'); const { isEmailDomainAllowed } = require('~/server/services/domains'); const { registerSchema } = require('~/strategies/validators'); @@ -37,10 +45,11 @@ const logoutUser = async (userId, refreshToken) => { const hash = await hashToken(refreshToken); // Find the session with the matching user and refreshTokenHash - const session = await Session.findOne({ user: userId, refreshTokenHash: hash }); + const session = await findSession({ userId: userId, refreshToken: hash }); + if (session) { try { - await Session.deleteOne({ _id: session._id }); + await deleteSession({ sessionId: session._id }); } catch (deleteErr) { logger.error('[logoutUser] Failed to delete session.', deleteErr); return { status: 500, message: 'Failed to delete session.' }; @@ -330,18 +339,19 @@ const setAuthTokens = async (userId, res, sessionId = null) => { const token = await generateToken(user); let session; + let refreshToken; let refreshTokenExpires; - if (sessionId) { - session = await Session.findById(sessionId); - refreshTokenExpires = session.expiration.getTime(); - } else { - session = new Session({ user: userId }); - const { REFRESH_TOKEN_EXPIRY } = process.env ?? {}; - const expires = eval(REFRESH_TOKEN_EXPIRY) ?? 1000 * 60 * 60 * 24 * 7; - refreshTokenExpires = Date.now() + expires; - } - const refreshToken = await session.generateRefreshToken(); + if (sessionId) { + session = await findSession({ sessionId: sessionId }); + refreshTokenExpires = session.expiration.getTime(); + refreshToken = await generateRefreshToken(session); + } else { + const result = await createSession(userId); + session = result.session; + refreshToken = result.refreshToken; + refreshTokenExpires = session.expiration.getTime(); + } res.cookie('refreshToken', refreshToken, { expires: new Date(refreshTokenExpires), diff --git a/client/src/components/Auth/ErrorMessage.tsx b/client/src/components/Auth/ErrorMessage.tsx index eab097c600..a072336645 100644 --- a/client/src/components/Auth/ErrorMessage.tsx +++ b/client/src/components/Auth/ErrorMessage.tsx @@ -2,7 +2,7 @@ export const ErrorMessage = ({ children }: { children: React.ReactNode }) => (
{children}
diff --git a/client/src/components/Auth/Login.tsx b/client/src/components/Auth/Login.tsx index b3d5a22e1b..0e62bdea71 100644 --- a/client/src/components/Auth/Login.tsx +++ b/client/src/components/Auth/Login.tsx @@ -26,7 +26,10 @@ function Login() {

{' '} {localize('com_auth_no_account')}{' '} - + {localize('com_auth_sign_up')}

diff --git a/client/src/components/Auth/LoginForm.tsx b/client/src/components/Auth/LoginForm.tsx index 3404a78729..0c532abe40 100644 --- a/client/src/components/Auth/LoginForm.tsx +++ b/client/src/components/Auth/LoginForm.tsx @@ -153,16 +153,24 @@ const LoginForm: React.FC = ({ onSubmit, startupConfig, error, {renderError('password')} {startupConfig.passwordResetEnabled && ( - + {localize('com_auth_password_forgot')} )}
diff --git a/client/src/components/Auth/Registration.tsx b/client/src/components/Auth/Registration.tsx index 14cbe95c79..4ae4e03b79 100644 --- a/client/src/components/Auth/Registration.tsx +++ b/client/src/components/Auth/Registration.tsx @@ -183,7 +183,12 @@ const Registration: React.FC = () => { disabled={Object.keys(errors).length > 0} type="submit" aria-label="Submit registration" - className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200" + className=" + w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white + transition-colors hover:bg-green-700 focus:outline-none focus:ring-2 + focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50 + disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700 + " > {isSubmitting ? : localize('com_auth_continue')} @@ -192,7 +197,11 @@ const Registration: React.FC = () => {

{localize('com_auth_already_have_account')}{' '} - + {localize('com_auth_login')}

diff --git a/client/src/components/Auth/RequestPasswordReset.tsx b/client/src/components/Auth/RequestPasswordReset.tsx index a6a5d69dd6..1010c90a98 100644 --- a/client/src/components/Auth/RequestPasswordReset.tsx +++ b/client/src/components/Auth/RequestPasswordReset.tsx @@ -10,7 +10,7 @@ import { useLocalize } from '~/hooks'; const BodyTextWrapper: FC<{ children: ReactNode }> = ({ children }) => { return (
{children} @@ -21,13 +21,14 @@ const BodyTextWrapper: FC<{ children: ReactNode }> = ({ children }) => { const ResetPasswordBodyText = () => { const localize = useLocalize(); return ( -
- {localize('com_auth_reset_password_if_email_exists')} - - - {localize('com_auth_back_to_login')} - - +
+

{localize('com_auth_reset_password_if_email_exists')}

+ + {localize('com_auth_back_to_login')} +
); }; @@ -76,12 +77,12 @@ function RequestPasswordReset() { return (
-
+
{errors.email && ( - +

{errors.email.message} - +

)}
-
+ ); diff --git a/client/src/components/Auth/ResetPassword.tsx b/client/src/components/Auth/ResetPassword.tsx index c7f26e7406..3039e81c29 100644 --- a/client/src/components/Auth/ResetPassword.tsx +++ b/client/src/components/Auth/ResetPassword.tsx @@ -35,7 +35,7 @@ function ResetPassword() { return ( <>
{localize('com_auth_login_with_new_password')} @@ -43,7 +43,7 @@ function ResetPassword() { @@ -163,7 +163,12 @@ function ResetPassword() { disabled={!!errors.password || !!errors.confirm_password} type="submit" aria-label={localize('com_auth_submit_registration')} - className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200" + className=" + w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white + transition-colors hover:bg-green-700 focus:outline-none focus:ring-2 + focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50 + disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700 + " > {localize('com_auth_continue')} diff --git a/client/src/components/Auth/__tests__/Login.spec.tsx b/client/src/components/Auth/__tests__/Login.spec.tsx index 288d796850..308d584a9e 100644 --- a/client/src/components/Auth/__tests__/Login.spec.tsx +++ b/client/src/components/Auth/__tests__/Login.spec.tsx @@ -1,6 +1,6 @@ import reactRouter from 'react-router-dom'; import userEvent from '@testing-library/user-event'; -import { render, waitFor } from 'test/layout-test-utils'; +import { getByTestId, render, waitFor } from 'test/layout-test-utils'; import * as mockDataProvider from 'librechat-data-provider/react-query'; import type { TStartupConfig } from 'librechat-data-provider'; import AuthLayout from '~/components/Auth/AuthLayout'; @@ -117,7 +117,7 @@ test('renders login form', () => { const { getByLabelText, getByRole } = setup(); expect(getByLabelText(/email/i)).toBeInTheDocument(); expect(getByLabelText(/password/i)).toBeInTheDocument(); - expect(getByRole('button', { name: /Sign in/i })).toBeInTheDocument(); + expect(getByTestId(document.body, 'login-button')).toBeInTheDocument(); expect(getByRole('link', { name: /Sign up/i })).toBeInTheDocument(); expect(getByRole('link', { name: /Sign up/i })).toHaveAttribute('href', '/register'); expect(getByRole('link', { name: /Continue with Google/i })).toBeInTheDocument(); @@ -144,7 +144,7 @@ test('renders login form', () => { test('calls loginUser.mutate on login', async () => { const mutate = jest.fn(); - const { getByLabelText, getByRole } = setup({ + const { getByLabelText } = setup({ // @ts-ignore - we don't need all parameters of the QueryObserverResult useLoginUserReturnValue: { isLoading: false, @@ -155,7 +155,7 @@ test('calls loginUser.mutate on login', async () => { const emailInput = getByLabelText(/email/i); const passwordInput = getByLabelText(/password/i); - const submitButton = getByRole('button', { name: /Sign in/i }); + const submitButton = getByTestId(document.body, 'login-button'); await userEvent.type(emailInput, 'test@test.com'); await userEvent.type(passwordInput, 'password'); @@ -165,7 +165,7 @@ test('calls loginUser.mutate on login', async () => { }); test('Navigates to / on successful login', async () => { - const { getByLabelText, getByRole, history } = setup({ + const { getByLabelText, history } = setup({ // @ts-ignore - we don't need all parameters of the QueryObserverResult useLoginUserReturnValue: { isLoading: false, @@ -185,7 +185,7 @@ test('Navigates to / on successful login', async () => { const emailInput = getByLabelText(/email/i); const passwordInput = getByLabelText(/password/i); - const submitButton = getByRole('button', { name: /Sign in/i }); + const submitButton = getByTestId(document.body, 'login-button'); await userEvent.type(emailInput, 'test@test.com'); await userEvent.type(passwordInput, 'password'); diff --git a/client/src/components/Auth/__tests__/LoginForm.spec.tsx b/client/src/components/Auth/__tests__/LoginForm.spec.tsx index eca8e6ef51..81d9df96a9 100644 --- a/client/src/components/Auth/__tests__/LoginForm.spec.tsx +++ b/client/src/components/Auth/__tests__/LoginForm.spec.tsx @@ -1,4 +1,4 @@ -import { render } from 'test/layout-test-utils'; +import { render, getByTestId } from 'test/layout-test-utils'; import userEvent from '@testing-library/user-event'; import * as mockDataProvider from 'librechat-data-provider/react-query'; import type { TStartupConfig } from 'librechat-data-provider'; @@ -112,7 +112,7 @@ test('submits login form', async () => { ); const emailInput = getByLabelText(/email/i); const passwordInput = getByLabelText(/password/i); - const submitButton = getByRole('button', { name: /Sign in/i }); + const submitButton = getByTestId(document.body, 'login-button'); await userEvent.type(emailInput, 'test@example.com'); await userEvent.type(passwordInput, 'password'); @@ -127,7 +127,7 @@ test('displays validation error messages', async () => { ); const emailInput = getByLabelText(/email/i); const passwordInput = getByLabelText(/password/i); - const submitButton = getByRole('button', { name: /Sign in/i }); + const submitButton = getByTestId(document.body, 'login-button'); await userEvent.type(emailInput, 'test'); await userEvent.type(passwordInput, 'pass'); diff --git a/client/src/utils/convos.spec.ts b/client/src/utils/convos.spec.ts index 53cb0d5dad..1b1dd5d583 100644 --- a/client/src/utils/convos.spec.ts +++ b/client/src/utils/convos.spec.ts @@ -579,8 +579,6 @@ describe('Conversation Utilities with Fake Data', () => { 5, ); - console.log(normalizedData); - expect(normalizedData.pages[0].conversations).toHaveLength(0); }); diff --git a/e2e/setup/cleanupUser.ts b/e2e/setup/cleanupUser.ts index 4ba398f423..fda3b45e97 100644 --- a/e2e/setup/cleanupUser.ts +++ b/e2e/setup/cleanupUser.ts @@ -1,5 +1,11 @@ import connectDb from '@librechat/backend/lib/db/connectDb'; -import { deleteMessages, deleteConvos, User, Session, Balance } from '@librechat/backend/models'; +import { + deleteMessages, + deleteConvos, + User, + deleteAllUserSessions, + Balance, +} from '@librechat/backend/models'; import { Transaction } from '@librechat/backend/models/Transaction'; type TUser = { email: string; password: string }; @@ -26,7 +32,8 @@ export default async function cleanupUser(user: TUser) { console.log(`🤖: ✅ Deleted ${deletedMessages} remaining message(s)`); } - await Session.deleteAllUserSessions(user); + // TODO: fix this to delete all user sessions with the user's email + await deleteAllUserSessions(user); await User.deleteMany({ _id: user }); await Balance.deleteMany({ user });