faed0d760f
- 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>
96 lines
4.2 KiB
TypeScript
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',
|
|
);
|
|
});
|
|
});
|