mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00

* refactor: move model definitions and database-related methods to packages/data-schemas * ci: update tests due to new DB structure fix: disable mocking `librechat-data-provider` feat: Add schema exports to data-schemas package - Introduced a new schema module that exports various schemas including action, agent, and user schemas. - Updated index.ts to include the new schema exports for better modularity and organization. ci: fix appleStrategy tests fix: Agent.spec.js ci: refactor handleTools tests to use MongoMemoryServer for in-memory database fix: getLogStores imports ci: update banViolation tests to use MongoMemoryServer and improve session mocking test: refactor samlStrategy tests to improve mock configurations and user handling ci: fix crypto mock in handleText tests for improved accuracy ci: refactor spendTokens tests to improve model imports and setup ci: refactor Message model tests to use MongoMemoryServer and improve database interactions * refactor: streamline IMessage interface and move feedback properties to types/message.ts * refactor: use exported initializeRoles from `data-schemas`, remove api workspace version (this serves as an example of future migrations that still need to happen) * refactor: update model imports to use destructuring from `~/db/models` for consistency and clarity * refactor: remove unused mongoose imports from model files for cleaner code * refactor: remove unused mongoose imports from Share, Prompt, and Transaction model files for cleaner code * refactor: remove unused import in Transaction model for cleaner code * ci: update deploy workflow to reference new Docker Dev Branch Images Build and add new workflow for building Docker images on dev branch * chore: cleanup imports
378 lines
12 KiB
JavaScript
378 lines
12 KiB
JavaScript
const jwt = require('jsonwebtoken');
|
|
const mongoose = require('mongoose');
|
|
const { logger } = require('@librechat/data-schemas');
|
|
const { Strategy: AppleStrategy } = require('passport-apple');
|
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
|
const { createSocialUser, handleExistingUser } = require('./process');
|
|
const { isEnabled } = require('~/server/utils');
|
|
const socialLogin = require('./socialLogin');
|
|
const { findUser } = require('~/models');
|
|
const { User } = require('~/db/models');
|
|
|
|
jest.mock('jsonwebtoken');
|
|
jest.mock('@librechat/data-schemas', () => {
|
|
const actualModule = jest.requireActual('@librechat/data-schemas');
|
|
return {
|
|
...actualModule,
|
|
logger: {
|
|
error: jest.fn(),
|
|
debug: jest.fn(),
|
|
},
|
|
};
|
|
});
|
|
jest.mock('./process', () => ({
|
|
createSocialUser: jest.fn(),
|
|
handleExistingUser: jest.fn(),
|
|
}));
|
|
jest.mock('~/server/utils', () => ({
|
|
isEnabled: jest.fn(),
|
|
}));
|
|
jest.mock('~/models', () => ({
|
|
findUser: jest.fn(),
|
|
}));
|
|
|
|
describe('Apple Login Strategy', () => {
|
|
let mongoServer;
|
|
let appleStrategyInstance;
|
|
const OLD_ENV = process.env;
|
|
let getProfileDetails;
|
|
|
|
// Start and stop in-memory MongoDB
|
|
beforeAll(async () => {
|
|
mongoServer = await MongoMemoryServer.create();
|
|
const mongoUri = mongoServer.getUri();
|
|
await mongoose.connect(mongoUri);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await mongoose.disconnect();
|
|
await mongoServer.stop();
|
|
process.env = OLD_ENV;
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Reset environment variables
|
|
process.env = { ...OLD_ENV };
|
|
process.env.APPLE_CLIENT_ID = 'fake_client_id';
|
|
process.env.APPLE_TEAM_ID = 'fake_team_id';
|
|
process.env.APPLE_CALLBACK_URL = '/auth/apple/callback';
|
|
process.env.DOMAIN_SERVER = 'https://example.com';
|
|
process.env.APPLE_KEY_ID = 'fake_key_id';
|
|
process.env.APPLE_PRIVATE_KEY_PATH = '/path/to/fake/private/key';
|
|
process.env.ALLOW_SOCIAL_REGISTRATION = 'true';
|
|
|
|
// Clear mocks and database
|
|
jest.clearAllMocks();
|
|
await User.deleteMany({});
|
|
|
|
// Define getProfileDetails within the test scope
|
|
getProfileDetails = ({ idToken, profile }) => {
|
|
if (!idToken) {
|
|
logger.error('idToken is missing');
|
|
throw new Error('idToken is missing');
|
|
}
|
|
|
|
const decoded = jwt.decode(idToken);
|
|
if (!decoded) {
|
|
logger.error('Failed to decode idToken');
|
|
throw new Error('idToken is invalid');
|
|
}
|
|
|
|
console.log('Decoded token:', decoded);
|
|
|
|
logger.debug(`Decoded Apple JWT: ${JSON.stringify(decoded, null, 2)}`);
|
|
|
|
return {
|
|
email: decoded.email,
|
|
id: decoded.sub,
|
|
avatarUrl: null, // Apple does not provide an avatar URL
|
|
username: decoded.email ? decoded.email.split('@')[0].toLowerCase() : `user_${decoded.sub}`,
|
|
name: decoded.name
|
|
? `${decoded.name.firstName} ${decoded.name.lastName}`
|
|
: profile.displayName || null,
|
|
emailVerified: true, // Apple verifies the email
|
|
};
|
|
};
|
|
|
|
// Mock isEnabled based on environment variable
|
|
isEnabled.mockImplementation((flag) => {
|
|
if (flag === 'true') {
|
|
return true;
|
|
}
|
|
if (flag === 'false') {
|
|
return false;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// Initialize the strategy with the mocked getProfileDetails
|
|
const appleLogin = socialLogin('apple', getProfileDetails);
|
|
appleStrategyInstance = new AppleStrategy(
|
|
{
|
|
clientID: process.env.APPLE_CLIENT_ID,
|
|
teamID: process.env.APPLE_TEAM_ID,
|
|
callbackURL: `${process.env.DOMAIN_SERVER}${process.env.APPLE_CALLBACK_URL}`,
|
|
keyID: process.env.APPLE_KEY_ID,
|
|
privateKeyLocation: process.env.APPLE_PRIVATE_KEY_PATH,
|
|
passReqToCallback: false,
|
|
},
|
|
appleLogin,
|
|
);
|
|
});
|
|
|
|
const mockProfile = {
|
|
displayName: 'John Doe',
|
|
};
|
|
|
|
describe('getProfileDetails', () => {
|
|
it('should throw an error if idToken is missing', () => {
|
|
expect(() => {
|
|
getProfileDetails({ idToken: null, profile: mockProfile });
|
|
}).toThrow('idToken is missing');
|
|
expect(logger.error).toHaveBeenCalledWith('idToken is missing');
|
|
});
|
|
|
|
it('should throw an error if idToken cannot be decoded', () => {
|
|
jwt.decode.mockReturnValue(null);
|
|
expect(() => {
|
|
getProfileDetails({ idToken: 'invalid_id_token', profile: mockProfile });
|
|
}).toThrow('idToken is invalid');
|
|
expect(logger.error).toHaveBeenCalledWith('Failed to decode idToken');
|
|
});
|
|
|
|
it('should extract user details correctly from idToken', () => {
|
|
const fakeDecodedToken = {
|
|
email: 'john.doe@example.com',
|
|
sub: 'apple-sub-1234',
|
|
name: {
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
},
|
|
};
|
|
|
|
jwt.decode.mockReturnValue(fakeDecodedToken);
|
|
|
|
const profileDetails = getProfileDetails({
|
|
idToken: 'fake_id_token',
|
|
profile: mockProfile,
|
|
});
|
|
|
|
expect(jwt.decode).toHaveBeenCalledWith('fake_id_token');
|
|
expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('Decoded Apple JWT'));
|
|
expect(profileDetails).toEqual({
|
|
email: 'john.doe@example.com',
|
|
id: 'apple-sub-1234',
|
|
avatarUrl: null,
|
|
username: 'john.doe',
|
|
name: 'John Doe',
|
|
emailVerified: true,
|
|
});
|
|
});
|
|
|
|
it('should handle missing email and use sub for username', () => {
|
|
const fakeDecodedToken = {
|
|
sub: 'apple-sub-5678',
|
|
};
|
|
|
|
jwt.decode.mockReturnValue(fakeDecodedToken);
|
|
|
|
const profileDetails = getProfileDetails({
|
|
idToken: 'fake_id_token',
|
|
profile: mockProfile,
|
|
});
|
|
|
|
expect(profileDetails).toEqual({
|
|
email: undefined,
|
|
id: 'apple-sub-5678',
|
|
avatarUrl: null,
|
|
username: 'user_apple-sub-5678',
|
|
name: 'John Doe',
|
|
emailVerified: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Strategy verify callback', () => {
|
|
const tokenset = {
|
|
id_token: 'fake_id_token',
|
|
};
|
|
|
|
const decodedToken = {
|
|
email: 'jane.doe@example.com',
|
|
sub: 'apple-sub-9012',
|
|
name: {
|
|
firstName: 'Jane',
|
|
lastName: 'Doe',
|
|
},
|
|
};
|
|
|
|
const fakeAccessToken = 'fake_access_token';
|
|
const fakeRefreshToken = 'fake_refresh_token';
|
|
|
|
beforeEach(() => {
|
|
jwt.decode.mockReturnValue(decodedToken);
|
|
findUser.mockResolvedValue(null);
|
|
});
|
|
|
|
it('should create a new user if one does not exist and registration is allowed', async () => {
|
|
// Mock findUser to return null (user does not exist)
|
|
findUser.mockResolvedValue(null);
|
|
|
|
// Mock createSocialUser to create a user
|
|
createSocialUser.mockImplementation(async (userData) => {
|
|
const user = new User(userData);
|
|
await user.save();
|
|
return user;
|
|
});
|
|
|
|
const mockVerifyCallback = jest.fn();
|
|
|
|
// Invoke the verify callback with correct arguments
|
|
await new Promise((resolve) => {
|
|
appleStrategyInstance._verify(
|
|
fakeAccessToken,
|
|
fakeRefreshToken,
|
|
tokenset.id_token,
|
|
mockProfile,
|
|
(err, user) => {
|
|
mockVerifyCallback(err, user);
|
|
resolve();
|
|
},
|
|
);
|
|
});
|
|
|
|
expect(mockVerifyCallback).toHaveBeenCalledWith(null, expect.any(User));
|
|
const user = mockVerifyCallback.mock.calls[0][1];
|
|
expect(user.email).toBe('jane.doe@example.com');
|
|
expect(user.username).toBe('jane.doe');
|
|
expect(user.name).toBe('Jane Doe');
|
|
expect(user.provider).toBe('apple');
|
|
});
|
|
|
|
it('should handle existing user and update avatarUrl', async () => {
|
|
// Create an existing user without saving to database
|
|
const existingUser = new User({
|
|
email: 'jane.doe@example.com',
|
|
username: 'jane.doe',
|
|
name: 'Jane Doe',
|
|
provider: 'apple',
|
|
providerId: 'apple-sub-9012',
|
|
avatarUrl: 'old_avatar.png',
|
|
});
|
|
|
|
// Mock findUser to return the existing user
|
|
findUser.mockResolvedValue(existingUser);
|
|
|
|
// Mock handleExistingUser to update avatarUrl without saving to database
|
|
handleExistingUser.mockImplementation(async (user, avatarUrl) => {
|
|
user.avatarUrl = avatarUrl;
|
|
// Don't call save() to avoid database operations
|
|
return user;
|
|
});
|
|
|
|
const mockVerifyCallback = jest.fn();
|
|
|
|
// Invoke the verify callback with correct arguments
|
|
await new Promise((resolve) => {
|
|
appleStrategyInstance._verify(
|
|
fakeAccessToken,
|
|
fakeRefreshToken,
|
|
tokenset.id_token,
|
|
mockProfile,
|
|
(err, user) => {
|
|
mockVerifyCallback(err, user);
|
|
resolve();
|
|
},
|
|
);
|
|
});
|
|
|
|
expect(mockVerifyCallback).toHaveBeenCalledWith(null, existingUser);
|
|
expect(existingUser.avatarUrl).toBeNull(); // As per getProfileDetails
|
|
expect(handleExistingUser).toHaveBeenCalledWith(existingUser, null);
|
|
});
|
|
|
|
it('should handle missing idToken gracefully', async () => {
|
|
const mockVerifyCallback = jest.fn();
|
|
|
|
// Invoke the verify callback with missing id_token
|
|
await new Promise((resolve) => {
|
|
appleStrategyInstance._verify(
|
|
fakeAccessToken,
|
|
fakeRefreshToken,
|
|
null, // idToken is missing
|
|
mockProfile,
|
|
(err, user) => {
|
|
mockVerifyCallback(err, user);
|
|
resolve();
|
|
},
|
|
);
|
|
});
|
|
|
|
expect(mockVerifyCallback).toHaveBeenCalledWith(expect.any(Error), undefined);
|
|
expect(mockVerifyCallback.mock.calls[0][0].message).toBe('idToken is missing');
|
|
// Ensure createSocialUser and handleExistingUser were not called
|
|
expect(createSocialUser).not.toHaveBeenCalled();
|
|
expect(handleExistingUser).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle decoding errors gracefully', async () => {
|
|
// Simulate decoding failure by returning null
|
|
jwt.decode.mockReturnValue(null);
|
|
|
|
const mockVerifyCallback = jest.fn();
|
|
|
|
// Invoke the verify callback with correct arguments
|
|
await new Promise((resolve) => {
|
|
appleStrategyInstance._verify(
|
|
fakeAccessToken,
|
|
fakeRefreshToken,
|
|
tokenset.id_token,
|
|
mockProfile,
|
|
(err, user) => {
|
|
mockVerifyCallback(err, user);
|
|
resolve();
|
|
},
|
|
);
|
|
});
|
|
|
|
expect(mockVerifyCallback).toHaveBeenCalledWith(expect.any(Error), undefined);
|
|
expect(mockVerifyCallback.mock.calls[0][0].message).toBe('idToken is invalid');
|
|
// Ensure createSocialUser and handleExistingUser were not called
|
|
expect(createSocialUser).not.toHaveBeenCalled();
|
|
expect(handleExistingUser).not.toHaveBeenCalled();
|
|
// Ensure logger.error was called
|
|
expect(logger.error).toHaveBeenCalledWith('Failed to decode idToken');
|
|
});
|
|
|
|
it('should handle errors during user creation', async () => {
|
|
// Mock findUser to return null (user does not exist)
|
|
findUser.mockResolvedValue(null);
|
|
|
|
// Mock createSocialUser to throw an error
|
|
createSocialUser.mockImplementation(() => {
|
|
throw new Error('Database error');
|
|
});
|
|
|
|
const mockVerifyCallback = jest.fn();
|
|
|
|
// Invoke the verify callback with correct arguments
|
|
await new Promise((resolve) => {
|
|
appleStrategyInstance._verify(
|
|
fakeAccessToken,
|
|
fakeRefreshToken,
|
|
tokenset.id_token,
|
|
mockProfile,
|
|
(err, user) => {
|
|
mockVerifyCallback(err, user);
|
|
resolve();
|
|
},
|
|
);
|
|
});
|
|
|
|
expect(mockVerifyCallback).toHaveBeenCalledWith(expect.any(Error), undefined);
|
|
expect(mockVerifyCallback.mock.calls[0][0].message).toBe('Database error');
|
|
// Ensure logger.error was called
|
|
expect(logger.error).toHaveBeenCalledWith('[appleLogin]', expect.any(Error));
|
|
});
|
|
});
|
|
});
|