test(dashboard): cover dashboard routes, ConfigurationStatus tier parity, and useMeshDataPlane (#1221)

* test(dashboard): cover dashboard routes, ConfigurationStatus tier parity, and useMeshDataPlane

The dashboard router had no dedicated Vitest coverage; tier parity in the
ConfigurationStatus component was only proved by manual inspection; and the
Admiral short-circuit in useMeshDataPlane had no automated regression net.

Add three spec files:

- backend/src/__tests__/dashboard-routes.test.ts: 11 cases against the live
  Express app. Both routes reject unauthenticated requests; the
  configuration response matches its documented shape; the tier x variant
  `locked` matrix is asserted end-to-end for Community, Skipper, and
  Admiral via LicenseService spies; a seeded Discord agent URL is shown
  never to appear in the serialized response; /stack-restarts clamps days
  values of 0, 999, and NaN without bailing.

- frontend/src/components/dashboard/__tests__/ConfigurationStatus.test.tsx:
  five render cases prove the parity contract. Community hides the entire
  Automation section plus the four gated rows (Notification routing,
  Webhooks, Scheduled tasks, Vulnerability scanning); Skipper shows
  everything except Scheduled tasks (Admiral-only); Admiral shows every
  gated row plus the SSO provider name mapping (oidc_google -> "Google").
  Skeleton and load-error paths are also covered.

- frontend/src/components/dashboard/__tests__/useMeshDataPlane.test.tsx:
  four hook cases prove the Admiral short-circuit. Non-Admiral sessions
  never fire /mesh/status; Admiral sessions fetch once and populate the
  localDataPlane payload; a 403 response leaves status null without
  raising; a response that omits localDataPlane also leaves status null.

Backend route suite + dashboard-only frontend suite green in isolation.
The full backend suite shows one pre-existing Windows-only EBUSY flake in
filesystem-backup.test.ts (SQLite file lock on unlink) that reproduces on
the unmodified branch tip and is unrelated to these changes.

* test(dashboard): drop backup.requiredTier from ConfigurationStatus fixture

The fixture's `backup.requiredTier: 'admiral'` field was authored to match
the type on this branch's original base. Main has since removed that field
from the ConfigurationStatus payload, so the fixture now over-specifies a
property the type forbids and fails tsc.

Drop the field to realign with the current type.
This commit is contained in:
Anso
2026-05-25 12:14:33 -04:00
committed by GitHub
parent 03a5826f7e
commit 05c3975d6d
3 changed files with 438 additions and 0 deletions
@@ -0,0 +1,94 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { renderHook, act } from '@testing-library/react';
const apiFetchMock = vi.fn();
vi.mock('@/lib/api', () => ({
apiFetch: (...args: unknown[]) => apiFetchMock(...args),
}));
const useAuthMock = vi.fn();
vi.mock('@/context/AuthContext', () => ({
useAuth: () => useAuthMock(),
}));
vi.mock('@/lib/utils', async () => {
const actual = await vi.importActual<typeof import('@/lib/utils')>('@/lib/utils');
return {
...actual,
visibilityInterval: () => () => {},
};
});
import { useMeshDataPlane } from '../useMeshDataPlane';
function okJson(payload: unknown): Response {
return new Response(JSON.stringify(payload), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
function statusJson(status: number, payload: unknown = {}): Response {
return new Response(JSON.stringify(payload), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
beforeEach(() => {
apiFetchMock.mockReset();
useAuthMock.mockReset();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('useMeshDataPlane', () => {
it('does not fetch /mesh/status when the session is non-Admiral', async () => {
useAuthMock.mockReturnValue({ permissions: { isAdmiral: false } });
const { result } = renderHook(() => useMeshDataPlane());
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
expect(apiFetchMock).not.toHaveBeenCalled();
expect(result.current.status).toBeNull();
expect(result.current.loading).toBe(false);
});
it('fetches once on mount and surfaces the localDataPlane payload for Admiral', async () => {
useAuthMock.mockReturnValue({ permissions: { isAdmiral: true } });
apiFetchMock.mockResolvedValue(okJson({
localDataPlane: { ok: true, reason: null, lastChecked: 1000 },
}));
const { result } = renderHook(() => useMeshDataPlane());
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
expect(apiFetchMock).toHaveBeenCalledWith('/mesh/status', expect.objectContaining({ localOnly: true }));
expect(result.current.status).toEqual({ ok: true, reason: null, lastChecked: 1000 });
expect(result.current.loading).toBe(false);
});
it('keeps status null on a 403 response without raising an error', async () => {
useAuthMock.mockReturnValue({ permissions: { isAdmiral: true } });
apiFetchMock.mockResolvedValue(statusJson(403, { error: 'forbidden' }));
const { result } = renderHook(() => useMeshDataPlane());
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
expect(result.current.status).toBeNull();
expect(result.current.loading).toBe(false);
});
it('falls back to null when the response omits localDataPlane', async () => {
useAuthMock.mockReturnValue({ permissions: { isAdmiral: true } });
apiFetchMock.mockResolvedValue(okJson({ nodes: [] }));
const { result } = renderHook(() => useMeshDataPlane());
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
expect(result.current.status).toBeNull();
expect(result.current.loading).toBe(false);
});
});