feat(14.1-04): cloud row click navigates to cloud-file-detail route; no auto preview/download
- onFileOpen in CloudFolderView replaced: row click now pushes router.push({
name: 'cloud-file-detail', params: {connectionId, itemId: file.provider_item_id} })
- Auto-download-on-unsupported_preview branch removed from row open path (D-14)
- No window.open / no provider URL navigation (preserves Phase 13 D-02 / T-13-07)
- Folder navigation (folder-navigate) is unchanged
- CloudFolderOpenPreview.test.js updated to assert new navigate-to-detail
behavior instead of old openCloudFile/downloadCloudFile assertions (D-14):
- All 3 describe blocks updated to assert mockPush to cloud-file-detail
- Assertions for no provider URLs / no anchor downloads preserved
- All 489 frontend tests pass; 88 backend tests pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
935accc91f
commit
6152a52bca
@@ -406,36 +406,31 @@ async function onQueueResolve({ action, item }) {
|
||||
/**
|
||||
* Handle file-open from StorageBrowser.
|
||||
*
|
||||
* D-02: Never calls window.open() with a raw provider URL.
|
||||
* T-13-07: Must use the authorized backend open endpoint.
|
||||
* D-18: Backend decides whether to serve binary preview or trigger authorized download.
|
||||
* Phase 14.1 / D-01 / CLOUD-02 / CACHE-03:
|
||||
* Cloud file rows now navigate to the cloud-file-detail route — they do NOT call
|
||||
* openCloudFile or downloadCloudFile directly from the row click.
|
||||
*
|
||||
* The authorized endpoint returns:
|
||||
* {kind: 'open', url: '<docuvault-relative-url>'} — for in-app preview
|
||||
* {kind: 'unsupported_preview', reason: '...'} — for unsupported formats (Office/Workspace)
|
||||
* Preview and download become explicit user actions on the detail surface (Plan 03),
|
||||
* never side effects of a row click (D-14).
|
||||
*
|
||||
* For unsupported formats the client calls the authorized download endpoint to get
|
||||
* the file through DocuVault's own proxy, never via a raw provider URL.
|
||||
* Route pushes use the opaque provider_item_id as the route param.
|
||||
* Vue Router handles URI encoding — never split or decode provider_item_id here.
|
||||
*
|
||||
* Never uses browser open(url) — this pushes a DocuVault-internal named route only
|
||||
* (preserves Phase 13 D-02 / T-13-07 prohibition on raw provider URL navigation).
|
||||
*
|
||||
* Folder navigation (folder-navigate event) is unchanged — only file rows are
|
||||
* affected by this change.
|
||||
*/
|
||||
async function onFileOpen(file) {
|
||||
function onFileOpen(file) {
|
||||
if (!file?.provider_item_id) return
|
||||
try {
|
||||
const result = await api.openCloudFile(connectionId.value, file.provider_item_id, file)
|
||||
if (result?.kind === 'unsupported_preview') {
|
||||
// D-18: authorized download fallback for Office / Workspace formats
|
||||
// Backend download endpoint serves bytes through DocuVault auth — no provider URL
|
||||
// D-18 fallback: authorized download through DocuVault (not raw provider URL)
|
||||
if (typeof api.downloadCloudFile === 'function') {
|
||||
await api.downloadCloudFile(connectionId.value, file.provider_item_id)
|
||||
} else {
|
||||
toast.show(`"${file.name}" cannot be previewed in-app.`, 'info')
|
||||
}
|
||||
}
|
||||
// For supported binary types (PDF, images), the backend streams bytes directly.
|
||||
// The view does not need to do anything else — the server handles the preview.
|
||||
} catch (e) {
|
||||
toast.show(`Could not open "${file.name}": ${e.message || 'Unknown error'}`, 'error')
|
||||
}
|
||||
router.push({
|
||||
name: 'cloud-file-detail',
|
||||
params: {
|
||||
connectionId: connectionId.value,
|
||||
itemId: file.provider_item_id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ── Phase 14: Analysis handlers ───────────────────────────────────────────────
|
||||
|
||||
@@ -137,20 +137,28 @@ afterEach(() => {
|
||||
sessionStorage.clear()
|
||||
})
|
||||
|
||||
// ── D-02 / T-13-07: Authorized open — no raw provider URLs ───────────────────
|
||||
// ── D-02 / T-13-07 / CLOUD-02 / D-14: Row click navigates to cloud-file-detail ─
|
||||
//
|
||||
// Phase 14.1 Plan 04 change: cloud file row clicks now navigate to the
|
||||
// cloud-file-detail route instead of calling openCloudFile + auto-download.
|
||||
// Preview and download become explicit actions on the detail surface.
|
||||
// This preserves D-02/T-13-07 (no window.open; no provider URL) while also
|
||||
// satisfying D-14 (no auto-download on row click) and CACHE-03 (bytes only
|
||||
// through the detail/authorized handlers).
|
||||
|
||||
describe('file_open_routes_through_authorized_backend', () => {
|
||||
it('file-open event triggers an authorized API call, not window.open()', async () => {
|
||||
it('file-open event navigates to cloud-file-detail route, 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.
|
||||
* Phase 14.1 / D-01 / CLOUD-02:
|
||||
* A cloud file row click must navigate to the cloud-file-detail named route
|
||||
* with the opaque provider_item_id as the route param.
|
||||
*
|
||||
* RED: current CloudFolderView has a placeholder for file-open that does nothing
|
||||
* or may call window.open(); the authorized API call is missing.
|
||||
* Must NOT:
|
||||
* - call window.open() with a raw provider URL (T-13-07 / D-02)
|
||||
* - call openCloudFile directly on row click (D-14 — preview is explicit on detail)
|
||||
* - auto-download on row click (D-14)
|
||||
*/
|
||||
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],
|
||||
@@ -173,26 +181,28 @@ describe('file_open_routes_through_authorized_backend', () => {
|
||||
// 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()
|
||||
)
|
||||
// Must NOT call the open/download API directly on row click (D-14)
|
||||
expect(api.openCloudFile).not.toHaveBeenCalled()
|
||||
|
||||
// Must navigate to cloud-file-detail route (Phase 14.1 D-01)
|
||||
expect(mockPush).toHaveBeenCalledWith({
|
||||
name: 'cloud-file-detail',
|
||||
params: {
|
||||
connectionId: 'uuid-conn-preview',
|
||||
itemId: PDF_FILE.provider_item_id,
|
||||
},
|
||||
})
|
||||
|
||||
openSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('file-open call uses connection_id and provider_item_id — never a raw URL', async () => {
|
||||
it('file-open navigation uses opaque 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.
|
||||
* T-13-07 / D-02: The route push must use the opaque provider_item_id
|
||||
* as the route param. Raw provider URLs must not appear as navigation targets.
|
||||
*
|
||||
* RED: openCloudFile API method does not exist yet.
|
||||
* Phase 14.1: replaced old openCloudFile assertion with route push assertion.
|
||||
*/
|
||||
api.openCloudFile.mockResolvedValue({ preview_url: '/api/cloud/preview/tok' })
|
||||
|
||||
api.getCloudFoldersByConnectionId.mockResolvedValue({
|
||||
items: [PDF_FILE],
|
||||
capabilities: null,
|
||||
@@ -209,31 +219,33 @@ describe('file_open_routes_through_authorized_backend', () => {
|
||||
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?:\/\//)
|
||||
}
|
||||
})
|
||||
// The route push must not contain any raw http/https provider URLs
|
||||
if (mockPush.mock.calls.length > 0) {
|
||||
const pushArg = mockPush.mock.calls[0][0]
|
||||
const stringified = JSON.stringify(pushArg)
|
||||
expect(stringified).not.toMatch(/^https?:\/\//)
|
||||
expect(stringified).not.toMatch(/googleapis\.com/)
|
||||
expect(stringified).not.toMatch(/graph\.microsoft\.com/)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ── D-18: Unsupported format fallback to authorized download ──────────────────
|
||||
// ── D-14 / D-18: Row click never auto-downloads — routes to detail instead ────
|
||||
//
|
||||
// Phase 14.1 Plan 04: unsupported-format auto-download is REMOVED from row click.
|
||||
// All file types (PDF, DOCX, Workspace) navigate to cloud-file-detail on row click.
|
||||
// Preview/download become explicit user actions on the detail surface (Plan 03).
|
||||
|
||||
describe('unsupported_format_uses_authorized_download_fallback', () => {
|
||||
it('Office document (docx) emits or triggers authorized download, not Office native preview', async () => {
|
||||
it('Office document (docx) row click navigates to detail — no auto-download', 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.
|
||||
* D-14 / D-18 (Phase 14.1): Office documents no longer auto-download from row click.
|
||||
* Clicking a DOCX row navigates to cloud-file-detail, same as any other file type.
|
||||
* Download is an explicit action on the detail surface.
|
||||
*
|
||||
* RED: no authorized download fallback path exists in CloudFolderView.
|
||||
* This replaces the Phase 13 "fallback to authorized download" behavior on row click.
|
||||
*/
|
||||
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
api.downloadCloudFile.mockResolvedValue({ download_url: '/api/cloud/download/session-tok' })
|
||||
|
||||
api.getCloudFoldersByConnectionId.mockResolvedValue({
|
||||
items: [DOCX_FILE],
|
||||
@@ -251,28 +263,32 @@ describe('unsupported_format_uses_authorized_download_fallback', () => {
|
||||
await browser.vm.$emit('file-open', DOCX_FILE)
|
||||
await flushPromises()
|
||||
|
||||
// Must not open a raw provider URL window
|
||||
// Must not open a raw provider URL window (D-02)
|
||||
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)
|
||||
// Must NOT auto-download on row click (D-14)
|
||||
expect(api.downloadCloudFile).not.toHaveBeenCalled()
|
||||
expect(api.openCloudFile).not.toHaveBeenCalled()
|
||||
|
||||
// Must navigate to cloud-file-detail (Phase 14.1 D-01)
|
||||
expect(mockPush).toHaveBeenCalledWith({
|
||||
name: 'cloud-file-detail',
|
||||
params: {
|
||||
connectionId: 'uuid-conn-preview',
|
||||
itemId: DOCX_FILE.provider_item_id,
|
||||
},
|
||||
})
|
||||
|
||||
openSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('Google Workspace document falls back to authorized download, not Workspace preview', async () => {
|
||||
it('Google Workspace document row click navigates to detail — no provider URL opened', 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.
|
||||
* D-14 / D-18 (Phase 14.1): Google Workspace documents no longer auto-open
|
||||
* a Workspace preview URL from row click. Row click navigates to cloud-file-detail.
|
||||
* No Google Docs/Sheets preview URL is ever passed to window.open.
|
||||
*/
|
||||
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
api.downloadCloudFile.mockResolvedValue({ download_url: '/api/cloud/download/session-gdoc' })
|
||||
|
||||
api.getCloudFoldersByConnectionId.mockResolvedValue({
|
||||
items: [GDOC_FILE],
|
||||
@@ -290,7 +306,7 @@ describe('unsupported_format_uses_authorized_download_fallback', () => {
|
||||
await browser.vm.$emit('file-open', GDOC_FILE)
|
||||
await flushPromises()
|
||||
|
||||
// Must not open Google Workspace preview URL via window.open
|
||||
// Must not open Google Workspace preview URL via window.open (D-02 / T-13-07)
|
||||
expect(openSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('docs.google.com'),
|
||||
expect.anything()
|
||||
@@ -299,26 +315,26 @@ describe('unsupported_format_uses_authorized_download_fallback', () => {
|
||||
expect.stringContaining('drive.google.com'),
|
||||
expect.anything()
|
||||
)
|
||||
// Must not auto-download (D-14)
|
||||
expect(api.downloadCloudFile).not.toHaveBeenCalled()
|
||||
|
||||
openSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
// ── D-02: No provider credentials in file-open response ──────────────────────
|
||||
// ── D-02: Row click navigates to internal route — no provider credential exposure ─
|
||||
//
|
||||
// Phase 14.1: row click now pushes cloud-file-detail (an internal route), so the
|
||||
// component never renders provider credentials or raw provider URLs in response to
|
||||
// a row click. The check that the rendered HTML contains no provider URLs is preserved.
|
||||
|
||||
describe('file_open_response_contains_no_provider_credentials', () => {
|
||||
it('preview_url returned from API is a DocuVault-relative URL, not a provider URL', async () => {
|
||||
it('row click navigates to internal route — no provider URL rendered in view', 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.
|
||||
* D-02 (Phase 14.1): Row click navigates to cloud-file-detail (internal route).
|
||||
* The view must never render any iframe/embed/anchor with a raw provider URL
|
||||
* as a result of a file row click.
|
||||
*/
|
||||
// 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,
|
||||
@@ -340,6 +356,9 @@ describe('file_open_response_contains_no_provider_credentials', () => {
|
||||
expect(html).not.toMatch(/googleapis\.com/)
|
||||
expect(html).not.toMatch(/graph\.microsoft\.com/)
|
||||
expect(html).not.toMatch(/onedrive\.live\.com/)
|
||||
|
||||
// Must navigate to internal route (not a provider URL)
|
||||
expect(mockPush).toHaveBeenCalledWith(expect.objectContaining({ name: 'cloud-file-detail' }))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -374,16 +393,13 @@ describe('cloud_folder_view_is_thin_data_provider', () => {
|
||||
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 () => {
|
||||
it('file-open is handled by the view as a route navigation (not re-emitted)', 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.
|
||||
* Phase 14.1: CloudFolderView intercepts the file-open event from StorageBrowser
|
||||
* and handles it by navigating to cloud-file-detail — not re-emitting the event.
|
||||
*
|
||||
* RED: CloudFolderView currently has a placeholder; file-open goes unhandled.
|
||||
* The view must NOT pass the raw file-open event up as an unhandled DOM event.
|
||||
*/
|
||||
api.openCloudFile.mockResolvedValue({ preview_url: '/api/cloud/preview/tok' })
|
||||
|
||||
api.getCloudFoldersByConnectionId.mockResolvedValue({
|
||||
items: [PDF_FILE],
|
||||
capabilities: null,
|
||||
@@ -406,19 +422,20 @@ describe('cloud_folder_view_is_thin_data_provider', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ── D-02: Preview download must not trigger a browser-to-device download ──────
|
||||
// ── D-02 / D-14: Row click must not trigger any browser download at all ─────────
|
||||
|
||||
describe('preview_does_not_trigger_device_download', () => {
|
||||
it('previewing a PDF does not create an anchor element and click it', async () => {
|
||||
it('PDF row click navigates to detail — does not create an anchor download hack', 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.
|
||||
* D-02 / D-14 (Phase 14.1):
|
||||
* Row click must not trigger any browser-to-device download — it navigates to
|
||||
* cloud-file-detail instead. No anchor-click hacks, no document.createElement('a')
|
||||
* for download purposes.
|
||||
*
|
||||
* RED: no anchor-click prevention mechanism exists currently.
|
||||
* Download is an explicit user action on the detail surface (Plan 03), not a
|
||||
* side effect of clicking the file row.
|
||||
*/
|
||||
const createElementSpy = vi.spyOn(document, 'createElement')
|
||||
api.openCloudFile.mockResolvedValue({ preview_url: '/api/cloud/preview/tok' })
|
||||
|
||||
api.getCloudFoldersByConnectionId.mockResolvedValue({
|
||||
items: [PDF_FILE],
|
||||
@@ -432,7 +449,7 @@ describe('preview_does_not_trigger_device_download', () => {
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
// Record anchor element creations before the open action
|
||||
// Record anchor element creations before the row click
|
||||
const anchorsBefore = createElementSpy.mock.calls.filter(c => c[0] === 'a').length
|
||||
|
||||
const browser = w.findComponent({ name: 'StorageBrowser' })
|
||||
@@ -443,6 +460,9 @@ describe('preview_does_not_trigger_device_download', () => {
|
||||
const anchorsAfter = createElementSpy.mock.calls.filter(c => c[0] === 'a').length
|
||||
expect(anchorsAfter).toBe(anchorsBefore)
|
||||
|
||||
// Row click navigates to cloud-file-detail (not download)
|
||||
expect(mockPush).toHaveBeenCalledWith(expect.objectContaining({ name: 'cloud-file-detail' }))
|
||||
|
||||
createElementSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user