/* * LinkdingSync Simple Test Runner * Copy entire file into Firefox DevTools Console * Then run: runAllTests() */ 'use strict'; (function(exports) { 'use strict'; const serverUrl = 'https://links.blabber1565.com'; const workApiKey = '4108e3aff26fb82bf074f5d4dfa4757763520b06'; const personalApiKey = '9b80accd3b9b4b91c2a7adc3dcf41621b025329a'; const workUser = 'linkdingsync_tester'; const personalUser = 'linkdingsync_tester_2'; const workBundle = 'work'; const personalBundle = 'personal'; const currentContext = { url: '', apiKey: '', userId: null }; function setContext(key, url, apiKey, userId) { currentContext.url = url.endsWith('/') ? url : url + '/'; currentContext.apiKey = apiKey; currentContext.userId = userId; } function callApi(method, endpoint, params = {}) { const url = new URL(endpoint, currentContext.url); Object.entries(params).forEach(([k, v]) => url.searchParams.append(k, v)); const headers = { 'Authorization': `Token ${currentContext.apiKey}` }; return fetch(url, { method, headers }).then(r => { if (!r.ok) throw new Error(r.status + ': ' + r.statusText); return r.json(); }); } function createBookmark(url, options = {}) { const testId = 'test-' + Date.now().toString().slice(-4) + '-' + Math.random().toString(36).slice(2,4); const baseUrl = new URL(url); baseUrl.hostname = testId + '.' + baseUrl.hostname; const data = { url: baseUrl.href, title: options.title || 'Test: ' + testId, description: 'Test bookmark', notes: JSON.stringify({ path: 'Test/' + testId, testId, userNotes: 'Test' }) }; return callApi('POST', '/api/bookmarks/', data).then(bm => { console.log(' Created: ID=' + bm.id); return bm; }); } function deleteBookmark(id) { return callApi('DELETE', '/api/bookmarks/' + id + '/').then(() => { console.log(' Deleted: ID=' + id); }); } function getAllBookmarks() { let bookmarks = []; let offset = 0; return callApi('GET', '/api/bookmarks/?limit=100&offset=' + offset).then(data => { bookmarks = bookmarks.concat(data.results || []); if (bookmarks.length > offset) { return getAllBookmarks().then(r => r); } return bookmarks; }); } function resetBookmarks() { console.log('[Reset] Clearing test bookmarks...'); return getAllBookmarks().then(all => { const tests = all.filter(b => b.testId); if (tests.length > 0) { console.log('[Reset] Found ' + tests.length + ' test bookmarks'); return Promise.all(tests.map(t => deleteBookmark(t.id))).then(() => { console.log('[Reset] Done'); }); } console.log('[Reset] No test bookmarks found'); }); } // ==================== TEST 1 ==================== function test1_SameUrlDifferentKeys() { console.log('\n=== Test 1: Same URL, Different API Keys ==='); setContext('work', serverUrl, workApiKey, workUser); const bm1 = createBookmark('https://test1.example.com', { title: 'Test 1 - Work' }); bm1.then(function() { setContext('personal', serverUrl, personalApiKey, personalUser); const bm2 = createBookmark('https://test1.example.com', { title: 'Test 1 - Personal' }); return bm2; }).then(function(bm2) { console.log(' Work ID: ' + bm1.id); console.log(' Personal ID: ' + bm2.id); if (bm1.id === bm2.id) { console.log(' [Test 1] ✗ Same ID - API keys do NOT provide isolation'); return { pass: false, same: true }; } else { console.log(' [Test 1] ✓ Different IDs - API keys provide isolation'); return { pass: true, same: false }; } }); } // ==================== TEST 2 ==================== function test2_CrossUserVisibility() { return test1_SameUrlDifferentKeys().then(function(r1) { console.log('\n=== Test 2: Cross-User Visibility ==='); setContext('work', serverUrl, workApiKey, workUser); const testUrl = 'https://test2.example.com'; const bm = createBookmark(testUrl, { title: 'Test 2 - Work' }); return bm.then(function(bm) { console.log(' Work bookmark ID: ' + bm.id); setContext('personal', serverUrl, personalApiKey, personalUser); return callApi('GET', '/api/bookmarks/?limit=100').then(function(data) { console.log(' Personal sees: ' + data.count + ' bookmarks'); if (data.results && data.results.length > 0) { console.log(' [Test 2] ✗ Users can see each other\'s bookmarks'); return { pass: false, visible: true }; } else { console.log(' [Test 2] ✓ Proper user isolation'); return { pass: true, visible: false }; } }); }); }); } // ==================== TEST 3 ==================== function test3_ConflictResolution() { return test2_CrossUserVisibility().then(function(r2) { console.log('\n=== Test 3: Conflict Resolution ==='); setContext('work', serverUrl, workApiKey, workUser); const url = 'https://test3.example.com'; return createBookmark(url, { title: 'Test 3', path: 'Work/Path' }).then(function(bm1) { console.log(' Work bookmark ID: ' + bm1.id); setContext('personal', serverUrl, personalApiKey, personalUser); return createBookmark(url, { title: 'Test 3', path: 'Personal/Path' }).then(function(bm2) { console.log(' Personal bookmark ID: ' + bm2.id); console.log(' Same ID? ' + (bm1.id === bm2.id)); if (bm1.id === bm2.id) { console.log(' [Test 3] ✗ Same bookmark - server merges by URL'); return callApi('GET', '/api/bookmarks/' + bm1.id + '/').then(function(data) { return { pass: false, merged: true, path: JSON.parse(data.notes).path }; }); } else { console.log(' [Test 3] ✓ Different bookmarks - server does NOT merge'); return { pass: true, merged: false }; } }); }); }); } // ==================== TEST 4 ==================== function test4_LastWriteWins() { return test3_ConflictResolution().then(function(r3) { console.log('\n=== Test 4: Last-Write-Wins ==='); setContext('work', serverUrl, workApiKey, workUser); const url = 'https://test4.example.com'; return createBookmark(url, { title: 'Initial', path: 'Initial' }).then(function(bm) { console.log(' Initial bookmark ID: ' + bm.id); return callApi('PUT', '/api/bookmarks/' + bm.id + '/', { title: 'Work Title', description: 'Work Desc', notes: JSON.stringify({ path: 'Work/Dev', userNotes: 'Work notes' }) }).then(function() { console.log(' Updated via Work: Work Title'); setContext('personal', serverUrl, personalApiKey, personalUser); return callApi('PUT', '/api/bookmarks/' + bm.id + '/', { title: 'Personal Title', description: 'Personal Desc', notes: JSON.stringify({ path: 'Personal/Notes', userNotes: 'Personal notes' }) }).then(function() { console.log(' Updated via Personal: Personal Title'); return callApi('GET', '/api/bookmarks/' + bm.id + '/'); }); }); }).then(function(final) { console.log('\n Final state:'); console.log(' Title: ' + final.title); console.log(' Description: ' + final.description); console.log(' Path: ' + JSON.parse(final.notes).path); if (final.title === 'Personal Title') { console.log(' [Test 4] ✓ Last-write-wins (Personal)'); return { pass: true, strategy: 'last-write-wins', winner: 'personal' }; } else if (final.title === 'Work Title') { console.log(' [Test 4] ✓ Last-write-wins (Work)'); return { pass: true, strategy: 'last-write-wins', winner: 'work' }; } else { console.log(' [Test 4] ⚠ Unexpected title: ' + final.title); return { pass: null, strategy: 'unknown' }; } }); }); } // ==================== TEST 5 ==================== function test5_DeletePropagation() { return test4_LastWriteWins().then(function(r4) { console.log('\n=== Test 5: Delete Propagation ==='); setContext('work', serverUrl, workApiKey, workUser); const url = 'https://test5.example.com'; return createBookmark(url, { title: 'Test 5 - Work', path: 'Work' }).then(function(bm1) { console.log(' Work bookmark ID: ' + bm1.id); setContext('personal', serverUrl, personalApiKey, personalUser); return createBookmark(url, { title: 'Test 5 - Personal', path: 'Personal' }).then(function(bm2) { console.log(' Personal bookmark ID: ' + bm2.id); console.log(' Same ID? ' + (bm1.id === bm2.id)); setContext('work', serverUrl, workApiKey, workUser); return callApi('DELETE', '/api/bookmarks/' + bm1.id + '/').then(function() { console.log(' Deleted via Work'); setContext('personal', serverUrl, personalApiKey, personalUser); return callApi('GET', '/api/bookmarks/?limit=100&url=' + url).then(function(data) { if (data.count === 0) { console.log(' [Test 5] ✗ Delete propagated - same bookmark'); return { pass: false, propagated: true }; } else { console.log(' [Test 5] ✓ Delete did not propagate - separate bookmarks'); return { pass: true, propagated: false }; } }); }); }); }); }); } // ==================== TEST 6 ==================== function test6_BundleFiltering() { return test5_DeletePropagation().then(function(r5) { console.log('\n=== Test 6: Bundle Filtering ==='); setContext('work', serverUrl, workApiKey, workUser); const url1 = 'https://bundle-work1.example.com'; const url2 = 'https://bundle-work2.example.com'; return Promise.all([ createBookmark(url1, { title: 'Bundle Work 1', path: 'Work/B1' }), createBookmark(url2, { title: 'Bundle Work 2', path: 'Work/B2' }) ]).then(function(bms) { console.log(' Created 2 work bookmarks'); setContext('work', serverUrl, workApiKey, workUser); return callApi('GET', '/api/bookmarks/?all=work&limit=100').then(function(data) { const workCount = data.count || data.results?.length || 0; setContext('personal', serverUrl, personalApiKey, personalUser); return callApi('GET', '/api/bookmarks/?all=personal&limit=100').then(function(pd) { const personalCount = pd.count || pd.results?.length || 0; console.log(' Work bundle: ' + workCount + ' bookmarks'); console.log(' Personal bundle: ' + personalCount + ' bookmarks'); console.log(' [Test 6] ✓ Bundle filtering works'); return { pass: true, work: workCount, personal: personalCount }; }); }); }); }); } // ==================== MAIN ==================== async function runAllTests() { console.log(''.padEnd(60, '=')); console.log(' LINKDINGSYNC - Test Suite'); console.log('='.repeat(60)); const results = []; try { results.push(await test1_SameUrlDifferentKeys()); results.push(await test2_CrossUserVisibility()); results.push(await test3_ConflictResolution()); results.push(await test4_LastWriteWins()); results.push(await test5_DeletePropagation()); results.push(await test6_BundleFiltering()); } catch (error) { console.error('Error:', error.message); } const passed = results.filter(r => r.pass === true).length; const failed = results.filter(r => r.pass === false).length; const warnings = results.filter(r => r.pass === null).length; console.log('\n'.padEnd(60, '=')); console.log(' Summary: Total=' + results.length + ', Passed=' + passed + ', Failed=' + failed + ', Warning=' + warnings); console.log('='.repeat(60)); return results; } async function runAllTestsWithReset() { console.log(''.padEnd(60, '=')); console.log(' LinkdingSync - Test Suite with Reset'); console.log('='.repeat(60)); console.log('[Reset] Cleaning up...'); await resetBookmarks(); console.log('[Reset] Done'); return await runAllTests(); } exports.runAllTests = runAllTests; exports.runAllTestsWithReset = runAllTestsWithReset; exports.reset = resetBookmarks; exports.Helpers = { createBookmark: createBookmark, deleteBookmark: deleteBookmark, getAllBookmarks: getAllBookmarks }; })(); console.log(''); console.log('LinkdingSync Simple Test Runner loaded'); console.log(''); console.log('Run: runAllTestsWithReset()');