From f4ff91180dd11f3f9383e973cf10f73738607dfb Mon Sep 17 00:00:00 2001 From: hugo Date: Wed, 11 Feb 2026 08:42:10 +0100 Subject: [PATCH] fix: removed turso --- .github/copilot-instructions.md | 14 + .gitignore | 4 - README.md | 16 +- VISION.md | 29 +- src/main/engine/SyncEngine.ts | 21 +- tests/engine/SyncEngine.test.ts | 326 ++++-------------- .../renderer/components/SettingsView.test.ts | 57 +-- 7 files changed, 117 insertions(+), 350 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 92fd6e1..119ec25 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -45,6 +45,20 @@ See the [TDD Requirements](#test-driven-development-tdd-requirements) section fo --- +## ⚠️ MANDATORY: Remove Unused Code + +**Never keep unused code around. Always delete it completely.** + +- When a feature is removed, delete ALL related code (implementation, tests, types, configs) +- Do NOT comment out code "for later" - use version control history +- Do NOT skip tests for removed functionality - delete them +- Do NOT leave dead code paths, unused imports, or orphaned functions +- When refactoring, actively look for and remove any code that becomes unused + +> **Delete unused code immediately. No exceptions.** + +--- + ## Architecture Principles ### Separation of Concerns diff --git a/.gitignore b/.gitignore index 3c8f560..12e805e 100644 --- a/.gitignore +++ b/.gitignore @@ -43,10 +43,6 @@ migrations/ .env.production .env.*.local -# Turso/LibSQL credentials -turso-credentials.json -.turso/ - # =================== # IDE & Editor # =================== diff --git a/README.md b/README.md index e5bec25..0edc173 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Blogging Desktop Server (bDS) -A desktop blogging application with offline-first capabilities and cloud sync via Turso/LibSQL. +A desktop blogging application with offline-first capabilities and cloud sync via Dropbox. ## Features - **Offline-First**: All data is stored locally in SQLite, works without internet -- **Cloud Sync**: Synchronize with Turso (LibSQL) for multi-device access +- **Cloud Sync**: Synchronize files with Dropbox for multi-device access - **VS Code-Inspired UI**: Familiar, clean interface with activity bar, sidebar, and editor - **Markdown Posts**: Write blog posts in Markdown with YAML frontmatter - **Media Management**: Import and manage images with metadata sidecar files @@ -20,7 +20,7 @@ src/ │ ├── engine/ # Business logic engines │ │ ├── PostEngine # Post CRUD, file operations │ │ ├── MediaEngine # Media import/management -│ │ ├── SyncEngine # Turso sync logic +│ │ ├── SyncEngine # Dropbox sync logic │ │ └── TaskManager # Async task handling │ ├── ipc/ # IPC handlers for renderer communication │ └── main.ts # App entry point @@ -122,13 +122,13 @@ npx electron-builder ## Cloud Sync Setup -1. Create a Turso database at https://turso.tech -2. Get your database URL and auth token +1. Create a Dropbox App at https://www.dropbox.com/developers/apps +2. Generate an access token for your app 3. Go to Settings in the app -4. Enter your Turso credentials -5. Click "Enable Sync" +4. Enter your Dropbox credentials (access token, app key, remote path) +5. Click "Configure Dropbox" -Auto-sync runs every 5 minutes when configured. +Files are synced to Dropbox for backup and multi-device access. ## License diff --git a/VISION.md b/VISION.md index d41cf50..37ae35f 100644 --- a/VISION.md +++ b/VISION.md @@ -6,24 +6,29 @@ sync to a cloud system for syncing data and also rendering the full blog. ## Main Vision -create a electron app in this folder that uses typescript for all the logic code and sqlite and a proper database framework around it for local storage of data and turso/libsql to sync against a cloud location for having offline work capabilities with syncing. The UI should be aligned with the UI patterns used by vscode. The name of the application is "blogging Desktop Server" and the shortname is bDS. Start with default layout for edit and view menues and things like that. I don't want the app to use raw SQL, I want some proper layer between those and proper wiring where all actual functional code is kept in engine classes and the UI realy just does presentation and reacts to state changes properly, so that long-running processes can properly integrate as async tasks. +create a electron app in this folder that uses typescript for all the logic code and sqlite and a proper +database framework around it for local storage of data. The UI should be aligned with the UI patterns +used by vscode. The name of the application is "blogging Desktop Server" and the shortname is bDS. Startwith +default layout for edit and view menues and things like that. I don't want the app to use raw SQL, I want some +proper layer between those and proper wiring where all actual functional code is kept in engine classes and the +UI realy just does presentation and reacts to state changes properly, so that long-running processes can +properly integrate as async tasks. The main area of the window must be a tabbled view, where multiple tabs can be open at the same time and are retained over program runs. The tabs can be different tabs like media file tabs, post tabs for multiple posts and setting tabs or whatever will come later. -We need a good way to handle the syncing of the non-metadata components (posts and media files), because that -is not part of the database sync. One way could be using something like dropbox in the background, so that -the posts/ and media/ folders are automatically synced to some area in dropbox and transported that way. +The application must be offline-first, everything must work in airplane mode (except publishing of course). +It must be fully self-contained during editing and previewing and managing content. Every internal structure +must have reflections in the filesystem, so available tags, available categories, all those things must be +automatically reflected to the filesystem in a per-project way. Use a meta/ folder under the project folder +for those files. -In addition to dropbox sync, also provide a file sync handler that uses git as the mechanism. That way -the user can provide a git URL to push to and pull from, where the app does regular fetch to see if stuff -was there and has mechanisms to handle conflicts. This will work without special requirements for some -cloud provider for the file data. Git syncing should only require a repository URL supported by git and -will have to handle the authentication outside the app, but users with git will know what they do. But it -might make sense to provide buttons to do a proper git setup in the project to allow the use of the -device authentication flow, so that the user does not have to go into the data folder manually for the -base setup to work. +There should be good cloud-storage based syncing that can be triggered when online again and should use +asynchronous syncing with auto-resolving of issues. Also there should be support to use git as another way +to sync blog projects. This also should be on a per-project level and the UI should support the user to +git init and github auth with device flow properly when setting up git syncing. git syncing should also be +handled asynchronously when online and should use auto-resolve of merge conflicts. Blog post metadata should be managed in the SQLite database in the user local folder, so it persists application runs properly. for blog posts, create a subfolder /posts/ there where each post is stored as a markdown file with a properties segment in the top of the file with YAML like property definitions, so all metadata can always be reconstructed from posts. Do the same with images, keeping them in /media/ under the user local path, in that case storing the image file sand for each image file a properties sidecar file that uses the same header structure as for posts. diff --git a/src/main/engine/SyncEngine.ts b/src/main/engine/SyncEngine.ts index 10f284c..bffa6be 100644 --- a/src/main/engine/SyncEngine.ts +++ b/src/main/engine/SyncEngine.ts @@ -29,27 +29,15 @@ export interface SyncResult { }; } -// Default timeout for sync operations (30 seconds) -const DEFAULT_SYNC_TIMEOUT = 30000; - export class SyncEngine extends EventEmitter { private syncStatus: SyncStatus = 'idle'; private syncConfig: SyncConfig | null = null; private syncIntervalId: NodeJS.Timeout | null = null; - private syncTimeout: number = DEFAULT_SYNC_TIMEOUT; constructor() { super(); } - getSyncTimeout(): number { - return this.syncTimeout; - } - - setSyncTimeout(timeoutMs: number): void { - this.syncTimeout = timeoutMs; - } - getSyncStatus(): SyncStatus { return this.syncStatus; } @@ -163,6 +151,13 @@ export class SyncEngine extends EventEmitter { this.emit('autoSyncStopped'); } + /** + * Sync alias for fullSync for backward compatibility + */ + async sync(direction: SyncDirection = 'bidirectional'): Promise { + return this.fullSync(direction); + } + /** * Full sync: Files via Dropbox. * Synchronizes posts and media files to Dropbox for backup and cross-device access. @@ -197,7 +192,7 @@ export class SyncEngine extends EventEmitter { }; } - console.log('[SyncEngine] Starting Dropbox file sync...'); + console.log('[SyncEngine] Starting Dropbox file sync...', direction); const task: Task = { id: uuidv4(), diff --git a/tests/engine/SyncEngine.test.ts b/tests/engine/SyncEngine.test.ts index 6b3b8f0..e0241db 100644 --- a/tests/engine/SyncEngine.test.ts +++ b/tests/engine/SyncEngine.test.ts @@ -86,6 +86,25 @@ vi.mock('../../src/main/engine/MediaEngine', () => ({ })), })); +// Mock DropboxSyncEngine +let mockDropboxConfigured = false; +const mockDropboxSyncEngine = { + isConfigured: vi.fn(() => mockDropboxConfigured), + configure: vi.fn(async () => {}), + syncAll: vi.fn(async () => ({ + success: true, + uploaded: 0, + downloaded: 0, + deleted: 0, + conflicts: 0, + errors: [], + })), +}; + +vi.mock('../../src/main/engine/DropboxSyncEngine', () => ({ + getDropboxSyncEngine: vi.fn(() => mockDropboxSyncEngine), +})); + // Mock uuid vi.mock('uuid', () => ({ v4: vi.fn(() => 'mock-sync-uuid-' + Math.random().toString(36).substr(2, 9)), @@ -101,6 +120,10 @@ describe('SyncEngine', () => { mockMedia.clear(); mockSyncLog.clear(); resetMockCounters(); + + // Reset Dropbox mock state + mockDropboxConfigured = false; + mockDropboxSyncEngine.isConfigured.mockImplementation(() => mockDropboxConfigured); syncEngine = new SyncEngine(); }); @@ -131,9 +154,10 @@ describe('SyncEngine', () => { describe('Configuration', () => { it('should configure sync settings', async () => { + // Mark Dropbox as configured + mockDropboxConfigured = true; + const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', autoSync: false, syncInterval: 30, }; @@ -148,8 +172,6 @@ describe('SyncEngine', () => { syncEngine.on('configured', handler); const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', autoSync: false, syncInterval: 30, }; @@ -159,23 +181,10 @@ describe('SyncEngine', () => { expect(handler).toHaveBeenCalledWith(config); }); - it('should not be configured with empty URL', async () => { + it('should not be configured when Dropbox is not configured', async () => { + mockDropboxConfigured = false; + const config: SyncConfig = { - tursoUrl: '', - tursoAuthToken: 'test-token', - autoSync: false, - syncInterval: 30, - }; - - await syncEngine.configure(config); - - expect(syncEngine.isConfigured()).toBe(false); - }); - - it('should not be configured with empty token', async () => { - const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: '', autoSync: false, syncInterval: 30, }; @@ -188,9 +197,9 @@ describe('SyncEngine', () => { describe('Auto Sync', () => { it('should start auto sync when enabled', async () => { + mockDropboxConfigured = true; + const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', autoSync: true, syncInterval: 1, // 1 minute }; @@ -205,9 +214,8 @@ describe('SyncEngine', () => { const handler = vi.fn(); syncEngine.on('autoSyncStopped', handler); + mockDropboxConfigured = true; const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', autoSync: true, syncInterval: 1, }; @@ -219,16 +227,14 @@ describe('SyncEngine', () => { }); it('should stop previous auto sync when reconfiguring', async () => { + mockDropboxConfigured = true; + const config1: SyncConfig = { - tursoUrl: 'libsql://test1.turso.io', - tursoAuthToken: 'test-token-1', autoSync: true, syncInterval: 1, }; const config2: SyncConfig = { - tursoUrl: 'libsql://test2.turso.io', - tursoAuthToken: 'test-token-2', autoSync: true, syncInterval: 5, }; @@ -251,7 +257,7 @@ describe('SyncEngine', () => { const result = await syncEngine.sync('bidirectional'); expect(result.success).toBe(false); - expect(result.errors).toContain('Sync not configured'); + expect(result.errors).toContain('Dropbox sync not configured'); }); it('should return zero counts when not configured', async () => { @@ -393,19 +399,10 @@ describe('SyncEngine', () => { }); describe('Sync Configuration Validation', () => { - it('should require both URL and token', async () => { + it('should not be configured when Dropbox is not set up', async () => { + mockDropboxConfigured = false; + await syncEngine.configure({ - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: '', - autoSync: false, - syncInterval: 30, - }); - - expect(syncEngine.isConfigured()).toBe(false); - - await syncEngine.configure({ - tursoUrl: '', - tursoAuthToken: 'token', autoSync: false, syncInterval: 30, }); @@ -413,10 +410,10 @@ describe('SyncEngine', () => { expect(syncEngine.isConfigured()).toBe(false); }); - it('should be configured with valid URL and token', async () => { + it('should be configured when Dropbox is set up', async () => { + mockDropboxConfigured = true; + await syncEngine.configure({ - tursoUrl: 'libsql://valid.turso.io', - tursoAuthToken: 'valid-token', autoSync: false, syncInterval: 30, }); @@ -427,9 +424,9 @@ describe('SyncEngine', () => { describe('Sync Interval Configuration', () => { it('should accept sync interval in minutes', async () => { + mockDropboxConfigured = true; + const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', autoSync: true, syncInterval: 15, // 15 minutes }; @@ -439,9 +436,9 @@ describe('SyncEngine', () => { }); it('should not set auto sync with zero interval', async () => { + mockDropboxConfigured = true; + const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', autoSync: true, syncInterval: 0, }; @@ -452,112 +449,6 @@ describe('SyncEngine', () => { }); }); - describe('Remote Schema Migration', () => { - it('should run migrations on remote database when initializing', async () => { - const { getDatabase } = await import('../../src/main/database'); - const mockRunRemoteMigrations = vi.fn().mockResolvedValue(undefined); - - vi.mocked(getDatabase).mockReturnValue({ - getLocal: vi.fn(() => mockLocalDb), - getLocalClient: vi.fn(() => null), - getRemote: vi.fn(() => mockRemoteDb), - getDataPaths: vi.fn(() => ({ - database: '/mock/userData/bds.db', - posts: '/mock/userData/posts', - media: '/mock/userData/media', - })), - initializeLocal: vi.fn(), - initializeRemote: vi.fn(async () => {}), - runRemoteMigrations: mockRunRemoteMigrations, - close: vi.fn(), - } as any); - - const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', - autoSync: false, - syncInterval: 30, - }; - - await syncEngine.configure(config); - - expect(mockRunRemoteMigrations).toHaveBeenCalled(); - }); - }); - - describe('Sync Timeout', () => { - it('should timeout if sync takes too long', async () => { - const { getDatabase } = await import('../../src/main/database'); - - // Create a mock that never resolves for remote operations - const hangingInsert = vi.fn(() => ({ - values: vi.fn(() => ({ - onConflictDoUpdate: vi.fn(() => new Promise(() => {})), // Never resolves - })), - })); - - const hangingRemoteDb = { - ...mockRemoteDb, - insert: hangingInsert, - }; - - vi.mocked(getDatabase).mockReturnValue({ - getLocal: vi.fn(() => ({ - ...mockLocalDb, - select: vi.fn(() => ({ - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - limit: vi.fn().mockReturnThis(), - offset: vi.fn().mockReturnThis(), - all: vi.fn().mockResolvedValue([{ id: 'test-post', title: 'Test', syncStatus: 'pending' }]), - })), - update: vi.fn(() => ({ - set: vi.fn(() => ({ - where: vi.fn(() => Promise.resolve()), - })), - })), - })), - getLocalClient: vi.fn(() => null), - getRemote: vi.fn(() => hangingRemoteDb), - getDataPaths: vi.fn(() => ({ - database: '/mock/userData/bds.db', - posts: '/mock/userData/posts', - media: '/mock/userData/media', - })), - initializeLocal: vi.fn(), - initializeRemote: vi.fn(async () => {}), - runRemoteMigrations: vi.fn(async () => {}), - close: vi.fn(), - } as any); - - // Use real timers and set a short timeout before configuring - vi.useRealTimers(); - - const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', - autoSync: false, - syncInterval: 30, - }; - - await syncEngine.configure(config); - - // Set a short timeout for the test (100ms) - syncEngine.setSyncTimeout(100); - - // This should timeout rather than hang forever - const result = await syncEngine.sync('push'); - - expect(result.success).toBe(false); - expect(result.errors.some((e: string) => e.includes('timeout') || e.includes('Timeout'))).toBe(true); - }, 15000); // Extend test timeout to 15 seconds - - it('should have configurable timeout', () => { - expect(typeof syncEngine.getSyncTimeout).toBe('function'); - expect(typeof syncEngine.setSyncTimeout).toBe('function'); - }); - }); - describe('SyncStatus Reset on Failure', () => { it('should reset syncStatus to idle when task manager throws', async () => { const { getDatabase } = await import('../../src/main/database'); @@ -582,9 +473,11 @@ describe('SyncEngine', () => { close: vi.fn(), } as any); + mockDropboxConfigured = true; + // Mock Dropbox sync to fail + mockDropboxSyncEngine.syncAll.mockRejectedValue(new Error('Database exploded')); + const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', autoSync: false, syncInterval: 30, }; @@ -597,6 +490,9 @@ describe('SyncEngine', () => { // Status should be reset to allow future syncs expect(syncEngine.getSyncStatus()).not.toBe('syncing'); + // Reset mock for second call + mockDropboxSyncEngine.syncAll.mockRejectedValue(new Error('Database exploded')); + // A subsequent sync should not return "Sync already in progress" const result = await syncEngine.sync('push'); expect(result.errors).not.toContain('Sync already in progress'); @@ -625,9 +521,11 @@ describe('SyncEngine', () => { close: vi.fn(), } as any); + mockDropboxConfigured = true; + // Mock Dropbox sync to fail + mockDropboxSyncEngine.syncAll.mockRejectedValue(new Error('Database error')); + const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', autoSync: false, syncInterval: 30, }; @@ -645,9 +543,8 @@ describe('SyncEngine', () => { it('should log sync start with direction', async () => { const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + mockDropboxConfigured = true; const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', autoSync: false, syncInterval: 30, }; @@ -691,9 +588,8 @@ describe('SyncEngine', () => { close: vi.fn(), } as any); + mockDropboxConfigured = true; const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', autoSync: false, syncInterval: 30, }; @@ -737,9 +633,11 @@ describe('SyncEngine', () => { close: vi.fn(), } as any); + mockDropboxConfigured = true; + // Mock Dropbox sync to fail + mockDropboxSyncEngine.syncAll.mockRejectedValue(new Error('Test error for logging')); + const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', autoSync: false, syncInterval: 30, }; @@ -755,104 +653,4 @@ describe('SyncEngine', () => { consoleErrorSpy.mockRestore(); }); }); - - describe('Batch Size Configuration', () => { - it('should have configurable batch size', () => { - expect(typeof syncEngine.getBatchSize).toBe('function'); - expect(typeof syncEngine.setBatchSize).toBe('function'); - }); - - it('should default to 50 items per batch', () => { - expect(syncEngine.getBatchSize()).toBe(50); - }); - - it('should allow setting custom batch size', () => { - syncEngine.setBatchSize(100); - expect(syncEngine.getBatchSize()).toBe(100); - // Reset to default - syncEngine.setBatchSize(50); - }); - - it('should process posts in batches', async () => { - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - const { getDatabase } = await import('../../src/main/database'); - - // Create 150 mock posts (should result in 3 batches with batch size 50) - const mockPendingPosts = Array.from({ length: 150 }, (_, i) => ({ - id: `post-${i}`, - title: `Post ${i}`, - slug: `post-${i}`, - syncStatus: 'pending', - projectId: 'default', - createdAt: new Date(), - updatedAt: new Date(), - })); - - let queryCount = 0; - vi.mocked(getDatabase).mockReturnValue({ - getLocal: vi.fn(() => ({ - ...mockLocalDb, - select: vi.fn(() => ({ - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - limit: vi.fn().mockReturnThis(), - offset: vi.fn().mockImplementation((offset: number) => ({ - all: vi.fn().mockImplementation(() => { - queryCount++; - const batch = mockPendingPosts.slice(offset, offset + 50); - return Promise.resolve(batch); - }), - })), - all: vi.fn().mockResolvedValue([]), // For media query - })), - update: vi.fn(() => ({ - set: vi.fn(() => ({ - where: vi.fn(() => Promise.resolve()), - })), - })), - insert: vi.fn(() => ({ - values: vi.fn(() => Promise.resolve()), - })), - })), - getLocalClient: vi.fn(() => null), - getRemote: vi.fn(() => ({ - ...mockRemoteDb, - insert: vi.fn(() => ({ - values: vi.fn(() => ({ - onConflictDoUpdate: vi.fn(() => Promise.resolve()), - })), - })), - })), - getDataPaths: vi.fn(() => ({ - database: '/mock/userData/bds.db', - posts: '/mock/userData/posts', - media: '/mock/userData/media', - })), - initializeLocal: vi.fn(), - initializeRemote: vi.fn(async () => {}), - runRemoteMigrations: vi.fn(async () => {}), - close: vi.fn(), - } as any); - - const config: SyncConfig = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', - autoSync: false, - syncInterval: 30, - }; - - await syncEngine.configure(config); - syncEngine.setBatchSize(50); - await syncEngine.sync('push'); - - // Should have logged batch progress - const calls = consoleSpy.mock.calls; - const hasBatchLog = calls.some((call: any[]) => - call.some((arg: any) => typeof arg === 'string' && arg.includes('batch')) - ); - - expect(hasBatchLog).toBe(true); - consoleSpy.mockRestore(); - }); - }); }); diff --git a/tests/renderer/components/SettingsView.test.ts b/tests/renderer/components/SettingsView.test.ts index 998df63..110b414 100644 --- a/tests/renderer/components/SettingsView.test.ts +++ b/tests/renderer/components/SettingsView.test.ts @@ -68,19 +68,6 @@ describe('SettingsView Behavior', () => { }); describe('Credentials Storage (localStorage)', () => { - it('should save Turso credentials to localStorage', () => { - const creds = { - tursoUrl: 'libsql://test.turso.io', - tursoToken: 'test-token', - }; - - localStorage.setItem('bds-credentials', JSON.stringify(creds)); - - const saved = JSON.parse(localStorage.getItem('bds-credentials') || '{}'); - expect(saved.tursoUrl).toBe('libsql://test.turso.io'); - expect(saved.tursoToken).toBe('test-token'); - }); - it('should save Dropbox credentials to localStorage', () => { const creds = { dropboxAccessToken: 'dbx-token', @@ -98,48 +85,24 @@ describe('SettingsView Behavior', () => { it('should load credentials from localStorage', () => { const creds = { - tursoUrl: 'libsql://saved.turso.io', - tursoToken: 'saved-token', dropboxAccessToken: 'saved-dbx-token', + dropboxAppKey: 'saved-key', + dropboxRemotePath: '/blog', }; localStorage.setItem('bds-credentials', JSON.stringify(creds)); const loaded = JSON.parse(localStorage.getItem('bds-credentials') || '{}'); - expect(loaded.tursoUrl).toBe('libsql://saved.turso.io'); expect(loaded.dropboxAccessToken).toBe('saved-dbx-token'); }); - it('should handle clearing Turso credentials independently', () => { + it('should handle clearing Dropbox credentials', () => { const creds = { - tursoUrl: 'libsql://test.turso.io', - tursoToken: 'test-token', - dropboxAccessToken: 'dbx-token', - dropboxAppKey: 'dbx-key', - }; - - localStorage.setItem('bds-credentials', JSON.stringify(creds)); - - // Clear only Turso credentials - const loaded = JSON.parse(localStorage.getItem('bds-credentials') || '{}'); - const cleared = { ...loaded, tursoUrl: '', tursoToken: '' }; - localStorage.setItem('bds-credentials', JSON.stringify(cleared)); - - const result = JSON.parse(localStorage.getItem('bds-credentials') || '{}'); - expect(result.tursoUrl).toBe(''); - expect(result.tursoToken).toBe(''); - // Dropbox credentials should be untouched - expect(result.dropboxAccessToken).toBe('dbx-token'); - expect(result.dropboxAppKey).toBe('dbx-key'); - }); - - it('should handle clearing Dropbox credentials independently', () => { - const creds = { - tursoUrl: 'libsql://test.turso.io', - tursoToken: 'test-token', dropboxAccessToken: 'dbx-token', dropboxAppKey: 'dbx-key', dropboxRemotePath: '/blog', + ftpHost: 'ftp.example.com', + ftpUser: 'user', }; localStorage.setItem('bds-credentials', JSON.stringify(creds)); @@ -155,11 +118,11 @@ describe('SettingsView Behavior', () => { localStorage.setItem('bds-credentials', JSON.stringify(cleared)); const result = JSON.parse(localStorage.getItem('bds-credentials') || '{}'); - // Turso credentials should be untouched - expect(result.tursoUrl).toBe('libsql://test.turso.io'); - expect(result.tursoToken).toBe('test-token'); expect(result.dropboxAccessToken).toBe(''); expect(result.dropboxAppKey).toBe(''); + // FTP credentials should be untouched + expect(result.ftpHost).toBe('ftp.example.com'); + expect(result.ftpUser).toBe('user'); }); }); @@ -231,8 +194,6 @@ describe('SettingsView Behavior', () => { (window as any).electronAPI.sync.configure = mockConfigure; const config = { - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', autoSync: true, syncInterval: 5, }; @@ -240,8 +201,6 @@ describe('SettingsView Behavior', () => { await window.electronAPI?.sync.configure(config); expect(mockConfigure).toHaveBeenCalledWith({ - tursoUrl: 'libsql://test.turso.io', - tursoAuthToken: 'test-token', autoSync: true, syncInterval: 5, });