This commit is contained in:
Bastian Wagner
2026-06-29 14:34:34 +02:00
parent ecb902565d
commit 9a721f9e86
18 changed files with 884 additions and 7 deletions

View 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);
}
});