📧 feat: Mailgun API Email Configuration (#7742)

* fix: add undefined password check in local user authentication

* fix: edge case - issue deleting user when no conversations in deleteUserController

* feat: Integrate Mailgun API for email sending functionality

* fix: undefined SESSION_EXPIRY handling and add tests

* fix: update import path for isEnabled utility in azureUtils.js to resolve circular dep.
This commit is contained in:
Danny Avila 2025-06-04 13:12:37 -04:00 committed by GitHub
parent 6bb78247b3
commit be4cf5846c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 311 additions and 29 deletions

View file

@ -0,0 +1,163 @@
import mongoose from 'mongoose';
import { createUserMethods } from './user';
import { signPayload } from '~/crypto';
import type { IUser } from '~/types';
jest.mock('~/crypto', () => ({
signPayload: jest.fn(),
}));
describe('User Methods', () => {
const mockSignPayload = signPayload as jest.MockedFunction<typeof signPayload>;
let userMethods: ReturnType<typeof createUserMethods>;
beforeEach(() => {
jest.clearAllMocks();
userMethods = createUserMethods(mongoose);
});
describe('generateToken', () => {
const mockUser = {
_id: 'user123',
username: 'testuser',
provider: 'local',
email: 'test@example.com',
name: 'Test User',
avatar: '',
role: 'user',
emailVerified: false,
createdAt: new Date(),
updatedAt: new Date(),
} as IUser;
afterEach(() => {
delete process.env.SESSION_EXPIRY;
delete process.env.JWT_SECRET;
});
it('should default to 15 minutes when SESSION_EXPIRY is not set', async () => {
process.env.JWT_SECRET = 'test-secret';
mockSignPayload.mockResolvedValue('mocked-token');
await userMethods.generateToken(mockUser);
expect(mockSignPayload).toHaveBeenCalledWith({
payload: {
id: mockUser._id,
username: mockUser.username,
provider: mockUser.provider,
email: mockUser.email,
},
secret: 'test-secret',
expirationTime: 900, // 15 minutes in seconds
});
});
it('should default to 15 minutes when SESSION_EXPIRY is empty string', async () => {
process.env.SESSION_EXPIRY = '';
process.env.JWT_SECRET = 'test-secret';
mockSignPayload.mockResolvedValue('mocked-token');
await userMethods.generateToken(mockUser);
expect(mockSignPayload).toHaveBeenCalledWith({
payload: {
id: mockUser._id,
username: mockUser.username,
provider: mockUser.provider,
email: mockUser.email,
},
secret: 'test-secret',
expirationTime: 900, // 15 minutes in seconds
});
});
it('should use custom expiry when SESSION_EXPIRY is set to a valid expression', async () => {
process.env.SESSION_EXPIRY = '1000 * 60 * 30'; // 30 minutes
process.env.JWT_SECRET = 'test-secret';
mockSignPayload.mockResolvedValue('mocked-token');
await userMethods.generateToken(mockUser);
expect(mockSignPayload).toHaveBeenCalledWith({
payload: {
id: mockUser._id,
username: mockUser.username,
provider: mockUser.provider,
email: mockUser.email,
},
secret: 'test-secret',
expirationTime: 1800, // 30 minutes in seconds
});
});
it('should default to 15 minutes when SESSION_EXPIRY evaluates to falsy value', async () => {
process.env.SESSION_EXPIRY = '0'; // This will evaluate to 0, which is falsy
process.env.JWT_SECRET = 'test-secret';
mockSignPayload.mockResolvedValue('mocked-token');
await userMethods.generateToken(mockUser);
expect(mockSignPayload).toHaveBeenCalledWith({
payload: {
id: mockUser._id,
username: mockUser.username,
provider: mockUser.provider,
email: mockUser.email,
},
secret: 'test-secret',
expirationTime: 900, // 15 minutes in seconds
});
});
it('should throw error when no user is provided', async () => {
process.env.JWT_SECRET = 'test-secret';
await expect(userMethods.generateToken(null as unknown as IUser)).rejects.toThrow(
'No user provided',
);
});
it('should return the token from signPayload', async () => {
process.env.SESSION_EXPIRY = '1000 * 60 * 60'; // 1 hour
process.env.JWT_SECRET = 'test-secret';
const expectedToken = 'generated-jwt-token';
mockSignPayload.mockResolvedValue(expectedToken);
const token = await userMethods.generateToken(mockUser);
expect(token).toBe(expectedToken);
});
it('should handle invalid SESSION_EXPIRY expressions gracefully', async () => {
process.env.SESSION_EXPIRY = 'invalid expression';
process.env.JWT_SECRET = 'test-secret';
mockSignPayload.mockResolvedValue('mocked-token');
// Mock console.warn to verify it's called
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
await userMethods.generateToken(mockUser);
// Should use default value when eval fails
expect(mockSignPayload).toHaveBeenCalledWith({
payload: {
id: mockUser._id,
username: mockUser.username,
provider: mockUser.provider,
email: mockUser.email,
},
secret: 'test-secret',
expirationTime: 900, // 15 minutes in seconds (default)
});
// Verify warning was logged
expect(consoleWarnSpy).toHaveBeenCalledWith(
'Invalid SESSION_EXPIRY expression, using default:',
expect.any(SyntaxError),
);
consoleWarnSpy.mockRestore();
});
});
});

View file

@ -145,7 +145,18 @@ export function createUserMethods(mongoose: typeof import('mongoose')) {
throw new Error('No user provided');
}
const expires = eval(process.env.SESSION_EXPIRY ?? '0') ?? 1000 * 60 * 15;
let expires = 1000 * 60 * 15;
if (process.env.SESSION_EXPIRY !== undefined && process.env.SESSION_EXPIRY !== '') {
try {
const evaluated = eval(process.env.SESSION_EXPIRY);
if (evaluated) {
expires = evaluated;
}
} catch (error) {
console.warn('Invalid SESSION_EXPIRY expression, using default:', error);
}
}
return await signPayload({
payload: {