diff --git a/package.json b/package.json index f9410c2..0ed7384 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "test:ics-parser": "node test-ics-parser.js", "test:ics-sub": "node --experimental-sqlite test-ics-subscription.js", "test:backup-scheduler": "node --experimental-sqlite test-backup-scheduler.js", - "test": "node --experimental-sqlite test-db.js && node --experimental-sqlite test-dashboard.js && node --experimental-sqlite test-tasks.js && node --experimental-sqlite test-shopping.js && node --experimental-sqlite test-meals.js && node --experimental-sqlite test-calendar.js && node --experimental-sqlite test-notes-contacts-budget.js && npm run test:ux-utils && npm run test:modal-utils && npm run test:reminders && npm run test:api && npm run test:ics-parser && npm run test:ics-sub && npm run test:setup && npm run test:kitchen-tabs && npm run test:backup-scheduler" + "test:caldav": "node --experimental-sqlite test-caldav-sync.js", + "test": "node --experimental-sqlite test-db.js && node --experimental-sqlite test-dashboard.js && node --experimental-sqlite test-tasks.js && node --experimental-sqlite test-shopping.js && node --experimental-sqlite test-meals.js && node --experimental-sqlite test-calendar.js && node --experimental-sqlite test-notes-contacts-budget.js && npm run test:ux-utils && npm run test:modal-utils && npm run test:reminders && npm run test:api && npm run test:ics-parser && npm run test:ics-sub && npm run test:setup && npm run test:kitchen-tabs && npm run test:backup-scheduler && npm run test:caldav" }, "dependencies": { "bcrypt": "^6.0.0", diff --git a/test-caldav-sync.js b/test-caldav-sync.js new file mode 100644 index 0000000..7dba725 --- /dev/null +++ b/test-caldav-sync.js @@ -0,0 +1,156 @@ +/** + * Test: CalDAV Multi-Account Sync + * Purpose: Verify CalDAV multi-account functionality + */ + +import { describe, it, before } from 'node:test'; +import assert from 'node:assert/strict'; +import { DatabaseSync } from 'node:sqlite'; + +const TEST_DB = ':memory:'; + +describe('CalDAV Multi-Account Sync', () => { + let db; + + before(() => { + // Create in-memory DB + db = new DatabaseSync(TEST_DB); + + // Create tables (simplified schema for testing) + db.exec(` + CREATE TABLE caldav_accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + caldav_url TEXT NOT NULL, + username TEXT NOT NULL, + password TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + last_sync TEXT, + UNIQUE(caldav_url, username) + ); + + CREATE TABLE caldav_calendar_selection ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL, + calendar_url TEXT NOT NULL, + calendar_name TEXT NOT NULL, + calendar_color TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (account_id) REFERENCES caldav_accounts(id) ON DELETE CASCADE, + UNIQUE(account_id, calendar_url) + ); + + CREATE TABLE calendar_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + external_calendar_id TEXT, + external_source TEXT, + target_caldav_account_id INTEGER, + target_caldav_calendar_url TEXT + ); + + CREATE TABLE external_calendars ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source TEXT NOT NULL, + external_id TEXT NOT NULL, + name TEXT NOT NULL, + color TEXT, + UNIQUE(source, external_id) + ); + + CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL + ); + + INSERT INTO users (username) VALUES ('testuser'); + `); + }); + + it('should create caldav_accounts table with correct schema', () => { + const result = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='caldav_accounts'").get(); + assert.ok(result, 'caldav_accounts table should exist'); + }); + + it('should create caldav_calendar_selection table with FK', () => { + const result = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='caldav_calendar_selection'").get(); + assert.ok(result, 'caldav_calendar_selection table should exist'); + }); + + it('should have target columns in calendar_events', () => { + const cols = db.prepare("PRAGMA table_info(calendar_events)").all(); + const colNames = cols.map(c => c.name); + + assert.ok(colNames.includes('target_caldav_account_id'), 'Should have target_caldav_account_id column'); + assert.ok(colNames.includes('target_caldav_calendar_url'), 'Should have target_caldav_calendar_url column'); + }); + + it('should insert account and enforce UNIQUE constraint', () => { + db.prepare(` + INSERT INTO caldav_accounts (name, caldav_url, username, password) + VALUES (?, ?, ?, ?) + `).run('Test Account', 'https://caldav.example.com', 'user', 'pass'); + + const account = db.prepare('SELECT * FROM caldav_accounts WHERE name = ?').get('Test Account'); + assert.ok(account, 'Account should be inserted'); + assert.strictEqual(account.caldav_url, 'https://caldav.example.com'); + + // Duplicate should fail + assert.throws(() => { + db.prepare(` + INSERT INTO caldav_accounts (name, caldav_url, username, password) + VALUES (?, ?, ?, ?) + `).run('Duplicate', 'https://caldav.example.com', 'user', 'pass'); + }, 'UNIQUE constraint should prevent duplicates'); + }); + + it('should insert calendar selection and link to account', () => { + const accountId = db.prepare('SELECT id FROM caldav_accounts WHERE name = ?').get('Test Account').id; + + db.prepare(` + INSERT INTO caldav_calendar_selection (account_id, calendar_url, calendar_name, enabled) + VALUES (?, ?, ?, ?) + `).run(accountId, 'https://cal.example.com/cal1', 'Private', 1); + + const calendar = db.prepare('SELECT * FROM caldav_calendar_selection WHERE account_id = ?').get(accountId); + assert.ok(calendar, 'Calendar should be inserted'); + assert.strictEqual(calendar.calendar_name, 'Private'); + assert.strictEqual(calendar.enabled, 1); + }); + + it('should CASCADE delete calendar_selection when account deleted', () => { + const accountId = db.prepare('SELECT id FROM caldav_accounts WHERE name = ?').get('Test Account').id; + + // Delete account + db.prepare('DELETE FROM caldav_accounts WHERE id = ?').run(accountId); + + // Calendar selection should be deleted + const remaining = db.prepare('SELECT * FROM caldav_calendar_selection WHERE account_id = ?').get(accountId); + assert.strictEqual(remaining, undefined, 'Calendar selection should be deleted via CASCADE'); + }); + + it('should handle enabled/disabled calendar selection', () => { + // Insert new account + db.prepare(` + INSERT INTO caldav_accounts (name, caldav_url, username, password) + VALUES (?, ?, ?, ?) + `).run('Account 2', 'https://caldav2.example.com', 'user2', 'pass2'); + + const accountId = db.prepare('SELECT id FROM caldav_accounts WHERE name = ?').get('Account 2').id; + + // Insert calendars + db.prepare(` + INSERT INTO caldav_calendar_selection (account_id, calendar_url, calendar_name, enabled) + VALUES (?, ?, ?, ?), (?, ?, ?, ?) + `).run( + accountId, 'https://cal.example.com/cal1', 'Private', 1, + accountId, 'https://cal.example.com/cal2', 'Work', 0 + ); + + // Query only enabled + const enabled = db.prepare('SELECT * FROM caldav_calendar_selection WHERE account_id = ? AND enabled = 1').all(accountId); + assert.strictEqual(enabled.length, 1, 'Should have 1 enabled calendar'); + assert.strictEqual(enabled[0].calendar_name, 'Private'); + }); +});