mails
This commit is contained in:
161
listify-api/src/tasks/task-digest.service.spec.ts
Normal file
161
listify-api/src/tasks/task-digest.service.spec.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { UserEntity } from '../auth/user.entity';
|
||||
import { MailService } from '../mail/mail.service';
|
||||
import { InMemoryRepository } from '../testing/in-memory-repository';
|
||||
import { TaskDigestService } from './task-digest.service';
|
||||
import { UserTaskEntity } from './user-task.entity';
|
||||
|
||||
describe('TaskDigestService', () => {
|
||||
let usersRepository: InMemoryRepository<UserEntity>;
|
||||
let tasksRepository: InMemoryRepository<UserTaskEntity>;
|
||||
let mailService: Pick<MailService, 'sendTaskDigestEmail'>;
|
||||
let service: TaskDigestService;
|
||||
|
||||
beforeEach(() => {
|
||||
usersRepository = new InMemoryRepository<UserEntity>();
|
||||
tasksRepository = new InMemoryRepository<UserTaskEntity>();
|
||||
mailService = {
|
||||
sendTaskDigestEmail: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
service = new TaskDigestService(
|
||||
usersRepository as never,
|
||||
tasksRepository as never,
|
||||
new ConfigService({
|
||||
CLIENT_URL: 'http://client.test',
|
||||
TASK_DIGEST_TIMEZONE: 'Europe/Budapest',
|
||||
}),
|
||||
mailService as MailService,
|
||||
);
|
||||
});
|
||||
|
||||
it('sends morning digest with today and overdue open tasks', async () => {
|
||||
await saveUser({ taskDigestPreference: 'both' });
|
||||
await saveTask({ id: 'today-task', dueDate: '2026-06-29' });
|
||||
await saveTask({ id: 'overdue-task', title: 'Alt', dueDate: '2026-06-28' });
|
||||
await saveTask({
|
||||
id: 'future-task',
|
||||
title: 'Zukunft',
|
||||
dueDate: '2026-06-30',
|
||||
});
|
||||
await saveTask({
|
||||
id: 'done-task',
|
||||
title: 'Fertig',
|
||||
dueDate: '2026-06-29',
|
||||
completed: true,
|
||||
});
|
||||
|
||||
await service.processDueDigests(new Date('2026-06-29T07:00:00.000Z'));
|
||||
|
||||
expect(mailService.sendTaskDigestEmail).toHaveBeenCalledWith(
|
||||
'owner@example.com',
|
||||
{
|
||||
displayName: 'Owner',
|
||||
slot: 'morning',
|
||||
date: '2026-06-29',
|
||||
tasksUrl: 'http://client.test/tasks',
|
||||
todayTasks: [
|
||||
{
|
||||
title: 'Heute',
|
||||
notes: undefined,
|
||||
dueDate: '2026-06-29',
|
||||
},
|
||||
],
|
||||
overdueTasks: [
|
||||
{
|
||||
title: 'Alt',
|
||||
notes: undefined,
|
||||
dueDate: '2026-06-28',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
expect(
|
||||
(await usersRepository.findOne({ where: { id: 'owner-1' } }))
|
||||
?.taskDigestMorningProcessedDate,
|
||||
).toBe('2026-06-29');
|
||||
});
|
||||
|
||||
it('does not send when there are no due tasks but marks the slot processed', async () => {
|
||||
await saveUser({ taskDigestPreference: 'both' });
|
||||
await saveTask({ id: 'future-task', dueDate: '2026-06-30' });
|
||||
|
||||
await service.processDueDigests(new Date('2026-06-29T07:00:00.000Z'));
|
||||
|
||||
expect(mailService.sendTaskDigestEmail).not.toHaveBeenCalled();
|
||||
expect(
|
||||
(await usersRepository.findOne({ where: { id: 'owner-1' } }))
|
||||
?.taskDigestMorningProcessedDate,
|
||||
).toBe('2026-06-29');
|
||||
});
|
||||
|
||||
it('respects none and morning-only preferences', async () => {
|
||||
await saveUser({
|
||||
id: 'none-user',
|
||||
email: 'none@example.com',
|
||||
taskDigestPreference: 'none',
|
||||
});
|
||||
await saveUser({
|
||||
id: 'morning-user',
|
||||
email: 'morning@example.com',
|
||||
taskDigestPreference: 'morning',
|
||||
});
|
||||
await saveUser({
|
||||
id: 'both-user',
|
||||
email: 'both@example.com',
|
||||
taskDigestPreference: 'both',
|
||||
});
|
||||
await saveTask({ id: 'none-task', ownerId: 'none-user' });
|
||||
await saveTask({ id: 'morning-task', ownerId: 'morning-user' });
|
||||
await saveTask({ id: 'both-task', ownerId: 'both-user' });
|
||||
|
||||
await service.processDueDigests(new Date('2026-06-29T13:00:00.000Z'));
|
||||
|
||||
expect(mailService.sendTaskDigestEmail).toHaveBeenCalledTimes(1);
|
||||
expect(mailService.sendTaskDigestEmail).toHaveBeenCalledWith(
|
||||
'both@example.com',
|
||||
expect.objectContaining({ slot: 'afternoon' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not process outside the configured delivery hours', async () => {
|
||||
await saveUser({ taskDigestPreference: 'both' });
|
||||
await saveTask({ id: 'today-task' });
|
||||
|
||||
await service.processDueDigests(new Date('2026-06-29T08:30:00.000Z'));
|
||||
|
||||
expect(mailService.sendTaskDigestEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
async function saveUser(overrides: Partial<UserEntity> = {}) {
|
||||
return usersRepository.save({
|
||||
id: overrides.id ?? 'owner-1',
|
||||
email: overrides.email ?? 'owner@example.com',
|
||||
name: overrides.name ?? 'Owner',
|
||||
passwordHash: 'hash',
|
||||
verified: overrides.verified ?? true,
|
||||
onboardingCompleted: false,
|
||||
taskDigestPreference: overrides.taskDigestPreference ?? 'both',
|
||||
taskDigestMorningProcessedDate:
|
||||
overrides.taskDigestMorningProcessedDate ?? null,
|
||||
taskDigestAfternoonProcessedDate:
|
||||
overrides.taskDigestAfternoonProcessedDate ?? null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as UserEntity);
|
||||
}
|
||||
|
||||
async function saveTask(overrides: Partial<UserTaskEntity> = {}) {
|
||||
return tasksRepository.save({
|
||||
id: overrides.id ?? 'task-1',
|
||||
ownerId: overrides.ownerId ?? 'owner-1',
|
||||
title: overrides.title ?? 'Heute',
|
||||
notes: overrides.notes ?? null,
|
||||
dueDate: overrides.dueDate ?? '2026-06-29',
|
||||
completed: overrides.completed ?? false,
|
||||
completedAt: overrides.completedAt ?? null,
|
||||
deletedAt: overrides.deletedAt ?? null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as UserTaskEntity);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user