fix(260923-le6): Aktualisieren-Knopf der Proxmox-Seite nur fuer Admins
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,9 @@ import { ServerCard } from './components/ServerCard';
|
||||
* Modulseite (Aufgabe 6) — liest ausschliesslich aus dem Zwischenlager, das
|
||||
* `GET servers` liefert; kein Live-Zugriff bei Proxmox von hier aus (D-05).
|
||||
* "Jetzt aktualisieren" loest je Server eine Abfrage aus und laedt die
|
||||
* Liste danach neu — waehrend des Laufs ist der Knopf gesperrt.
|
||||
* Liste danach neu — waehrend des Laufs ist der Knopf gesperrt. Der Knopf
|
||||
* erscheint nur fuer Admins, weil `POST servers/:id/poll` `@Roles(ADMIN,
|
||||
* SUPER_ADMIN)` verlangt; fuer andere waere er wirkungslos (260923-le6).
|
||||
*/
|
||||
export default function ProxmoxPage() {
|
||||
const t = useTranslations('proxmox');
|
||||
@@ -51,7 +53,7 @@ export default function ProxmoxPage() {
|
||||
<p className="mt-1 text-sm text-muted-foreground">{t('description')}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{servers !== null && servers.length > 0 && (
|
||||
{isAdmin && servers !== null && servers.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefresh}
|
||||
@@ -96,7 +98,7 @@ export default function ProxmoxPage() {
|
||||
<ul className="space-y-3">
|
||||
{servers.map((server) => (
|
||||
<li key={server.id}>
|
||||
<ServerCard server={server} />
|
||||
<ServerCard server={server} isAdmin={isAdmin} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { ProxmoxServer } from '@/lib/proxmox-api';
|
||||
|
||||
// Mock @/lib/proxmox-api — die Seite ruft `listServers` beim Laden auf,
|
||||
// "Jetzt aktualisieren" ruft `pollServer` je Server.
|
||||
const mockListServers = vi.fn();
|
||||
const mockPollServer = vi.fn();
|
||||
|
||||
vi.mock('@/lib/proxmox-api', () => ({
|
||||
listServers: (...args: unknown[]) => mockListServers(...args),
|
||||
pollServer: (...args: unknown[]) => mockPollServer(...args),
|
||||
}));
|
||||
|
||||
// Mock the auth store — mirrors settings-roles.test.tsx (selector-passthrough).
|
||||
const mockAuthStore = vi.fn();
|
||||
vi.mock('@/lib/stores/auth-store', () => ({
|
||||
useAuthStore: (selector: (state: unknown) => unknown) => mockAuthStore(selector),
|
||||
}));
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ href, children, ...rest }: { href: string; children: React.ReactNode }) => (
|
||||
<a href={href} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: () => (key: string, params?: Record<string, string | number>) => {
|
||||
const translations: Record<string, string> = {
|
||||
title: 'Proxmox',
|
||||
description: 'Zustand Ihrer Proxmox-Server (PVE/PBS/PMG) auf einen Blick.',
|
||||
loading: 'Lade Serverliste...',
|
||||
loadError: 'Die Serverliste konnte nicht geladen werden.',
|
||||
emptyState:
|
||||
'Noch kein Server eingetragen. Legen Sie in den Moduleinstellungen einen Server an.',
|
||||
'card.refresh': 'Jetzt aktualisieren',
|
||||
'card.refreshing': 'Wird aktualisiert...',
|
||||
'card.settingsLink': 'Zu den Einstellungen',
|
||||
'card.unknownValue': 'unbekannt',
|
||||
'card.lastPolledLabel': 'Letzte Abfrage',
|
||||
'card.notPolledYet': 'Noch keine Abfrage gelaufen. Klicken Sie oben auf „{refreshLabel}“.',
|
||||
'card.notPolledYetAutomatic':
|
||||
'Noch keine Abfrage gelaufen. Die Werte erscheinen nach der nächsten automatischen Abfrage.',
|
||||
};
|
||||
let result = translations[key] ?? key;
|
||||
if (params) {
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
result = result.replace(`{${k}}`, String(v));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
}));
|
||||
|
||||
function mockUser(user: { role: 'SUPER_ADMIN' | 'ADMIN' | 'USER' } | null) {
|
||||
mockAuthStore.mockImplementation((selector: (state: { user: typeof user }) => unknown) =>
|
||||
selector({ user }),
|
||||
);
|
||||
}
|
||||
|
||||
// Ein nie abgefragter Server (wie im Befund-2-Test von ServerCard.test.tsx):
|
||||
// leere Zwischenlagerzeile direkt nach `createServer`.
|
||||
function makeUnpolledServer(overrides: Partial<ProxmoxServer> = {}): ProxmoxServer {
|
||||
return {
|
||||
id: 'srv-1',
|
||||
tenantId: 't1',
|
||||
name: 'pve-1',
|
||||
productType: 'pve',
|
||||
baseUrl: 'https://pve.intern',
|
||||
authMethod: 'token',
|
||||
tokenId: 'root@pam!x',
|
||||
username: null,
|
||||
tlsRejectUnauthorized: true,
|
||||
isActive: true,
|
||||
pollIntervalMin: 5,
|
||||
position: 0,
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
status: {
|
||||
id: 's1',
|
||||
serverId: 'srv-1',
|
||||
lastPolledAt: null,
|
||||
lastOkAt: null,
|
||||
reachable: false,
|
||||
errorKind: null,
|
||||
errorDetail: null,
|
||||
rawSample: null,
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
metrics: null,
|
||||
},
|
||||
...overrides,
|
||||
} as ProxmoxServer;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mockListServers.mockReset();
|
||||
mockPollServer.mockReset();
|
||||
mockAuthStore.mockReset();
|
||||
});
|
||||
|
||||
describe('ProxmoxPage role gating (260923-le6)', () => {
|
||||
it('Rolle USER: kein Knopf "Jetzt aktualisieren", Karten-Hinweis ist der automatische Text', async () => {
|
||||
mockUser({ role: 'USER' });
|
||||
mockListServers.mockResolvedValue([makeUnpolledServer()]);
|
||||
|
||||
const { default: ProxmoxPage } = await import('./page');
|
||||
render(<ProxmoxPage />);
|
||||
|
||||
await screen.findByText('pve-1');
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Jetzt aktualisieren' })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Noch keine Abfrage gelaufen. Die Werte erscheinen nach der nächsten automatischen Abfrage.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('kein Benutzer geladen (user: null): kein Knopf', async () => {
|
||||
mockUser(null);
|
||||
mockListServers.mockResolvedValue([makeUnpolledServer()]);
|
||||
|
||||
const { default: ProxmoxPage } = await import('./page');
|
||||
render(<ProxmoxPage />);
|
||||
|
||||
await screen.findByText('pve-1');
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Jetzt aktualisieren' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Rolle ADMIN: Knopf sichtbar, Karten-Hinweis ist der Admin-Text mit Knopfverweis', async () => {
|
||||
mockUser({ role: 'ADMIN' });
|
||||
mockListServers.mockResolvedValue([makeUnpolledServer()]);
|
||||
|
||||
const { default: ProxmoxPage } = await import('./page');
|
||||
render(<ProxmoxPage />);
|
||||
|
||||
await screen.findByText('pve-1');
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Jetzt aktualisieren' })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Noch keine Abfrage gelaufen. Klicken Sie oben auf „Jetzt aktualisieren“.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Rolle SUPER_ADMIN: Knopf sichtbar', async () => {
|
||||
mockUser({ role: 'SUPER_ADMIN' });
|
||||
mockListServers.mockResolvedValue([makeUnpolledServer()]);
|
||||
|
||||
const { default: ProxmoxPage } = await import('./page');
|
||||
render(<ProxmoxPage />);
|
||||
|
||||
await screen.findByText('pve-1');
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Jetzt aktualisieren' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('leere Serverliste bei ADMIN: weiterhin kein Knopf', async () => {
|
||||
mockUser({ role: 'ADMIN' });
|
||||
mockListServers.mockResolvedValue([]);
|
||||
|
||||
const { default: ProxmoxPage } = await import('./page');
|
||||
render(<ProxmoxPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Noch kein Server eingetragen. Legen Sie in den Moduleinstellungen einen Server an.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Jetzt aktualisieren' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user