Files
sencho/frontend/src/components/ui/routing-node-card.test.tsx
T
Anso c82a39c65a fix(mesh): hide node and stack management controls from non-admins (#1284)
* fix(mesh): hide node and stack management controls from non-admins

The Routing tab rendered the per-node mesh enable/disable toggle and the
stack opt-in/opt-out controls for any Admiral-tier user, but those backend
routes require the admin role. A non-admin viewer on an Admiral instance
saw controls that returned 403.

Thread a canManage flag (true only for admins) from the Fleet view into
the Routing tab, its node cards, and the opt-in sheet so non-admins get a
read-only Routing tab: the enable/disable toggle, add-stack, and
opt-in/opt-out controls are hidden, while status, aliases, topology,
activity, diagnostics, and the alias test probe stay available. This
mirrors the Federation tab's existing read-only treatment for non-admins.

Add backend route-gating tests covering the tier and admin-role guards on
every mesh route, and frontend render-gate tests for the node card and the
opt-in sheet in both density layouts.

* refactor(mesh): require canManage on the routing-node-card primitive

Remove the permissive `canManage = true` default on the shared
routing-node-card primitive so a new call site cannot render the
management controls without an explicit decision. Every current caller
already passes the flag; the type now enforces it. Drop the omitted-prop
test, which covered a state the compiler now prevents.
2026-06-02 16:09:25 -04:00

106 lines
4.7 KiB
TypeScript

/**
* Render-gate coverage for the routing-node-card `canManage` prop.
*
* Enabling/disabling mesh on a node and opting a stack in are admin-only on the
* backend. This locks the matching UI gate: a manager sees the enable/disable
* toggle and the enable/add CTAs, a non-manager sees neither (just a hint) while
* the read-only affordances, diagnostics in particular, stay available. Without
* this the card can drift back to rendering a control the API answers with 403.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { RoutingNodeCard, type RoutingNodeCardProps } from '@/components/ui/routing-node-card';
function renderCard(overrides: Partial<RoutingNodeCardProps> = {}) {
const props: RoutingNodeCardProps = {
crumb: ['Routing', 'Node', 'node-alpha'],
name: 'node-alpha',
nodeState: 'idle',
meta: { pilotConnected: true, reverseBridge: 'na', stacks: 0, aliases: 0 },
aliases: [],
onToggleEnabled: vi.fn(),
onShowDiagnostics: vi.fn(),
onAddStack: vi.fn(),
onRetry: vi.fn(),
footerContext: 'Mesh off',
// canManage is required on the primitive; tests override it per case.
canManage: true,
...overrides,
};
return render(<RoutingNodeCard {...props} />);
}
describe('routing-node-card canManage gate', () => {
it('shows the enable toggle and the enable CTA for a manager', () => {
renderCard({ nodeState: 'idle', canManage: true });
expect(screen.getByRole('switch')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Enable mesh on node-alpha/i })).toBeInTheDocument();
expect(screen.queryByText(/Managing the mesh requires an administrator/i)).not.toBeInTheDocument();
});
it('hides the toggle and enable CTA for a non-manager but keeps diagnostics', () => {
renderCard({ nodeState: 'idle', canManage: false });
expect(screen.queryByRole('switch')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Enable mesh on/i })).not.toBeInTheDocument();
expect(screen.getByText(/Managing the mesh requires an administrator/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Diagnostics/i })).toBeInTheDocument();
});
it('hides the add-stack CTA for a non-manager on a meshed node', () => {
renderCard({
nodeState: 'meshed',
meta: { pilotConnected: true, reverseBridge: 'up', stacks: 0, aliases: 0 },
canManage: false,
});
expect(screen.queryByRole('switch')).not.toBeInTheDocument();
expect(screen.queryByText(/Add stack to mesh/i)).not.toBeInTheDocument();
expect(screen.getByText(/Managing the mesh requires an administrator/i)).toBeInTheDocument();
});
it('keeps the retry CTA for a non-manager on a degraded node', () => {
// Retry is a read-only refresh, not a management action, so it must
// survive the gate. A regression dropping the management-state check
// would hide it for non-admins.
renderCard({ nodeState: 'degraded', canManage: false });
expect(screen.getByRole('button', { name: /Retry now/i })).toBeInTheDocument();
expect(screen.queryByText(/Managing the mesh requires an administrator/i)).not.toBeInTheDocument();
});
it('keeps the retry CTA for a non-manager on an offline node', () => {
renderCard({ nodeState: 'offline', canManage: false });
expect(screen.getByRole('button', { name: /Retry now/i })).toBeInTheDocument();
});
});
describe('routing-node-card canManage gate (compact density)', () => {
beforeEach(() => {
window.localStorage.setItem('sencho.appearance.density', 'compact');
});
afterEach(() => {
window.localStorage.removeItem('sencho.appearance.density');
});
it('hides the toggle for a non-manager on a meshed node', () => {
renderCard({
nodeState: 'meshed',
meta: { pilotConnected: true, reverseBridge: 'up', stacks: 0, aliases: 0 },
canManage: false,
});
expect(screen.queryByRole('switch')).not.toBeInTheDocument();
});
it('shows the toggle for a manager on a meshed node', () => {
renderCard({
nodeState: 'meshed',
meta: { pilotConnected: true, reverseBridge: 'up', stacks: 0, aliases: 0 },
canManage: true,
});
expect(screen.getByRole('switch')).toBeInTheDocument();
});
it('keeps the retry CTA for a non-manager on an offline node', () => {
renderCard({ nodeState: 'offline', canManage: false });
expect(screen.getByRole('button', { name: /Retry now/i })).toBeInTheDocument();
});
});