| | import mongoose from 'mongoose'; |
| | import { MongoMemoryServer } from 'mongodb-memory-server'; |
| | import type * as t from '~/types'; |
| | import { createUserMethods } from './user'; |
| | import userSchema from '~/schema/user'; |
| | import balanceSchema from '~/schema/balance'; |
| |
|
| | |
| | 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>; |
| |
|
| | beforeAll(async () => { |
| | mongoServer = await MongoMemoryServer.create(); |
| | const mongoUri = mongoServer.getUri(); |
| | await mongoose.connect(mongoUri); |
| |
|
| | |
| | User = mongoose.models.User || mongoose.model<t.IUser>('User', userSchema); |
| | Balance = mongoose.models.Balance || mongoose.model<t.IBalance>('Balance', balanceSchema); |
| |
|
| | |
| | methods = createUserMethods(mongoose); |
| | }); |
| |
|
| | afterAll(async () => { |
| | await mongoose.disconnect(); |
| | await mongoServer.stop(); |
| | }); |
| |
|
| | beforeEach(async () => { |
| | await mongoose.connection.dropDatabase(); |
| | }); |
| |
|
| | 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', |
| | provider: 'local', |
| | }); |
| |
|
| | |
| | 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); |
| |
|
| | |
| | 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 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('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); |
| | }); |
| |
|
| | 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 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' }); |
| |
|
| | |
| | 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', |
| | }); |
| |
|
| | |
| | const updated = await methods.toggleUserMemories(user._id?.toString() || '', true); |
| |
|
| | expect(updated?.personalization).toBeDefined(); |
| | expect(updated?.personalization?.memories).toBe(true); |
| |
|
| | |
| | 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(); |
| | }); |
| | }); |
| |
|
| | 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', |
| | }); |
| |
|
| | |
| | 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', |
| | }); |
| |
|
| | |
| | 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 () => { |
| | |
| | await User.create({ |
| | name: 'John Doe', |
| | email: 'john.doe@company.com', |
| | provider: 'openid', |
| | openidId: 'old-provider-id', |
| | }); |
| |
|
| | |
| | |
| | |
| | |
| | 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', |
| | }); |
| |
|
| | |
| | const found = await methods.findUser({ email: ' SAML.USER@ENTERPRISE.COM ' }); |
| |
|
| | expect(found).toBeDefined(); |
| | expect(found?.provider).toBe('saml'); |
| | }); |
| | }); |
| | }); |
| |
|