Various QOL updates
This commit is contained in:
@@ -38,31 +38,23 @@ vi.mock("@/hooks/useCurrentOrganization", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api", () => ({
|
||||
api: {
|
||||
zerotier: {
|
||||
getNetwork: H.mockGetNetwork,
|
||||
getNetworkMembers: H.mockGetNetworkMembers,
|
||||
activateMembership: H.mockActivateMembership,
|
||||
deactivateMembership: H.mockDeactivateMembership,
|
||||
getNetworkPendingRequests: H.mockGetNetworkPendingRequests,
|
||||
approveRequest: H.mockApproveRequest,
|
||||
rejectRequest: H.mockRejectRequest,
|
||||
vi.mock("@/lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/lib/api")>();
|
||||
return {
|
||||
...actual,
|
||||
api: {
|
||||
zerotier: {
|
||||
getNetwork: H.mockGetNetwork,
|
||||
getNetworkMembers: H.mockGetNetworkMembers,
|
||||
activateMembership: H.mockActivateMembership,
|
||||
deactivateMembership: H.mockDeactivateMembership,
|
||||
getNetworkPendingRequests: H.mockGetNetworkPendingRequests,
|
||||
approveRequest: H.mockApproveRequest,
|
||||
rejectRequest: H.mockRejectRequest,
|
||||
},
|
||||
},
|
||||
},
|
||||
ApiError: class ApiError extends Error {
|
||||
code: number;
|
||||
type: string;
|
||||
details: Record<string, unknown>;
|
||||
constructor(message: string, code: number, type: string, details: Record<string, unknown> = {}) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.code = code;
|
||||
this.type = type;
|
||||
this.details = details;
|
||||
}
|
||||
},
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/hooks/use-toast", () => ({
|
||||
useToast: () => ({
|
||||
@@ -669,10 +661,12 @@ describe("NetworkManagementPage", () => {
|
||||
device_id: "dev-laptop-1",
|
||||
portal_network_id: "net-abc",
|
||||
user_network_approval_id: null,
|
||||
state: "active_authorized",
|
||||
active: true,
|
||||
status: "approved",
|
||||
grant_type: "assigned",
|
||||
granted_by_user_id: null,
|
||||
justification: null,
|
||||
join_seen: true,
|
||||
currently_authorized: true,
|
||||
approved_for_activation: true,
|
||||
created_at: "2025-01-01T00:00:00Z",
|
||||
updated_at: "2025-01-01T00:00:00Z",
|
||||
deleted_at: null,
|
||||
@@ -701,10 +695,12 @@ describe("NetworkManagementPage", () => {
|
||||
device_id: "dev-desktop-1",
|
||||
portal_network_id: "net-abc",
|
||||
user_network_approval_id: null,
|
||||
state: "joined_deauthorized",
|
||||
active: false,
|
||||
status: "approved",
|
||||
grant_type: "assigned",
|
||||
granted_by_user_id: null,
|
||||
justification: null,
|
||||
join_seen: false,
|
||||
currently_authorized: false,
|
||||
approved_for_activation: false,
|
||||
created_at: "2025-02-01T00:00:00Z",
|
||||
updated_at: "2025-02-01T00:00:00Z",
|
||||
deleted_at: null,
|
||||
@@ -718,10 +714,12 @@ describe("NetworkManagementPage", () => {
|
||||
device_id: "dev-phone-1",
|
||||
portal_network_id: "net-abc",
|
||||
user_network_approval_id: null,
|
||||
state: "approved_inactive",
|
||||
active: false,
|
||||
status: "approved",
|
||||
grant_type: "assigned",
|
||||
granted_by_user_id: null,
|
||||
justification: null,
|
||||
join_seen: true,
|
||||
currently_authorized: false,
|
||||
approved_for_activation: true,
|
||||
created_at: "2025-03-01T00:00:00Z",
|
||||
updated_at: "2025-03-01T00:00:00Z",
|
||||
deleted_at: null,
|
||||
@@ -735,10 +733,12 @@ describe("NetworkManagementPage", () => {
|
||||
device_id: "dev-server-1",
|
||||
portal_network_id: "net-abc",
|
||||
user_network_approval_id: null,
|
||||
state: "pending_request",
|
||||
active: false,
|
||||
status: "pending",
|
||||
grant_type: "requested",
|
||||
granted_by_user_id: null,
|
||||
justification: null,
|
||||
join_seen: false,
|
||||
currently_authorized: false,
|
||||
approved_for_activation: false,
|
||||
created_at: "2025-04-01T00:00:00Z",
|
||||
updated_at: "2025-04-01T00:00:00Z",
|
||||
deleted_at: null,
|
||||
@@ -966,7 +966,7 @@ describe("NetworkManagementPage", () => {
|
||||
|
||||
// ── Device Details ──────────────────────────────────────────────────────────
|
||||
|
||||
test("renders device state badge for active_authorized membership", async () => {
|
||||
test("renders Active badge for active_authorized membership", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
@@ -975,102 +975,90 @@ describe("NetworkManagementPage", () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Active")).toBeDefined();
|
||||
});
|
||||
// active_authorized → green badge classes
|
||||
const badge = screen.getByText("Active");
|
||||
expect(badge.className).toContain("bg-green-100");
|
||||
expect(badge.className).toContain("text-green-700");
|
||||
});
|
||||
|
||||
test("renders device state badge for joined_deauthorized membership", async () => {
|
||||
test("renders Inactive badge for approved_inactive membership", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Deauthorized")).toBeDefined();
|
||||
expect(screen.getByText("Inactive")).toBeDefined();
|
||||
});
|
||||
const badge = screen.getByText("Deauthorized");
|
||||
expect(badge.className).toContain("bg-red-100");
|
||||
expect(badge.className).toContain("text-red-700");
|
||||
});
|
||||
|
||||
test("renders device state badge for pending_request membership", async () => {
|
||||
test("renders Inactive badge for pending_request membership", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_PENDING_REQUEST]);
|
||||
|
||||
const userButton = screen.getByText("user-c").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Pending Request")).toBeDefined();
|
||||
expect(screen.getByText("Inactive")).toBeDefined();
|
||||
});
|
||||
const badge = screen.getByText("Pending Request");
|
||||
expect(badge.className).toContain("bg-yellow-100");
|
||||
expect(badge.className).toContain("text-yellow-700");
|
||||
});
|
||||
|
||||
test("renders device state badge for approved_inactive membership", async () => {
|
||||
test("renders Inactive badge for second user (approved_inactive)", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_SECOND_USER]);
|
||||
|
||||
const userButton = screen.getByText("user-b").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Approved")).toBeDefined();
|
||||
expect(screen.getByText("Inactive")).toBeDefined();
|
||||
});
|
||||
const badge = screen.getByText("Approved");
|
||||
expect(badge.className).toContain("bg-blue-100");
|
||||
expect(badge.className).toContain("text-blue-700");
|
||||
});
|
||||
|
||||
// ── Authorization Status ────────────────────────────────────────────────────
|
||||
|
||||
test("shows 'Authorized' with green check icon when currently_authorized is true", async () => {
|
||||
test("shows 'Active' badge when membership is active", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Authorized")).toBeDefined();
|
||||
const actives = screen.getAllByText("Active");
|
||||
expect(actives.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test("shows 'Unauthorized' with X icon when currently_authorized is false", async () => {
|
||||
test("shows 'Inactive' badge when membership is not active", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Unauthorized")).toBeDefined();
|
||||
expect(screen.getByText("Inactive")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Active Session Info ─────────────────────────────────────────────────────
|
||||
|
||||
test("shows session info when active_session is present and is_active", async () => {
|
||||
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");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Session active/)).toBeDefined();
|
||||
expect(screen.getByText(/remaining/)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test("does NOT show session info when active_session is null", async () => {
|
||||
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");
|
||||
fireEvent.click(userButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Deauthorized")).toBeDefined();
|
||||
expect(screen.getByText("Inactive")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.queryByText(/Session active/)).toBeNull();
|
||||
expect(screen.queryByText(/remaining/)).toBeNull();
|
||||
});
|
||||
|
||||
// ── Join Seen ───────────────────────────────────────────────────────────────
|
||||
@@ -1099,7 +1087,7 @@ describe("NetworkManagementPage", () => {
|
||||
|
||||
// ── Activate / Deactivate Buttons ───────────────────────────────────────────
|
||||
|
||||
test("renders Deactivate button for currently_authorized memberships", async () => {
|
||||
test("renders Deactivate button for active memberships", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
@@ -1111,7 +1099,7 @@ describe("NetworkManagementPage", () => {
|
||||
expect(screen.queryByRole("button", { name: "Activate" })).toBeNull();
|
||||
});
|
||||
|
||||
test("renders Activate button for non-currently_authorized memberships", async () => {
|
||||
test("renders Activate button for approved-but-inactive memberships", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
const userButton = screen.getByText("user-a").closest("button");
|
||||
@@ -1123,7 +1111,7 @@ describe("NetworkManagementPage", () => {
|
||||
expect(screen.queryByRole("button", { name: "Deactivate" })).toBeNull();
|
||||
});
|
||||
|
||||
test("clicking Activate calls api.zerotier.activateMembership with correct orgId and membershipId", async () => {
|
||||
test("clicking Activate opens dialog then calls api.zerotier.activateMembership with correct orgId, membershipId, and lifetime", async () => {
|
||||
H.mockActivateMembership.mockResolvedValue({});
|
||||
// Second call to getNetworkMembers (refresh after activate)
|
||||
H.mockGetNetworkMembers
|
||||
@@ -1138,8 +1126,15 @@ describe("NetworkManagementPage", () => {
|
||||
const activateBtn = await screen.findByRole("button", { name: "Activate" });
|
||||
fireEvent.click(activateBtn);
|
||||
|
||||
// Dialog opens; click the Activate button in the dialog
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Set Activation Duration")).toBeDefined();
|
||||
});
|
||||
const dialogBtns = screen.getAllByRole("button", { name: "Activate" });
|
||||
fireEvent.click(dialogBtns[dialogBtns.length - 1]);
|
||||
|
||||
expect(H.mockActivateMembership).toHaveBeenCalledTimes(1);
|
||||
expect(H.mockActivateMembership).toHaveBeenCalledWith("org-1", "mem-unauth-1");
|
||||
expect(H.mockActivateMembership).toHaveBeenCalledWith("org-1", "mem-unauth-1", 480);
|
||||
});
|
||||
|
||||
test("clicking Deactivate calls api.zerotier.deactivateMembership with correct orgId and membershipId", async () => {
|
||||
@@ -1175,6 +1170,13 @@ describe("NetworkManagementPage", () => {
|
||||
const activateBtn = await screen.findByRole("button", { name: "Activate" });
|
||||
fireEvent.click(activateBtn);
|
||||
|
||||
// Confirm in dialog
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Set Activation Duration")).toBeDefined();
|
||||
});
|
||||
const dialogBtns = screen.getAllByRole("button", { name: "Activate" });
|
||||
fireEvent.click(dialogBtns[dialogBtns.length - 1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(H.mockToast).toHaveBeenCalledWith(expect.objectContaining({ title: "Membership activated" }));
|
||||
});
|
||||
@@ -1211,6 +1213,13 @@ describe("NetworkManagementPage", () => {
|
||||
const activateBtn = await screen.findByRole("button", { name: "Activate" });
|
||||
fireEvent.click(activateBtn);
|
||||
|
||||
// Confirm in dialog
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Set Activation Duration")).toBeDefined();
|
||||
});
|
||||
const dialogBtns = screen.getAllByRole("button", { name: "Activate" });
|
||||
fireEvent.click(dialogBtns[dialogBtns.length - 1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(H.mockToast).toHaveBeenCalledWith(expect.objectContaining({
|
||||
variant: "destructive",
|
||||
@@ -1252,6 +1261,13 @@ describe("NetworkManagementPage", () => {
|
||||
const activateBtn = await screen.findByRole("button", { name: "Activate" });
|
||||
fireEvent.click(activateBtn);
|
||||
|
||||
// Confirm in dialog
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Set Activation Duration")).toBeDefined();
|
||||
});
|
||||
const dialogBtns = screen.getAllByRole("button", { name: "Activate" });
|
||||
fireEvent.click(dialogBtns[dialogBtns.length - 1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(H.mockToast).toHaveBeenCalledWith(expect.objectContaining({
|
||||
variant: "destructive",
|
||||
@@ -1265,8 +1281,8 @@ describe("NetworkManagementPage", () => {
|
||||
H.mockActivateMembership.mockResolvedValue({});
|
||||
const updatedMembership = {
|
||||
...MEMBERSHIP_UNAUTHORIZED_NO_SESSION,
|
||||
state: "active_authorized",
|
||||
currently_authorized: true,
|
||||
active: true,
|
||||
status: "approved",
|
||||
};
|
||||
H.mockGetNetworkMembers
|
||||
.mockResolvedValueOnce({ memberships: [MEMBERSHIP_UNAUTHORIZED_NO_SESSION], count: 1 })
|
||||
@@ -1280,6 +1296,13 @@ describe("NetworkManagementPage", () => {
|
||||
const activateBtn = await screen.findByRole("button", { name: "Activate" });
|
||||
fireEvent.click(activateBtn);
|
||||
|
||||
// Confirm in dialog
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Set Activation Duration")).toBeDefined();
|
||||
});
|
||||
const dialogBtns = screen.getAllByRole("button", { name: "Activate" });
|
||||
fireEvent.click(dialogBtns[dialogBtns.length - 1]);
|
||||
|
||||
// Verify getNetworkMembers was called twice: once on mount, once on refresh
|
||||
await waitFor(() => {
|
||||
expect(H.mockGetNetworkMembers).toHaveBeenCalledTimes(2);
|
||||
@@ -1292,8 +1315,8 @@ describe("NetworkManagementPage", () => {
|
||||
H.mockDeactivateMembership.mockResolvedValue({});
|
||||
const updatedMembership = {
|
||||
...MEMBERSHIP_AUTHORIZED_WITH_SESSION,
|
||||
state: "joined_deauthorized",
|
||||
currently_authorized: false,
|
||||
active: false,
|
||||
status: "approved",
|
||||
};
|
||||
H.mockGetNetworkMembers
|
||||
.mockResolvedValueOnce({ memberships: [MEMBERSHIP_AUTHORIZED_WITH_SESSION], count: 1 })
|
||||
@@ -1344,15 +1367,15 @@ describe("NetworkManagementPage", () => {
|
||||
test("shows active device count in user section header", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION, MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
// 1 active (currently_authorized) + 1 inactive → "1 active"
|
||||
expect(screen.getByText(/1 active/)).toBeDefined();
|
||||
// 1 active + 1 inactive → "1 active"
|
||||
expect(screen.getByText(/^1 active$/)).toBeDefined();
|
||||
});
|
||||
|
||||
test("does not show active count when no devices are authorized", async () => {
|
||||
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
|
||||
|
||||
// 0 active devices → no "active" text
|
||||
expect(screen.queryByText(/active/)).toBeNull();
|
||||
// 0 active devices → no "active" count in user section
|
||||
expect(screen.queryByText(/^\d+ active$/)).toBeNull();
|
||||
});
|
||||
|
||||
// ── Adversarial: Unicode user_ids ───────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user