fix(reminders): keine Dauermeldung ohne localStorage, Mail nach Zeitaenderung, null-Pruefung
- Merkliste zusaetzlich im Arbeitsspeicher (sonst alle 10 s dieselbe Meldung) - Aendern der Faelligkeit atomar gegen gleichzeitiges Faelligwerden, setzt Mail-Spur zurueck - UpdateReminderDto lehnt null ab (400 statt 500) - keine Mails an deaktivierte Benutzer - Spaeter erinnern beschriftet heute/morgen nach dem berechneten Zeitpunkt - Bearbeiten schickt dueAt nur bei geaenderter Zeit Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,13 @@ function makeFakePrisma() {
|
||||
const reminder = {
|
||||
create: vi.fn(async ({ data }: { data: any }) => {
|
||||
const id = `r-${++seq}`;
|
||||
const row = { id, createdAt: new Date(), updatedAt: new Date(), emailEnabled: false, ...data };
|
||||
const row = {
|
||||
id,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
emailEnabled: false,
|
||||
...data,
|
||||
};
|
||||
rows.set(id, row);
|
||||
return row;
|
||||
}),
|
||||
@@ -37,6 +43,14 @@ function makeFakePrisma() {
|
||||
rows.set(where.id, row);
|
||||
return row;
|
||||
}),
|
||||
// Wie die eine SQL-Anweisung: Bedingung pruefen und schreiben ohne `await` dazwischen.
|
||||
updateMany: vi.fn(async ({ where, data }: { where: any; data: any }) => {
|
||||
const r = rows.get(where.id);
|
||||
if (!r || r.tenantId !== where.tenantId || r.userId !== where.userId) return { count: 0 };
|
||||
if (where.dueAt?.gt && !(r.dueAt.getTime() > where.dueAt.gt.getTime())) return { count: 0 };
|
||||
rows.set(where.id, { ...r, ...data });
|
||||
return { count: 1 };
|
||||
}),
|
||||
delete: vi.fn(async ({ where }: { where: any }) => {
|
||||
rows.delete(where.id);
|
||||
}),
|
||||
@@ -106,7 +120,9 @@ describe('RemindersService — anlegen', () => {
|
||||
service.create('t1', 'u1', { title: 'a', dueAt: inHours(1) }),
|
||||
).rejects.toBeInstanceOf(ConflictException);
|
||||
// ein anderer Benutzer ist davon nicht betroffen
|
||||
await expect(service.create('t1', 'u2', { title: 'a', dueAt: inHours(1) })).resolves.toBeTruthy();
|
||||
await expect(
|
||||
service.create('t1', 'u2', { title: 'a', dueAt: inHours(1) }),
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,15 +169,21 @@ describe('RemindersService — fremde und unbekannte Kennungen (D-05, T-IF2-01)'
|
||||
])('%s: aendern, verschieben und loeschen ergeben 404', async (_n, owner) => {
|
||||
const { prisma, service } = setup();
|
||||
seed(prisma, 'x', past(), owner);
|
||||
await expect(service.update('t1', 'u1', 'x', { title: 'n' })).rejects.toBeInstanceOf(NotFoundException);
|
||||
await expect(service.snooze('t1', 'u1', 'x', { dueAt: inHours(1) })).rejects.toBeInstanceOf(NotFoundException);
|
||||
await expect(service.update('t1', 'u1', 'x', { title: 'n' })).rejects.toBeInstanceOf(
|
||||
NotFoundException,
|
||||
);
|
||||
await expect(service.snooze('t1', 'u1', 'x', { dueAt: inHours(1) })).rejects.toBeInstanceOf(
|
||||
NotFoundException,
|
||||
);
|
||||
await expect(service.remove('t1', 'u1', 'x')).rejects.toBeInstanceOf(NotFoundException);
|
||||
expect(prisma.rows.has('x')).toBe(true);
|
||||
});
|
||||
|
||||
it('eine unbekannte Kennung ergibt 404', async () => {
|
||||
const { service } = setup();
|
||||
await expect(service.remove('t1', 'u1', 'gibt-es-nicht')).rejects.toBeInstanceOf(NotFoundException);
|
||||
await expect(service.remove('t1', 'u1', 'gibt-es-nicht')).rejects.toBeInstanceOf(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
|
||||
it('das where jeder Abfrage traegt Mandant und Benutzer', async () => {
|
||||
@@ -173,10 +195,11 @@ describe('RemindersService — fremde und unbekannte Kennungen (D-05, T-IF2-01)'
|
||||
tenantId: 't1',
|
||||
userId: 'u1',
|
||||
});
|
||||
expect(prisma.reminder.update.mock.calls[0]?.[0]?.where).toEqual({
|
||||
expect(prisma.reminder.updateMany.mock.calls[0]?.[0]?.where).toEqual({
|
||||
id: 'x',
|
||||
tenantId: 't1',
|
||||
userId: 'u1',
|
||||
dueAt: { gt: expect.any(Date) },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -185,7 +208,9 @@ describe('RemindersService — aendern', () => {
|
||||
it('eine faellige Erinnerung laesst sich nicht aendern (409)', async () => {
|
||||
const { prisma, service } = setup();
|
||||
seed(prisma, 'x', past());
|
||||
await expect(service.update('t1', 'u1', 'x', { title: 'n' })).rejects.toBeInstanceOf(ConflictException);
|
||||
await expect(service.update('t1', 'u1', 'x', { title: 'n' })).rejects.toBeInstanceOf(
|
||||
ConflictException,
|
||||
);
|
||||
});
|
||||
|
||||
it('eine vergangene neue Faelligkeit ergibt 400', async () => {
|
||||
@@ -200,14 +225,61 @@ describe('RemindersService — aendern', () => {
|
||||
const { prisma, service } = setup();
|
||||
seed(prisma, 'x', future());
|
||||
await service.update('t1', 'u1', 'x', { title: 'neu' });
|
||||
expect(prisma.reminder.update.mock.calls[0]?.[0]?.data).toEqual({ title: 'neu' });
|
||||
expect(prisma.reminder.updateMany.mock.calls[0]?.[0]?.data).toEqual({ title: 'neu' });
|
||||
const newDue = inHours(9);
|
||||
await service.update('t1', 'u1', 'x', { dueAt: newDue, description: '' });
|
||||
expect(prisma.reminder.update.mock.calls[1]?.[0]?.data).toEqual({
|
||||
expect(prisma.reminder.updateMany.mock.calls[1]?.[0]?.data).toEqual({
|
||||
description: '',
|
||||
dueAt: new Date(newDue),
|
||||
emailSentAt: null,
|
||||
emailAttempts: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('eine neue Faelligkeit setzt emailSentAt und emailAttempts zurueck, sonst bleiben sie', async () => {
|
||||
const { prisma, service } = setup();
|
||||
seed(prisma, 'x', future());
|
||||
await service.update('t1', 'u1', 'x', { title: 'neu' });
|
||||
expect(prisma.rows.get('x').emailAttempts).toBe(2);
|
||||
expect(prisma.rows.get('x').emailSentAt).not.toBeNull();
|
||||
const newDue = inHours(5);
|
||||
const out: any = await service.update('t1', 'u1', 'x', { dueAt: newDue });
|
||||
expect(prisma.rows.get('x')).toMatchObject({
|
||||
dueAt: new Date(newDue),
|
||||
emailSentAt: null,
|
||||
emailAttempts: 0,
|
||||
});
|
||||
expect(out.dueAt).toEqual(new Date(newDue));
|
||||
});
|
||||
|
||||
it('wird die Erinnerung zwischen Pruefung und Schreiben faellig, gibt es 409 und keine Aenderung', async () => {
|
||||
const { prisma, service } = setup();
|
||||
seed(prisma, 'x', future());
|
||||
// Nach dem Laden (Pruefung bestanden) ist die Zeile inzwischen faellig.
|
||||
prisma.reminder.findFirst.mockImplementationOnce(async () => {
|
||||
const r = prisma.rows.get('x');
|
||||
const loaded = { ...r };
|
||||
prisma.rows.set('x', { ...r, dueAt: past() });
|
||||
return loaded;
|
||||
});
|
||||
await expect(service.update('t1', 'u1', 'x', { title: 'neu' })).rejects.toBeInstanceOf(
|
||||
ConflictException,
|
||||
);
|
||||
expect(prisma.rows.get('x').title).toBe('alt');
|
||||
});
|
||||
|
||||
it('wird die Erinnerung zwischen Pruefung und Schreiben geloescht, gibt es 404', async () => {
|
||||
const { prisma, service } = setup();
|
||||
seed(prisma, 'x', future());
|
||||
prisma.reminder.findFirst.mockImplementationOnce(async () => {
|
||||
const loaded = { ...prisma.rows.get('x') };
|
||||
prisma.rows.delete('x');
|
||||
return loaded;
|
||||
});
|
||||
await expect(service.update('t1', 'u1', 'x', { title: 'neu' })).rejects.toBeInstanceOf(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RemindersService — spaeter erinnern (D-03)', () => {
|
||||
@@ -276,7 +348,10 @@ describe('RemindersService — E-Mail-Erinnerung (Aufgabe 3, E-09)', () => {
|
||||
hasEmail: false,
|
||||
});
|
||||
expect(forTenant).toHaveBeenLastCalledWith(expect.anything(), 't1', 'u-ohne-mail');
|
||||
expect(prisma.user.findFirst.mock.calls[0]?.[0]?.where).toEqual({ id: 'u-ohne-mail', tenantId: 't1' });
|
||||
expect(prisma.user.findFirst.mock.calls[0]?.[0]?.where).toEqual({
|
||||
id: 'u-ohne-mail',
|
||||
tenantId: 't1',
|
||||
});
|
||||
});
|
||||
|
||||
it('anlegen mit emailEnabled speichert das Feld, wenn E-Mail moeglich ist', async () => {
|
||||
@@ -298,7 +373,11 @@ describe('RemindersService — E-Mail-Erinnerung (Aufgabe 3, E-09)', () => {
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
const noMail = setup();
|
||||
await expect(
|
||||
noMail.service.create('t1', 'u-ohne-mail', { title: 'a', dueAt: inHours(1), emailEnabled: true }),
|
||||
noMail.service.create('t1', 'u-ohne-mail', {
|
||||
title: 'a',
|
||||
dueAt: inHours(1),
|
||||
emailEnabled: true,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(noSmtp.prisma.rows.size).toBe(0);
|
||||
expect(noMail.prisma.rows.size).toBe(0);
|
||||
@@ -311,6 +390,6 @@ describe('RemindersService — E-Mail-Erinnerung (Aufgabe 3, E-09)', () => {
|
||||
BadRequestException,
|
||||
);
|
||||
await service.update('t1', 'u1', 'x', { emailEnabled: false });
|
||||
expect(prisma.reminder.update.mock.calls[0]?.[0]?.data).toEqual({ emailEnabled: false });
|
||||
expect(prisma.reminder.updateMany.mock.calls[0]?.[0]?.data).toEqual({ emailEnabled: false });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user