feat(12-03): connection-ID routing, rename, session folder memory, thin views

- Route /cloud/:connectionId/:folderId replaces /cloud/:provider/:folderId
- api/cloud.js: getCloudFoldersByConnectionId + renameCloudConnection
- cloudConnections store: rename, selectConnection, setBrowseState, defaultDisplayName, session storage helpers
- CloudStorageView: thin — passes connectionRoots to StorageBrowser
- CloudFolderView: thin — uses connectionId param, never provider slug
- SettingsCloudTab: inline rename input for each active connection
- 27 tests pass: store, view, settings
This commit is contained in:
curo1305
2026-06-18 23:25:29 +02:00
parent 6f0ecfa39b
commit bf9af11274
10 changed files with 594 additions and 91 deletions
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
// Mock api/client.js — no real HTTP calls in unit tests (CLAUDE.md W4)
@@ -7,14 +7,22 @@ vi.mock('../../api/client.js', () => ({
disconnectCloud: vi.fn(),
connectWebDav: vi.fn(),
updateDefaultStorage: vi.fn(),
renameCloudConnection: vi.fn(),
getCloudFoldersByConnectionId: vi.fn(),
}))
import { useCloudConnectionsStore } from '../cloudConnections.js'
import { useCloudConnectionsStore, saveLastFolder, loadLastFolder } from '../cloudConnections.js'
import * as api from '../../api/client.js'
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
// Clear sessionStorage between tests
sessionStorage.clear()
})
afterEach(() => {
sessionStorage.clear()
})
describe('useCloudConnectionsStore', () => {
@@ -56,4 +64,95 @@ describe('useCloudConnectionsStore', () => {
await store.disconnectAll()
expect(store.connections).toHaveLength(0)
})
// Rename tests
it('rename updates the connection in state on success', async () => {
api.renameCloudConnection.mockResolvedValue({
id: 'conn-1', provider: 'google_drive', display_name: 'My Drive',
})
const store = useCloudConnectionsStore()
store.connections = [{ id: 'conn-1', provider: 'google_drive', display_name: 'Google Drive' }]
await store.rename('conn-1', 'My Drive')
expect(store.connections[0].display_name).toBe('My Drive')
expect(api.renameCloudConnection).toHaveBeenCalledWith('conn-1', 'My Drive')
})
it('rename rejects on empty display name', async () => {
const store = useCloudConnectionsStore()
await expect(store.rename('conn-1', '')).rejects.toThrow()
})
it('rename rejects on whitespace-only display name', async () => {
const store = useCloudConnectionsStore()
await expect(store.rename('conn-1', ' ')).rejects.toThrow()
})
it('rename propagates API error', async () => {
api.renameCloudConnection.mockRejectedValue(new Error('API error'))
const store = useCloudConnectionsStore()
store.connections = [{ id: 'conn-1', provider: 'google_drive', display_name: 'Google Drive' }]
await expect(store.rename('conn-1', 'New Name')).rejects.toThrow('API error')
})
// Two same-provider connections render as separate roots
it('two google_drive connections render as separate roots with UUID suffixes in defaultDisplayName', () => {
const store = useCloudConnectionsStore()
store.connections = [
{ id: 'aabb-ccdd-1', provider: 'google_drive', display_name: null },
{ id: 'eeff-gggg-2', provider: 'google_drive', display_name: null },
]
const nameA = store.defaultDisplayName(store.connections[0])
const nameB = store.defaultDisplayName(store.connections[1])
// Both should contain "Google Drive" and a 4-char suffix
expect(nameA).toContain('Google Drive')
expect(nameB).toContain('Google Drive')
expect(nameA).toContain('aabb')
expect(nameB).toContain('eeff')
})
it('single connection has no suffix in defaultDisplayName', () => {
const store = useCloudConnectionsStore()
store.connections = [{ id: 'conn-1', provider: 'google_drive' }]
expect(store.defaultDisplayName(store.connections[0])).toBe('Google Drive')
})
// Connection UUID based URLs
it('selectConnection sets selectedConnectionId', () => {
const store = useCloudConnectionsStore()
store.connections = [{ id: 'uuid-123', provider: 'onedrive' }]
store.selectConnection('uuid-123')
expect(store.selectedConnectionId).toBe('uuid-123')
expect(store.selectedConnection?.id).toBe('uuid-123')
})
// Session storage for folder navigation
it('saveLastFolder stores folder in sessionStorage', () => {
saveLastFolder('conn-abc', 'Documents/Work')
expect(sessionStorage.getItem('docuvault:cloud:folder:conn-abc')).toBe('Documents/Work')
})
it('loadLastFolder retrieves stored folder', () => {
sessionStorage.setItem('docuvault:cloud:folder:conn-xyz', 'Photos')
expect(loadLastFolder('conn-xyz')).toBe('Photos')
})
it('saveLastFolder removes key for root folder', () => {
sessionStorage.setItem('docuvault:cloud:folder:conn-abc', 'Docs')
saveLastFolder('conn-abc', 'root')
expect(sessionStorage.getItem('docuvault:cloud:folder:conn-abc')).toBeNull()
})
it('loadLastFolder returns null when nothing stored', () => {
expect(loadLastFolder('nonexistent')).toBeNull()
})
it('sessionStorage stores only folder references, not tokens', () => {
// Ensure save only stores the folderId string under the namespaced key
saveLastFolder('conn-123', 'Projects/Secret')
const stored = sessionStorage.getItem('docuvault:cloud:folder:conn-123')
expect(stored).toBe('Projects/Secret')
// Stored value is a plain string path, not a JSON object with credentials
expect(typeof stored).toBe('string')
expect(stored.startsWith('{')).toBe(false)
})
})