fix(260923-ku6): Verbindungstest prueft Formularwerte statt gespeicherten Stand
Nachbesserung aus dem Browser-Rundgang zu 260923-dhh (Befund 1): "Verbindung testen" pruefte bislang immer den in der Datenbank gespeicherten Server, nicht das ungespeicherte Formular. Eine im Formular abgeschaltete Zertifikatspruefung oder ein neu eingetipptes Token-Geheimnis wurden dadurch beim Test ignoriert und erst nach "Speichern" wirksam — eine Falle fuer genau den Ablauf, den Nutzer instinktiv waehlen (eintippen, testen, dann erst speichern). Neues `TestProxmoxServerDto` plus Merge-Baustein `resolveEffectiveTestServer` in `ProxmoxService`: normale Felder folgen dem Formular (auch wenn absichtlich geleert), Geheimnisfelder folgen der bestehenden "leer -> gespeicherten Wert behalten"-Regel, weil `ServerForm` sie beim Laden nie aus der Datenbank vorbefuellt. Neue Route `POST servers/test` (ohne `:id`) deckt die Neuanlage ab, wo es noch keinen gespeicherten Server gibt. Der Testen-Knopf steht jetzt immer zur Verfuegung, nicht mehr nur nach dem ersten Speichern. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,11 +4,13 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
const mockCreateServer = vi.fn();
|
||||
const mockUpdateServer = vi.fn();
|
||||
const mockTestServer = vi.fn();
|
||||
const mockTestDraftServer = vi.fn();
|
||||
|
||||
vi.mock('@/lib/proxmox-api', () => ({
|
||||
createServer: (...args: unknown[]) => mockCreateServer(...args),
|
||||
updateServer: (...args: unknown[]) => mockUpdateServer(...args),
|
||||
testServer: (...args: unknown[]) => mockTestServer(...args),
|
||||
testDraftServer: (...args: unknown[]) => mockTestDraftServer(...args),
|
||||
}));
|
||||
|
||||
vi.mock('next-intl', () => ({
|
||||
@@ -62,6 +64,7 @@ afterEach(() => {
|
||||
mockCreateServer.mockReset();
|
||||
mockUpdateServer.mockReset();
|
||||
mockTestServer.mockReset();
|
||||
mockTestDraftServer.mockReset();
|
||||
});
|
||||
|
||||
const EXISTING_SERVER = {
|
||||
@@ -178,10 +181,64 @@ describe('ServerForm', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('ohne gespeicherten Server (Neuanlage) gibt es keinen Verbindung-testen-Knopf', async () => {
|
||||
it('Nachbesserung Befund 1: bei der Neuanlage (kein gespeicherter Server) steht der Testen-Knopf zur Verfuegung und ruft den Neuanlage-Testweg auf', async () => {
|
||||
mockTestDraftServer.mockResolvedValue({
|
||||
reachable: true,
|
||||
errorKind: null,
|
||||
errorDetail: null,
|
||||
metrics: null,
|
||||
rawSample: null,
|
||||
});
|
||||
const { ServerForm } = await import('./ServerForm');
|
||||
render(<ServerForm server={null} isAdmin onSaved={vi.fn()} onCancel={vi.fn()} />);
|
||||
|
||||
expect(screen.queryByText('Verbindung testen')).not.toBeInTheDocument();
|
||||
fireEvent.change(screen.getByLabelText('Adresse'), {
|
||||
target: { value: 'https://pve.neu:8006' },
|
||||
});
|
||||
fireEvent.click(screen.getByText('Verbindung testen'));
|
||||
|
||||
await waitFor(() => expect(mockTestDraftServer).toHaveBeenCalled());
|
||||
expect(mockTestServer).not.toHaveBeenCalled();
|
||||
await waitFor(() => expect(screen.getByText('Verbindung erfolgreich.')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('Nachbesserung Befund 1: der Test prueft die im Formular abgeschaltete Zertifikatspruefung, nicht den gespeicherten Stand', async () => {
|
||||
mockTestServer.mockResolvedValue({
|
||||
reachable: true,
|
||||
errorKind: null,
|
||||
errorDetail: null,
|
||||
metrics: null,
|
||||
rawSample: null,
|
||||
});
|
||||
const { ServerForm } = await import('./ServerForm');
|
||||
render(<ServerForm server={EXISTING_SERVER} isAdmin onSaved={vi.fn()} onCancel={vi.fn()} />);
|
||||
|
||||
// EXISTING_SERVER wurde MIT Zertifikatspruefung gespeichert — im Formular jetzt abschalten.
|
||||
fireEvent.click(screen.getByLabelText('Zertifikat prüfen'));
|
||||
fireEvent.click(screen.getByText('Verbindung testen'));
|
||||
|
||||
await waitFor(() => expect(mockTestServer).toHaveBeenCalled());
|
||||
const [, payload] = mockTestServer.mock.calls[0];
|
||||
// Ohne die Korrektur wuerde `testServer` ganz ohne Formularstand aufgerufen — roter Test.
|
||||
expect(payload.tlsRejectUnauthorized).toBe(false);
|
||||
});
|
||||
|
||||
it('Nachbesserung Befund 1: ein leer gelassenes Geheimnisfeld sendet beim Testen kein tokenSecret (gespeicherter Wert bleibt massgeblich)', async () => {
|
||||
mockTestServer.mockResolvedValue({
|
||||
reachable: true,
|
||||
errorKind: null,
|
||||
errorDetail: null,
|
||||
metrics: null,
|
||||
rawSample: null,
|
||||
});
|
||||
const pveServer = { ...EXISTING_SERVER, productType: 'pve' as const, authMethod: 'token' as const };
|
||||
const { ServerForm } = await import('./ServerForm');
|
||||
render(<ServerForm server={pveServer} isAdmin onSaved={vi.fn()} onCancel={vi.fn()} />);
|
||||
|
||||
fireEvent.click(screen.getByText('Verbindung testen'));
|
||||
|
||||
await waitFor(() => expect(mockTestServer).toHaveBeenCalled());
|
||||
const [, payload] = mockTestServer.mock.calls[0];
|
||||
expect(payload.tokenSecret).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import {
|
||||
createServer,
|
||||
testDraftServer,
|
||||
testServer,
|
||||
updateServer,
|
||||
type ProxmoxAuthMethod,
|
||||
@@ -114,6 +115,17 @@ export function ServerForm({ server, isAdmin, onSaved, onCancel }: ServerFormPro
|
||||
isActive: form.isActive,
|
||||
});
|
||||
|
||||
/**
|
||||
* Wie `buildPayload()`, aber OHNE `name` (Nachbesserung Befund 1): der
|
||||
* Verbindungstest braucht den Namen nicht, und ein waehrend der Neuanlage
|
||||
* noch leer gelassenes Namensfeld wuerde sonst die serverseitige
|
||||
* `@IsNotEmpty()`-Pruefung auf `name` bei jedem Testklick blockieren.
|
||||
*/
|
||||
const buildTestPayload = () => {
|
||||
const { name: _name, ...rest } = buildPayload();
|
||||
return rest;
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
setSaveError(null);
|
||||
@@ -132,11 +144,17 @@ export function ServerForm({ server, isAdmin, onSaved, onCancel }: ServerFormPro
|
||||
};
|
||||
|
||||
const handleTest = async () => {
|
||||
if (!savedServer) return;
|
||||
setIsTesting(true);
|
||||
setTestResult(null);
|
||||
try {
|
||||
const result = await testServer(savedServer.id);
|
||||
// Nachbesserung Befund 1: der Test prueft immer den aktuellen
|
||||
// Formularstand (`buildPayload()`), nie nur den gespeicherten Stand.
|
||||
// Ein leer gelassenes Geheimnisfeld wird dabei als `undefined`
|
||||
// gesendet — der Server faellt dann auf den gespeicherten Wert
|
||||
// zurueck (siehe `ProxmoxService.resolveEffectiveTestServer`).
|
||||
const result = savedServer
|
||||
? await testServer(savedServer.id, buildTestPayload())
|
||||
: await testDraftServer(buildTestPayload());
|
||||
setTestResult(result);
|
||||
} catch (err) {
|
||||
setTestResult({
|
||||
@@ -355,16 +373,20 @@ export function ServerForm({ server, isAdmin, onSaved, onCancel }: ServerFormPro
|
||||
{isSaving ? t('settings.saving') : t('settings.save')}
|
||||
</button>
|
||||
|
||||
{savedServer && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTest}
|
||||
disabled={isTesting}
|
||||
className="rounded border border-border px-4 py-2 text-sm text-foreground hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isTesting ? t('settings.testTesting') : t('settings.testConnection')}
|
||||
</button>
|
||||
)}
|
||||
{/*
|
||||
Nachbesserung Befund 1: der Testen-Knopf steht IMMER zur
|
||||
Verfuegung, auch waehrend der Neuanlage vor dem ersten Speichern
|
||||
(vorher: nur bei `savedServer`) — `handleTest` waehlt selbst den
|
||||
passenden Endpunkt (`testServer` vs. `testDraftServer`).
|
||||
*/}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTest}
|
||||
disabled={isTesting || !form.baseUrl}
|
||||
className="rounded border border-border px-4 py-2 text-sm text-foreground hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isTesting ? t('settings.testTesting') : t('settings.testConnection')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -193,11 +193,41 @@ export async function pollServer(id: string): Promise<ProxmoxTestResult> {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** POST /modules/proxmox/servers/:id/test — Verbindungstest, schreibt NICHT ins Zwischenlager. */
|
||||
export async function testServer(id: string): Promise<ProxmoxTestResult> {
|
||||
/**
|
||||
* POST /modules/proxmox/servers/:id/test — Verbindungstest fuer einen
|
||||
* gespeicherten Server, schreibt NICHT ins Zwischenlager. `payload` traegt
|
||||
* den aktuellen Formularstand (Nachbesserung Befund 1): der Test prueft
|
||||
* damit, was im Formular steht, statt blind den gespeicherten Stand — ein
|
||||
* leer gelassenes Geheimnisfeld (`tokenSecret`/`password: undefined`) laesst
|
||||
* den Server serverseitig auf den gespeicherten Wert zurueckfallen.
|
||||
*/
|
||||
export async function testServer(
|
||||
id: string,
|
||||
payload: UpdateProxmoxServerPayload,
|
||||
): Promise<ProxmoxTestResult> {
|
||||
const res = await fetch(`${API_URL}/modules/proxmox/servers/${id}/test`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error(await readErrorMessage(res, 'Failed to test proxmox server'));
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /modules/proxmox/servers/test — Verbindungstest waehrend der
|
||||
* Neuanlage (Nachbesserung Befund 1): es gibt noch keinen gespeicherten
|
||||
* Server, `payload` ist deshalb die einzige Quelle.
|
||||
*/
|
||||
export async function testDraftServer(
|
||||
payload: UpdateProxmoxServerPayload,
|
||||
): Promise<ProxmoxTestResult> {
|
||||
const res = await fetch(`${API_URL}/modules/proxmox/servers/test`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error(await readErrorMessage(res, 'Failed to test proxmox server'));
|
||||
return res.json();
|
||||
|
||||
Reference in New Issue
Block a user