mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 23:06:49 +00:00
8980910153
* feat: add node-scoped Networking operator page Adds a Networking view with overview, topology, inventory, and findings. Shared aggregate reads back the page; Resources keeps prune and redirects here. Includes fail-closed network delete guards, operator docs, and /nodes/:slug/networking routing. * fix: rename unused variable n to _n to satisfy no-unused-vars lint * fix: keep top bar search clickable when nav grows * feat: complete Compose-first Networking Phase 2 operator assistant * fix: move networking action visibility helper out of component module * feat(networking): complete Compose-first Networking operator page Finish the node-scoped Networking page (Overview, Networks, Topology, Findings) with design-system parity and correct finding semantics. - Rebuild detail sheets on SystemSheet/SheetSection; align the tab band, masthead, and mobile tone with Fleet and Security. - Encode the host-mode and exposure severity matrix; fix collision counts so intentional shared externals are not flagged; add one typed drift predicate shared by inventory, topology, badges, and overview counts. - Preserve per-container attachments and IPs on topology node clicks; drawer-only click with an explicit logs action; ownership and boolean filters; bound large graphs before layout. - Aggregate cached Compose Doctor findings into the Findings tab with honest source labels, structural merge and dedupe, staleness reconciliation, and a shared exposure-context helper both engines use. - Networks tab: privacy-safe service search, precise ownership counts, schema v3 with version-2 adapters on every endpoint, pre-confirm delete reasons, and the shared sortable table with an internal scroll region. - Interop: Fleet node-card networking signal with pending-intent navigation, stack-to-node backlink, and Dossier/Drift deep links. - Enrich sanitized inspect with an allowlisted connected-container list; fetch topology once and filter client-side. - Docs and tests across every new finding kind, adapter, and flow. * fix(networking): correct drift count, exposure fail-soft, and inspect crash paths Address code-review findings on the Networking page implementation: - Fix the Overview drift count to use the shared drift-kind predicate instead of a hardcoded list that omitted external-network-missing. - Gate Compose Doctor's unclassified-exposure and reverse-proxy-undocumented rules on exposure-context availability, so a DB read failure no longer fabricates findings (mirrors the live engine's existing fail-soft behavior). - Guard the per-stack exposure-intent read in topology aggregation so a transient DB failure degrades to unknown intent instead of failing the whole response. - Harden the network detail drawer against a partial inspect payload from an older remote node, and log the real error instead of a bare catch. - Remove now-duplicated severity-rank and drift-kind helpers in favor of the shared modules; drop dead backend-only exports; widen the frontend schema version type to a plain number instead of casting past a literal type. - Add coverage for the delete-guard precedence, the full host-mode severity matrix, the schema-2 compatibility adapter, and the sanitized connected- container allowlist; tighten two tests that were not exercising the behavior they claimed to. * fix: add missing onOpenNodeNetworking prop to FleetView experimental test The added required prop on FleetViewProps broke the merge-build when the test file (on main but not on this branch) was compiled against the updated FleetView interface.
177 lines
6.9 KiB
TypeScript
177 lines
6.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
import { FleetView } from '../FleetView';
|
|
|
|
const useExperimentalMock = vi.fn(() => ({ experimental: true, experimentalReady: true }));
|
|
vi.mock('@/hooks/useExperimental', () => ({
|
|
useExperimental: () => useExperimentalMock(),
|
|
}));
|
|
|
|
vi.mock('@/context/LicenseContext', () => ({
|
|
useLicense: () => ({ isPaid: true }),
|
|
}));
|
|
vi.mock('@/context/AuthContext', () => ({
|
|
useAuth: () => ({ isAdmin: true }),
|
|
}));
|
|
vi.mock('@/context/NodeContext', () => ({
|
|
useNodes: () => ({ hasCapability: () => false }),
|
|
}));
|
|
|
|
vi.mock('../FleetView/hooks/useFleetPreferences', () => ({
|
|
useFleetPreferences: () => ({ prefs: {}, updatePrefs: vi.fn() }),
|
|
}));
|
|
vi.mock('../FleetView/hooks/useFleetUpdateStatus', () => ({
|
|
useFleetUpdateStatus: () => ({
|
|
updateStatuses: [],
|
|
localUpdateConfirm: null,
|
|
setShowUpdateModal: vi.fn(),
|
|
fetchUpdateStatus: vi.fn(),
|
|
showUpdateModal: false,
|
|
checkingUpdates: false,
|
|
updatingNodeId: null,
|
|
reconnecting: false,
|
|
preUpdateStartedAt: null,
|
|
triggerNodeUpdate: vi.fn(),
|
|
retryNodeUpdate: vi.fn(),
|
|
dismissNodeUpdate: vi.fn(),
|
|
checkUpdates: vi.fn(),
|
|
confirmLocalUpdate: vi.fn(),
|
|
cancelLocalUpdate: vi.fn(),
|
|
}),
|
|
}));
|
|
vi.mock('../FleetView/hooks/useFleetOverview', () => ({
|
|
useFleetOverview: () => ({
|
|
nodes: [],
|
|
processedNodes: [],
|
|
allNodes: [],
|
|
topologyNodes: [],
|
|
viewMode: 'cards',
|
|
setViewMode: vi.fn(),
|
|
searchQuery: '',
|
|
setSearchQuery: vi.fn(),
|
|
fleetPalette: {},
|
|
labelFilters: {},
|
|
setLabelFilters: vi.fn(),
|
|
clearFilters: vi.fn(),
|
|
fleetStackLabelMap: {},
|
|
updateStatusMap: {},
|
|
mastheadStats: {
|
|
nodeCount: 0,
|
|
onlineCount: 0,
|
|
criticalCount: 0,
|
|
avgCpuNum: 0,
|
|
worstCpu: 0,
|
|
totalMemUsed: 0,
|
|
totalMemTotal: 0,
|
|
totalContainers: 0,
|
|
totalContainersAll: 0,
|
|
},
|
|
lastSyncAt: null,
|
|
loading: false,
|
|
refreshing: false,
|
|
fetchOverview: vi.fn(),
|
|
}),
|
|
}));
|
|
vi.mock('../FleetView/hooks/useFleetPolling', () => ({
|
|
useFleetPolling: () => {},
|
|
}));
|
|
vi.mock('../FleetView/hooks/useFleetDossierExport', () => ({
|
|
useFleetDossierExport: () => ({ exporting: false, exportDossier: vi.fn() }),
|
|
}));
|
|
vi.mock('@/hooks/useTopologyPreferences', () => ({
|
|
useTopologyPreferences: () => ({ prefs: { mode: 'hub', positions: {} }, setMode: vi.fn(), setPositions: vi.fn() }),
|
|
}));
|
|
vi.mock('../nodes/useNodeActions', () => ({
|
|
useNodeActions: () => ({ openEdit: vi.fn(), openDelete: vi.fn(), NodeActionModals: null }),
|
|
}));
|
|
vi.mock('../fleet/FleetMasthead', () => ({ FleetMasthead: () => <div data-testid="masthead" /> }));
|
|
vi.mock('../FleetView/OverviewTab', () => ({ OverviewTab: () => <div data-testid="overview-tab" /> }));
|
|
vi.mock('../FleetView/ReconnectingOverlay', () => ({ ReconnectingOverlay: () => null }));
|
|
vi.mock('../FleetView/NodeUpdatesSheet', () => ({ NodeUpdatesSheet: () => null }));
|
|
vi.mock('../FleetView/LocalUpdateConfirmDialog', () => ({ LocalUpdateConfirmDialog: () => null }));
|
|
vi.mock('../FleetSnapshots', () => ({ default: () => null }));
|
|
vi.mock('../fleet/FleetConfiguration', () => ({ FleetConfiguration: () => null }));
|
|
vi.mock('../fleet/RoutingTab', () => ({ RoutingTab: () => <div data-testid="routing-tab" /> }));
|
|
vi.mock('../fleet/FederationTab', () => ({ FederationTab: () => <div data-testid="federation-tab" /> }));
|
|
vi.mock('../blueprints/DeploymentsTab', () => ({ DeploymentsTab: () => <div data-testid="deployments-tab" /> }));
|
|
vi.mock('../fleet/FleetActions/FleetActionsTab', () => ({ FleetActionsTab: () => <div data-testid="actions-tab" /> }));
|
|
vi.mock('../fleet/secrets/SecretsTab', () => ({ SecretsTab: () => <div data-testid="secrets-tab" /> }));
|
|
vi.mock('../fleet/DependencyMapTab', () => ({ DependencyMapTab: () => null }));
|
|
vi.mock('../fleet/ContainerLabelsTab', () => ({ ContainerLabelsTab: () => null }));
|
|
vi.mock('../PaidGate', () => ({ PaidGate: ({ children }: { children: React.ReactNode }) => <>{children}</> }));
|
|
|
|
describe('FleetView experimental discovery', () => {
|
|
beforeEach(() => {
|
|
useExperimentalMock.mockReturnValue({ experimental: true, experimentalReady: true });
|
|
});
|
|
|
|
it('shows Routing and Secrets when experimental discovery is on for paid admin', () => {
|
|
render(<FleetView onNavigateToNode={vi.fn()} onOpenNodeNetworking={vi.fn()} />);
|
|
expect(screen.getByRole('tab', { name: /routing/i })).toBeTruthy();
|
|
expect(screen.getByRole('tab', { name: /secrets/i })).toBeTruthy();
|
|
});
|
|
|
|
it('hides Routing and Secrets when experimental discovery is off', () => {
|
|
useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: true });
|
|
render(<FleetView onNavigateToNode={vi.fn()} onOpenNodeNetworking={vi.fn()} />);
|
|
expect(screen.queryByRole('tab', { name: /routing/i })).toBeNull();
|
|
expect(screen.queryByRole('tab', { name: /secrets/i })).toBeNull();
|
|
expect(screen.getByRole('tab', { name: /deployments/i })).toBeTruthy();
|
|
expect(screen.getByRole('tab', { name: /federation/i })).toBeTruthy();
|
|
expect(screen.getByRole('tab', { name: /actions/i })).toBeTruthy();
|
|
});
|
|
|
|
it('delayed experimental true preserves controlled routing without Overview callback', async () => {
|
|
useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: false });
|
|
const onTab = vi.fn();
|
|
const { rerender } = render(
|
|
<FleetView
|
|
onNavigateToNode={vi.fn()}
|
|
onOpenNodeNetworking={vi.fn()}
|
|
fleetActiveTab="routing"
|
|
onFleetActiveTabChange={onTab}
|
|
/>,
|
|
);
|
|
expect(screen.queryByRole('tab', { name: /routing/i })).toBeNull();
|
|
expect(onTab).not.toHaveBeenCalled();
|
|
|
|
useExperimentalMock.mockReturnValue({ experimental: true, experimentalReady: true });
|
|
rerender(
|
|
<FleetView
|
|
onNavigateToNode={vi.fn()}
|
|
onOpenNodeNetworking={vi.fn()}
|
|
fleetActiveTab="routing"
|
|
onFleetActiveTabChange={onTab}
|
|
/>,
|
|
);
|
|
await waitFor(() => expect(screen.getByRole('tab', { name: /routing/i })).toBeTruthy());
|
|
expect(onTab).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('resolved experimental false falls back controlled routing to overview once', async () => {
|
|
useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: false });
|
|
const onTab = vi.fn();
|
|
const { rerender } = render(
|
|
<FleetView
|
|
onNavigateToNode={vi.fn()}
|
|
onOpenNodeNetworking={vi.fn()}
|
|
fleetActiveTab="routing"
|
|
onFleetActiveTabChange={onTab}
|
|
/>,
|
|
);
|
|
expect(onTab).not.toHaveBeenCalled();
|
|
|
|
useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: true });
|
|
rerender(
|
|
<FleetView
|
|
onNavigateToNode={vi.fn()}
|
|
onOpenNodeNetworking={vi.fn()}
|
|
fleetActiveTab="routing"
|
|
onFleetActiveTabChange={onTab}
|
|
/>,
|
|
);
|
|
await waitFor(() => expect(onTab).toHaveBeenCalledWith('overview'));
|
|
expect(onTab).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|