mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-31 20:58:04 +00:00
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.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Render-gate coverage for MeshOptInSheet's opt-in/out controls.
|
||||
*
|
||||
* Opting a stack in or out is admin-only on the backend
|
||||
* (POST /api/mesh/nodes/:id/stacks/:stack/opt-in|opt-out require admin). This
|
||||
* test locks the matching UI gate: a manager sees Add/Remove buttons, a
|
||||
* non-manager sees the membership read-only with a hint and still gets the
|
||||
* read-only topology affordance. The read-only branch must never issue the
|
||||
* admin-only mutation.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { MeshStackEntry } from '@/types/mesh';
|
||||
|
||||
vi.mock('@/lib/api', () => ({ apiFetch: vi.fn() }));
|
||||
vi.mock('@/components/ui/toast-store', () => ({
|
||||
toast: { error: vi.fn(), success: vi.fn(), warning: vi.fn(), info: vi.fn(), loading: vi.fn(), dismiss: vi.fn() },
|
||||
}));
|
||||
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { MeshOptInSheet } from './MeshOptInSheet';
|
||||
|
||||
const STACKS: MeshStackEntry[] = [
|
||||
{ name: 'web', optedIn: true },
|
||||
{ name: 'db', optedIn: false },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(apiFetch).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ stacks: STACKS }),
|
||||
} as unknown as Response);
|
||||
});
|
||||
|
||||
function renderSheet(canManage: boolean) {
|
||||
return render(
|
||||
<MeshOptInSheet
|
||||
open={true}
|
||||
onOpenChange={() => {}}
|
||||
nodeId={1}
|
||||
nodeName="node-alpha"
|
||||
onChanged={() => {}}
|
||||
onViewTopology={() => {}}
|
||||
canManage={canManage}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('MeshOptInSheet canManage gate', () => {
|
||||
it('shows opt-in/out controls for a manager', async () => {
|
||||
renderSheet(true);
|
||||
expect(await screen.findByText('web')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Remove from mesh/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Add to mesh/i })).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Changing mesh membership requires an administrator/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the membership read-only for a non-manager', async () => {
|
||||
renderSheet(false);
|
||||
expect(await screen.findByText('web')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /Remove from mesh/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /Add to mesh/i })).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/Changing mesh membership requires an administrator/i)).toBeInTheDocument();
|
||||
// Topology is read-only and stays available for opted-in stacks.
|
||||
expect(screen.getByRole('button', { name: /View topology for web/i })).toBeInTheDocument();
|
||||
// The read-only branch must never issue an opt-in/opt-out request.
|
||||
expect(vi.mocked(apiFetch)).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('/opt-'),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user