Files
OpenHands/frontend/__tests__/utils/permission-guard.test.ts
sp.wack cd2d0ee9a5 feat(frontend): Organizational support (#9496)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
Co-authored-by: Abhay Mishra <grabhaymishra@gmail.com>
Co-authored-by: Hyun Han <62870362+smosco@users.noreply.github.com>
Co-authored-by: Nhan Nguyen <nhan13574@gmail.com>
Co-authored-by: Bharath A V <avbharath1221@gmail.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: Chloe <chloe@openhands.com>
Co-authored-by: HeyItsChloe <54480367+HeyItsChloe@users.noreply.github.com>
2026-03-13 23:38:54 +07:00

176 lines
5.5 KiB
TypeScript

import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { redirect } from "react-router";
// Mock dependencies before importing the module under test
vi.mock("react-router", () => ({
redirect: vi.fn((path: string) => ({ type: "redirect", path })),
}));
vi.mock("#/utils/org/permission-checks", () => ({
getActiveOrganizationUser: vi.fn(),
}));
vi.mock("#/api/option-service/option-service.api", () => ({
default: {
getConfig: vi.fn().mockResolvedValue({
app_mode: "saas",
feature_flags: {
hide_users_page: false,
hide_billing_page: false,
hide_integrations_page: false,
hide_llm_settings: false,
},
}),
},
}));
const mockConfig = {
app_mode: "saas",
feature_flags: {
hide_users_page: false,
hide_billing_page: false,
hide_integrations_page: false,
hide_llm_settings: false,
},
};
vi.mock("#/query-client-config", () => ({
queryClient: {
getQueryData: vi.fn(() => mockConfig),
setQueryData: vi.fn(),
},
}));
// Import after mocks are set up
import { createPermissionGuard } from "#/utils/org/permission-guard";
import { getActiveOrganizationUser } from "#/utils/org/permission-checks";
// Helper to create a mock request
const createMockRequest = (pathname: string = "/settings/billing") => ({
request: new Request(`http://localhost${pathname}`),
});
describe("createPermissionGuard", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.resetAllMocks();
});
describe("permission checking", () => {
it("should redirect when user lacks required permission", async () => {
// Arrange: member lacks view_billing permission
vi.mocked(getActiveOrganizationUser).mockResolvedValue({
org_id: "org-1",
user_id: "user-1",
email: "test@example.com",
role: "member",
llm_api_key: "",
max_iterations: 100,
llm_model: "gpt-4",
llm_api_key_for_byor: null,
llm_base_url: "",
status: "active",
});
// Act
const guard = createPermissionGuard("view_billing");
await guard(createMockRequest("/settings/billing"));
// Assert: should redirect to first available path (/settings/user in SaaS mode)
expect(redirect).toHaveBeenCalledWith("/settings/user");
});
it("should allow access when user has required permission", async () => {
// Arrange: admin has view_billing permission
vi.mocked(getActiveOrganizationUser).mockResolvedValue({
org_id: "org-1",
user_id: "user-1",
email: "admin@example.com",
role: "admin",
llm_api_key: "",
max_iterations: 100,
llm_model: "gpt-4",
llm_api_key_for_byor: null,
llm_base_url: "",
status: "active",
});
// Act
const guard = createPermissionGuard("view_billing");
const result = await guard(createMockRequest("/settings/billing"));
// Assert: should not redirect, return null
expect(redirect).not.toHaveBeenCalled();
expect(result).toBeNull();
});
it("should redirect when user is undefined (no org selected)", async () => {
// Arrange: no user (e.g., no organization selected)
vi.mocked(getActiveOrganizationUser).mockResolvedValue(undefined);
// Act
const guard = createPermissionGuard("view_billing");
await guard(createMockRequest("/settings/billing"));
// Assert: should redirect to first available path
expect(redirect).toHaveBeenCalledWith("/settings/user");
});
it("should redirect when user is undefined even for member-level permissions", async () => {
// Arrange: no user — manage_secrets is a member-level permission,
// but undefined user should NOT get member access
vi.mocked(getActiveOrganizationUser).mockResolvedValue(undefined);
// Act
const guard = createPermissionGuard("manage_secrets");
await guard(createMockRequest("/settings/secrets"));
// Assert: should redirect, not silently grant member-level access
expect(redirect).toHaveBeenCalledWith("/settings/user");
});
});
describe("custom redirect path", () => {
it("should redirect to custom path when specified", async () => {
// Arrange: member lacks permission
vi.mocked(getActiveOrganizationUser).mockResolvedValue({
org_id: "org-1",
user_id: "user-1",
email: "test@example.com",
role: "member",
llm_api_key: "",
max_iterations: 100,
llm_model: "gpt-4",
llm_api_key_for_byor: null,
llm_base_url: "",
status: "active",
});
// Act
const guard = createPermissionGuard("view_billing", "/custom/redirect");
await guard(createMockRequest("/settings/billing"));
// Assert: should redirect to custom path
expect(redirect).toHaveBeenCalledWith("/custom/redirect");
});
});
describe("infinite loop prevention", () => {
it("should return null instead of redirecting when fallback path equals current path", async () => {
// Arrange: no user
vi.mocked(getActiveOrganizationUser).mockResolvedValue(undefined);
// Act: access /settings/user when fallback would also be /settings/user
const guard = createPermissionGuard("view_billing");
const result = await guard(createMockRequest("/settings/user"));
// Assert: should NOT redirect to avoid infinite loop
expect(redirect).not.toHaveBeenCalled();
expect(result).toBeNull();
});
});
});