feat: display human-readable user and device names in network management
This commit is contained in:
+1
-2
@@ -1,8 +1,7 @@
|
||||
# ===========================================
|
||||
# Secuird UI Configuration
|
||||
# ===========================================
|
||||
# Copy this file to .env.local for local development
|
||||
# or use mode-specific env files (.env.development, .env.staging, .env.production)
|
||||
# Copy this file to .env for your environment
|
||||
|
||||
# API Configuration
|
||||
VITE_API_BASE_URL=https://api.gatehouse.local/api/v1
|
||||
|
||||
@@ -2106,6 +2106,8 @@ export interface UserNetworkApproval {
|
||||
id: string;
|
||||
organization_id: string;
|
||||
user_id: string;
|
||||
user_name: string | null;
|
||||
user_email: string | null;
|
||||
portal_network_id: string;
|
||||
granted_by_user_id: string | null;
|
||||
grant_type: ApprovalGrantType;
|
||||
@@ -2121,7 +2123,11 @@ export interface DeviceNetworkMembership {
|
||||
id: string;
|
||||
organization_id: string;
|
||||
user_id: string;
|
||||
user_name: string | null;
|
||||
user_email: string | null;
|
||||
device_id: string;
|
||||
device_name: string | null;
|
||||
device_node_id: string | null;
|
||||
portal_network_id: string;
|
||||
user_network_approval_id: string | null;
|
||||
active: boolean;
|
||||
|
||||
@@ -621,7 +621,10 @@ export default function NetworkManagementPage() {
|
||||
<Users className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate font-mono text-sm">{userId}</p>
|
||||
<p className="font-medium truncate text-sm">{userMemberships[0]?.user_name || userId}</p>
|
||||
{userMemberships[0]?.user_email && (
|
||||
<p className="text-xs text-muted-foreground">{userMemberships[0].user_email}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{userMemberships.length} device{userMemberships.length !== 1 ? "s" : ""}
|
||||
{activeCount > 0 && <span className="text-green-600 ml-2">{activeCount} active</span>}
|
||||
@@ -652,7 +655,10 @@ export default function NetworkManagementPage() {
|
||||
<div key={m.id} className="flex items-start gap-3 p-3 pl-11">
|
||||
<Monitor className="w-4 h-4 text-muted-foreground flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0 space-y-1">
|
||||
<p className="text-sm font-medium truncate">{m.device_id}</p>
|
||||
<p className="text-sm font-medium truncate">{m.device_name || m.device_id}</p>
|
||||
{m.device_node_id && (
|
||||
<p className="text-xs text-muted-foreground font-mono">{m.device_node_id}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<ActiveBadge active={m.active} />
|
||||
<Badge variant="outline" className="text-xs text-muted-foreground">
|
||||
@@ -782,10 +788,13 @@ export default function NetworkManagementPage() {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<p className="font-medium font-mono text-sm">{r.user_id}</p>
|
||||
<p className="font-medium text-sm">{r.user_name || r.user_id}</p>
|
||||
<ApprovalStateBadge state={r.state} />
|
||||
<Badge variant="outline" className="text-xs capitalize">{r.grant_type}</Badge>
|
||||
</div>
|
||||
{r.user_email && (
|
||||
<p className="text-xs text-muted-foreground">{r.user_email}</p>
|
||||
)}
|
||||
{r.justification && (
|
||||
<p className="text-xs text-muted-foreground mt-1">"{r.justification}"</p>
|
||||
)}
|
||||
@@ -1021,7 +1030,7 @@ export default function NetworkManagementPage() {
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Remove all devices for this user?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will remove all memberships for user <span className="font-mono font-medium">{confirmRemoveUser}</span>. All of their devices will lose access to this network.
|
||||
This will remove all memberships for user <span className="font-medium">{members.find(m => m.user_id === confirmRemoveUser)?.user_name || confirmRemoveUser}</span>. All of their devices will lose access to this network.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
||||
@@ -658,7 +658,11 @@ describe("NetworkManagementPage", () => {
|
||||
id: "mem-auth-1",
|
||||
organization_id: "org-1",
|
||||
user_id: "user-a",
|
||||
user_name: "Alice Smith",
|
||||
user_email: "alice@test.com",
|
||||
device_id: "dev-laptop-1",
|
||||
device_name: "Alice Laptop",
|
||||
device_node_id: "abc123",
|
||||
portal_network_id: "net-abc",
|
||||
user_network_approval_id: null,
|
||||
active: true,
|
||||
@@ -692,7 +696,11 @@ describe("NetworkManagementPage", () => {
|
||||
id: "mem-unauth-1",
|
||||
organization_id: "org-1",
|
||||
user_id: "user-a",
|
||||
user_name: "Alice Smith",
|
||||
user_email: "alice@test.com",
|
||||
device_id: "dev-desktop-1",
|
||||
device_name: "Alice Desktop",
|
||||
device_node_id: "def456",
|
||||
portal_network_id: "net-abc",
|
||||
user_network_approval_id: null,
|
||||
active: false,
|
||||
@@ -711,7 +719,11 @@ describe("NetworkManagementPage", () => {
|
||||
id: "mem-auth-2",
|
||||
organization_id: "org-1",
|
||||
user_id: "user-b",
|
||||
user_name: "Bob Jones",
|
||||
user_email: "bob@test.com",
|
||||
device_id: "dev-phone-1",
|
||||
device_name: "Bob Phone",
|
||||
device_node_id: "ghi789",
|
||||
portal_network_id: "net-abc",
|
||||
user_network_approval_id: null,
|
||||
active: false,
|
||||
@@ -730,7 +742,11 @@ describe("NetworkManagementPage", () => {
|
||||
id: "mem-pending-1",
|
||||
organization_id: "org-1",
|
||||
user_id: "user-c",
|
||||
user_name: "Charlie Brown",
|
||||
user_email: "charlie@test.com",
|
||||
device_id: "dev-server-1",
|
||||
device_name: "Charlie Server",
|
||||
device_node_id: "jkl012",
|
||||
portal_network_id: "net-abc",
|
||||
user_network_approval_id: null,
|
||||
active: false,
|
||||
@@ -874,8 +890,8 @@ describe("NetworkManagementPage", () => {
|
||||
test("groups memberships by user_id and renders user sections", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION, MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
// Both memberships belong to user-a → one user section
|
||||
expect(screen.getByText("user-a")).toBeDefined();
|
||||
// Both memberships belong to Alice Smith → one user section
|
||||
expect(screen.getByText("Alice Smith")).toBeDefined();
|
||||
// Should show "2 devices"
|
||||
expect(screen.getByText(/2 devices/)).toBeDefined();
|
||||
});
|
||||
@@ -883,15 +899,14 @@ describe("NetworkManagementPage", () => {
|
||||
test("renders multiple user sections for different user_ids", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION, MEMBERSHIP_SECOND_USER]);
|
||||
|
||||
expect(screen.getByText("user-a")).toBeDefined();
|
||||
expect(screen.getByText("user-b")).toBeDefined();
|
||||
expect(screen.getByText("Alice Smith")).toBeDefined();
|
||||
expect(screen.getByText("Bob Jones")).toBeDefined();
|
||||
});
|
||||
|
||||
test("displays user_id in monospace font (font-mono class)", async () => {
|
||||
test("displays user_name when available", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userIdElement = screen.getByText("user-a");
|
||||
expect(userIdElement.className).toContain("font-mono");
|
||||
expect(screen.getByText("Alice Smith")).toBeDefined();
|
||||
});
|
||||
|
||||
// ── Expand / Collapse ───────────────────────────────────────────────────────
|
||||
@@ -900,31 +915,31 @@ describe("NetworkManagementPage", () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION, MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
// Devices should NOT be visible before expanding
|
||||
expect(screen.queryByText("dev-laptop-1")).toBeNull();
|
||||
expect(screen.queryByText("dev-desktop-1")).toBeNull();
|
||||
expect(screen.queryByText("Alice Laptop")).toBeNull();
|
||||
expect(screen.queryByText("Alice Desktop")).toBeNull();
|
||||
|
||||
// Click the user section to expand
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
expect(userButton).not.toBeNull();
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
// Now devices should be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("dev-laptop-1")).toBeDefined();
|
||||
expect(screen.getByText("Alice Laptop")).toBeDefined();
|
||||
});
|
||||
expect(screen.getByText("dev-desktop-1")).toBeDefined();
|
||||
expect(screen.getByText("Alice Desktop")).toBeDefined();
|
||||
});
|
||||
|
||||
test("clicking expanded user section collapses devices", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
expect(userButton).not.toBeNull();
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
// Devices should be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("dev-laptop-1")).toBeDefined();
|
||||
expect(screen.getByText("Alice Laptop")).toBeDefined();
|
||||
});
|
||||
|
||||
// Click again to collapse
|
||||
@@ -932,36 +947,36 @@ describe("NetworkManagementPage", () => {
|
||||
|
||||
// Devices should disappear
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("dev-laptop-1")).toBeNull();
|
||||
expect(screen.queryByText("Alice Laptop")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test("expand/collapse is independent per user section", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION, MEMBERSHIP_SECOND_USER]);
|
||||
|
||||
const userAButton = screen.getByText("user-a").closest("button")!;
|
||||
const userBButton = screen.getByText("user-b").closest("button")!;
|
||||
const userAButton = screen.getByText("Alice Smith").closest("button")!;
|
||||
const userBButton = screen.getByText("Bob Jones").closest("button")!;
|
||||
|
||||
// Expand user-a only
|
||||
// Expand Alice Smith only
|
||||
fireEvent.click(userAButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("dev-laptop-1")).toBeDefined();
|
||||
expect(screen.getByText("Alice Laptop")).toBeDefined();
|
||||
});
|
||||
// user-b's device should still be hidden
|
||||
expect(screen.queryByText("dev-phone-1")).toBeNull();
|
||||
// Bob Jones' device should still be hidden
|
||||
expect(screen.queryByText("Bob Phone")).toBeNull();
|
||||
|
||||
// Expand user-b too
|
||||
// Expand Bob Jones too
|
||||
fireEvent.click(userBButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("dev-phone-1")).toBeDefined();
|
||||
expect(screen.getByText("Bob Phone")).toBeDefined();
|
||||
});
|
||||
|
||||
// Collapse user-a — user-b should remain expanded
|
||||
// Collapse Alice Smith — Bob Jones should remain expanded
|
||||
fireEvent.click(userAButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("dev-laptop-1")).toBeNull();
|
||||
expect(screen.queryByText("Alice Laptop")).toBeNull();
|
||||
});
|
||||
expect(screen.getByText("dev-phone-1")).toBeDefined();
|
||||
expect(screen.getByText("Bob Phone")).toBeDefined();
|
||||
});
|
||||
|
||||
// ── Device Details ──────────────────────────────────────────────────────────
|
||||
@@ -969,7 +984,7 @@ describe("NetworkManagementPage", () => {
|
||||
test("renders Active badge for active_authorized membership", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -980,7 +995,7 @@ describe("NetworkManagementPage", () => {
|
||||
test("renders Inactive badge for approved_inactive membership", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -991,7 +1006,7 @@ describe("NetworkManagementPage", () => {
|
||||
test("renders Inactive badge for pending_request membership", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_PENDING_REQUEST]);
|
||||
|
||||
const userButton = screen.getByText("user-c").closest("button");
|
||||
const userButton = screen.getByText("Charlie Brown").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1002,7 +1017,7 @@ describe("NetworkManagementPage", () => {
|
||||
test("renders Inactive badge for second user (approved_inactive)", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_SECOND_USER]);
|
||||
|
||||
const userButton = screen.getByText("user-b").closest("button");
|
||||
const userButton = screen.getByText("Bob Jones").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1015,7 +1030,7 @@ describe("NetworkManagementPage", () => {
|
||||
test("shows 'Active' badge when membership is active", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1027,7 +1042,7 @@ describe("NetworkManagementPage", () => {
|
||||
test("shows 'Inactive' badge when membership is not active", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1040,7 +1055,7 @@ describe("NetworkManagementPage", () => {
|
||||
test("shows session progress bar when active_session is present and is_active", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1051,7 +1066,7 @@ describe("NetworkManagementPage", () => {
|
||||
test("does NOT show session progress when active_session is null", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1066,7 +1081,7 @@ describe("NetworkManagementPage", () => {
|
||||
test("shows join_seen as 'Yes' when true", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1077,7 +1092,7 @@ describe("NetworkManagementPage", () => {
|
||||
test("shows join_seen as 'No' when false", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1090,7 +1105,7 @@ describe("NetworkManagementPage", () => {
|
||||
test("renders Deactivate button for active memberships", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1102,7 +1117,7 @@ describe("NetworkManagementPage", () => {
|
||||
test("renders Activate button for approved-but-inactive memberships", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1120,7 +1135,7 @@ describe("NetworkManagementPage", () => {
|
||||
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
const activateBtn = await screen.findByRole("button", { name: "Activate" });
|
||||
@@ -1146,7 +1161,7 @@ describe("NetworkManagementPage", () => {
|
||||
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
const deactivateBtn = await screen.findByRole("button", { name: "Deactivate" });
|
||||
@@ -1164,7 +1179,7 @@ describe("NetworkManagementPage", () => {
|
||||
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
const activateBtn = await screen.findByRole("button", { name: "Activate" });
|
||||
@@ -1190,7 +1205,7 @@ describe("NetworkManagementPage", () => {
|
||||
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
const deactivateBtn = await screen.findByRole("button", { name: "Deactivate" });
|
||||
@@ -1207,7 +1222,7 @@ describe("NetworkManagementPage", () => {
|
||||
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
const activateBtn = await screen.findByRole("button", { name: "Activate" });
|
||||
@@ -1234,7 +1249,7 @@ describe("NetworkManagementPage", () => {
|
||||
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
const deactivateBtn = await screen.findByRole("button", { name: "Deactivate" });
|
||||
@@ -1255,7 +1270,7 @@ describe("NetworkManagementPage", () => {
|
||||
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
const activateBtn = await screen.findByRole("button", { name: "Activate" });
|
||||
@@ -1290,7 +1305,7 @@ describe("NetworkManagementPage", () => {
|
||||
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
const activateBtn = await screen.findByRole("button", { name: "Activate" });
|
||||
@@ -1324,7 +1339,7 @@ describe("NetworkManagementPage", () => {
|
||||
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
const userButton = screen.getByText("Alice Smith").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
const deactivateBtn = await screen.findByRole("button", { name: "Deactivate" });
|
||||
@@ -1385,7 +1400,9 @@ describe("NetworkManagementPage", () => {
|
||||
...MEMBERSHIP_AUTHORIZED_WITH_SESSION,
|
||||
id: "mem-uni-1",
|
||||
user_id: "user-äéîøü-中文",
|
||||
user_name: "Unicode User",
|
||||
device_id: "dev-utf8",
|
||||
device_name: null,
|
||||
};
|
||||
H.mockGetNetwork.mockResolvedValue({ network: DEV_NETWORK });
|
||||
H.mockGetNetworkMembers.mockResolvedValue({ memberships: [unicodeMembership], count: 1 });
|
||||
@@ -1401,7 +1418,7 @@ describe("NetworkManagementPage", () => {
|
||||
fireEvent.keyDown(membersTab, { key: "Enter" });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("user-äéîøü-中文")).toBeDefined();
|
||||
expect(screen.getByText("Unicode User")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1412,7 +1429,9 @@ describe("NetworkManagementPage", () => {
|
||||
...MEMBERSHIP_AUTHORIZED_WITH_SESSION,
|
||||
id: "mem-xss-1",
|
||||
user_id: "user-xss",
|
||||
user_name: null,
|
||||
device_id: "<script>alert(1)</script>",
|
||||
device_name: null,
|
||||
};
|
||||
H.mockGetNetwork.mockResolvedValue({ network: DEV_NETWORK });
|
||||
H.mockGetNetworkMembers.mockResolvedValue({ memberships: [xssMembership], count: 1 });
|
||||
@@ -1444,7 +1463,10 @@ describe("NetworkManagementPage", () => {
|
||||
...MEMBERSHIP_AUTHORIZED_WITH_SESSION,
|
||||
id: `mem-many-${i}`,
|
||||
user_id: `user-${i}`,
|
||||
user_name: `User ${i}`,
|
||||
user_email: `user${i}@test.com`,
|
||||
device_id: `dev-${i}`,
|
||||
device_name: `Device ${i}`,
|
||||
active_session: null,
|
||||
}));
|
||||
H.mockGetNetwork.mockResolvedValue({ network: DEV_NETWORK });
|
||||
@@ -1465,8 +1487,8 @@ describe("NetworkManagementPage", () => {
|
||||
});
|
||||
|
||||
// Should find at least the first and last user
|
||||
expect(screen.getByText("user-0")).toBeDefined();
|
||||
expect(screen.getByText("user-49")).toBeDefined();
|
||||
expect(screen.getByText("User 0")).toBeDefined();
|
||||
expect(screen.getByText("User 49")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1479,6 +1501,8 @@ describe("NetworkManagementPage", () => {
|
||||
id: "req-pending-1",
|
||||
organization_id: "org-1",
|
||||
user_id: "user-requestor",
|
||||
user_name: "Requestor User",
|
||||
user_email: "requestor@test.com",
|
||||
portal_network_id: "net-abc",
|
||||
granted_by_user_id: null,
|
||||
grant_type: "requested" as const,
|
||||
@@ -1493,6 +1517,8 @@ describe("NetworkManagementPage", () => {
|
||||
id: "req-approved-1",
|
||||
organization_id: "org-1",
|
||||
user_id: "user-approved",
|
||||
user_name: "Approved User",
|
||||
user_email: "approved@test.com",
|
||||
portal_network_id: "net-abc",
|
||||
granted_by_user_id: "user-admin",
|
||||
grant_type: "assigned" as const,
|
||||
@@ -1507,6 +1533,8 @@ describe("NetworkManagementPage", () => {
|
||||
id: "req-rejected-1",
|
||||
organization_id: "org-1",
|
||||
user_id: "user-rejected",
|
||||
user_name: "Rejected User",
|
||||
user_email: "rejected@test.com",
|
||||
portal_network_id: "net-abc",
|
||||
granted_by_user_id: null,
|
||||
grant_type: "requested" as const,
|
||||
@@ -1670,12 +1698,16 @@ describe("NetworkManagementPage", () => {
|
||||
|
||||
// ── Request list display ────────────────────────────────────────────────────
|
||||
|
||||
test("displays user_id in monospace font", async () => {
|
||||
test("displays user_name when available", async () => {
|
||||
await setupRequestsTab([PENDING_REQUEST]);
|
||||
|
||||
const userIdElement = screen.getByText("user-requestor");
|
||||
expect(userIdElement).toBeDefined();
|
||||
expect(userIdElement.className).toContain("font-mono");
|
||||
expect(screen.getByText("Requestor User")).toBeDefined();
|
||||
});
|
||||
|
||||
test("displays grant_type badge", async () => {
|
||||
await setupRequestsTab([PENDING_REQUEST]);
|
||||
|
||||
expect(screen.getByText("requested")).toBeDefined();
|
||||
});
|
||||
|
||||
test("displays grant_type badge", async () => {
|
||||
@@ -1694,7 +1726,7 @@ describe("NetworkManagementPage", () => {
|
||||
await setupRequestsTab([APPROVED_REQUEST]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("user-approved")).toBeDefined();
|
||||
expect(screen.getByText("Approved User")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.queryByText(/"/)).toBeNull();
|
||||
@@ -2019,15 +2051,15 @@ describe("NetworkManagementPage", () => {
|
||||
test("renders multiple requests in the list", async () => {
|
||||
await setupRequestsTab([PENDING_REQUEST, APPROVED_REQUEST]);
|
||||
|
||||
expect(screen.getByText("user-requestor")).toBeDefined();
|
||||
expect(screen.getByText("user-approved")).toBeDefined();
|
||||
expect(screen.getByText("Requestor User")).toBeDefined();
|
||||
expect(screen.getByText("Approved User")).toBeDefined();
|
||||
});
|
||||
|
||||
test("mixed list: only pending requests have action buttons", async () => {
|
||||
await setupRequestsTab([PENDING_REQUEST, APPROVED_REQUEST]);
|
||||
|
||||
// user-requestor (pending) should have action buttons
|
||||
// user-approved (approved) should not
|
||||
// Requestor User (pending) should have action buttons
|
||||
// Approved User (approved) should not
|
||||
const approveButtons = screen.getAllByRole("button", { name: "Approve" });
|
||||
expect(approveButtons.length).toBe(1);
|
||||
|
||||
@@ -2042,10 +2074,11 @@ describe("NetworkManagementPage", () => {
|
||||
...PENDING_REQUEST,
|
||||
id: "req-uni-1",
|
||||
user_id: "user-äéîøü-中文-requestor",
|
||||
user_name: "Unicode Requestor",
|
||||
};
|
||||
await setupRequestsTab([unicodeRequest]);
|
||||
|
||||
expect(screen.getByText("user-äéîøü-中文-requestor")).toBeDefined();
|
||||
expect(screen.getByText("Unicode Requestor")).toBeDefined();
|
||||
});
|
||||
|
||||
// ── Adversarial: XSS-safe justification ──────────────────────────────────────
|
||||
@@ -2069,6 +2102,7 @@ describe("NetworkManagementPage", () => {
|
||||
...PENDING_REQUEST,
|
||||
id: `req-many-${i}`,
|
||||
user_id: `user-req-${i}`,
|
||||
user_name: `Requestor ${i}`,
|
||||
}));
|
||||
H.mockGetNetwork.mockResolvedValue({ network: DEV_NETWORK });
|
||||
H.mockGetNetworkPendingRequests.mockResolvedValue({ requests: manyRequests, count: 50 });
|
||||
@@ -2087,8 +2121,8 @@ describe("NetworkManagementPage", () => {
|
||||
expect(screen.getByText("50 pending")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.getByText("user-req-0")).toBeDefined();
|
||||
expect(screen.getByText("user-req-49")).toBeDefined();
|
||||
expect(screen.getByText("Requestor 0")).toBeDefined();
|
||||
expect(screen.getByText("Requestor 49")).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user