LibreChat/packages/data-schemas/src/methods/user.methods.spec.ts
Ravi Kumar L 329ed48246
📉 perf: cache OpenID JWT user documents (#14187)
* feat(auth): cache OpenID JWT user documents

* fix(auth): invalidate cached auth users on role changes
2026-07-12 07:53:50 -04:00

932 lines
30 KiB
TypeScript

import mongoose from 'mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { AUTH_USER_DOC_BY_ID_PREFIX, CacheKeys } from 'librechat-data-provider';
import type * as t from '~/types';
import balanceSchema from '~/schema/balance';
import { createUserMethods } from './user';
import userSchema from '~/schema/user';
/** Mocking crypto for generateToken */
jest.mock('~/crypto', () => ({
signPayload: jest.fn().mockResolvedValue('mocked-token'),
}));
let mongoServer: MongoMemoryServer;
let User: mongoose.Model<t.IUser>;
let Balance: mongoose.Model<t.IBalance>;
let methods: ReturnType<typeof createUserMethods>;
const ORIGINAL_AUTH_USER_CACHE_ENV = {
AUTH_USER_CACHE_MODE: process.env.AUTH_USER_CACHE_MODE,
};
function restoreAuthUserCacheEnv() {
for (const [key, value] of Object.entries(ORIGINAL_AUTH_USER_CACHE_ENV)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
}
function enableAuthUserDocCache() {
process.env.AUTH_USER_CACHE_MODE = 'on';
}
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
/** Register models */
User = mongoose.models.User || mongoose.model<t.IUser>('User', userSchema);
Balance = mongoose.models.Balance || mongoose.model<t.IBalance>('Balance', balanceSchema);
/** Initialize methods */
methods = createUserMethods(mongoose);
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
beforeEach(async () => {
restoreAuthUserCacheEnv();
await mongoose.connection.dropDatabase();
});
afterEach(() => {
restoreAuthUserCacheEnv();
});
describe('User schema indexes', () => {
test('should define an issuer-bound idOnTheSource lookup index', async () => {
await User.syncIndexes();
const indexes = await User.collection.indexes();
expect(indexes).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: { idOnTheSource: 1, openidIssuer: 1, tenantId: 1 },
}),
]),
);
});
test('should allow the same OpenID subject from different issuers', async () => {
await User.syncIndexes();
await User.create({
email: 'issuer-a@example.com',
provider: 'openid',
openidId: 'shared-sub',
openidIssuer: 'https://issuer-a.example.com',
});
await expect(
User.create({
email: 'issuer-b@example.com',
provider: 'openid',
openidId: 'shared-sub',
openidIssuer: 'https://issuer-b.example.com',
}),
).resolves.toBeTruthy();
await expect(
User.create({
email: 'issuer-a-duplicate@example.com',
provider: 'openid',
openidId: 'shared-sub',
openidIssuer: 'https://issuer-a.example.com',
}),
).rejects.toThrow(/duplicate key/);
});
});
describe('User Methods - Database Tests', () => {
describe('findUser', () => {
test('should find user by exact email', async () => {
await User.create({
name: 'Test User',
email: 'test@example.com',
provider: 'local',
});
const found = await methods.findUser({ email: 'test@example.com' });
expect(found).toBeDefined();
expect(found?.email).toBe('test@example.com');
});
test('should find user by email with different case (case-insensitive)', async () => {
await User.create({
name: 'Test User',
email: 'test@example.com', // stored lowercase by schema
provider: 'local',
});
/** Test various case combinations - all should find the same user */
const foundUpper = await methods.findUser({ email: 'TEST@EXAMPLE.COM' });
const foundMixed = await methods.findUser({ email: 'Test@Example.COM' });
const foundLower = await methods.findUser({ email: 'test@example.com' });
expect(foundUpper).toBeDefined();
expect(foundUpper?.email).toBe('test@example.com');
expect(foundMixed).toBeDefined();
expect(foundMixed?.email).toBe('test@example.com');
expect(foundLower).toBeDefined();
expect(foundLower?.email).toBe('test@example.com');
});
test('should find user by email with leading/trailing whitespace (trimmed)', async () => {
await User.create({
name: 'Test User',
email: 'test@example.com',
provider: 'local',
});
const foundWithSpaces = await methods.findUser({ email: ' test@example.com ' });
const foundWithTabs = await methods.findUser({ email: '\ttest@example.com\t' });
expect(foundWithSpaces).toBeDefined();
expect(foundWithSpaces?.email).toBe('test@example.com');
expect(foundWithTabs).toBeDefined();
expect(foundWithTabs?.email).toBe('test@example.com');
});
test('should find user by email with both case difference and whitespace', async () => {
await User.create({
name: 'Test User',
email: 'john.doe@example.com',
provider: 'local',
});
const found = await methods.findUser({ email: ' John.Doe@EXAMPLE.COM ' });
expect(found).toBeDefined();
expect(found?.email).toBe('john.doe@example.com');
});
test('should normalize email in $or conditions', async () => {
await User.create({
name: 'Test User',
email: 'test@example.com',
provider: 'openid',
openidId: 'openid-123',
});
const found = await methods.findUser({
$or: [{ openidId: 'different-id' }, { email: 'TEST@EXAMPLE.COM' }],
});
expect(found).toBeDefined();
expect(found?.email).toBe('test@example.com');
});
test('should find user by non-email criteria without affecting them', async () => {
await User.create({
name: 'Test User',
email: 'test@example.com',
provider: 'openid',
openidId: 'openid-123',
});
const found = await methods.findUser({ openidId: 'openid-123' });
expect(found).toBeDefined();
expect(found?.openidId).toBe('openid-123');
});
test('should apply field selection correctly', async () => {
await User.create({
name: 'Test User',
email: 'test@example.com',
provider: 'local',
username: 'testuser',
});
const found = await methods.findUser({ email: 'test@example.com' }, 'email name');
expect(found).toBeDefined();
expect(found?.email).toBe('test@example.com');
expect(found?.name).toBe('Test User');
expect(found?.username).toBeUndefined();
expect(found?.provider).toBeUndefined();
});
test('should return null for non-existent user', async () => {
const found = await methods.findUser({ email: 'nonexistent@example.com' });
expect(found).toBeNull();
});
});
describe('createUser', () => {
test('should create a user and return ObjectId by default', async () => {
const result = await methods.createUser({
name: 'New User',
email: 'new@example.com',
provider: 'local',
});
expect(result).toBeInstanceOf(mongoose.Types.ObjectId);
const user = await User.findById(result);
expect(user).toBeDefined();
expect(user?.name).toBe('New User');
expect(user?.email).toBe('new@example.com');
});
test('should create a user and return user object when returnUser is true', async () => {
const result = await methods.createUser(
{
name: 'New User',
email: 'new@example.com',
provider: 'local',
},
undefined,
true,
true,
);
expect(result).toHaveProperty('_id');
expect(result).toHaveProperty('name', 'New User');
expect(result).toHaveProperty('email', 'new@example.com');
});
test('should store email as lowercase regardless of input case', async () => {
await methods.createUser({
name: 'New User',
email: 'NEW@EXAMPLE.COM',
provider: 'local',
});
const user = await User.findOne({ email: 'new@example.com' });
expect(user).toBeDefined();
expect(user?.email).toBe('new@example.com');
});
test('should create user with TTL when disableTTL is false', async () => {
const result = await methods.createUser(
{
name: 'TTL User',
email: 'ttl@example.com',
provider: 'local',
},
undefined,
false,
true,
);
expect(result).toHaveProperty('expiresAt');
const expiresAt = (result as t.IUser).expiresAt;
expect(expiresAt).toBeInstanceOf(Date);
/** Should expire in approximately 1 week */
const oneWeekMs = 604800 * 1000;
const expectedExpiry = Date.now() + oneWeekMs;
expect(expiresAt!.getTime()).toBeGreaterThan(expectedExpiry - 10000);
expect(expiresAt!.getTime()).toBeLessThan(expectedExpiry + 10000);
});
test('should create balance record when balanceConfig is provided', async () => {
const userId = await methods.createUser(
{
name: 'Balance User',
email: 'balance@example.com',
provider: 'local',
},
{
enabled: true,
startBalance: 1000,
},
);
const balance = await Balance.findOne({ user: userId });
expect(balance).toBeDefined();
expect(balance?.tokenCredits).toBe(1000);
});
});
describe('updateUser', () => {
test('should update user fields', async () => {
const user = await User.create({
name: 'Original Name',
email: 'test@example.com',
provider: 'local',
});
const updated = await methods.updateUser(user._id?.toString() ?? '', {
name: 'Updated Name',
});
expect(updated).toBeDefined();
expect(updated?.name).toBe('Updated Name');
expect(updated?.email).toBe('test@example.com');
});
test('should remove expiresAt field on update', async () => {
const user = await User.create({
name: 'TTL User',
email: 'ttl@example.com',
provider: 'local',
expiresAt: new Date(Date.now() + 604800 * 1000),
});
const updated = await methods.updateUser(user._id?.toString() || '', {
name: 'No longer TTL',
});
expect(updated).toBeDefined();
expect(updated?.expiresAt).toBeUndefined();
});
test('should invalidate cached auth user documents on update', async () => {
enableAuthUserDocCache();
const user = await User.create({
name: 'Cached Auth User',
email: 'cached-auth@example.com',
provider: 'openid',
});
const indexKey = `${AUTH_USER_DOC_BY_ID_PREFIX}:${user._id?.toString()}`;
const cache = {
get: jest.fn().mockResolvedValue(['auth-cache-key-a', 'auth-cache-key-b']),
delete: jest.fn().mockResolvedValue(true),
};
const getCache = jest.fn().mockReturnValue(cache);
const methodsWithCache = createUserMethods(mongoose, { getCache });
await methodsWithCache.updateUser(user._id?.toString() ?? '', {
name: 'Updated Cached Auth User',
});
expect(getCache).toHaveBeenCalledWith(CacheKeys.AUTH_USER_DOC);
expect(cache.get).toHaveBeenCalledWith(indexKey);
expect(cache.delete).toHaveBeenCalledWith('auth-cache-key-a');
expect(cache.delete).toHaveBeenCalledWith('auth-cache-key-b');
expect(cache.delete).toHaveBeenCalledWith(indexKey);
});
test('should invalidate cached auth user documents on delete', async () => {
enableAuthUserDocCache();
const user = await User.create({
name: 'Deleted Cached Auth User',
email: 'deleted-cached-auth@example.com',
provider: 'openid',
});
const indexKey = `${AUTH_USER_DOC_BY_ID_PREFIX}:${user._id?.toString()}`;
const cache = {
get: jest.fn().mockResolvedValue(['auth-cache-key-a']),
delete: jest.fn().mockResolvedValue(true),
};
const getCache = jest.fn().mockReturnValue(cache);
const methodsWithCache = createUserMethods(mongoose, { getCache });
await methodsWithCache.deleteUserById(user._id?.toString() ?? '');
expect(getCache).toHaveBeenCalledWith(CacheKeys.AUTH_USER_DOC);
expect(cache.get).toHaveBeenCalledWith(indexKey);
expect(cache.delete).toHaveBeenCalledWith('auth-cache-key-a');
expect(cache.delete).toHaveBeenCalledWith(indexKey);
});
test('should return null for non-existent user', async () => {
const fakeId = new mongoose.Types.ObjectId();
const result = await methods.updateUser(fakeId.toString(), { name: 'Test' });
expect(result).toBeNull();
});
});
describe('getUserById', () => {
test('should get user by ID', async () => {
const user = await User.create({
name: 'Test User',
email: 'test@example.com',
provider: 'local',
});
const found = await methods.getUserById(user._id?.toString() || '');
expect(found).toBeDefined();
expect(found?.name).toBe('Test User');
});
test('should apply field selection', async () => {
const user = await User.create({
name: 'Test User',
email: 'test@example.com',
provider: 'local',
username: 'testuser',
});
const found = await methods.getUserById(user._id?.toString() || '', 'name email');
expect(found).toBeDefined();
expect(found?.name).toBe('Test User');
expect(found?.email).toBe('test@example.com');
expect(found?.username).toBeUndefined();
});
test('should return null for non-existent ID', async () => {
const fakeId = new mongoose.Types.ObjectId();
const found = await methods.getUserById(fakeId.toString());
expect(found).toBeNull();
});
});
describe('acceptTerms', () => {
test('sets termsAccepted and stamps termsAcceptedAt on first acceptance', async () => {
const user = await User.create({
name: 'Terms User',
email: 'terms@example.com',
provider: 'local',
});
const before = Date.now();
const updated = await methods.acceptTerms(user._id?.toString() ?? '');
const after = Date.now();
expect(updated?.termsAccepted).toBe(true);
expect(updated?.termsAcceptedAt).toBeInstanceOf(Date);
const stamped = (updated?.termsAcceptedAt as Date).getTime();
expect(stamped).toBeGreaterThanOrEqual(before - 1000);
expect(stamped).toBeLessThanOrEqual(after + 1000);
});
test('preserves the original termsAcceptedAt on repeat acceptance', async () => {
const originalAcceptedAt = new Date('2026-01-01T00:00:00.000Z');
const user = await User.create({
name: 'Repeat User',
email: 'repeat@example.com',
provider: 'local',
termsAccepted: true,
termsAcceptedAt: originalAcceptedAt,
});
const updated = await methods.acceptTerms(user._id?.toString() ?? '');
expect(updated?.termsAccepted).toBe(true);
expect((updated?.termsAcceptedAt as Date).getTime()).toBe(originalAcceptedAt.getTime());
});
test('backfills termsAcceptedAt for a legacy accepted user without a timestamp', async () => {
const user = await User.create({
name: 'Legacy User',
email: 'legacy@example.com',
provider: 'local',
termsAccepted: true,
});
const updated = await methods.acceptTerms(user._id?.toString() ?? '');
expect(updated?.termsAccepted).toBe(true);
expect(updated?.termsAcceptedAt).toBeInstanceOf(Date);
});
test('returns null for non-existent user', async () => {
const fakeId = new mongoose.Types.ObjectId();
const result = await methods.acceptTerms(fakeId.toString());
expect(result).toBeNull();
});
test('should invalidate cached auth user documents on acceptance', async () => {
enableAuthUserDocCache();
const user = await User.create({
name: 'Cached Terms User',
email: 'cached-terms@example.com',
provider: 'openid',
});
const indexKey = `${AUTH_USER_DOC_BY_ID_PREFIX}:${user._id?.toString()}`;
const cache = {
get: jest.fn().mockResolvedValue(['auth-cache-key-a']),
delete: jest.fn().mockResolvedValue(true),
};
const getCache = jest.fn().mockReturnValue(cache);
const methodsWithCache = createUserMethods(mongoose, { getCache });
await methodsWithCache.acceptTerms(user._id?.toString() ?? '');
expect(getCache).toHaveBeenCalledWith(CacheKeys.AUTH_USER_DOC);
expect(cache.get).toHaveBeenCalledWith(indexKey);
expect(cache.delete).toHaveBeenCalledWith('auth-cache-key-a');
expect(cache.delete).toHaveBeenCalledWith(indexKey);
});
});
describe('deleteUserById', () => {
test('should delete user by ID', async () => {
const user = await User.create({
name: 'To Delete',
email: 'delete@example.com',
provider: 'local',
});
const result = await methods.deleteUserById(user._id?.toString() || '');
expect(result.deletedCount).toBe(1);
expect(result.message).toBe('User was deleted successfully.');
const found = await User.findById(user._id);
expect(found).toBeNull();
});
test('should return zero count for non-existent user', async () => {
const fakeId = new mongoose.Types.ObjectId();
const result = await methods.deleteUserById(fakeId.toString());
expect(result.deletedCount).toBe(0);
expect(result.message).toBe('No user found with that ID.');
});
});
describe('countUsers', () => {
test('should count all users', async () => {
await User.create([
{ name: 'User 1', email: 'user1@example.com', provider: 'local' },
{ name: 'User 2', email: 'user2@example.com', provider: 'local' },
{ name: 'User 3', email: 'user3@example.com', provider: 'openid' },
]);
const count = await methods.countUsers();
expect(count).toBe(3);
});
test('should count users with filter', async () => {
await User.create([
{ name: 'User 1', email: 'user1@example.com', provider: 'local' },
{ name: 'User 2', email: 'user2@example.com', provider: 'local' },
{ name: 'User 3', email: 'user3@example.com', provider: 'openid' },
]);
const count = await methods.countUsers({ provider: 'local' });
expect(count).toBe(2);
});
test('should return zero for empty collection', async () => {
const count = await methods.countUsers();
expect(count).toBe(0);
});
});
describe('searchUsers', () => {
beforeEach(async () => {
await User.create([
{ name: 'John Doe', email: 'john@example.com', username: 'johnd', provider: 'local' },
{ name: 'Jane Smith', email: 'jane@example.com', username: 'janes', provider: 'local' },
{
name: 'Bob Johnson',
email: 'bob@example.com',
username: 'bobbyj',
provider: 'local',
},
{
name: 'Alice Wonder',
email: 'alice@test.com',
username: 'alice',
provider: 'openid',
},
]);
});
test('should search by name', async () => {
const results = await methods.searchUsers({ searchPattern: 'John' });
expect(results).toHaveLength(2); // John Doe and Bob Johnson
});
test('should search by email', async () => {
const results = await methods.searchUsers({ searchPattern: 'example.com' });
expect(results).toHaveLength(3);
});
test('should search by username', async () => {
const results = await methods.searchUsers({ searchPattern: 'alice' });
expect(results).toHaveLength(1);
expect((results[0] as unknown as t.IUser)?.username).toBe('alice');
});
test('should be case-insensitive', async () => {
const results = await methods.searchUsers({ searchPattern: 'JOHN' });
expect(results.length).toBeGreaterThan(0);
});
test('should respect limit', async () => {
const results = await methods.searchUsers({ searchPattern: 'example', limit: 2 });
expect(results).toHaveLength(2);
});
test('should return empty array for empty search pattern', async () => {
const results = await methods.searchUsers({ searchPattern: '' });
expect(results).toEqual([]);
});
test('should return empty array for whitespace-only pattern', async () => {
const results = await methods.searchUsers({ searchPattern: ' ' });
expect(results).toEqual([]);
});
test('should treat regex metacharacters as literal search text', async () => {
await User.create({
name: 'Literal .* User',
email: 'literal-star@test.com',
username: 'literal-star',
provider: 'local',
});
const results = await methods.searchUsers({ searchPattern: '.*' });
expect(results).toHaveLength(1);
expect((results[0] as unknown as t.IUser).name).toBe('Literal .* User');
});
test('should handle invalid regex syntax as literal search text', async () => {
await User.create({
name: 'Regex [invalid User',
email: 'regex-invalid@test.com',
username: 'regex-invalid',
provider: 'local',
});
const results = await methods.searchUsers({ searchPattern: '[invalid' });
expect(results).toHaveLength(1);
expect((results[0] as unknown as t.IUser).name).toBe('Regex [invalid User');
});
test('should apply field selection', async () => {
const results = await methods.searchUsers({
searchPattern: 'john',
fieldsToSelect: 'name email',
});
expect(results.length).toBeGreaterThan(0);
expect(results[0]).toHaveProperty('name');
expect(results[0]).toHaveProperty('email');
expect(results[0]).not.toHaveProperty('username');
});
test('should sort by relevance (exact match first)', async () => {
const results = await methods.searchUsers({ searchPattern: 'alice' });
/** 'alice' username should score highest due to exact match */
expect((results[0] as unknown as t.IUser).username).toBe('alice');
});
});
describe('toggleUserMemories', () => {
test('should enable memories for user', async () => {
const user = await User.create({
name: 'Test User',
email: 'test@example.com',
provider: 'local',
});
const updated = await methods.toggleUserMemories(user._id?.toString() || '', true);
expect(updated).toBeDefined();
expect(updated?.personalization?.memories).toBe(true);
});
test('should disable memories for user', async () => {
const user = await User.create({
name: 'Test User',
email: 'test@example.com',
provider: 'local',
personalization: { memories: true },
});
const updated = await methods.toggleUserMemories(user._id?.toString() || '', false);
expect(updated).toBeDefined();
expect(updated?.personalization?.memories).toBe(false);
});
test('should update personalization.memories field', async () => {
const user = await User.create({
name: 'Test User',
email: 'test@example.com',
provider: 'local',
});
/** Toggle memories to true */
const updated = await methods.toggleUserMemories(user._id?.toString() || '', true);
expect(updated?.personalization).toBeDefined();
expect(updated?.personalization?.memories).toBe(true);
/** Toggle back to false */
const updatedAgain = await methods.toggleUserMemories(user._id?.toString() || '', false);
expect(updatedAgain?.personalization?.memories).toBe(false);
});
test('should return null for non-existent user', async () => {
const fakeId = new mongoose.Types.ObjectId();
const result = await methods.toggleUserMemories(fakeId.toString(), true);
expect(result).toBeNull();
});
test('should invalidate cached auth user documents when memories preference changes', async () => {
enableAuthUserDocCache();
const user = await User.create({
name: 'Cached Memory User',
email: 'cached-memory@example.com',
provider: 'openid',
personalization: { memories: true },
});
const indexKey = `${AUTH_USER_DOC_BY_ID_PREFIX}:${user._id?.toString()}`;
const cache = {
get: jest.fn().mockResolvedValue(['auth-cache-key-a']),
set: jest.fn().mockResolvedValue(true),
delete: jest.fn().mockResolvedValue(true),
};
const getCache = jest.fn().mockReturnValue(cache);
const methodsWithCache = createUserMethods(mongoose, { getCache });
await methodsWithCache.toggleUserMemories(user._id?.toString() ?? '', false);
expect(getCache).toHaveBeenCalledWith(CacheKeys.AUTH_USER_DOC);
expect(cache.get).toHaveBeenCalledWith(indexKey);
expect(cache.delete).toHaveBeenCalledWith('auth-cache-key-a');
expect(cache.delete).toHaveBeenCalledWith(indexKey);
});
});
describe('Email Normalization Edge Cases', () => {
test('should handle email with multiple spaces', async () => {
await User.create({
name: 'Test User',
email: 'test@example.com',
provider: 'local',
});
const found = await methods.findUser({ email: ' test@example.com ' });
expect(found).toBeDefined();
expect(found?.email).toBe('test@example.com');
});
test('should handle mixed case with international characters', async () => {
await User.create({
name: 'Test User',
email: 'user@example.com',
provider: 'local',
});
const found = await methods.findUser({ email: 'USER@EXAMPLE.COM' });
expect(found).toBeDefined();
});
test('should handle email normalization in complex $or queries', async () => {
const user1 = await User.create({
name: 'User One',
email: 'user1@example.com',
provider: 'openid',
openidId: 'openid-1',
});
await User.create({
name: 'User Two',
email: 'user2@example.com',
provider: 'openid',
openidId: 'openid-2',
});
/** Search with mixed case email in $or */
const found = await methods.findUser({
$or: [{ openidId: 'nonexistent' }, { email: 'USER1@EXAMPLE.COM' }],
});
expect(found).toBeDefined();
expect(found?._id?.toString()).toBe(user1._id?.toString());
});
test('should not normalize non-string email values', async () => {
await User.create({
name: 'Test User',
email: 'test@example.com',
provider: 'local',
});
/** Using regex for email (should not be normalized) */
const found = await methods.findUser({ email: /test@example\.com/i });
expect(found).toBeDefined();
expect(found?.email).toBe('test@example.com');
});
test('should handle OpenID provider migration scenario', async () => {
/** Simulate user stored with lowercase email */
await User.create({
name: 'John Doe',
email: 'john.doe@company.com',
provider: 'openid',
openidId: 'old-provider-id',
});
/**
* New OpenID provider returns email with different casing
* This simulates the exact bug reported in the GitHub issue
*/
const emailFromNewProvider = 'John.Doe@Company.COM';
const found = await methods.findUser({ email: emailFromNewProvider });
expect(found).toBeDefined();
expect(found?.email).toBe('john.doe@company.com');
expect(found?.name).toBe('John Doe');
});
test('should handle SAML provider email normalization', async () => {
await User.create({
name: 'SAML User',
email: 'saml.user@enterprise.com',
provider: 'saml',
samlId: 'saml-123',
});
/** SAML providers sometimes return emails in different formats */
const found = await methods.findUser({ email: ' SAML.USER@ENTERPRISE.COM ' });
expect(found).toBeDefined();
expect(found?.provider).toBe('saml');
});
});
describe('findUsers with options', () => {
beforeEach(async () => {
await User.create([
{ name: 'Alice', email: 'alice@example.com', provider: 'local' },
{ name: 'Bob', email: 'bob@example.com', provider: 'local' },
{ name: 'Charlie', email: 'charlie@example.com', provider: 'local' },
{ name: 'Diana', email: 'diana@example.com', provider: 'local' },
{ name: 'Eve', email: 'eve@example.com', provider: 'local' },
]);
});
test('limit restricts the number of returned documents', async () => {
const users = await methods.findUsers({}, null, { limit: 2 });
expect(users).toHaveLength(2);
});
test('offset skips the first N documents', async () => {
const all = await methods.findUsers({}, 'name', { sort: { name: 1 } });
const skipped = await methods.findUsers({}, 'name', { offset: 2, sort: { name: 1 } });
expect(skipped).toHaveLength(3);
expect(skipped[0].name).toBe(all[2].name);
});
test('sort orders results by the specified field', async () => {
const asc = await methods.findUsers({}, 'name', { sort: { name: 1 } });
const desc = await methods.findUsers({}, 'name', { sort: { name: -1 } });
expect(asc[0].name).toBe('Alice');
expect(asc[4].name).toBe('Eve');
expect(desc[0].name).toBe('Eve');
expect(desc[4].name).toBe('Alice');
});
test('limit + offset returns the correct page', async () => {
const sorted = await methods.findUsers({}, 'name', { sort: { name: 1 } });
const page2 = await methods.findUsers({}, 'name', {
limit: 2,
offset: 2,
sort: { name: 1 },
});
expect(page2).toHaveLength(2);
expect(page2[0].name).toBe(sorted[2].name);
expect(page2[1].name).toBe(sorted[3].name);
});
test('limit of 0 does not restrict results', async () => {
const users = await methods.findUsers({}, null, { limit: 0 });
expect(users).toHaveLength(5);
});
test('returns all documents when no options provided', async () => {
const users = await methods.findUsers({});
expect(users).toHaveLength(5);
});
});
});