mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-07 22:14:03 -05:00
825 lines
26 KiB
TypeScript
825 lines
26 KiB
TypeScript
import { describe, expect, it, vi, test, beforeEach, afterEach } from "vitest";
|
|
import { render, screen, within, waitFor } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { createRoutesStub } from "react-router";
|
|
import { selectOrganization } from "test-utils";
|
|
import { organizationService } from "#/api/organization-service/organization-service.api";
|
|
import ManageOrganizationMembers from "#/routes/manage-organization-members";
|
|
import SettingsScreen, {
|
|
clientLoader as settingsClientLoader,
|
|
} from "#/routes/settings";
|
|
import {
|
|
ORGS_AND_MEMBERS,
|
|
resetOrgMockData,
|
|
resetOrgsAndMembersMockData,
|
|
} from "#/mocks/org-handlers";
|
|
import OptionService from "#/api/option-service/option-service.api";
|
|
|
|
function ManageOrganizationMembersWithPortalRoot() {
|
|
return (
|
|
<div>
|
|
<ManageOrganizationMembers />
|
|
<div data-testid="portal-root" id="portal-root" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const RouteStub = createRoutesStub([
|
|
{
|
|
// @ts-expect-error - ignoreing error for test stub
|
|
loader: settingsClientLoader,
|
|
Component: SettingsScreen,
|
|
path: "/settings",
|
|
HydrateFallback: () => <div>Loading...</div>,
|
|
children: [
|
|
{
|
|
Component: ManageOrganizationMembersWithPortalRoot,
|
|
path: "/settings/org-members",
|
|
},
|
|
{
|
|
Component: () => <div data-testid="user-settings" />,
|
|
path: "/settings/member",
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
let queryClient: QueryClient;
|
|
|
|
describe("Manage Organization Members Route", () => {
|
|
const getMeSpy = vi.spyOn(organizationService, "getMe");
|
|
|
|
beforeEach(() => {
|
|
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
|
// @ts-expect-error - only return APP_MODE for these tests
|
|
getConfigSpy.mockResolvedValue({
|
|
APP_MODE: "saas",
|
|
});
|
|
|
|
queryClient = new QueryClient();
|
|
|
|
// Set default mock for user (admin role has invite permission)
|
|
getMeSpy.mockResolvedValue({
|
|
org_id: "1",
|
|
user_id: "1",
|
|
email: "test@example.com",
|
|
role: "admin",
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active",
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
// Reset organization mock data to ensure clean state between tests
|
|
resetOrgMockData();
|
|
// Reset ORGS_AND_MEMBERS to initial state
|
|
resetOrgsAndMembersMockData();
|
|
// Clear queryClient cache to ensure fresh data for next test
|
|
queryClient.clear();
|
|
});
|
|
|
|
const renderManageOrganizationMembers = () =>
|
|
render(<RouteStub initialEntries={["/settings/org-members"]} />, {
|
|
wrapper: ({ children }) => (
|
|
<QueryClientProvider client={queryClient}>
|
|
{children}
|
|
</QueryClientProvider>
|
|
),
|
|
});
|
|
|
|
// Helper function to find a member by email
|
|
const findMemberByEmail = async (email: string) => {
|
|
const memberListItems = await screen.findAllByTestId("member-item");
|
|
const member = memberListItems.find((item) =>
|
|
within(item).queryByText(email),
|
|
);
|
|
if (!member) {
|
|
throw new Error(`Could not find member with email: ${email}`);
|
|
}
|
|
return member;
|
|
};
|
|
|
|
// Helper function to open role dropdown for a member
|
|
const openRoleDropdown = async (
|
|
memberElement: HTMLElement,
|
|
roleText: string,
|
|
) => {
|
|
// Find the role text that's clickable (has cursor-pointer class or is the main role display)
|
|
// Use a more specific query to avoid matching dropdown options
|
|
const roleElement = within(memberElement).getByText(
|
|
new RegExp(`^${roleText}$`, "i"),
|
|
);
|
|
await userEvent.click(roleElement);
|
|
return within(memberElement).getByTestId(
|
|
"organization-member-role-context-menu",
|
|
);
|
|
};
|
|
|
|
// Helper function to change member role
|
|
const changeMemberRole = async (
|
|
memberElement: HTMLElement,
|
|
currentRole: string,
|
|
newRole: string,
|
|
) => {
|
|
const dropdown = await openRoleDropdown(memberElement, currentRole);
|
|
const roleOption = within(dropdown).getByText(new RegExp(newRole, "i"));
|
|
await userEvent.click(roleOption);
|
|
};
|
|
|
|
// Helper function to verify dropdown is not visible
|
|
const expectDropdownNotVisible = (memberElement: HTMLElement) => {
|
|
expect(
|
|
within(memberElement).queryByTestId(
|
|
"organization-member-role-context-menu",
|
|
),
|
|
).not.toBeInTheDocument();
|
|
};
|
|
|
|
// Helper function to setup test with user and organization
|
|
const setupTestWithUserAndOrg = async (
|
|
userData: {
|
|
org_id: string;
|
|
user_id: string;
|
|
email: string;
|
|
role: "owner" | "admin" | "member";
|
|
llm_api_key: string;
|
|
max_iterations: number;
|
|
llm_model: string;
|
|
llm_api_key_for_byor: string | null;
|
|
llm_base_url: string;
|
|
status: "active" | "invited" | "inactive";
|
|
},
|
|
orgIndex: number,
|
|
) => {
|
|
getMeSpy.mockResolvedValue(userData);
|
|
renderManageOrganizationMembers();
|
|
await screen.findByTestId("manage-organization-members-settings");
|
|
await selectOrganization({ orgIndex });
|
|
};
|
|
|
|
// Helper function to create updateMember spy
|
|
const createUpdateMemberRoleSpy = () =>
|
|
vi.spyOn(organizationService, "updateMember");
|
|
|
|
// Helper function to verify role change is not permitted
|
|
const verifyRoleChangeNotPermitted = async (
|
|
userData: {
|
|
org_id: string;
|
|
user_id: string;
|
|
email: string;
|
|
role: "owner" | "admin" | "member";
|
|
llm_api_key: string;
|
|
max_iterations: number;
|
|
llm_model: string;
|
|
llm_api_key_for_byor: string | null;
|
|
llm_base_url: string;
|
|
status: "active" | "invited" | "inactive";
|
|
},
|
|
orgIndex: number,
|
|
targetMemberIndex: number,
|
|
expectedRoleText: string,
|
|
) => {
|
|
await setupTestWithUserAndOrg(userData, orgIndex);
|
|
|
|
const memberListItems = await screen.findAllByTestId("member-item");
|
|
const targetMember = memberListItems[targetMemberIndex];
|
|
const roleText = within(targetMember).getByText(
|
|
new RegExp(`^${expectedRoleText}$`, "i"),
|
|
);
|
|
expect(roleText).toBeInTheDocument();
|
|
await userEvent.click(roleText);
|
|
|
|
// Verify that the dropdown does not open
|
|
expectDropdownNotVisible(targetMember);
|
|
};
|
|
|
|
// Helper function to setup invite test (render and select organization)
|
|
const setupInviteTest = async (orgIndex: number = 0) => {
|
|
renderManageOrganizationMembers();
|
|
await selectOrganization({ orgIndex });
|
|
};
|
|
|
|
// Helper function to setup test with organization (waits for settings screen)
|
|
const setupTestWithOrg = async (orgIndex: number = 0) => {
|
|
renderManageOrganizationMembers();
|
|
await screen.findByTestId("manage-organization-members-settings");
|
|
await selectOrganization({ orgIndex });
|
|
};
|
|
|
|
// Helper function to find invite button
|
|
const findInviteButton = async () =>
|
|
await screen.findByRole("button", {
|
|
name: /ORG\$INVITE_ORGANIZATION_MEMBER/i,
|
|
});
|
|
|
|
// Helper function to verify all three role options are present in dropdown
|
|
const expectAllRoleOptionsPresent = (dropdown: HTMLElement) => {
|
|
expect(within(dropdown).getByText(/owner/i)).toBeInTheDocument();
|
|
expect(within(dropdown).getByText(/admin/i)).toBeInTheDocument();
|
|
expect(within(dropdown).getByText(/member/i)).toBeInTheDocument();
|
|
};
|
|
|
|
// Helper function to close dropdown by clicking outside
|
|
const closeDropdown = async () => {
|
|
await userEvent.click(document.body);
|
|
};
|
|
|
|
// Helper function to verify owner option is not present in dropdown
|
|
const expectOwnerOptionNotPresent = (dropdown: HTMLElement) => {
|
|
expect(within(dropdown).queryByText(/owner/i)).not.toBeInTheDocument();
|
|
};
|
|
|
|
it("should render", async () => {
|
|
renderManageOrganizationMembers();
|
|
await screen.findByTestId("manage-organization-members-settings");
|
|
});
|
|
|
|
it("should navigate away from the page if not saas", async () => {
|
|
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
|
// @ts-expect-error - only return APP_MODE for these tests
|
|
getConfigSpy.mockResolvedValue({
|
|
APP_MODE: "oss",
|
|
});
|
|
|
|
renderManageOrganizationMembers();
|
|
expect(
|
|
screen.queryByTestId("manage-organization-members-settings"),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should allow the user to select an organization", async () => {
|
|
const getOrganizationMembersSpy = vi.spyOn(
|
|
organizationService,
|
|
"getOrganizationMembers",
|
|
);
|
|
|
|
renderManageOrganizationMembers();
|
|
await screen.findByTestId("manage-organization-members-settings");
|
|
|
|
expect(getOrganizationMembersSpy).not.toHaveBeenCalled();
|
|
|
|
await selectOrganization({ orgIndex: 0 });
|
|
expect(getOrganizationMembersSpy).toHaveBeenCalledExactlyOnceWith({
|
|
orgId: "1",
|
|
});
|
|
});
|
|
|
|
it("should render the list of organization members", async () => {
|
|
await setupTestWithOrg(0);
|
|
const members = ORGS_AND_MEMBERS["1"];
|
|
|
|
const memberListItems = await screen.findAllByTestId("member-item");
|
|
expect(memberListItems).toHaveLength(members.length);
|
|
|
|
members.forEach((member) => {
|
|
expect(screen.getByText(member.email)).toBeInTheDocument();
|
|
expect(screen.getByText(member.role)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
test("an admin should be able to change the role of a organization member", async () => {
|
|
await setupTestWithUserAndOrg(
|
|
{
|
|
org_id: "1",
|
|
user_id: "1",
|
|
email: "test@example.com",
|
|
role: "admin",
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active",
|
|
},
|
|
0,
|
|
);
|
|
|
|
const updateMemberRoleSpy = createUpdateMemberRoleSpy();
|
|
|
|
const memberListItems = await screen.findAllByTestId("member-item");
|
|
const userRoleMember = memberListItems[2]; // third member is "user"
|
|
|
|
let userCombobox = within(userRoleMember).getByText(/^Member$/i);
|
|
expect(userCombobox).toBeInTheDocument();
|
|
|
|
// Change role from user to admin
|
|
await changeMemberRole(userRoleMember, "member", "admin");
|
|
|
|
expect(updateMemberRoleSpy).toHaveBeenCalledExactlyOnceWith({
|
|
userId: "3", // assuming the third member is the one being updated
|
|
orgId: "1",
|
|
role: "admin",
|
|
});
|
|
expectDropdownNotVisible(userRoleMember);
|
|
|
|
// Verify the role has been updated in the UI
|
|
userCombobox = within(userRoleMember).getByText(/^Admin$/i);
|
|
expect(userCombobox).toBeInTheDocument();
|
|
|
|
// Revert the role back to user
|
|
await changeMemberRole(userRoleMember, "admin", "member");
|
|
|
|
expect(updateMemberRoleSpy).toHaveBeenNthCalledWith(2, {
|
|
userId: "3",
|
|
orgId: "1",
|
|
role: "member",
|
|
});
|
|
|
|
// Verify the role has been reverted in the UI
|
|
userCombobox = within(userRoleMember).getByText(/^Member$/i);
|
|
expect(userCombobox).toBeInTheDocument();
|
|
});
|
|
|
|
it("should not allow an admin to change the owner's role", async () => {
|
|
await verifyRoleChangeNotPermitted(
|
|
{
|
|
org_id: "3",
|
|
user_id: "1",
|
|
email: "test@example.com",
|
|
role: "admin",
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active",
|
|
},
|
|
2, // user is admin in org 3
|
|
0, // first member is "owner"
|
|
"Owner",
|
|
);
|
|
});
|
|
|
|
it("should not allow an admin to change another admin's role", async () => {
|
|
await verifyRoleChangeNotPermitted(
|
|
{
|
|
org_id: "3",
|
|
user_id: "1",
|
|
email: "test@example.com",
|
|
role: "admin",
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active",
|
|
},
|
|
2, // user is admin in org 3
|
|
1, // second member is "admin"
|
|
"Admin",
|
|
);
|
|
});
|
|
|
|
it("should not allow a user to change their own role", async () => {
|
|
// Mock the /me endpoint to return a user ID that matches one of the members
|
|
await verifyRoleChangeNotPermitted(
|
|
{
|
|
org_id: "1",
|
|
user_id: "1", // Same as first member from org 1
|
|
email: "alice@acme.org",
|
|
role: "owner",
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active",
|
|
},
|
|
0,
|
|
0, // First member (user_id: "1")
|
|
"Owner",
|
|
);
|
|
});
|
|
|
|
it("should show a remove option in the role dropdown and remove the user from the list", async () => {
|
|
const removeMemberSpy = vi.spyOn(organizationService, "removeMember");
|
|
|
|
await setupTestWithOrg(0);
|
|
|
|
// Get initial member count
|
|
const memberListItems = await screen.findAllByTestId("member-item");
|
|
const initialMemberCount = memberListItems.length;
|
|
|
|
const userRoleMember = memberListItems[2]; // third member is "user"
|
|
const userEmail = within(userRoleMember).getByText("charlie@acme.org");
|
|
expect(userEmail).toBeInTheDocument();
|
|
|
|
const userCombobox = within(userRoleMember).getByText(/^Member$/i);
|
|
await userEvent.click(userCombobox);
|
|
|
|
const dropdown = within(userRoleMember).getByTestId(
|
|
"organization-member-role-context-menu",
|
|
);
|
|
|
|
// Check that remove option exists
|
|
const removeOption = within(dropdown).getByTestId("remove-option");
|
|
expect(removeOption).toBeInTheDocument();
|
|
|
|
await userEvent.click(removeOption);
|
|
|
|
expect(removeMemberSpy).toHaveBeenCalledExactlyOnceWith({
|
|
orgId: "1",
|
|
userId: "3",
|
|
});
|
|
|
|
// Verify the user is no longer in the list
|
|
await waitFor(() => {
|
|
const updatedMemberListItems = screen.getAllByTestId("member-item");
|
|
expect(updatedMemberListItems).toHaveLength(initialMemberCount - 1);
|
|
});
|
|
|
|
// Verify the specific user email is no longer present
|
|
expect(screen.queryByText("charlie@acme.org")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it.todo(
|
|
"should not allow a user to change another user's role if they are the same role",
|
|
);
|
|
|
|
describe("Inviting Organization Members", () => {
|
|
it("should render an invite organization member button", async () => {
|
|
await setupInviteTest();
|
|
|
|
const inviteButton = await findInviteButton();
|
|
expect(inviteButton).toBeInTheDocument();
|
|
});
|
|
|
|
it("should render a modal when the invite button is clicked", async () => {
|
|
await setupInviteTest();
|
|
|
|
expect(screen.queryByTestId("invite-modal")).not.toBeInTheDocument();
|
|
const inviteButton = await findInviteButton();
|
|
await userEvent.click(inviteButton);
|
|
|
|
const portalRoot = screen.getByTestId("portal-root");
|
|
expect(
|
|
within(portalRoot).getByTestId("invite-modal"),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("should close the modal when the close button is clicked", async () => {
|
|
await setupInviteTest();
|
|
|
|
const inviteButton = await findInviteButton();
|
|
await userEvent.click(inviteButton);
|
|
|
|
const modal = screen.getByTestId("invite-modal");
|
|
const closeButton = within(modal).getByText("BUTTON$CANCEL");
|
|
await userEvent.click(closeButton);
|
|
|
|
expect(screen.queryByTestId("invite-modal")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should render a list item in an invited state when a the user is is invited", async () => {
|
|
const getOrganizationMembersSpy = vi.spyOn(
|
|
organizationService,
|
|
"getOrganizationMembers",
|
|
);
|
|
|
|
getOrganizationMembersSpy.mockResolvedValue([
|
|
{
|
|
org_id: "1",
|
|
user_id: "4",
|
|
email: "tom@acme.org",
|
|
role: "member",
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "invited",
|
|
},
|
|
]);
|
|
|
|
await setupInviteTest();
|
|
|
|
const members = await screen.findAllByTestId("member-item");
|
|
expect(members).toHaveLength(1);
|
|
|
|
const invitedMember = members[0];
|
|
expect(invitedMember).toBeInTheDocument();
|
|
|
|
// should have an "invited" badge
|
|
const invitedBadge = within(invitedMember).getByText(/invited/i);
|
|
expect(invitedBadge).toBeInTheDocument();
|
|
|
|
// should not have a role combobox
|
|
await userEvent.click(within(invitedMember).getByText(/^Member$/i));
|
|
expect(
|
|
within(invitedMember).queryByTestId(
|
|
"organization-member-role-context-menu",
|
|
),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Role-based invite permission behavior", () => {
|
|
it.each([
|
|
{ role: "owner" as const, roleName: "Owner" },
|
|
{ role: "admin" as const, roleName: "Admin" },
|
|
])(
|
|
"should show invite button when user has canInviteUsers permission ($roleName role)",
|
|
async ({ role }) => {
|
|
getMeSpy.mockResolvedValue({
|
|
org_id: "1",
|
|
user_id: "1",
|
|
email: "test@example.com",
|
|
role,
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active",
|
|
});
|
|
|
|
await setupTestWithOrg(0);
|
|
|
|
const inviteButton = await findInviteButton();
|
|
|
|
expect(inviteButton).toBeInTheDocument();
|
|
expect(inviteButton).not.toBeDisabled();
|
|
},
|
|
);
|
|
|
|
it("should not show invite button when user lacks canInviteUsers permission (User role)", async () => {
|
|
const userData = {
|
|
org_id: "1",
|
|
user_id: "1",
|
|
email: "test@example.com",
|
|
role: "member" as const,
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active" as const,
|
|
};
|
|
|
|
// Set mock and remove cached query before rendering
|
|
getMeSpy.mockResolvedValue(userData);
|
|
// Remove any cached "me" queries so fresh data is fetched
|
|
queryClient.removeQueries({ queryKey: ["organizations"] });
|
|
|
|
await setupTestWithOrg(0);
|
|
|
|
// Directly set the query data to force component re-render with user role
|
|
// This ensures the component uses the user role data instead of cached admin data
|
|
queryClient.setQueryData(["organizations", "1", "me"], userData);
|
|
|
|
// Wait for the component to update with the new query data
|
|
await waitFor(
|
|
() => {
|
|
const inviteButton = screen.queryByRole("button", {
|
|
name: /ORG\$INVITE_ORGANIZATION_MEMBER/i,
|
|
});
|
|
expect(inviteButton).not.toBeInTheDocument();
|
|
},
|
|
{ timeout: 3000 },
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Role-based role change permission behavior", () => {
|
|
it("should not allow an owner to change another owner's role", async () => {
|
|
await verifyRoleChangeNotPermitted(
|
|
{
|
|
org_id: "1",
|
|
user_id: "1", // First member is owner in org 1
|
|
email: "alice@acme.org",
|
|
role: "owner",
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active",
|
|
},
|
|
0,
|
|
0, // First member is owner
|
|
"owner",
|
|
);
|
|
});
|
|
|
|
it("Owner should see all three role options (owner, admin, user) in dropdown regardless of target member's role", async () => {
|
|
await setupTestWithUserAndOrg(
|
|
{
|
|
org_id: "1",
|
|
user_id: "1", // First member is owner in org 1
|
|
email: "alice@acme.org",
|
|
role: "owner",
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active",
|
|
},
|
|
0,
|
|
);
|
|
|
|
const memberListItems = await screen.findAllByTestId("member-item");
|
|
|
|
// Test with admin member
|
|
const adminMember = memberListItems[1]; // Second member is admin (user_id: "2")
|
|
const adminDropdown = await openRoleDropdown(adminMember, "admin");
|
|
|
|
// Verify all three role options are present for admin member
|
|
expectAllRoleOptionsPresent(adminDropdown);
|
|
|
|
// Close dropdown by clicking outside
|
|
await closeDropdown();
|
|
|
|
// Test with user member
|
|
const userMember = await findMemberByEmail("charlie@acme.org");
|
|
const userDropdown = await openRoleDropdown(userMember, "member");
|
|
|
|
// Verify all three role options are present for user member
|
|
expectAllRoleOptionsPresent(userDropdown);
|
|
});
|
|
|
|
it("Admin should not see owner option in role dropdown for any member", async () => {
|
|
await setupTestWithUserAndOrg(
|
|
{
|
|
org_id: "3",
|
|
user_id: "7", // Ray is admin in org 3
|
|
email: "ray@all-hands.dev",
|
|
role: "admin",
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active",
|
|
},
|
|
2, // org 3
|
|
);
|
|
|
|
const memberListItems = await screen.findAllByTestId("member-item");
|
|
|
|
// Check user member dropdown
|
|
const userMember = memberListItems[2]; // user member
|
|
const userDropdown = await openRoleDropdown(userMember, "member");
|
|
expectOwnerOptionNotPresent(userDropdown);
|
|
await closeDropdown();
|
|
|
|
// Check another user member dropdown if exists
|
|
if (memberListItems.length > 3) {
|
|
const anotherUserMember = memberListItems[3]; // another user member
|
|
const anotherUserDropdown = await openRoleDropdown(
|
|
anotherUserMember,
|
|
"member",
|
|
);
|
|
expectOwnerOptionNotPresent(anotherUserDropdown);
|
|
}
|
|
});
|
|
|
|
it("Owner should be able to change any member's role to owner", async () => {
|
|
await setupTestWithUserAndOrg(
|
|
{
|
|
org_id: "1",
|
|
user_id: "1", // First member is owner in org 1
|
|
email: "alice@acme.org",
|
|
role: "owner",
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active",
|
|
},
|
|
0,
|
|
);
|
|
|
|
const updateMemberRoleSpy = createUpdateMemberRoleSpy();
|
|
|
|
const memberListItems = await screen.findAllByTestId("member-item");
|
|
|
|
// Test changing admin to owner
|
|
const adminMember = memberListItems[1]; // Second member is admin (user_id: "2")
|
|
await changeMemberRole(adminMember, "admin", "owner");
|
|
|
|
expect(updateMemberRoleSpy).toHaveBeenNthCalledWith(1, {
|
|
userId: "2",
|
|
orgId: "1",
|
|
role: "owner",
|
|
});
|
|
|
|
// Test changing user to owner
|
|
const userMember = await findMemberByEmail("charlie@acme.org");
|
|
await changeMemberRole(userMember, "member", "owner");
|
|
|
|
expect(updateMemberRoleSpy).toHaveBeenNthCalledWith(2, {
|
|
userId: "3",
|
|
orgId: "1",
|
|
role: "owner",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
description:
|
|
"Owner should be able to change admin's role to admin (no change)",
|
|
userData: {
|
|
org_id: "1",
|
|
user_id: "1", // First member is owner in org 1
|
|
email: "alice@acme.org",
|
|
role: "owner" as const,
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active" as const,
|
|
},
|
|
orgIndex: 0,
|
|
memberEmail: "bob@acme.org",
|
|
currentRole: "admin",
|
|
newRole: "admin",
|
|
expectedApiCall: {
|
|
userId: "2",
|
|
orgId: "1",
|
|
role: "admin" as const,
|
|
},
|
|
},
|
|
{
|
|
description:
|
|
"Admin should be able to change user's role to user (no change)",
|
|
userData: {
|
|
org_id: "3",
|
|
user_id: "7", // Ray is admin in org 3
|
|
email: "ray@all-hands.dev",
|
|
role: "admin" as const,
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active" as const,
|
|
},
|
|
orgIndex: 2, // org 3
|
|
memberEmail: "stephan@all-hands.dev",
|
|
currentRole: "member",
|
|
newRole: "member",
|
|
expectedApiCall: {
|
|
userId: "9",
|
|
orgId: "3",
|
|
role: "member" as const,
|
|
},
|
|
},
|
|
{
|
|
description: "Admin should be able to change member's role to admin",
|
|
userData: {
|
|
org_id: "3",
|
|
user_id: "7", // Ray is admin in org 3
|
|
email: "ray@all-hands.dev",
|
|
role: "admin" as const,
|
|
llm_api_key: "**********",
|
|
max_iterations: 20,
|
|
llm_model: "gpt-4",
|
|
llm_api_key_for_byor: null,
|
|
llm_base_url: "https://api.openai.com",
|
|
status: "active" as const,
|
|
},
|
|
orgIndex: 2, // org 3
|
|
memberEmail: "stephan@all-hands.dev",
|
|
currentRole: "member",
|
|
newRole: "admin",
|
|
expectedApiCall: {
|
|
userId: "9",
|
|
orgId: "3",
|
|
role: "admin" as const,
|
|
},
|
|
},
|
|
])(
|
|
"$description",
|
|
async ({
|
|
userData,
|
|
orgIndex,
|
|
memberEmail,
|
|
currentRole,
|
|
newRole,
|
|
expectedApiCall,
|
|
}) => {
|
|
await setupTestWithUserAndOrg(userData, orgIndex);
|
|
|
|
const updateMemberRoleSpy = createUpdateMemberRoleSpy();
|
|
|
|
const member = await findMemberByEmail(memberEmail);
|
|
|
|
await changeMemberRole(member, currentRole, newRole);
|
|
|
|
expect(updateMemberRoleSpy).toHaveBeenCalledExactlyOnceWith(
|
|
expectedApiCall,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
});
|