mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
refactor: update getme according to actual expected endpoint
This commit is contained in:
parent
33f3861d95
commit
df950ec11c
@ -3,13 +3,14 @@ import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { selectOrganization } from "test-utils";
|
||||
import { selectOrganization, createGetMeResponse } from "test-utils";
|
||||
import ManageOrg from "#/routes/manage-org";
|
||||
import { organizationService } from "#/api/organization-service/organization-service.api";
|
||||
import SettingsScreen, { clientLoader } from "#/routes/settings";
|
||||
import { resetOrgMockData } from "#/mocks/org-handlers";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import BillingService from "#/api/billing-service/billing-service.api";
|
||||
import { OrganizationMember } from "#/types/org";
|
||||
|
||||
function ManageOrgWithPortalRoot() {
|
||||
return (
|
||||
@ -78,13 +79,16 @@ describe("Manage Org Route", () => {
|
||||
};
|
||||
|
||||
// Helper function to set up user mock
|
||||
const setupUserMock = (userData: {
|
||||
id: string;
|
||||
email: string;
|
||||
role: "owner" | "admin" | "user";
|
||||
status: "active" | "invited";
|
||||
}) => {
|
||||
getMeSpy.mockResolvedValue(userData);
|
||||
const setupUserMock = (
|
||||
userData: {
|
||||
id: string;
|
||||
email: string;
|
||||
role: "owner" | "admin" | "user";
|
||||
status: "active" | "invited";
|
||||
},
|
||||
orgId: string = "1",
|
||||
) => {
|
||||
getMeSpy.mockResolvedValue(createGetMeResponse(userData, orgId));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@ -95,7 +99,8 @@ describe("Manage Org Route", () => {
|
||||
});
|
||||
|
||||
// Set default mock for user (owner role has all permissions)
|
||||
setupUserMock(TEST_USERS.OWNER);
|
||||
// Default to orgId "1" for most tests
|
||||
setupUserMock(TEST_USERS.OWNER, "1");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -574,7 +579,7 @@ describe("Manage Org Route", () => {
|
||||
|
||||
it("should NOT allow roles other than owners to change org name", async () => {
|
||||
// Set admin role before rendering
|
||||
setupUserMock(TEST_USERS.ADMIN);
|
||||
setupUserMock(TEST_USERS.ADMIN, "3");
|
||||
|
||||
renderManageOrg();
|
||||
await screen.findByTestId("manage-org-screen");
|
||||
@ -589,7 +594,7 @@ describe("Manage Org Route", () => {
|
||||
});
|
||||
|
||||
it("should NOT allow roles other than owners to delete an organization", async () => {
|
||||
setupUserMock(TEST_USERS.ADMIN);
|
||||
setupUserMock(TEST_USERS.ADMIN, "3");
|
||||
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
// @ts-expect-error - only return the properties we need for this test
|
||||
@ -646,7 +651,7 @@ describe("Manage Org Route", () => {
|
||||
|
||||
describe("Role-based delete organization permission behavior", () => {
|
||||
it("should show delete organization button when user has canDeleteOrganization permission (Owner role)", async () => {
|
||||
setupUserMock(TEST_USERS.OWNER);
|
||||
setupUserMock(TEST_USERS.OWNER, "1");
|
||||
|
||||
renderManageOrg();
|
||||
await screen.findByTestId("manage-org-screen");
|
||||
@ -667,12 +672,15 @@ describe("Manage Org Route", () => {
|
||||
])(
|
||||
"should not show delete organization button when user lacks canDeleteOrganization permission ($roleName role)",
|
||||
async ({ role }) => {
|
||||
setupUserMock({
|
||||
id: "1",
|
||||
email: "test@example.com",
|
||||
role,
|
||||
status: "active",
|
||||
});
|
||||
setupUserMock(
|
||||
{
|
||||
id: "1",
|
||||
email: "test@example.com",
|
||||
role,
|
||||
status: "active",
|
||||
},
|
||||
"1",
|
||||
);
|
||||
|
||||
renderManageOrg();
|
||||
await screen.findByTestId("manage-org-screen");
|
||||
@ -688,7 +696,7 @@ describe("Manage Org Route", () => {
|
||||
);
|
||||
|
||||
it("should open delete confirmation modal when delete button is clicked (with permission)", async () => {
|
||||
setupUserMock(TEST_USERS.OWNER);
|
||||
setupUserMock(TEST_USERS.OWNER, "1");
|
||||
|
||||
renderManageOrg();
|
||||
await screen.findByTestId("manage-org-screen");
|
||||
|
||||
@ -3,7 +3,7 @@ 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 { selectOrganization, createGetMeResponse } from "test-utils";
|
||||
import { organizationService } from "#/api/organization-service/organization-service.api";
|
||||
import ManageOrganizationMembers from "#/routes/manage-organization-members";
|
||||
import SettingsScreen, {
|
||||
@ -15,6 +15,7 @@ import {
|
||||
resetOrgsAndMembersMockData,
|
||||
} from "#/mocks/org-handlers";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import { OrganizationMember } from "#/types/org";
|
||||
|
||||
function ManageOrganizationMembersWithPortalRoot() {
|
||||
return (
|
||||
@ -60,12 +61,18 @@ describe("Manage Organization Members Route", () => {
|
||||
queryClient = new QueryClient();
|
||||
|
||||
// Set default mock for user (admin role has invite permission)
|
||||
getMeSpy.mockResolvedValue({
|
||||
id: "1",
|
||||
email: "test@example.com",
|
||||
role: "admin",
|
||||
status: "active",
|
||||
});
|
||||
// orgIndex 0 maps to orgId "1"
|
||||
getMeSpy.mockResolvedValue(
|
||||
createGetMeResponse(
|
||||
{
|
||||
id: "1",
|
||||
email: "test@example.com",
|
||||
role: "admin",
|
||||
status: "active",
|
||||
},
|
||||
"1",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -145,7 +152,9 @@ describe("Manage Organization Members Route", () => {
|
||||
},
|
||||
orgIndex: number,
|
||||
) => {
|
||||
getMeSpy.mockResolvedValue(userData);
|
||||
// Map orgIndex to orgId: 0 -> "1", 1 -> "2", 2 -> "3"
|
||||
const orgId = String(orgIndex + 1);
|
||||
getMeSpy.mockResolvedValue(createGetMeResponse(userData, orgId));
|
||||
renderManageOrganizationMembers();
|
||||
await screen.findByTestId("manage-organization-members-settings");
|
||||
await selectOrganization({ orgIndex });
|
||||
@ -478,12 +487,17 @@ describe("Manage Organization Members Route", () => {
|
||||
])(
|
||||
"should show invite button when user has canInviteUsers permission ($roleName role)",
|
||||
async ({ role }) => {
|
||||
getMeSpy.mockResolvedValue({
|
||||
id: "1",
|
||||
email: "test@example.com",
|
||||
role,
|
||||
status: "active",
|
||||
});
|
||||
getMeSpy.mockResolvedValue(
|
||||
createGetMeResponse(
|
||||
{
|
||||
id: "1",
|
||||
email: "test@example.com",
|
||||
role,
|
||||
status: "active",
|
||||
},
|
||||
"1",
|
||||
),
|
||||
);
|
||||
|
||||
await setupTestWithOrg(0);
|
||||
|
||||
@ -503,7 +517,7 @@ describe("Manage Organization Members Route", () => {
|
||||
};
|
||||
|
||||
// Set mock and remove cached query before rendering
|
||||
getMeSpy.mockResolvedValue(userData);
|
||||
getMeSpy.mockResolvedValue(createGetMeResponse(userData, "1"));
|
||||
// Remove any cached "me" queries so fresh data is fetched
|
||||
queryClient.removeQueries({ queryKey: ["organizations"] });
|
||||
|
||||
|
||||
@ -2,12 +2,13 @@ import {
|
||||
Organization,
|
||||
OrganizationMember,
|
||||
OrganizationUserRole,
|
||||
GetMeResponse,
|
||||
} from "#/types/org";
|
||||
import { openHands } from "../open-hands-axios";
|
||||
|
||||
export const organizationService = {
|
||||
getMe: async ({ orgId }: { orgId: string }) => {
|
||||
const { data } = await openHands.get<OrganizationMember>(
|
||||
const { data } = await openHands.get<GetMeResponse>(
|
||||
`/api/organizations/${orgId}/me`,
|
||||
);
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { useConfig } from "./use-config";
|
||||
import { organizationService } from "#/api/organization-service/organization-service.api";
|
||||
import { useSelectedOrganizationId } from "#/context/use-selected-organization";
|
||||
import { OrganizationMember } from "#/types/org";
|
||||
|
||||
export const useMe = () => {
|
||||
const { data: config } = useConfig();
|
||||
@ -11,7 +12,24 @@ export const useMe = () => {
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["organizations", orgId, "me"],
|
||||
queryFn: () => organizationService.getMe({ orgId: orgId! }),
|
||||
queryFn: async () => {
|
||||
const response = await organizationService.getMe({ orgId: orgId! });
|
||||
// Find the current organization (isCurrent: true)
|
||||
const currentOrg = response.orgs.find((org) => org.isCurrent);
|
||||
|
||||
if (!currentOrg) {
|
||||
throw new Error("Current organization not found in response");
|
||||
}
|
||||
|
||||
const me: OrganizationMember = {
|
||||
id: response.userId,
|
||||
email: response.email,
|
||||
role: currentOrg.role,
|
||||
status: "active",
|
||||
};
|
||||
|
||||
return me;
|
||||
},
|
||||
enabled: isSaas && !!orgId,
|
||||
});
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
Organization,
|
||||
OrganizationMember,
|
||||
OrganizationUserRole,
|
||||
GetMeResponse,
|
||||
} from "#/types/org";
|
||||
|
||||
const MOCK_ME: Omit<OrganizationMember, "role"> = {
|
||||
@ -134,26 +135,28 @@ export const ORG_HANDLERS = [
|
||||
);
|
||||
}
|
||||
|
||||
let role: OrganizationUserRole = "user";
|
||||
switch (orgId) {
|
||||
case "1":
|
||||
role = "owner";
|
||||
break;
|
||||
case "2":
|
||||
role = "user";
|
||||
break;
|
||||
case "3":
|
||||
role = "admin";
|
||||
break;
|
||||
default:
|
||||
role = "user";
|
||||
}
|
||||
|
||||
const me: OrganizationMember = {
|
||||
...MOCK_ME,
|
||||
role,
|
||||
// Define user's role in each organization
|
||||
const orgRoles: Record<string, OrganizationUserRole> = {
|
||||
"1": "owner",
|
||||
"2": "user",
|
||||
"3": "admin",
|
||||
};
|
||||
return HttpResponse.json(me);
|
||||
|
||||
// Build the orgs array with all organizations the user belongs to
|
||||
const userOrgs = INITIAL_MOCK_ORGS.map((org) => ({
|
||||
orgId: org.id,
|
||||
orgName: org.name,
|
||||
role: orgRoles[org.id] || "user",
|
||||
isCurrent: org.id === orgId,
|
||||
}));
|
||||
|
||||
const response: GetMeResponse = {
|
||||
userId: MOCK_ME.id,
|
||||
email: MOCK_ME.email,
|
||||
orgs: userOrgs,
|
||||
};
|
||||
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
http.get("/api/organizations/:orgId/members", ({ params }) => {
|
||||
|
||||
@ -15,9 +15,8 @@ import { useMe } from "#/hooks/query/use-me";
|
||||
import { rolePermissions } from "#/utils/org/permissions";
|
||||
import {
|
||||
getSelectedOrgFromQueryClient,
|
||||
getMeFromQueryClient,
|
||||
fetchAndCacheMe,
|
||||
} from "#/utils/query-client-getters";
|
||||
import { queryClient } from "#/query-client-config";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { amountIsValid } from "#/utils/amount-is-valid";
|
||||
|
||||
@ -285,12 +284,7 @@ function AddCreditsModal({ onClose }: AddCreditsModalProps) {
|
||||
|
||||
export const clientLoader = async () => {
|
||||
const selectedOrgId = getSelectedOrgFromQueryClient();
|
||||
let me = getMeFromQueryClient(selectedOrgId);
|
||||
|
||||
if (!me && selectedOrgId) {
|
||||
me = await organizationService.getMe({ orgId: selectedOrgId });
|
||||
queryClient.setQueryData(["organizations", selectedOrgId, "me"], me);
|
||||
}
|
||||
const me = selectedOrgId ? await fetchAndCacheMe(selectedOrgId) : null;
|
||||
|
||||
if (!me || me.role === "user") {
|
||||
// if user is USER role, redirect to user settings
|
||||
|
||||
@ -12,22 +12,15 @@ import { useRemoveMember } from "#/hooks/mutation/use-remove-member";
|
||||
import { useMe } from "#/hooks/query/use-me";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { rolePermissions } from "#/utils/org/permissions";
|
||||
import { organizationService } from "#/api/organization-service/organization-service.api";
|
||||
import { queryClient } from "#/query-client-config";
|
||||
import {
|
||||
getSelectedOrgFromQueryClient,
|
||||
getMeFromQueryClient,
|
||||
fetchAndCacheMe,
|
||||
} from "#/utils/query-client-getters";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export const clientLoader = async () => {
|
||||
const selectedOrgId = getSelectedOrgFromQueryClient();
|
||||
let me = getMeFromQueryClient(selectedOrgId);
|
||||
|
||||
if (!me && selectedOrgId) {
|
||||
me = await organizationService.getMe({ orgId: selectedOrgId });
|
||||
queryClient.setQueryData(["organizations", selectedOrgId, "me"], me);
|
||||
}
|
||||
const me = selectedOrgId ? await fetchAndCacheMe(selectedOrgId) : null;
|
||||
|
||||
if (!me || me.role === "user") {
|
||||
// if user is USER role, redirect to user settings
|
||||
|
||||
@ -12,3 +12,16 @@ export interface OrganizationMember {
|
||||
role: OrganizationUserRole;
|
||||
status: "active" | "invited";
|
||||
}
|
||||
|
||||
export interface UserOrgInfo {
|
||||
orgId: string;
|
||||
orgName: string;
|
||||
role: OrganizationUserRole;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
export interface GetMeResponse {
|
||||
userId: string;
|
||||
email: string;
|
||||
orgs: UserOrgInfo[];
|
||||
}
|
||||
|
||||
@ -1,8 +1,48 @@
|
||||
import { queryClient } from "#/query-client-config";
|
||||
import { OrganizationMember } from "#/types/org";
|
||||
import { organizationService } from "#/api/organization-service/organization-service.api";
|
||||
|
||||
export const getMeFromQueryClient = (orgId: string | undefined) =>
|
||||
queryClient.getQueryData<OrganizationMember>(["organizations", orgId, "me"]);
|
||||
|
||||
export const getSelectedOrgFromQueryClient = () =>
|
||||
queryClient.getQueryData<string>(["selected_organization"]);
|
||||
|
||||
/**
|
||||
* Fetches and transforms the user's organization membership data.
|
||||
* Checks cache first, then fetches from API if not cached.
|
||||
* Transforms GetMeResponse to OrganizationMember format.
|
||||
*
|
||||
* @param orgId - The organization ID to fetch membership for
|
||||
* @returns The transformed OrganizationMember, or null if not found
|
||||
*/
|
||||
export const fetchAndCacheMe = async (
|
||||
orgId: string,
|
||||
): Promise<OrganizationMember | null> => {
|
||||
// Check cache first
|
||||
let me = getMeFromQueryClient(orgId);
|
||||
if (me) {
|
||||
return me;
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
const response = await organizationService.getMe({ orgId });
|
||||
const currentOrg = response.orgs.find((org) => org.isCurrent);
|
||||
|
||||
if (!currentOrg) {
|
||||
throw new Error("Current organization not found in response");
|
||||
}
|
||||
|
||||
// Transform to OrganizationMember format
|
||||
me = {
|
||||
id: response.userId,
|
||||
email: response.email,
|
||||
role: currentOrg.role,
|
||||
status: "active",
|
||||
};
|
||||
|
||||
// Cache the result
|
||||
queryClient.setQueryData(["organizations", orgId, "me"], me);
|
||||
|
||||
return me;
|
||||
};
|
||||
|
||||
@ -7,6 +7,7 @@ import { expect, vi } from "vitest";
|
||||
import { AxiosError } from "axios";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { INITIAL_MOCK_ORGS } from "#/mocks/org-handlers";
|
||||
import { GetMeResponse, OrganizationMember } from "#/types/org";
|
||||
|
||||
// Mock useParams before importing components
|
||||
vi.mock("react-router", async () => {
|
||||
@ -98,3 +99,37 @@ export const selectOrganization = async ({
|
||||
const option = await screen.findByText(targetOrg.name);
|
||||
await userEvent.click(option);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to convert OrganizationMember to GetMeResponse for testing.
|
||||
* This creates a GetMeResponse structure that matches the new API format.
|
||||
*
|
||||
* @param userData - The user data (OrganizationMember format)
|
||||
* @param orgId - The organization ID that should be marked as current
|
||||
* @returns GetMeResponse with all orgs, marking the specified orgId as current
|
||||
*/
|
||||
export const createGetMeResponse = (
|
||||
userData: OrganizationMember,
|
||||
orgId: string,
|
||||
): GetMeResponse => {
|
||||
// Map orgIndex to orgId: 0 -> "1", 1 -> "2", 2 -> "3"
|
||||
const orgRoles: Record<string, OrganizationMember["role"]> = {
|
||||
"1": "owner",
|
||||
"2": "user",
|
||||
"3": "admin",
|
||||
};
|
||||
|
||||
// Build orgs array with all organizations, marking the requested one as current
|
||||
const orgs = INITIAL_MOCK_ORGS.map((org) => ({
|
||||
orgId: org.id,
|
||||
orgName: org.name,
|
||||
role: org.id === orgId ? userData.role : (orgRoles[org.id] || "user"),
|
||||
isCurrent: org.id === orgId,
|
||||
}));
|
||||
|
||||
return {
|
||||
userId: userData.id,
|
||||
email: userData.email,
|
||||
orgs,
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user