feat(260929-if2): Erinnerungen anlegen und zur Faelligkeit benachrichtigen (Tracer)
- Reminder-Tabelle mit Zeilenschutz (Mandant+Benutzer, Systemlesen fuer den E-Mail-Planer), API reminders (Liste, Anlegen) - Kachel "Erinnerungen", globaler Melder im Portalrahmen (Browser und Desktop, je Faelligkeit einmal) - Desktop: Laufzeit-Berechtigung fuer Benachrichtigungen nur fuer die gespeicherte Server-Adresse - Zugriffsklassifikation nachgemessen Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { BadRequestException, ConflictException } from '@nestjs/common';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// `forTenant` reicht den Klienten durch — die Bindung selbst prueft
|
||||
// rls-access-inventory.spec.ts; hier zaehlt, mit welchen Argumenten gebunden wird.
|
||||
vi.mock('../prisma/prisma-tenant.extension', () => ({
|
||||
forTenant: vi.fn((p: unknown) => p),
|
||||
}));
|
||||
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import { RemindersService } from './reminders.service';
|
||||
|
||||
function makeFakePrisma() {
|
||||
const rows = new Map<string, any>();
|
||||
let seq = 0;
|
||||
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 };
|
||||
rows.set(id, row);
|
||||
return row;
|
||||
}),
|
||||
findMany: vi.fn(async ({ where, orderBy }: { where?: any; orderBy?: any } = {}) => {
|
||||
let list = [...rows.values()];
|
||||
if (where?.tenantId) list = list.filter((r) => r.tenantId === where.tenantId);
|
||||
if (where?.userId) list = list.filter((r) => r.userId === where.userId);
|
||||
if (orderBy?.dueAt === 'asc') list.sort((a, b) => a.dueAt.getTime() - b.dueAt.getTime());
|
||||
return list;
|
||||
}),
|
||||
count: vi.fn(async ({ where }: { where?: any } = {}) => {
|
||||
return [...rows.values()].filter(
|
||||
(r) => r.tenantId === where?.tenantId && r.userId === where?.userId,
|
||||
).length;
|
||||
}),
|
||||
};
|
||||
return { reminder, rows };
|
||||
}
|
||||
|
||||
function setup() {
|
||||
const prisma = makeFakePrisma();
|
||||
return { prisma, service: new RemindersService(prisma as any) };
|
||||
}
|
||||
|
||||
const inHours = (h: number) => new Date(Date.now() + h * 3600_000).toISOString();
|
||||
|
||||
describe('RemindersService — anlegen', () => {
|
||||
it('speichert tenantId und userId aus den Argumenten, nie aus dem DTO', async () => {
|
||||
const { prisma, service } = setup();
|
||||
await service.create('t1', 'u1', {
|
||||
title: 'Zahnarzt',
|
||||
dueAt: inHours(2),
|
||||
tenantId: 'evil',
|
||||
userId: 'evil',
|
||||
} as any);
|
||||
const data = prisma.reminder.create.mock.calls[0][0].data;
|
||||
expect(data.tenantId).toBe('t1');
|
||||
expect(data.userId).toBe('u1');
|
||||
expect(data.description).toBe('');
|
||||
});
|
||||
|
||||
it('bindet mit Mandant UND Benutzer', async () => {
|
||||
const { service } = setup();
|
||||
await service.create('t1', 'u1', { title: 'a', dueAt: inHours(1) });
|
||||
expect(forTenant).toHaveBeenLastCalledWith(expect.anything(), 't1', 'u1');
|
||||
});
|
||||
|
||||
it('lehnt eine vergangene Faelligkeit mit 400 ab', async () => {
|
||||
const { service } = setup();
|
||||
await expect(
|
||||
service.create('t1', 'u1', { title: 'a', dueAt: inHours(-1) }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('lehnt eine Faelligkeit ueber 5 Jahre voraus mit 400 ab', async () => {
|
||||
const { service } = setup();
|
||||
await expect(
|
||||
service.create('t1', 'u1', { title: 'a', dueAt: inHours(24 * 365 * 5 + 48) }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('die 101. Erinnerung eines Benutzers ergibt 409', async () => {
|
||||
const { prisma, service } = setup();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
prisma.rows.set(`x${i}`, { id: `x${i}`, tenantId: 't1', userId: 'u1', dueAt: new Date() });
|
||||
}
|
||||
await expect(
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RemindersService — auflisten', () => {
|
||||
it('liefert nur Zeilen von Mandant und Benutzer, nach Faelligkeit aufsteigend', async () => {
|
||||
const { prisma, service } = setup();
|
||||
await service.create('t1', 'u1', { title: 'spaet', dueAt: inHours(5) });
|
||||
await service.create('t1', 'u1', { title: 'frueh', dueAt: inHours(1) });
|
||||
await service.create('t1', 'u2', { title: 'fremd', dueAt: inHours(2) });
|
||||
await service.create('t2', 'u1', { title: 'anderer Mandant', dueAt: inHours(2) });
|
||||
const list = await service.list('t1', 'u1');
|
||||
expect(list.map((r: any) => r.title)).toEqual(['frueh', 'spaet']);
|
||||
const where = prisma.reminder.findMany.mock.calls[0]?.[0]?.where;
|
||||
expect(where).toEqual({ tenantId: 't1', userId: 'u1' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user