/** * Phase 13 Plan 02 Task 1 — CloudFolderView open/preview and authorized download tests. * * RED tests: these define the only acceptable open/preview/download behavior for Phase 13. * They MUST FAIL against the current placeholder cloud handlers in CloudFolderView. * * Coverage: * D-02: Preview stays inside DocuVault; no provider credentials or raw URLs exposed. * D-18: Binary file preview only; unsupported formats fall back to authorized download. * T-13-07: No window.open() or raw provider URL usage anywhere. * CLOUD-02 requirement: authorized open/preview/download through DocuVault auth. * * Security constraints: * - No provider download URLs in API responses accepted by the view. * - Ownership must be checked via the backend endpoint, not client-side. * - No navigator.msSaveBlob or window.URL.createObjectURL with unverified data. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' // ── Router stubs (hoisted before module imports) ────────────────────────────── const mockPush = vi.fn() const mockReplace = vi.fn() vi.mock('vue-router', () => ({ useRouter: () => ({ push: mockPush, replace: mockReplace }), useRoute: () => ({ params: { connectionId: 'uuid-conn-preview', folderId: 'root' }, query: {}, }), })) // ── Store stubs ─────────────────────────────────────────────────────────────── const mockFetchConnections = vi.fn().mockResolvedValue(undefined) const mockSelectConnection = vi.fn() const mockSetBrowseState = vi.fn() vi.mock('../../stores/cloudConnections.js', () => ({ useCloudConnectionsStore: () => ({ connections: [ { id: 'uuid-conn-preview', provider: 'google_drive', display_name: 'My Drive' }, ], loading: false, capabilities: null, folderFreshness: null, lastRefreshedAt: null, byteAvailability: null, fetchConnections: mockFetchConnections, selectConnection: mockSelectConnection, setBrowseState: mockSetBrowseState, defaultDisplayName: (c) => c.provider, }), saveLastFolder: vi.fn(), loadLastFolder: vi.fn(() => null), })) vi.mock('../../stores/toast.js', () => ({ useToastStore: () => ({ show: vi.fn() }), })) // ── API stubs — use vi.fn() inside factory to avoid hoisting issues ─────────── vi.mock('../../api/client.js', () => ({ getCloudFoldersByConnectionId: vi.fn().mockResolvedValue({ items: [], capabilities: null }), uploadToCloud: vi.fn(), listCloudConnections: vi.fn().mockResolvedValue({ items: [] }), openCloudFile: vi.fn(), downloadCloudFile: vi.fn(), })) import CloudFolderView from '../CloudFolderView.vue' import * as api from '../../api/client.js' // ── StorageBrowser stub that can emit events ────────────────────────────────── function makeBrowserStub(extraEmits = []) { return { name: 'StorageBrowser', template: '
', props: [ 'mode', 'folders', 'files', 'breadcrumb', 'uploadQueue', 'loading', 'emptyMessage', 'emptyHint', 'capabilities', 'connectionRoot', 'folderFreshness', 'lastRefreshedAt', 'byteAvailability', ], emits: ['breadcrumb-navigate', 'upload', 'folder-navigate', 'file-open', 'file-download-fallback', 'upload-queue-resolve', ...extraEmits], } } // ── Cloud file fixtures ─────────────────────────────────────────────────────── const PDF_FILE = { id: 'row-id-pdf', provider_item_id: 'provider/ref/report.pdf', name: 'report.pdf', kind: 'file', content_type: 'application/pdf', size: 45000, modified_at: '2026-06-01T12:00:00Z', etag: '"etag-pdf"', capabilities: {}, } const DOCX_FILE = { id: 'row-id-docx', provider_item_id: 'provider/ref/report.docx', name: 'report.docx', kind: 'file', content_type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', size: 20000, modified_at: '2026-06-02T09:00:00Z', etag: '"etag-docx"', capabilities: {}, } const GDOC_FILE = { id: 'row-id-gdoc', provider_item_id: 'provider/ref/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms', name: 'Design Doc', kind: 'file', content_type: 'application/vnd.google-apps.document', size: null, modified_at: '2026-06-03T15:00:00Z', etag: null, capabilities: {}, } const globalStubs = { StorageBrowser: makeBrowserStub(), } beforeEach(() => { setActivePinia(createPinia()) vi.clearAllMocks() sessionStorage.clear() }) afterEach(() => { sessionStorage.clear() }) // ── D-02 / T-13-07: Authorized open — no raw provider URLs ─────────────────── describe('file_open_routes_through_authorized_backend', () => { it('file-open event triggers an authorized API call, not window.open()', async () => { /** * D-02 and T-13-07: Opening a cloud file must never call window.open() with * a raw provider URL. Instead CloudFolderView must call the authorized * backend open/preview endpoint. * * RED: current CloudFolderView has a placeholder for file-open that does nothing * or may call window.open(); the authorized API call is missing. */ const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null) api.openCloudFile.mockResolvedValue({ preview_url: '/api/cloud/preview/session-token-abc' }) api.getCloudFoldersByConnectionId.mockResolvedValue({ items: [PDF_FILE], capabilities: null, freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-20T00:00:00Z' }, }) const BrowserStub = makeBrowserStub() const w = mount(CloudFolderView, { global: { stubs: { StorageBrowser: BrowserStub } }, }) await flushPromises() // Emit file-open from the StorageBrowser const browser = w.findComponent({ name: 'StorageBrowser' }) expect(browser.exists()).toBe(true) await browser.vm.$emit('file-open', PDF_FILE) await flushPromises() // Must NOT open a browser window with raw provider URL expect(openSpy).not.toHaveBeenCalled() // Must call the authorized backend open endpoint expect(api.openCloudFile).toHaveBeenCalledWith( 'uuid-conn-preview', PDF_FILE.provider_item_id, expect.anything() ) openSpy.mockRestore() }) it('file-open call uses connection_id and provider_item_id — never a raw URL', async () => { /** * T-13-07: The authorized open endpoint must be parameterized by * connection_id and provider_item_id. Raw provider download URLs must * not appear as arguments. * * RED: openCloudFile API method does not exist yet. */ api.openCloudFile.mockResolvedValue({ preview_url: '/api/cloud/preview/tok' }) api.getCloudFoldersByConnectionId.mockResolvedValue({ items: [PDF_FILE], capabilities: null, freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-20T00:00:00Z' }, }) const BrowserStub = makeBrowserStub() const w = mount(CloudFolderView, { global: { stubs: { StorageBrowser: BrowserStub } }, }) await flushPromises() const browser = w.findComponent({ name: 'StorageBrowser' }) await browser.vm.$emit('file-open', PDF_FILE) await flushPromises() if (api.openCloudFile.mock.calls.length > 0) { const callArgs = api.openCloudFile.mock.calls[0] // Arguments must not contain http/https raw URLs callArgs.forEach(arg => { if (typeof arg === 'string') { expect(arg).not.toMatch(/^https?:\/\//) } }) } }) }) // ── D-18: Unsupported format fallback to authorized download ────────────────── describe('unsupported_format_uses_authorized_download_fallback', () => { it('Office document (docx) emits or triggers authorized download, not Office native preview', async () => { /** * D-18: Office and Workspace formats are not supported for in-app preview. * The view must route them to the authorized download fallback endpoint rather * than open a Microsoft/Google preview URL. * * RED: no authorized download fallback path exists in CloudFolderView. */ const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null) api.downloadCloudFile.mockResolvedValue({ download_url: '/api/cloud/download/session-tok' }) api.getCloudFoldersByConnectionId.mockResolvedValue({ items: [DOCX_FILE], capabilities: null, freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-20T00:00:00Z' }, }) const BrowserStub = makeBrowserStub() const w = mount(CloudFolderView, { global: { stubs: { StorageBrowser: BrowserStub } }, }) await flushPromises() const browser = w.findComponent({ name: 'StorageBrowser' }) await browser.vm.$emit('file-open', DOCX_FILE) await flushPromises() // Must not open a raw provider URL window expect(openSpy).not.toHaveBeenCalled() // Must call the authorized backend endpoint for unsupported formats // (either openCloudFile that handles the fallback, or downloadCloudFile explicitly) const anyAuthCall = api.openCloudFile.mock.calls.length > 0 || api.downloadCloudFile.mock.calls.length > 0 expect(anyAuthCall).toBe(true) openSpy.mockRestore() }) it('Google Workspace document falls back to authorized download, not Workspace preview', async () => { /** * D-18: Google Workspace documents (application/vnd.google-apps.*) are * excluded from in-app preview. They must use the authorized download fallback. * No Google Docs/Sheets preview URL must be opened. * * RED: no Workspace-aware fallback exists. */ const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null) api.downloadCloudFile.mockResolvedValue({ download_url: '/api/cloud/download/session-gdoc' }) api.getCloudFoldersByConnectionId.mockResolvedValue({ items: [GDOC_FILE], capabilities: null, freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-20T00:00:00Z' }, }) const BrowserStub = makeBrowserStub() const w = mount(CloudFolderView, { global: { stubs: { StorageBrowser: BrowserStub } }, }) await flushPromises() const browser = w.findComponent({ name: 'StorageBrowser' }) await browser.vm.$emit('file-open', GDOC_FILE) await flushPromises() // Must not open Google Workspace preview URL via window.open expect(openSpy).not.toHaveBeenCalledWith( expect.stringContaining('docs.google.com'), expect.anything() ) expect(openSpy).not.toHaveBeenCalledWith( expect.stringContaining('drive.google.com'), expect.anything() ) openSpy.mockRestore() }) }) // ── D-02: No provider credentials in file-open response ────────────────────── describe('file_open_response_contains_no_provider_credentials', () => { it('preview_url returned from API is a DocuVault-relative URL, not a provider URL', async () => { /** * D-02: The backend authorized open endpoint must return a DocuVault-relative * preview URL, not a raw Google/OneDrive/WebDAV URL. The frontend must * verify this is not a provider URL before rendering. * * RED: no preview URL validation logic exists in CloudFolderView. */ // Backend returns a proper DocuVault-relative preview URL const DOCUVAULT_PREVIEW_URL = '/api/cloud/preview/abc123' api.openCloudFile.mockResolvedValue({ preview_url: DOCUVAULT_PREVIEW_URL }) api.getCloudFoldersByConnectionId.mockResolvedValue({ items: [PDF_FILE], capabilities: null, freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-20T00:00:00Z' }, }) const BrowserStub = makeBrowserStub() const w = mount(CloudFolderView, { global: { stubs: { StorageBrowser: BrowserStub } }, }) await flushPromises() const browser = w.findComponent({ name: 'StorageBrowser' }) await browser.vm.$emit('file-open', PDF_FILE) await flushPromises() // Component must not render any iframe/embed with a provider URL const html = w.html() expect(html).not.toMatch(/googleapis\.com/) expect(html).not.toMatch(/graph\.microsoft\.com/) expect(html).not.toMatch(/onedrive\.live\.com/) }) }) // ── CloudFolderView thin-view: delegates to StorageBrowser, no parallel grid ── describe('cloud_folder_view_is_thin_data_provider', () => { it('CloudFolderView does not contain a cloud-specific parallel file grid', async () => { /** * D-01: CloudFolderView must remain a thin data provider. * No cloud-only table or grid layout must be added. * * This test uses the real StorageBrowser stub (not the whole component) * to confirm the view itself has no parallel layout. */ api.getCloudFoldersByConnectionId.mockResolvedValue({ items: [PDF_FILE, DOCX_FILE], capabilities: null, freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-20T00:00:00Z' }, }) const w = mount(CloudFolderView, { global: { stubs: globalStubs }, }) await flushPromises() // View must delegate to StorageBrowser — no table or grid in the view itself expect(w.findAll('table').length).toBe(0) expect(w.find('[data-test="cloud-only-grid"]').exists()).toBe(false) expect(w.find('[data-test="cloud-only-table"]').exists()).toBe(false) // StorageBrowser must be present expect(w.find('[data-test="storage-browser"]').exists()).toBe(true) }) it('file-open is handled by the view, not re-emitted up to the router', async () => { /** * CloudFolderView must intercept the file-open event from StorageBrowser * and handle it (call the authorized API). It must not pass the raw event * up to a parent router or emit it as an unhandled event. * * RED: CloudFolderView currently has a placeholder; file-open goes unhandled. */ api.openCloudFile.mockResolvedValue({ preview_url: '/api/cloud/preview/tok' }) api.getCloudFoldersByConnectionId.mockResolvedValue({ items: [PDF_FILE], capabilities: null, freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-20T00:00:00Z' }, }) const BrowserStub = makeBrowserStub() const w = mount(CloudFolderView, { global: { stubs: { StorageBrowser: BrowserStub } }, }) await flushPromises() const browser = w.findComponent({ name: 'StorageBrowser' }) await browser.vm.$emit('file-open', PDF_FILE) await flushPromises() // The view must handle file-open (call authorized API) // It must NOT re-emit file-open as an unhandled DOM event expect(w.emitted('file-open')).toBeFalsy() }) }) // ── D-02: Preview download must not trigger a browser-to-device download ────── describe('preview_does_not_trigger_device_download', () => { it('previewing a PDF does not create an anchor element and click it', async () => { /** * D-02: "Preview stays inside DocuVault and must not trigger a * browser-to-device download." Anchor click hacks bypass the authorized * download path and expose provider content directly to the filesystem. * * RED: no anchor-click prevention mechanism exists currently. */ const createElementSpy = vi.spyOn(document, 'createElement') api.openCloudFile.mockResolvedValue({ preview_url: '/api/cloud/preview/tok' }) api.getCloudFoldersByConnectionId.mockResolvedValue({ items: [PDF_FILE], capabilities: null, freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-20T00:00:00Z' }, }) const BrowserStub = makeBrowserStub() const w = mount(CloudFolderView, { global: { stubs: { StorageBrowser: BrowserStub } }, }) await flushPromises() // Record anchor element creations before the open action const anchorsBefore = createElementSpy.mock.calls.filter(c => c[0] === 'a').length const browser = w.findComponent({ name: 'StorageBrowser' }) await browser.vm.$emit('file-open', PDF_FILE) await flushPromises() // No new anchor element should have been created for the download hack const anchorsAfter = createElementSpy.mock.calls.filter(c => c[0] === 'a').length expect(anchorsAfter).toBe(anchorsBefore) createElementSpy.mockRestore() }) }) // ── Phase 14 Plan 06: Cache transparency — frontend never sees object_key ────── describe('cache_backed_response_has_no_object_key', () => { it('openCloudFile response processed by the view does not expose cache object_key', async () => { /** * Phase 14 Plan 06 / T-14-02: Even when the backend serves bytes from the * byte cache (MinIO), the API response shape seen by CloudFolderView must * not include an object_key field. * * The cache lifecycle is entirely backend-owned — the frontend receives * the same {kind: 'open', url: ...} JSON response regardless of whether * bytes came from the provider or from the MinIO cache. */ // Response matching what the backend sends (cache hit or miss — same shape) const OPEN_RESPONSE_WITH_NO_CACHE_FIELDS = { kind: 'open', url: '/api/cloud/connections/uuid-conn-preview/items/provider-id/download', reason: 'authorized', } api.openCloudFile.mockResolvedValue(OPEN_RESPONSE_WITH_NO_CACHE_FIELDS) api.getCloudFoldersByConnectionId.mockResolvedValue({ items: [PDF_FILE], capabilities: null, freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-20T00:00:00Z' }, }) const BrowserStub = makeBrowserStub() const w = mount(CloudFolderView, { global: { stubs: { StorageBrowser: BrowserStub } }, }) await flushPromises() const browser = w.findComponent({ name: 'StorageBrowser' }) await browser.vm.$emit('file-open', PDF_FILE) await flushPromises() // The rendered HTML must not expose any cache internals const html = w.html() expect(html).not.toContain('object_key') expect(html).not.toContain('cache/') expect(html).not.toContain('credentials_enc') // The API call arguments must not contain object_key or MinIO paths if (api.openCloudFile.mock.calls.length > 0) { const callArgs = api.openCloudFile.mock.calls[0] callArgs.forEach(arg => { if (typeof arg === 'string') { expect(arg).not.toContain('object_key') expect(arg).not.toMatch(/^cache\/[0-9a-f-]+\//) } }) } }) })