Files
tessera-ctl/apps/web/src/lib/custom-modules-api.test.ts
T
schalli ac104495b4 feat(quick-261008-who): eigene Module beim Start vorladen — Durchstich
- Spalte User.customModulePreloadIds, PUT /custom-modules/:id/preload (Grenze 8, 404 fuer Fremdes)
- Vorlade-Planer und -Komponente, Speicher preload()/pinned, Ersatzgroesse im Behaelter
- E2E-Skript gegen die API

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:14:45 +02:00

115 lines
4.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
CustomModuleRequestError,
checkCustomModuleUrl,
createCustomModule,
deleteCustomModule,
getCustomModule,
listCustomModules,
setCustomModulePreload,
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');
});
});
describe('setCustomModulePreload (quick-261008-who)', () => {
it('ruft PUT /custom-modules/:id/preload mit JSON-Koerper und credentials include', async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ id: 'a b', preload: true, preloadCount: 1 }),
});
const res = await setCustomModulePreload('a b', true);
expect(res).toEqual({ id: 'a b', preload: true, preloadCount: 1 });
const [url, init] = mockFetch.mock.calls[0];
expect(String(url)).toMatch(/\/custom-modules\/a%20b\/preload$/);
expect(init.method).toBe('PUT');
expect(init.credentials).toBe('include');
expect(JSON.parse(init.body)).toEqual({ preload: true });
});
it('409 bleibt als CustomModuleRequestError mit Status erkennbar', async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 409,
json: async () => ({ message: 'Grenze' }),
});
await expect(setCustomModulePreload('a', true)).rejects.toMatchObject({
status: 409,
message: 'Grenze',
});
await expect(setCustomModulePreload('a', true)).rejects.toBeInstanceOf(
CustomModuleRequestError,
);
});
});