fix: Berechtigungen der Tagesübersicht, WebDAV-Ausbau, Tests instand gesetzt
Build and Push Multi-Platform Images / build-and-push (push) Successful in 55s

Die tägliche E-Mail-Zusammenfassung zeigte Nutzern Bereiche, für die sie
keine Berechtigung haben. Zwei unabhängige Ursachen:

- Das Backend prüfte weiterhin die Altgruppe PM_Belege. Die Umbenennung
  zu PM_Buchhaltung (ca1d371) war nur im Frontend angekommen, weshalb
  der Digest "Manuell bearbeiten" und "In Agrarmonitor" anbot, während
  die Oberfläche beide Bereiche sperrte.
- Der Cron-Versand wertet die Gruppen aus user_settings aus. Diese Spalte
  wurde nur beim Aufruf der Benutzereinstellungen gefüllt; entzogene
  Berechtigungen erreichten den Digest daher unter Umständen nie.

Behoben durch Angleichen des Gruppen-Mappings und den neuen
UserIdentitySyncService, der E-Mail, Benutzername und Gruppen bei jedem
authentifizierten Request aus dem Token spiegelt – ohne den Request zu
blockieren und ohne DB-Zugriff, solange sich das Token nicht ändert. Die
doppelte Identitätspflege im UserSettingsService entfällt.

WebDAV wird nicht eingesetzt und ist entfernt; Export-Ziele bieten nur
noch FTP. Damit verschwindet das ESM-Paket webdav, an dem zwei
Jest-Suites bereits beim Parsen scheiterten.

Veraltete Tests instand gesetzt: email.controller und settings.controller
mockten weniger Abhängigkeiten, als die Klassen inzwischen haben;
postprocessing.service.spec beschrieb noch das alte Regelmodell mit
Einzelfeldern statt FilterJson und ist gegen die heutige Filter-Engine
neu geschrieben (AND/OR, verschachtelte Gruppen, Fehlerprotokollierung).

Enthält außerdem den Arbeitsstand der E-Rechnungs-Mandantenzuordnung, da
sich beide Änderungen dieselben Dateien teilen (SettingsPage, package.json).

98 Tests in 12 Suites grün, Backend- und Frontend-Build sauber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 00:06:50 +02:00
co-authored by Claude Opus 5
parent 32ba772e4e
commit b7b65c64ae
41 changed files with 2462 additions and 443 deletions
@@ -2,6 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { EmailController } from './email.controller';
import { Email } from '../database/entities/email.entity';
import { Attachment } from '../database/entities/attachment.entity';
import { Content } from '../database/entities/content.entity';
import { PaperlessService } from '../paperless/paperless.service';
import { ImapFolderService } from './imap-folder.service';
const mockEmails: Partial<Email>[] = [
{
@@ -26,25 +30,56 @@ const mockEmails: Partial<Email>[] = [
},
];
const mockQueryBuilder = {
orderBy: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue(mockEmails),
};
const mockRepo = {
createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),
findOneByOrFail: jest.fn().mockResolvedValue(mockEmails[0]),
};
const mockAttachments: Partial<Attachment>[] = [
{ Id: 10, EmailMessageId: 1, FileName: 'rechnung.pdf' },
];
describe('EmailController', () => {
let controller: EmailController;
let queryBuilder: {
leftJoinAndSelect: jest.Mock;
orderBy: jest.Mock;
take: jest.Mock;
where: jest.Mock;
getMany: jest.Mock;
};
let emailRepo: {
createQueryBuilder: jest.Mock;
findOneOrFail: jest.Mock;
save: jest.Mock;
};
let attachmentRepo: { find: jest.Mock; findOne: jest.Mock };
beforeEach(async () => {
queryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue(mockEmails),
};
emailRepo = {
createQueryBuilder: jest.fn().mockReturnValue(queryBuilder),
findOneOrFail: jest.fn().mockResolvedValue({ ...mockEmails[0] }),
save: jest.fn().mockImplementation((e: Email) => Promise.resolve(e)),
};
attachmentRepo = {
find: jest.fn().mockResolvedValue(mockAttachments),
findOne: jest.fn().mockResolvedValue(mockAttachments[0]),
};
const module: TestingModule = await Test.createTestingModule({
controllers: [EmailController],
providers: [{ provide: getRepositoryToken(Email), useValue: mockRepo }],
providers: [
{ provide: getRepositoryToken(Email), useValue: emailRepo },
{ provide: getRepositoryToken(Attachment), useValue: attachmentRepo },
{
provide: getRepositoryToken(Content),
useValue: { findOne: jest.fn() },
},
{ provide: PaperlessService, useValue: {} },
{ provide: ImapFolderService, useValue: {} },
],
}).compile();
controller = module.get<EmailController>(EmailController);
@@ -57,19 +92,52 @@ describe('EmailController', () => {
it('getEmails returns list', async () => {
const result = await controller.getEmails();
expect(result).toHaveLength(2);
expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith('e.Date', 'DESC');
expect(queryBuilder.orderBy).toHaveBeenCalledWith('e.Date', 'DESC');
expect(queryBuilder.leftJoinAndSelect).toHaveBeenCalledWith(
'e.Attachments',
'a',
);
});
it('getEmails begrenzt ohne Angabe auf 50 Einträge', async () => {
await controller.getEmails();
expect(queryBuilder.take).toHaveBeenCalledWith(50);
});
it('getEmails filters by status', async () => {
await controller.getEmails('1');
expect(mockQueryBuilder.where).toHaveBeenCalledWith('e.Status = :status', {
expect(queryBuilder.where).toHaveBeenCalledWith('e.Status = :status', {
status: 1,
});
});
it('getEmails filtert nicht ohne Status', async () => {
await controller.getEmails();
expect(queryBuilder.where).not.toHaveBeenCalled();
});
it('getEmail returns single item', async () => {
const result = await controller.getEmail('1');
expect(result).toEqual(mockEmails[0]);
expect(mockRepo.findOneByOrFail).toHaveBeenCalledWith({ Id: 1 });
expect(emailRepo.findOneOrFail).toHaveBeenCalledWith({
where: { Id: 1 },
relations: ['Attachments'],
});
});
it('getAttachments lädt die Anhänge einer E-Mail', async () => {
const result = await controller.getAttachments('1');
expect(result).toEqual(mockAttachments);
expect(attachmentRepo.find).toHaveBeenCalledWith({
where: { EmailMessageId: 1 },
order: { Id: 'ASC' },
});
});
it('updateStatus speichert den neuen Status', async () => {
await controller.updateStatus('1', 2);
expect(emailRepo.save).toHaveBeenCalledWith(
expect.objectContaining({ Id: 1, Status: 2 }),
);
});
});