feat(api,web): eigene Module — Tabelle, API, Seitenleiste, Rahmen-Seite
- Tabelle CustomModule mit Zeilenschutz (tenant_isolation_policy), Migration 20260929120000 - API /custom-modules: Lesen für jeden Angemeldeten, Schreiben nur Administrator, nur https ohne Zugangsdaten - Seitenleiste zeigt eigene Module unter ihrer Kategorie, Rahmen-Seite mit Sandbox und „In neuem Tab öffnen“ - MODULE_CATEGORIES als gemeinsame Liste, Zugriffsklassifikation nachgemessen fortgeschrieben Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
CustomModuleRequestError,
|
||||
checkCustomModuleUrl,
|
||||
createCustomModule,
|
||||
deleteCustomModule,
|
||||
getCustomModule,
|
||||
listCustomModules,
|
||||
updateCustomModule,
|
||||
} from './custom-modules-api';
|
||||
|
||||
const { mockFetch } = vi.hoisted(() => ({ mockFetch: vi.fn() }));
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetch.mockReset();
|
||||
vi.stubGlobal('fetch', mockFetch);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('checkCustomModuleUrl', () => {
|
||||
it('nimmt eine https-Adresse an', () => {
|
||||
expect(checkCustomModuleUrl('https://a.de')).toBe('ok');
|
||||
});
|
||||
it('lehnt http und Unparsbares als notHttps ab', () => {
|
||||
expect(checkCustomModuleUrl('http://a.de')).toBe('notHttps');
|
||||
expect(checkCustomModuleUrl('kaputt')).toBe('notHttps');
|
||||
expect(checkCustomModuleUrl('javascript:alert(1)')).toBe('notHttps');
|
||||
expect(checkCustomModuleUrl('')).toBe('notHttps');
|
||||
});
|
||||
it('erkennt Zugangsdaten in der Adresse', () => {
|
||||
expect(checkCustomModuleUrl('https://u:p@a.de')).toBe('credentials');
|
||||
expect(checkCustomModuleUrl('https://u@a.de')).toBe('credentials');
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom-modules-api', () => {
|
||||
it('listCustomModules ruft GET /custom-modules mit credentials include', async () => {
|
||||
mockFetch.mockResolvedValue(new Response(JSON.stringify([{ id: 'a' }]), { status: 200 }));
|
||||
const list = await listCustomModules();
|
||||
expect(list).toEqual([{ id: 'a' }]);
|
||||
const [url, init] = mockFetch.mock.calls[0];
|
||||
expect(String(url)).toMatch(/\/custom-modules$/);
|
||||
expect(init.credentials).toBe('include');
|
||||
});
|
||||
|
||||
it('getCustomModule liefert null bei 404', async () => {
|
||||
mockFetch.mockResolvedValue(new Response('{}', { status: 404 }));
|
||||
await expect(getCustomModule('x')).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('getCustomModule liefert die Zeile bei 200', async () => {
|
||||
mockFetch.mockResolvedValue(new Response(JSON.stringify({ id: 'x' }), { status: 200 }));
|
||||
await expect(getCustomModule('x')).resolves.toEqual({ id: 'x' });
|
||||
});
|
||||
|
||||
it('createCustomModule schickt POST mit JSON und wirft bei Fehler mit Servermeldung', async () => {
|
||||
mockFetch.mockResolvedValue(
|
||||
new Response(JSON.stringify({ message: ['Nur https'] }), { status: 400 }),
|
||||
);
|
||||
const input = { name: 'a', url: 'http://a.de', category: 'fleet' };
|
||||
const err = await createCustomModule(input).catch((e) => e);
|
||||
expect(err).toBeInstanceOf(CustomModuleRequestError);
|
||||
expect(err.status).toBe(400);
|
||||
expect(err.message).toBe('Nur https');
|
||||
const [, init] = mockFetch.mock.calls[0];
|
||||
expect(init.method).toBe('POST');
|
||||
expect(JSON.parse(init.body)).toEqual(input);
|
||||
});
|
||||
|
||||
it('updateCustomModule schickt PATCH, deleteCustomModule DELETE', async () => {
|
||||
mockFetch.mockResolvedValue(new Response('{}', { status: 200 }));
|
||||
await updateCustomModule('x', { name: 'n' });
|
||||
expect(mockFetch.mock.calls[0][1].method).toBe('PATCH');
|
||||
expect(String(mockFetch.mock.calls[0][0])).toMatch(/\/custom-modules\/x$/);
|
||||
await deleteCustomModule('x');
|
||||
expect(mockFetch.mock.calls[1][1].method).toBe('DELETE');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user