feat(nextcloud-status): Modul mit Statusabruf, Versionsbewertung und Kachelansicht
- NextcloudInstance mit Zeilenschutz, Migration 20261002150000 - Bewertung (Ampel) als reine Funktion, status.php-Abruf mit Grenzen, endoflife.date-Zwischenspeicher - API: Liste mit Bewertung, Anlegen und Einzelprüfung nur mit Verwalten - Modulseite mit Kacheln, Registrierung in Seitenleiste, Marktplatz und Symbolen Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('../prisma/prisma-tenant.extension', () => ({
|
||||
forTenant: vi.fn((p: unknown) => p),
|
||||
forSystem: vi.fn((p: unknown) => p),
|
||||
}));
|
||||
vi.mock('./nextcloud-status-fetch', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('./nextcloud-status-fetch')>();
|
||||
return { ...actual, fetchNextcloudStatus: vi.fn() };
|
||||
});
|
||||
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import type { NextcloudReference } from './nextcloud-rating';
|
||||
import { NextcloudStatusService, PUBLIC_SELECT } from './nextcloud-status.service';
|
||||
import { fetchNextcloudStatus } from './nextcloud-status-fetch';
|
||||
|
||||
const REFERENCE: NextcloudReference = {
|
||||
fetchedAt: '2026-10-02T10:00:00.000Z',
|
||||
cycles: [{ cycle: 35, eol: '2099-09-30', latest: '35.0.1' }],
|
||||
};
|
||||
|
||||
function makeRow(over: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 'i1',
|
||||
tenantId: 't1',
|
||||
customerName: 'Kunde A',
|
||||
baseUrl: 'https://cloud.a.de',
|
||||
logoUrl: null,
|
||||
logoMime: null,
|
||||
logoVersion: 0,
|
||||
lastCheckedAt: new Date('2026-10-02T11:00:00Z'),
|
||||
reachable: true,
|
||||
maintenance: false,
|
||||
needsDbUpgrade: false,
|
||||
versionString: '35.0.1',
|
||||
edition: null,
|
||||
errorKind: null,
|
||||
errorDetail: null,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
describe('NextcloudStatusService', () => {
|
||||
let prisma: { nextcloudInstance: Record<string, ReturnType<typeof vi.fn>> };
|
||||
let release: { getReference: ReturnType<typeof vi.fn> };
|
||||
let service: NextcloudStatusService;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(fetchNextcloudStatus).mockReset();
|
||||
vi.mocked(forTenant).mockClear();
|
||||
prisma = {
|
||||
nextcloudInstance: {
|
||||
findMany: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
};
|
||||
release = { getReference: vi.fn().mockResolvedValue(REFERENCE) };
|
||||
service = new NextcloudStatusService(prisma as never, release as never);
|
||||
});
|
||||
|
||||
it('listForTenant waehlt keine Logo-Bytes und liefert Bewertung und neueste Version', async () => {
|
||||
prisma.nextcloudInstance.findMany.mockResolvedValue([
|
||||
makeRow(),
|
||||
makeRow({ id: 'i2', logoMime: 'image/png', logoVersion: 3 }),
|
||||
]);
|
||||
const result = await service.listForTenant('t1');
|
||||
const args = prisma.nextcloudInstance.findMany.mock.calls[0][0];
|
||||
expect(args.select).toBe(PUBLIC_SELECT);
|
||||
expect(args.select).not.toHaveProperty('logoData');
|
||||
expect(args.where).toEqual({ tenantId: 't1' });
|
||||
expect(forTenant).toHaveBeenCalledWith(prisma, 't1');
|
||||
expect(result.reference).toEqual({ newestVersion: '35.0.1', fetchedAt: REFERENCE.fetchedAt });
|
||||
expect(result.instances).toHaveLength(2);
|
||||
expect(result.instances[0].rating).toMatchObject({ level: 'green', reason: 'current' });
|
||||
expect(result.instances[0].hasUploadedLogo).toBe(false);
|
||||
expect(result.instances[1]).toMatchObject({ hasUploadedLogo: true, logoVersion: 3 });
|
||||
expect(JSON.stringify(result)).not.toContain('logoData');
|
||||
});
|
||||
|
||||
it('listForTenant ohne Vergleichsdaten bewertet grau', async () => {
|
||||
release.getReference.mockResolvedValue(null);
|
||||
prisma.nextcloudInstance.findMany.mockResolvedValue([makeRow()]);
|
||||
const result = await service.listForTenant('t1');
|
||||
expect(result.reference).toEqual({ newestVersion: null, fetchedAt: null });
|
||||
expect(result.instances[0].rating.level).toBe('unknown');
|
||||
});
|
||||
|
||||
it('createInstance normalisiert die Adresse, legt an und prueft sofort', async () => {
|
||||
prisma.nextcloudInstance.create.mockResolvedValue({ id: 'i1' });
|
||||
prisma.nextcloudInstance.findFirst.mockResolvedValue({
|
||||
id: 'i1',
|
||||
baseUrl: 'https://cloud.a.de',
|
||||
});
|
||||
vi.mocked(fetchNextcloudStatus).mockResolvedValue({
|
||||
reachable: true,
|
||||
maintenance: false,
|
||||
needsDbUpgrade: false,
|
||||
versionString: '35.0.1',
|
||||
edition: null,
|
||||
productName: 'Nextcloud',
|
||||
errorKind: null,
|
||||
errorDetail: null,
|
||||
});
|
||||
prisma.nextcloudInstance.update.mockResolvedValue(makeRow());
|
||||
const view = await service.createInstance('t1', {
|
||||
customerName: ' Kunde A ',
|
||||
baseUrl: 'https://cloud.a.de/status.php/',
|
||||
});
|
||||
expect(prisma.nextcloudInstance.create.mock.calls[0][0].data).toEqual({
|
||||
tenantId: 't1',
|
||||
customerName: 'Kunde A',
|
||||
baseUrl: 'https://cloud.a.de',
|
||||
logoUrl: null,
|
||||
});
|
||||
expect(fetchNextcloudStatus).toHaveBeenCalledWith('https://cloud.a.de');
|
||||
expect(view.id).toBe('i1');
|
||||
});
|
||||
|
||||
it('createInstance mit ungueltiger Adresse -> BadRequest mit deutscher Meldung', async () => {
|
||||
await expect(
|
||||
service.createInstance('t1', { customerName: 'X', baseUrl: 'ftp://x' }),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
await expect(
|
||||
service.createInstance('t1', { customerName: 'X', baseUrl: 'ftp://x' }),
|
||||
).rejects.toThrow('Bitte geben Sie eine gültige Adresse mit http:// oder https:// ein.');
|
||||
expect(prisma.nextcloudInstance.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('checkInstance schreibt alle Statusspalten', async () => {
|
||||
prisma.nextcloudInstance.findFirst.mockResolvedValue({
|
||||
id: 'i1',
|
||||
baseUrl: 'https://cloud.a.de',
|
||||
});
|
||||
vi.mocked(fetchNextcloudStatus).mockResolvedValue({
|
||||
reachable: false,
|
||||
maintenance: null,
|
||||
needsDbUpgrade: null,
|
||||
versionString: null,
|
||||
edition: null,
|
||||
productName: null,
|
||||
errorKind: 'http-status',
|
||||
errorDetail: 'HTTP 503',
|
||||
});
|
||||
prisma.nextcloudInstance.update.mockResolvedValue(
|
||||
makeRow({
|
||||
reachable: false,
|
||||
versionString: null,
|
||||
errorKind: 'http-status',
|
||||
errorDetail: 'HTTP 503',
|
||||
}),
|
||||
);
|
||||
const view = await service.checkInstance('t1', 'i1');
|
||||
expect(prisma.nextcloudInstance.findFirst.mock.calls[0][0].where).toEqual({
|
||||
id: 'i1',
|
||||
tenantId: 't1',
|
||||
});
|
||||
const data = prisma.nextcloudInstance.update.mock.calls[0][0].data;
|
||||
expect(data).toMatchObject({
|
||||
reachable: false,
|
||||
maintenance: null,
|
||||
needsDbUpgrade: null,
|
||||
versionString: null,
|
||||
edition: null,
|
||||
errorKind: 'http-status',
|
||||
errorDetail: 'HTTP 503',
|
||||
});
|
||||
expect(data.lastCheckedAt).toBeInstanceOf(Date);
|
||||
expect(prisma.nextcloudInstance.update.mock.calls[0][0].select).toBe(PUBLIC_SELECT);
|
||||
expect(view.rating).toMatchObject({ level: 'red', reason: 'unreachable' });
|
||||
});
|
||||
|
||||
it('checkInstance fuer unbekannte Kennung -> NotFound, kein Abruf', async () => {
|
||||
prisma.nextcloudInstance.findFirst.mockResolvedValue(null);
|
||||
await expect(service.checkInstance('t1', 'nix')).rejects.toThrow(NotFoundException);
|
||||
expect(fetchNextcloudStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user