refactor: update getme according to actual expected endpoint

This commit is contained in:
hieptl 2025-12-15 23:19:57 +07:00
parent 33f3861d95
commit df950ec11c
10 changed files with 191 additions and 72 deletions

View File

@ -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");

View File

@ -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"] });

View File

@ -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`,
);

View File

@ -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,
});
};

View File

@ -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 }) => {

View File

@ -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

View File

@ -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

View File

@ -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[];
}

View File

@ -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;
};

View File

@ -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,
};
};