feat(db & e2e): Enhance DB Schemas/Controllers and Improve E2E Tests (#966)

* feat: add global teardown to remove test data and add registration/log-out to auth flow

* refactor(models/Conversation): index user field and add JSDoc to deleteConvos

* refactor: add user index to message schema and ensure user is saved to each Message

* refactor: add user to each saveMessage call

* fix: handle case where title is null in zod schema

* feat(e2e): ensure messages are deleted on cleanUp

* fix: set last convo for all endpoints on conversation update

* fix: enable registration for CI env
This commit is contained in:
Danny Avila 2023-09-18 15:19:50 -04:00 committed by GitHub
parent fd70e21732
commit 6358383001
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 229 additions and 72 deletions

View file

@ -9,11 +9,13 @@ const config: PlaywrightTestConfig = {
...mainConfig,
retries: 0,
globalSetup: require.resolve('./setup/global-setup.local'),
globalTeardown: require.resolve('./setup/global-teardown.local'),
webServer: {
...mainConfig.webServer,
command: `node ${absolutePath}`,
env: {
...process.env,
SEARCH: 'false',
NODE_ENV: 'development',
SESSION_EXPIRY: '60000',
REFRESH_TOKEN_EXPIRY: '300000',

View file

@ -6,6 +6,7 @@ dotenv.config();
export default defineConfig({
globalSetup: require.resolve('./setup/global-setup'),
globalTeardown: require.resolve('./setup/global-teardown'),
testDir: 'specs/',
outputDir: 'specs/.test-results',
/* Run tests in files in parallel.
@ -61,8 +62,10 @@ export default defineConfig({
reuseExistingServer: true,
env: {
...process.env,
SEARCH: 'false',
NODE_ENV: 'development',
SESSION_EXPIRY: '60000',
ALLOW_REGISTRATION: 'true',
REFRESH_TOKEN_EXPIRY: '300000',
},
},

View file

@ -2,10 +2,31 @@ import { Page, FullConfig, chromium } from '@playwright/test';
import dotenv from 'dotenv';
dotenv.config();
type User = { username: string; password: string };
type User = { email: string; name: string; password: string };
async function register(page: Page, user: User) {
await page.getByRole('link', { name: 'Sign up' }).click();
await page.getByLabel('Full name').click();
await page.getByLabel('Full name').fill('test');
await page.getByText('Username (optional)').click();
await page.getByLabel('Username (optional)').fill('test');
await page.getByLabel('Email').click();
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Email').press('Tab');
await page.getByTestId('password').click();
await page.getByTestId('password').fill(user.password);
await page.getByTestId('confirm_password').click();
await page.getByTestId('confirm_password').fill(user.password);
await page.getByLabel('Submit registration').click();
}
async function logout(page: Page, user: User) {
await page.getByRole('button', { name: user.name }).click();
await page.getByRole('button', { name: 'Log out' }).click();
}
async function login(page: Page, user: User) {
await page.locator('input[name="email"]').fill(user.username);
await page.locator('input[name="email"]').fill(user.email);
await page.locator('input[name="password"]').fill(user.password);
await page.locator('input[name="password"]').press('Enter');
}
@ -15,22 +36,36 @@ async function authenticate(config: FullConfig, user: User) {
const { baseURL, storageState } = config.projects[0].use;
console.log('🤖: using baseURL', baseURL);
console.dir(user, { depth: null });
const browser = await chromium.launch();
const browser = await chromium.launch({
// headless: false,
});
const page = await browser.newPage();
console.log('🤖: 🗝 authenticating user:', user.username);
console.log('🤖: 🗝 authenticating user:', user.email);
if (!baseURL) {
throw new Error('🤖: baseURL is not defined');
}
await page.goto(baseURL, { timeout: 5000 });
await login(page, user);
await page.waitForURL(`${baseURL}/chat/new`);
console.log('🤖: ✔️ user successfully authenticated');
// Set localStorage before navigating to the page
await page.context().addInitScript(() => {
localStorage.setItem('navVisible', 'true');
});
console.log('🤖: ✔️ localStorage: set Nav as Visible', storageState);
await page.goto(baseURL, { timeout: 5000 });
await register(page, user);
await page.waitForURL(`${baseURL}/chat/new`);
console.log('🤖: ✔️ user successfully registered');
// Logout
await logout(page, user);
await page.waitForURL(`${baseURL}/login`);
console.log('🤖: ✔️ user successfully logged out');
await login(page, user);
await page.waitForURL(`${baseURL}/chat/new`);
console.log('🤖: ✔️ user successfully authenticated');
await page.context().storageState({ path: storageState as string });
console.log('🤖: ✔️ authentication state successfully saved in', storageState);
await browser.close();

43
e2e/setup/cleanupUser.ts Normal file
View file

@ -0,0 +1,43 @@
import connectDb from '@librechat/backend/lib/db/connectDb';
import User from '@librechat/backend/models/User';
import Session from '@librechat/backend/models/Session';
import { deleteMessages } from '@librechat/backend/models/Message';
import { deleteConvos } from '@librechat/backend/models/Conversation';
type TUser = { email: string; password: string };
export default async function cleanupUser(user: TUser) {
const { email } = user;
try {
console.log('🤖: global teardown has been started');
const db = await connectDb();
console.log('🤖: ✅ Connected to Database');
const { _id } = await User.findOne({ email }).lean();
console.log('🤖: ✅ Found user in Database');
// Delete all conversations & associated messages
const { deletedCount, messages } = await deleteConvos(_id, {});
if (messages.deletedCount > 0 || deletedCount > 0) {
console.log(`🤖: ✅ Deleted ${deletedCount} convos & ${messages.deletedCount} messages`);
}
// Ensure all user messages are deleted
const { deletedCount: deletedMessages } = await deleteMessages({ user: _id });
if (deletedMessages > 0) {
console.log(`🤖: ✅ Deleted ${deletedMessages} remaining message(s)`);
}
await Session.deleteAllUserSessions(_id);
await User.deleteMany({ email });
console.log('🤖: ✅ Deleted user from Database');
await db.connection.close();
} catch (error) {
console.error('Error:', error);
}
}
process.on('uncaughtException', (err) => console.error('Uncaught Exception:', err));

View file

@ -3,7 +3,8 @@ import authenticate from './authenticate';
async function globalSetup(config: FullConfig) {
const user = {
username: String(process.env.E2E_USER_EMAIL),
name: 'test',
email: String(process.env.E2E_USER_EMAIL),
password: String(process.env.E2E_USER_PASSWORD),
};

View file

@ -0,0 +1,12 @@
import localUser from '../config.local';
import cleanupUser from './cleanupUser';
async function globalTeardown() {
try {
await cleanupUser(localUser);
} catch (error) {
console.error('Error:', error);
}
}
export default globalTeardown;

View file

@ -0,0 +1,16 @@
import cleanupUser from './cleanupUser';
async function globalTeardown() {
const user = {
email: String(process.env.E2E_USER_EMAIL),
password: String(process.env.E2E_USER_PASSWORD),
};
try {
await cleanupUser(user);
} catch (error) {
console.error('Error:', error);
}
}
export default globalTeardown;