Files
tessera-ctl/apps/api/src/nextcloud-status/nextcloud-status.controller.spec.ts
T
schalli faed0d760f feat(nextcloud-status): Benachrichtigung abonnieren und Mail bei Störung
- Glocke je Kachel (persönlich, Benutzen-Ebene), Tabelle NextcloudAlertSubscription mit RLS
- Zwei-Fehlschläge-Regel und gemeldeter Zustand auf der Zeile, Anspruch vor dem Mailversand
- Mail (nur Deutsch) über MailService, bis zu drei Versuche, Zugriff beim Senden erneut geprüft
- Fehlercodes in der Mail als lesbarer Hinweis

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 15:23:34 +02:00

96 lines
4.2 KiB
TypeScript

import 'reflect-metadata';
import { GUARDS_METADATA } from '@nestjs/common/constants';
import { describe, expect, it, vi } from 'vitest';
import { ROLES_KEY } from '../auth/decorators/roles.decorator';
import { MODULE_MANAGE_KEY, MODULE_SLUG_KEY, ModuleGuard } from '../module-registry/module.guard';
import { NextcloudStatusController } from './nextcloud-status.controller';
const proto = NextcloudStatusController.prototype as unknown as Record<string, unknown>;
describe('NextcloudStatusController Metadaten', () => {
it('Klasse traegt Modul-Slug und ModuleGuard', () => {
expect(Reflect.getMetadata(MODULE_SLUG_KEY, NextcloudStatusController)).toBe(
'nextcloud-status',
);
expect(Reflect.getMetadata(GUARDS_METADATA, NextcloudStatusController)).toContain(ModuleGuard);
});
it.each(['list', 'logo', 'subscribe', 'unsubscribe'])('%s bleibt auf Benutzen-Ebene', (name) => {
expect(Reflect.getMetadata(MODULE_MANAGE_KEY, proto[name] as object)).toBeUndefined();
expect(Reflect.getMetadata(ROLES_KEY, proto[name] as object)).toBeUndefined();
});
it.each([
'create',
'update',
'remove',
'checkAll',
'checkOne',
'uploadLogo',
'removeLogo',
])('%s verlangt Verwalten ohne Rollen-Decorator', (name) => {
const fn = proto[name] as object;
expect(Reflect.getMetadata(MODULE_MANAGE_KEY, fn)).toBe(true);
expect(Reflect.getMetadata(MODULE_SLUG_KEY, fn)).toBe('nextcloud-status');
expect(Reflect.getMetadata(ROLES_KEY, fn)).toBeUndefined();
});
it('die statische Route POST instances/check steht vor jedem Handler mit :id (kein 404-Shadowing)', () => {
const names = Object.getOwnPropertyNames(NextcloudStatusController.prototype).filter(
(n) => n !== 'constructor' && typeof proto[n] === 'function',
);
const pathOf = (n: string) => Reflect.getMetadata('path', proto[n] as object) as string;
expect(pathOf('checkAll')).toBe('instances/check');
const checkIndex = names.indexOf('checkAll');
const idHandlers = names.filter((n) => (pathOf(n) ?? '').includes(':id'));
expect(idHandlers.length).toBeGreaterThan(0);
for (const name of idHandlers) {
expect(checkIndex, `checkAll vor ${name}`).toBeLessThan(names.indexOf(name));
}
});
});
describe('NextcloudStatusController Glocke (quick-261002-kxc)', () => {
const pathOf = (n: string) => Reflect.getMetadata('path', proto[n] as object) as string;
const methodOf = (n: string) => Reflect.getMetadata('method', proto[n] as object) as number;
it('POST und DELETE instances/:id/subscription', () => {
expect(pathOf('subscribe')).toBe('instances/:id/subscription');
expect(pathOf('unsubscribe')).toBe('instances/:id/subscription');
// RequestMethod.POST = 1, DELETE = 3
expect(methodOf('subscribe')).toBe(1);
expect(methodOf('unsubscribe')).toBe(3);
});
it('Benutzer kommt aus dem Token, Mandant aus der Anfrage — nie aus Body oder Pfad', async () => {
const service = {
listForTenant: vi.fn().mockResolvedValue({ instances: [] }),
checkAllForTenant: vi.fn().mockResolvedValue({ instances: [] }),
};
const alerts = {
subscribe: vi.fn().mockResolvedValue({ subscribed: true }),
unsubscribe: vi.fn().mockResolvedValue({ subscribed: false }),
};
const controller = new NextcloudStatusController(service as never, alerts as never);
const req = { tenantId: 't1', body: { userId: 'fremd', tenantId: 'fremd' } } as never;
const user = { id: 'u1' } as never;
expect(await controller.subscribe(req, user, 'i1')).toEqual({ subscribed: true });
expect(alerts.subscribe).toHaveBeenCalledWith('t1', 'u1', 'i1');
expect(await controller.unsubscribe(req, user, 'i1')).toEqual({ subscribed: false });
expect(alerts.unsubscribe).toHaveBeenCalledWith('t1', 'u1', 'i1');
await controller.list(req, user);
expect(service.listForTenant).toHaveBeenCalledWith('t1', 'u1');
await controller.checkAll(req, user);
expect(service.checkAllForTenant).toHaveBeenCalledWith('t1', 'u1');
});
it('ohne Mandantenkontext -> 403', async () => {
const controller = new NextcloudStatusController({} as never, {} as never);
await expect(controller.subscribe({} as never, { id: 'u1' } as never, 'i1')).rejects.toThrow(
'Kein Mandantenkontext',
);
});
});