diff --git a/frontend/editor/src/core/hooks/useFileManager.test.ts b/frontend/editor/src/core/hooks/useFileManager.test.ts new file mode 100644 index 00000000000..34036663cff --- /dev/null +++ b/frontend/editor/src/core/hooks/useFileManager.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, act } from "@testing-library/react"; +import { useFileManager } from "@app/hooks/useFileManager"; +import apiClient from "@app/services/apiClient"; + +vi.mock("@app/services/apiClient", () => ({ + default: { get: vi.fn() }, +})); +vi.mock("@app/contexts/IndexedDBContext", () => ({ + useIndexedDB: () => ({}), +})); +vi.mock("@app/services/fileStorage", () => ({ + fileStorage: { getLeafStirlingFileStubs: vi.fn(async () => []) }, +})); +vi.mock("@app/services/pruneMissingRecentFiles", () => ({ + pruneMissingRecentFiles: vi.fn(async (stubs: unknown[]) => stubs), +})); +vi.mock("@app/hooks/useDiskLinkReconcile", () => ({ + useDiskLinkReconcile: () => ({ + openFileIdsRef: { current: [] }, + onOpenFilesDetached: vi.fn(), + }), +})); +vi.mock("@app/contexts/AppConfigContext", () => ({ + useAppConfig: () => ({ + config: { storageEnabled: true, storageShareLinksEnabled: true }, + }), +})); +const authState = vi.hoisted(() => ({ isAnonymous: false })); +vi.mock("@app/auth/UseSession", () => ({ useAuth: () => authState })); + +const mockGet = vi.mocked(apiClient.get); + +describe("useFileManager server files", () => { + beforeEach(() => { + vi.clearAllMocks(); + authState.isAnonymous = false; + mockGet.mockResolvedValue({ data: [] }); + }); + + it("does not ask the server for a guest's stored files", async () => { + authState.isAnonymous = true; + const { result } = renderHook(() => useFileManager()); + + await act(async () => { + await result.current.loadRecentFiles(); + }); + + expect(mockGet).not.toHaveBeenCalled(); + }); + + it("loads stored files and accessed share links for an account", async () => { + const { result } = renderHook(() => useFileManager()); + + await act(async () => { + await result.current.loadRecentFiles(); + }); + + const urls = mockGet.mock.calls.map(([url]) => url); + expect(urls).toEqual([ + "/api/v1/storage/files", + "/api/v1/storage/share-links/accessed", + ]); + }); +}); diff --git a/frontend/editor/src/core/hooks/useFileManager.ts b/frontend/editor/src/core/hooks/useFileManager.ts index 87cde4d83c8..82cf2bf222f 100644 --- a/frontend/editor/src/core/hooks/useFileManager.ts +++ b/frontend/editor/src/core/hooks/useFileManager.ts @@ -6,6 +6,7 @@ import { StirlingFileStub, StirlingFile } from "@app/types/fileContext"; import { FileId } from "@app/types/fileContext"; import apiClient from "@app/services/apiClient"; import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { useAuth } from "@app/auth/UseSession"; import { useDiskLinkReconcile } from "@app/hooks/useDiskLinkReconcile"; interface StoredFileResponse { @@ -37,6 +38,7 @@ export const useFileManager = () => { const [loading, setLoading] = useState(false); const indexedDB = useIndexedDB(); const { config } = useAppConfig(); + const { isAnonymous } = useAuth(); // Refs inside, so loadRecentFiles isn't recreated on every workbench change - // its consumers re-run it on identity change, which would loop. @@ -125,7 +127,9 @@ export const useFileManager = () => { ); let combinedStubs = stirlingFileStubs; - const shouldFetchServerFiles = config?.storageEnabled === true; + // Guests have no server storage; the request would only 401. + const shouldFetchServerFiles = + config?.storageEnabled === true && !isAnonymous; if (shouldFetchServerFiles) { try { @@ -380,6 +384,7 @@ export const useFileManager = () => { config?.enableLogin, config?.storageEnabled, config?.storageShareLinksEnabled, + isAnonymous, normalizeServerFileName, openFileIdsRef, onOpenFilesDetached,