refactor: email repository (#17746)

This commit is contained in:
Jason Rasmussen 2025-04-21 12:53:37 -04:00 committed by GitHub
parent 488dc4efbd
commit 56a4aa9ffe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 74 additions and 78 deletions

View file

@ -3,7 +3,7 @@ import { defaults, SystemConfig } from 'src/config';
import { AlbumUser } from 'src/database';
import { SystemConfigDto } from 'src/dtos/system-config.dto';
import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum';
import { EmailTemplate } from 'src/repositories/notification.repository';
import { EmailTemplate } from 'src/repositories/email.repository';
import { NotificationService } from 'src/services/notification.service';
import { INotifyAlbumUpdateJob } from 'src/types';
import { albumStub } from 'test/fixtures/album.stub';
@ -74,18 +74,18 @@ describe(NotificationService.name, () => {
const oldConfig = configs.smtpDisabled;
const newConfig = configs.smtpEnabled;
mocks.notification.verifySmtp.mockResolvedValue(true);
mocks.email.verifySmtp.mockResolvedValue(true);
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
expect(mocks.notification.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport);
expect(mocks.email.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport);
});
it('validates smtp config when transport changes', async () => {
const oldConfig = configs.smtpEnabled;
const newConfig = configs.smtpTransport;
mocks.notification.verifySmtp.mockResolvedValue(true);
mocks.email.verifySmtp.mockResolvedValue(true);
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
expect(mocks.notification.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport);
expect(mocks.email.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport);
});
it('skips smtp validation when there are no changes', async () => {
@ -93,7 +93,7 @@ describe(NotificationService.name, () => {
const newConfig = { ...configs.smtpEnabled };
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
expect(mocks.notification.verifySmtp).not.toHaveBeenCalled();
expect(mocks.email.verifySmtp).not.toHaveBeenCalled();
});
it('skips smtp validation with DTO when there are no changes', async () => {
@ -101,7 +101,7 @@ describe(NotificationService.name, () => {
const newConfig = plainToInstance(SystemConfigDto, configs.smtpEnabled);
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
expect(mocks.notification.verifySmtp).not.toHaveBeenCalled();
expect(mocks.email.verifySmtp).not.toHaveBeenCalled();
});
it('skips smtp validation when smtp is disabled', async () => {
@ -109,14 +109,14 @@ describe(NotificationService.name, () => {
const newConfig = { ...configs.smtpDisabled };
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
expect(mocks.notification.verifySmtp).not.toHaveBeenCalled();
expect(mocks.email.verifySmtp).not.toHaveBeenCalled();
});
it('should fail if smtp configuration is invalid', async () => {
const oldConfig = configs.smtpDisabled;
const newConfig = configs.smtpEnabled;
mocks.notification.verifySmtp.mockRejectedValue(new Error('Failed validating smtp'));
mocks.email.verifySmtp.mockRejectedValue(new Error('Failed validating smtp'));
await expect(sut.onConfigValidate({ oldConfig, newConfig })).rejects.toBeInstanceOf(Error);
});
});
@ -248,7 +248,7 @@ describe(NotificationService.name, () => {
it('should throw error if smtp validation fails', async () => {
mocks.user.get.mockResolvedValue(userStub.admin);
mocks.notification.verifySmtp.mockRejectedValue('');
mocks.email.verifySmtp.mockRejectedValue('');
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow(
'Failed to verify SMTP configuration',
@ -257,16 +257,16 @@ describe(NotificationService.name, () => {
it('should send email to default domain', async () => {
mocks.user.get.mockResolvedValue(userStub.admin);
mocks.notification.verifySmtp.mockResolvedValue(true);
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
mocks.email.verifySmtp.mockResolvedValue(true);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow();
expect(mocks.notification.renderEmail).toHaveBeenCalledWith({
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
template: EmailTemplate.TEST_EMAIL,
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
});
expect(mocks.notification.sendEmail).toHaveBeenCalledWith(
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
expect.objectContaining({
subject: 'Test email from Immich',
smtp: configs.smtpTransport.notifications.smtp.transport,
@ -276,17 +276,17 @@ describe(NotificationService.name, () => {
it('should send email to external domain', async () => {
mocks.user.get.mockResolvedValue(userStub.admin);
mocks.notification.verifySmtp.mockResolvedValue(true);
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.email.verifySmtp.mockResolvedValue(true);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } });
mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow();
expect(mocks.notification.renderEmail).toHaveBeenCalledWith({
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
template: EmailTemplate.TEST_EMAIL,
data: { baseUrl: 'https://demo.immich.app', displayName: userStub.admin.name },
});
expect(mocks.notification.sendEmail).toHaveBeenCalledWith(
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
expect.objectContaining({
subject: 'Test email from Immich',
smtp: configs.smtpTransport.notifications.smtp.transport,
@ -296,18 +296,18 @@ describe(NotificationService.name, () => {
it('should send email with replyTo', async () => {
mocks.user.get.mockResolvedValue(userStub.admin);
mocks.notification.verifySmtp.mockResolvedValue(true);
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
mocks.email.verifySmtp.mockResolvedValue(true);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
await expect(
sut.sendTestEmail('', { ...configs.smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }),
).resolves.not.toThrow();
expect(mocks.notification.renderEmail).toHaveBeenCalledWith({
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
template: EmailTemplate.TEST_EMAIL,
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
});
expect(mocks.notification.sendEmail).toHaveBeenCalledWith(
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
expect.objectContaining({
subject: 'Test email from Immich',
smtp: configs.smtpTransport.notifications.smtp.transport,
@ -325,7 +325,7 @@ describe(NotificationService.name, () => {
it('should be successful', async () => {
mocks.user.get.mockResolvedValue(userStub.admin);
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
await expect(sut.handleUserSignup({ id: '' })).resolves.toBe(JobStatus.SUCCESS);
expect(mocks.job.queue).toHaveBeenCalledWith({
@ -390,7 +390,7 @@ describe(NotificationService.name, () => {
],
});
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS);
expect(mocks.job.queue).toHaveBeenCalledWith({
@ -411,7 +411,7 @@ describe(NotificationService.name, () => {
],
});
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS);
@ -440,7 +440,7 @@ describe(NotificationService.name, () => {
],
});
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([
{ id: '1', type: AssetFileType.THUMBNAIL, path: 'path-to-thumb.jpg' },
]);
@ -471,7 +471,7 @@ describe(NotificationService.name, () => {
],
});
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetStub.image.files[2]]);
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS);
@ -508,12 +508,12 @@ describe(NotificationService.name, () => {
albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser],
});
mocks.user.get.mockResolvedValueOnce(userStub.user1);
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] });
expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
expect(mocks.notification.renderEmail).not.toHaveBeenCalled();
expect(mocks.email.renderEmail).not.toHaveBeenCalled();
});
it('should skip recipient with disabled email notifications', async () => {
@ -530,12 +530,12 @@ describe(NotificationService.name, () => {
},
],
});
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] });
expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
expect(mocks.notification.renderEmail).not.toHaveBeenCalled();
expect(mocks.email.renderEmail).not.toHaveBeenCalled();
});
it('should skip recipient with disabled email notifications for the album update event', async () => {
@ -552,12 +552,12 @@ describe(NotificationService.name, () => {
},
],
});
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] });
expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
expect(mocks.notification.renderEmail).not.toHaveBeenCalled();
expect(mocks.email.renderEmail).not.toHaveBeenCalled();
});
it('should send email', async () => {
@ -566,12 +566,12 @@ describe(NotificationService.name, () => {
albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser],
});
mocks.user.get.mockResolvedValue(userStub.user1);
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] });
expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
expect(mocks.notification.renderEmail).toHaveBeenCalled();
expect(mocks.email.renderEmail).toHaveBeenCalled();
expect(mocks.job.queue).toHaveBeenCalled();
});
@ -599,24 +599,20 @@ describe(NotificationService.name, () => {
mocks.systemMetadata.get.mockResolvedValue({
notifications: { smtp: { enabled: true, from: 'test@immich.app' } },
});
mocks.notification.sendEmail.mockResolvedValue({ messageId: '', response: '' });
mocks.email.sendEmail.mockResolvedValue({ messageId: '', response: '' });
await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SUCCESS);
expect(mocks.notification.sendEmail).toHaveBeenCalledWith(
expect.objectContaining({ replyTo: 'test@immich.app' }),
);
expect(mocks.email.sendEmail).toHaveBeenCalledWith(expect.objectContaining({ replyTo: 'test@immich.app' }));
});
it('should send mail with replyTo successfully', async () => {
mocks.systemMetadata.get.mockResolvedValue({
notifications: { smtp: { enabled: true, from: 'test@immich.app', replyTo: 'demo@immich.app' } },
});
mocks.notification.sendEmail.mockResolvedValue({ messageId: '', response: '' });
mocks.email.sendEmail.mockResolvedValue({ messageId: '', response: '' });
await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SUCCESS);
expect(mocks.notification.sendEmail).toHaveBeenCalledWith(
expect.objectContaining({ replyTo: 'demo@immich.app' }),
);
expect(mocks.email.sendEmail).toHaveBeenCalledWith(expect.objectContaining({ replyTo: 'demo@immich.app' }));
});
});
});