mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
226 lines
7.5 KiB
TypeScript
226 lines
7.5 KiB
TypeScript
import { renderHook, waitFor } from "@testing-library/react";
|
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import React from "react";
|
|
import { useSettings } from "#/hooks/query/use-settings";
|
|
import { useGetSecrets } from "#/hooks/query/use-get-secrets";
|
|
import { useApiKeys } from "#/hooks/query/use-api-keys";
|
|
import SettingsService from "#/api/settings-service/settings-service.api";
|
|
import { SecretsService } from "#/api/secrets-service";
|
|
import ApiKeysClient from "#/api/api-keys";
|
|
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
|
import { useSelectedOrganizationStore } from "#/stores/selected-organization-store";
|
|
|
|
vi.mock("#/hooks/query/use-config", () => ({
|
|
useConfig: () => ({
|
|
data: { app_mode: "saas" },
|
|
}),
|
|
}));
|
|
|
|
vi.mock("#/hooks/query/use-is-authed", () => ({
|
|
useIsAuthed: () => ({
|
|
data: true,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("#/hooks/use-is-on-intermediate-page", () => ({
|
|
useIsOnIntermediatePage: () => false,
|
|
}));
|
|
|
|
describe("Organization-scoped query hooks", () => {
|
|
let queryClient: QueryClient;
|
|
|
|
const createWrapper = () => {
|
|
return ({ children }: { children: React.ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
};
|
|
|
|
beforeEach(() => {
|
|
queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: false,
|
|
},
|
|
},
|
|
});
|
|
useSelectedOrganizationStore.setState({ organizationId: "org-1" });
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("useSettings", () => {
|
|
it("should include organizationId in query key for proper cache isolation", async () => {
|
|
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
|
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
|
|
|
const { result } = renderHook(() => useSettings(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isFetched).toBe(true));
|
|
|
|
// Verify the query was cached with the org-specific key
|
|
const cachedData = queryClient.getQueryData(["settings", "org-1"]);
|
|
expect(cachedData).toBeDefined();
|
|
|
|
// Verify no data is cached under the old key without org ID
|
|
const oldKeyData = queryClient.getQueryData(["settings"]);
|
|
expect(oldKeyData).toBeUndefined();
|
|
});
|
|
|
|
it("should refetch when organization changes", async () => {
|
|
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
|
getSettingsSpy.mockResolvedValue({
|
|
...MOCK_DEFAULT_USER_SETTINGS,
|
|
language: "en",
|
|
});
|
|
|
|
// First render with org-1
|
|
const { result, rerender } = renderHook(() => useSettings(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isFetched).toBe(true));
|
|
expect(getSettingsSpy).toHaveBeenCalledTimes(1);
|
|
|
|
// Change organization
|
|
useSelectedOrganizationStore.setState({ organizationId: "org-2" });
|
|
getSettingsSpy.mockResolvedValue({
|
|
...MOCK_DEFAULT_USER_SETTINGS,
|
|
language: "es",
|
|
});
|
|
|
|
// Rerender to pick up the new org ID
|
|
rerender();
|
|
|
|
await waitFor(() => {
|
|
// Should have fetched again for the new org
|
|
expect(getSettingsSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
// Verify both org caches exist independently
|
|
const org1Data = queryClient.getQueryData(["settings", "org-1"]);
|
|
const org2Data = queryClient.getQueryData(["settings", "org-2"]);
|
|
expect(org1Data).toBeDefined();
|
|
expect(org2Data).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("useGetSecrets", () => {
|
|
it("should include organizationId in query key for proper cache isolation", async () => {
|
|
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
|
|
getSecretsSpy.mockResolvedValue([]);
|
|
|
|
const { result } = renderHook(() => useGetSecrets(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isFetched).toBe(true));
|
|
|
|
// Verify the query was cached with the org-specific key
|
|
const cachedData = queryClient.getQueryData(["secrets", "org-1"]);
|
|
expect(cachedData).toBeDefined();
|
|
|
|
// Verify no data is cached under the old key without org ID
|
|
const oldKeyData = queryClient.getQueryData(["secrets"]);
|
|
expect(oldKeyData).toBeUndefined();
|
|
});
|
|
|
|
it("should fetch different data when organization changes", async () => {
|
|
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
|
|
|
|
// Mock different secrets for different orgs
|
|
getSecretsSpy.mockResolvedValueOnce([
|
|
{ name: "SECRET_ORG_1", description: "Org 1 secret" },
|
|
]);
|
|
|
|
const { result, rerender } = renderHook(() => useGetSecrets(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isFetched).toBe(true));
|
|
expect(result.current.data).toHaveLength(1);
|
|
expect(result.current.data?.[0].name).toBe("SECRET_ORG_1");
|
|
|
|
// Change organization
|
|
useSelectedOrganizationStore.setState({ organizationId: "org-2" });
|
|
getSecretsSpy.mockResolvedValueOnce([
|
|
{ name: "SECRET_ORG_2", description: "Org 2 secret" },
|
|
]);
|
|
|
|
rerender();
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.data?.[0]?.name).toBe("SECRET_ORG_2");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("useApiKeys", () => {
|
|
it("should include organizationId in query key for proper cache isolation", async () => {
|
|
const getApiKeysSpy = vi.spyOn(ApiKeysClient, "getApiKeys");
|
|
getApiKeysSpy.mockResolvedValue([]);
|
|
|
|
const { result } = renderHook(() => useApiKeys(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isFetched).toBe(true));
|
|
|
|
// Verify the query was cached with the org-specific key
|
|
const cachedData = queryClient.getQueryData(["api-keys", "org-1"]);
|
|
expect(cachedData).toBeDefined();
|
|
|
|
// Verify no data is cached under the old key without org ID
|
|
const oldKeyData = queryClient.getQueryData(["api-keys"]);
|
|
expect(oldKeyData).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("Cache isolation between organizations", () => {
|
|
it("should maintain separate caches for each organization", async () => {
|
|
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
|
|
|
// Simulate fetching for org-1
|
|
getSettingsSpy.mockResolvedValueOnce({
|
|
...MOCK_DEFAULT_USER_SETTINGS,
|
|
language: "en",
|
|
});
|
|
|
|
useSelectedOrganizationStore.setState({ organizationId: "org-1" });
|
|
const { rerender } = renderHook(() => useSettings(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(queryClient.getQueryData(["settings", "org-1"])).toBeDefined();
|
|
});
|
|
|
|
// Switch to org-2
|
|
getSettingsSpy.mockResolvedValueOnce({
|
|
...MOCK_DEFAULT_USER_SETTINGS,
|
|
language: "fr",
|
|
});
|
|
|
|
useSelectedOrganizationStore.setState({ organizationId: "org-2" });
|
|
rerender();
|
|
|
|
await waitFor(() => {
|
|
expect(queryClient.getQueryData(["settings", "org-2"])).toBeDefined();
|
|
});
|
|
|
|
// Switch back to org-1 - should use cached data, not refetch
|
|
useSelectedOrganizationStore.setState({ organizationId: "org-1" });
|
|
rerender();
|
|
|
|
// org-1 data should still be in cache
|
|
const org1Cache = queryClient.getQueryData(["settings", "org-1"]) as any;
|
|
expect(org1Cache?.language).toBe("en");
|
|
|
|
// org-2 data should also still be in cache
|
|
const org2Cache = queryClient.getQueryData(["settings", "org-2"]) as any;
|
|
expect(org2Cache?.language).toBe("fr");
|
|
});
|
|
});
|
|
});
|