mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-20 23:32:19 +00:00
c82a39c65a
* 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.
106 lines
4.7 KiB
TypeScript
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();
|
|
});
|
|
});
|