fix: removed turso

This commit is contained in:
2026-02-11 08:42:10 +01:00
parent a8499626c0
commit f4ff91180d
7 changed files with 117 additions and 350 deletions

View File

@@ -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

4
.gitignore vendored
View File

@@ -43,10 +43,6 @@ migrations/
.env.production
.env.*.local
# Turso/LibSQL credentials
turso-credentials.json
.turso/
# ===================
# IDE & Editor
# ===================

View File

@@ -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

View File

@@ -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.

View File

@@ -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<SyncResult> {
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<SyncResult> = {
id: uuidv4(),

View File

@@ -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();
});
});
});

View File

@@ -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,
});