mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 12:21:31 +00:00
Merge Fix 11 (MED-11 discoverability): UsersPage sidebar nav entry
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -93,6 +93,20 @@
|
||||
exposed-sentinel, last-error-red-treatment, and canRefresh=false-
|
||||
hides-the-button.
|
||||
|
||||
- **UsersPage sidebar nav entry (Audit 2026-05-11 Fix 11 — MED-11
|
||||
discoverability).** The MED-11 closure shipped `UsersPage.tsx` + wired
|
||||
the `/auth/users` route in `web/src/main.tsx`, but the sidebar
|
||||
navigation never gained a corresponding entry. Operators reached the
|
||||
federated-user-admin surface (used during compliance audits — "show
|
||||
me last login for every IdP-federated user") only by knowing the URL.
|
||||
A page that exists but isn't navigable is a half-finished page. New
|
||||
Users entry under the Auth section in `web/src/components/Layout.tsx`
|
||||
sits between Sessions and Roles (federated-identity grouping). Three
|
||||
Vitest tests in `Layout.test.tsx` pin the link's presence, the
|
||||
`/auth/users` destination, and the DOM ordering relative to Sessions
|
||||
so a future refactor that re-orders or removes the entry surfaces in
|
||||
the diff.
|
||||
|
||||
- **Scope-aware actor-role revoke (Audit 2026-05-11 A-4).**
|
||||
HIGH-10 made it possible to grant the same role to the same actor at
|
||||
multiple scopes (e.g. `r-operator` on `profile=p-acme` AND `profile=p-globex`)
|
||||
|
||||
@@ -125,3 +125,58 @@ describe('Layout — UX-001 Setup guide sidebar button', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Audit 2026-05-11 Fix 11 — UsersPage sidebar nav entry (MED-11 discoverability)
|
||||
//
|
||||
// The MED-11 closure shipped UsersPage + wired the /auth/users route but left
|
||||
// the sidebar without a nav entry. Operators had to know the URL to reach the
|
||||
// federated-user-management surface. This test pins the link's presence + the
|
||||
// expected destination + the data-testid (so future E2E coverage can target it
|
||||
// without depending on visible label text — operators may rename "Users" to
|
||||
// "Federated users" later).
|
||||
//
|
||||
// We do NOT mock useAuthMe here because Layout doesn't gate nav entries on
|
||||
// permission today; every entry in the nav array renders unconditionally and
|
||||
// the target page handles its own 403 state. If Layout starts gating nav
|
||||
// entries in the future, these tests will fail at the visibility check and
|
||||
// the new gate's mock needs to be added to renderLayout().
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
describe('Layout — Fix 11 UsersPage nav entry', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders a "Users" link in the sidebar with the nav-auth-users testid', () => {
|
||||
renderLayout();
|
||||
const link = screen.getByTestId('nav-auth-users');
|
||||
expect(link).toBeInTheDocument();
|
||||
// The accessible name doubles as the operator-facing label and is what
|
||||
// future testing-library `getByRole('link', { name: /Users/i })` queries
|
||||
// will key off; pin it so a label rename surfaces in the diff.
|
||||
expect(link.textContent).toContain('Users');
|
||||
});
|
||||
|
||||
it('the Users link points at /auth/users', () => {
|
||||
renderLayout();
|
||||
const link = screen.getByTestId('nav-auth-users') as HTMLAnchorElement;
|
||||
// NavLink renders an <a href=...>; assert the destination matches the
|
||||
// route wired in web/src/main.tsx so a future re-keying of either side
|
||||
// surfaces here. We don't assert the full URL because MemoryRouter
|
||||
// prepends nothing.
|
||||
expect(link.getAttribute('href')).toBe('/auth/users');
|
||||
});
|
||||
|
||||
it('the Users link sits adjacent to the Sessions link (federated-identity grouping)', () => {
|
||||
renderLayout();
|
||||
const sessions = screen.getByRole('link', { name: /Sessions/i });
|
||||
const users = screen.getByTestId('nav-auth-users');
|
||||
// DOM order: Sessions immediately precedes Users. The placement matters
|
||||
// for the operator's mental model — both surfaces operate on the
|
||||
// federated-identity stack. If the order flips, the diff should be
|
||||
// intentional, not accidental.
|
||||
expect(sessions.compareDocumentPosition(users) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,14 @@ const nav = [
|
||||
// Bundle 2 Phase 8 — OIDC + Sessions.
|
||||
{ to: '/auth/oidc/providers', label: 'OIDC Providers', icon: 'M12 11c0 3.517-1.009 6.799-2.753 9.571m-3.44-2.04l.054-.09A13.916 13.916 0 008 11a4 4 0 118 0c0 1.017-.07 2.019-.203 3m-2.118 6.844A21.88 21.88 0 0015.171 17m3.839 1.132c.645-2.266.99-4.659.99-7.132A8 8 0 008 4.07M3 15.364c.64-1.319 1-2.8 1-4.364 0-1.457.39-2.823 1.07-4' },
|
||||
{ to: '/auth/sessions', label: 'Sessions', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z' },
|
||||
// Audit 2026-05-11 Fix 11 — UsersPage sidebar entry (MED-11 discoverability).
|
||||
// The MED-11 closure wired UsersPage but no nav entry; operators had to know
|
||||
// the URL /auth/users to reach the federated-user-management surface. This
|
||||
// entry sits adjacent to Sessions because the two share the same mental
|
||||
// model (federated identity admin). UsersPage handles its own 403 state for
|
||||
// callers without auth.user.read so we don't need to gate the nav entry;
|
||||
// every other entry in this array uses the same unconditional pattern.
|
||||
{ to: '/auth/users', label: 'Users', icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z', testID: 'nav-auth-users' },
|
||||
{ to: '/auth/roles', label: 'Roles', icon: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z' },
|
||||
{ to: '/auth/keys', label: 'API Keys', icon: 'M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z' },
|
||||
{ to: '/auth/approvals', label: 'Approvals', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' },
|
||||
@@ -76,6 +84,7 @@ export default function Layout() {
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
end={item.to === '/'}
|
||||
data-testid={'testID' in item ? item.testID : undefined}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-3 py-2 text-[13px] rounded transition-all duration-150 ${
|
||||
isActive
|
||||
|
||||
Reference in New Issue
Block a user