2024-01-30 14:34:02 -05:00
|
|
|
// file deepcode ignore NoHardcodedPasswords: No hard-coded passwords in tests
|
2024-02-26 14:12:25 -05:00
|
|
|
const { errorsToString } = require('librechat-data-provider');
|
|
|
|
const { loginSchema, registerSchema } = require('./validators');
|
2023-09-14 15:12:22 -04:00
|
|
|
|
|
|
|
describe('Zod Schemas', () => {
|
|
|
|
describe('loginSchema', () => {
|
|
|
|
it('should validate a correct login object', () => {
|
|
|
|
const result = loginSchema.safeParse({
|
|
|
|
email: 'test@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should invalidate an incorrect email', () => {
|
|
|
|
const result = loginSchema.safeParse({
|
|
|
|
email: 'testexample.com',
|
|
|
|
password: 'password123',
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should invalidate a short password', () => {
|
|
|
|
const result = loginSchema.safeParse({
|
|
|
|
email: 'test@example.com',
|
|
|
|
password: 'pass',
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle email with unusual characters', () => {
|
|
|
|
const emails = ['test+alias@example.com', 'test@subdomain.example.co.uk'];
|
|
|
|
emails.forEach((email) => {
|
|
|
|
const result = loginSchema.safeParse({
|
|
|
|
email,
|
|
|
|
password: 'password123',
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should invalidate email without a domain', () => {
|
|
|
|
const result = loginSchema.safeParse({
|
|
|
|
email: 'test@.com',
|
|
|
|
password: 'password123',
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should invalidate password with only spaces', () => {
|
|
|
|
const result = loginSchema.safeParse({
|
|
|
|
email: 'test@example.com',
|
|
|
|
password: ' ',
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should invalidate password that is too long', () => {
|
|
|
|
const result = loginSchema.safeParse({
|
|
|
|
email: 'test@example.com',
|
|
|
|
password: 'a'.repeat(129),
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should invalidate empty email or password', () => {
|
|
|
|
const result = loginSchema.safeParse({
|
|
|
|
email: '',
|
|
|
|
password: '',
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('registerSchema', () => {
|
|
|
|
it('should validate a correct register object', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
username: 'john_doe',
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should allow the username to be omitted', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should invalidate a short name', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'Jo',
|
|
|
|
username: 'john_doe',
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle empty username by transforming to null', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
username: '',
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
expect(result.data.username).toBe(null);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle name with special characters', () => {
|
|
|
|
const names = ['Jöhn Dœ', 'John <Doe>'];
|
|
|
|
names.forEach((name) => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name,
|
|
|
|
username: 'john_doe',
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle username with special characters', () => {
|
|
|
|
const usernames = ['john.doe@', 'john..doe'];
|
|
|
|
usernames.forEach((username) => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
username,
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should invalidate mismatched password and confirm_password', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
username: 'john_doe',
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password124',
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle email without a TLD', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
username: 'john_doe',
|
|
|
|
email: 'john@domain',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle email with multiple @ symbols', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
username: 'john_doe',
|
|
|
|
email: 'john@domain@com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle name that is too long', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'a'.repeat(81),
|
|
|
|
username: 'john_doe',
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle username that is too long', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
username: 'a'.repeat(81),
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle password or confirm_password that is too long', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
username: 'john_doe',
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'a'.repeat(129),
|
|
|
|
confirm_password: 'a'.repeat(129),
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle password or confirm_password that is just spaces', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
username: 'john_doe',
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: ' ',
|
|
|
|
confirm_password: ' ',
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle null values for fields', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: null,
|
|
|
|
username: null,
|
|
|
|
email: null,
|
|
|
|
password: null,
|
|
|
|
confirm_password: null,
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle undefined values for fields', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: undefined,
|
|
|
|
username: undefined,
|
|
|
|
email: undefined,
|
|
|
|
password: undefined,
|
|
|
|
confirm_password: undefined,
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle extra fields not defined in the schema', () => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
username: 'john_doe',
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
2025-08-27 16:30:56 -04:00
|
|
|
extraField: "I shouldn't be here",
|
2023-09-14 15:12:22 -04:00
|
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
});
|
2023-09-18 16:57:12 -04:00
|
|
|
|
|
|
|
it('should handle username with special characters from various languages', () => {
|
|
|
|
const usernames = [
|
|
|
|
// General
|
|
|
|
'éèäöü',
|
|
|
|
|
|
|
|
// German
|
|
|
|
'Jöhn.Döe@',
|
|
|
|
'Jöhn_Ü',
|
|
|
|
'Jöhnß',
|
|
|
|
|
|
|
|
// French
|
|
|
|
'Jéan-Piérre',
|
|
|
|
'Élève',
|
|
|
|
'Fiançée',
|
|
|
|
'Mère',
|
|
|
|
|
|
|
|
// Spanish
|
|
|
|
'Niño',
|
|
|
|
'Señor',
|
|
|
|
'Muñoz',
|
|
|
|
|
|
|
|
// Portuguese
|
|
|
|
'João',
|
|
|
|
'Coração',
|
|
|
|
'Pão',
|
|
|
|
|
|
|
|
// Italian
|
|
|
|
'Pietro',
|
|
|
|
'Bambino',
|
|
|
|
'Forlì',
|
|
|
|
|
|
|
|
// Romanian
|
|
|
|
'Mâncare',
|
|
|
|
'Școală',
|
|
|
|
'Țară',
|
|
|
|
|
|
|
|
// Catalan
|
|
|
|
'Niç',
|
|
|
|
'Màquina',
|
|
|
|
'Çap',
|
|
|
|
|
|
|
|
// Swedish
|
|
|
|
'Fjärran',
|
|
|
|
'Skål',
|
|
|
|
'Öland',
|
|
|
|
|
|
|
|
// Norwegian
|
|
|
|
'Blåbær',
|
|
|
|
'Fjord',
|
|
|
|
'Årstid',
|
|
|
|
|
|
|
|
// Danish
|
|
|
|
'Flød',
|
|
|
|
'Søster',
|
|
|
|
'Århus',
|
|
|
|
|
|
|
|
// Icelandic
|
|
|
|
'Þór',
|
|
|
|
'Ætt',
|
|
|
|
'Öx',
|
|
|
|
|
|
|
|
// Turkish
|
|
|
|
'Şehir',
|
|
|
|
'Çocuk',
|
|
|
|
'Gözlük',
|
|
|
|
|
|
|
|
// Polish
|
|
|
|
'Łódź',
|
|
|
|
'Część',
|
|
|
|
'Świat',
|
|
|
|
|
|
|
|
// Czech
|
|
|
|
'Čaj',
|
|
|
|
'Řeka',
|
|
|
|
'Život',
|
|
|
|
|
|
|
|
// Slovak
|
|
|
|
'Kočka',
|
|
|
|
'Ľudia',
|
|
|
|
'Žaba',
|
|
|
|
|
|
|
|
// Croatian
|
|
|
|
'Čovjek',
|
|
|
|
'Šuma',
|
|
|
|
'Žaba',
|
|
|
|
|
|
|
|
// Hungarian
|
|
|
|
'Tűz',
|
|
|
|
'Ősz',
|
|
|
|
'Ünnep',
|
|
|
|
|
|
|
|
// Finnish
|
|
|
|
'Mäki',
|
|
|
|
'Yö',
|
|
|
|
'Äiti',
|
|
|
|
|
|
|
|
// Estonian
|
|
|
|
'Tänav',
|
|
|
|
'Öö',
|
|
|
|
'Ülikool',
|
|
|
|
|
|
|
|
// Latvian
|
|
|
|
'Ēka',
|
|
|
|
'Ūdens',
|
|
|
|
'Čempions',
|
|
|
|
|
|
|
|
// Lithuanian
|
|
|
|
'Ūsas',
|
|
|
|
'Ąžuolas',
|
|
|
|
'Čia',
|
|
|
|
|
|
|
|
// Dutch
|
|
|
|
'Maïs',
|
|
|
|
'Geërfd',
|
|
|
|
'Coördinatie',
|
|
|
|
];
|
|
|
|
|
|
|
|
const failingUsernames = usernames.reduce((acc, username) => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
username,
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
acc.push({ username, error: result.error });
|
|
|
|
}
|
|
|
|
|
|
|
|
return acc;
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
if (failingUsernames.length > 0) {
|
|
|
|
console.log('Failing Usernames:', failingUsernames);
|
|
|
|
}
|
|
|
|
expect(failingUsernames).toEqual([]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should reject invalid usernames', () => {
|
|
|
|
const invalidUsernames = [
|
|
|
|
'john{doe}', // Contains `{` and `}`
|
|
|
|
'j', // Only one character
|
|
|
|
'a'.repeat(81), // More than 80 characters
|
2025-08-27 16:30:56 -04:00
|
|
|
"' OR '1'='1'; --", // SQL Injection
|
2023-09-18 16:57:12 -04:00
|
|
|
'{$ne: null}', // MongoDB Injection
|
|
|
|
'<script>alert("XSS")</script>', // Basic XSS
|
|
|
|
'"><script>alert("XSS")</script>', // XSS breaking out of an attribute
|
|
|
|
'"><img src=x onerror=alert("XSS")>', // XSS using an image tag
|
|
|
|
];
|
|
|
|
|
|
|
|
const passingUsernames = [];
|
|
|
|
const failingUsernames = invalidUsernames.reduce((acc, username) => {
|
|
|
|
const result = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
username,
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
acc.push({ username, error: result.error });
|
|
|
|
}
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
passingUsernames.push({ username });
|
|
|
|
}
|
|
|
|
|
|
|
|
return acc;
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
expect(failingUsernames.length).toEqual(invalidUsernames.length); // They should match since all invalidUsernames should fail.
|
|
|
|
});
|
2023-09-14 15:12:22 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('errorsToString', () => {
|
|
|
|
it('should convert errors to string', () => {
|
|
|
|
const { error } = registerSchema.safeParse({
|
|
|
|
name: 'Jo',
|
|
|
|
username: 'john_doe',
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'password123',
|
|
|
|
confirm_password: 'password123',
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = errorsToString(error.errors);
|
|
|
|
expect(result).toBe('name: String must contain at least 3 character(s)');
|
|
|
|
});
|
|
|
|
});
|
2025-08-27 16:30:56 -04:00
|
|
|
|
|
|
|
describe('MIN_PASSWORD_LENGTH environment variable', () => {
|
|
|
|
// Note: These tests verify the behavior based on whatever MIN_PASSWORD_LENGTH
|
|
|
|
// was set when the validators module was loaded
|
|
|
|
const minLength = parseInt(process.env.MIN_PASSWORD_LENGTH, 10) || 8;
|
|
|
|
|
|
|
|
it('should respect the configured minimum password length for login', () => {
|
|
|
|
// Test password exactly at minimum length
|
|
|
|
const resultValid = loginSchema.safeParse({
|
|
|
|
email: 'test@example.com',
|
|
|
|
password: 'a'.repeat(minLength),
|
|
|
|
});
|
|
|
|
expect(resultValid.success).toBe(true);
|
|
|
|
|
|
|
|
// Test password one character below minimum
|
|
|
|
if (minLength > 1) {
|
|
|
|
const resultInvalid = loginSchema.safeParse({
|
|
|
|
email: 'test@example.com',
|
|
|
|
password: 'a'.repeat(minLength - 1),
|
|
|
|
});
|
|
|
|
expect(resultInvalid.success).toBe(false);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should respect the configured minimum password length for registration', () => {
|
|
|
|
// Test password exactly at minimum length
|
|
|
|
const resultValid = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'a'.repeat(minLength),
|
|
|
|
confirm_password: 'a'.repeat(minLength),
|
|
|
|
});
|
|
|
|
expect(resultValid.success).toBe(true);
|
|
|
|
|
|
|
|
// Test password one character below minimum
|
|
|
|
if (minLength > 1) {
|
|
|
|
const resultInvalid = registerSchema.safeParse({
|
|
|
|
name: 'John Doe',
|
|
|
|
email: 'john@example.com',
|
|
|
|
password: 'a'.repeat(minLength - 1),
|
|
|
|
confirm_password: 'a'.repeat(minLength - 1),
|
|
|
|
});
|
|
|
|
expect(resultInvalid.success).toBe(false);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle edge case of very short minimum password length', () => {
|
|
|
|
// This test is meaningful only if MIN_PASSWORD_LENGTH is set to a very low value
|
|
|
|
if (minLength <= 3) {
|
|
|
|
const result = loginSchema.safeParse({
|
|
|
|
email: 'test@example.com',
|
|
|
|
password: 'abc',
|
|
|
|
});
|
|
|
|
expect(result.success).toBe(minLength <= 3);
|
|
|
|
} else {
|
|
|
|
// Skip this test if minimum length is > 3
|
|
|
|
expect(true).toBe(true);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2023-09-14 15:12:22 -04:00
|
|
|
});
|