/* * LinkdingSync Verified Test Runner * Includes verification and cleanup between tests */ 'use strict'; (function(w) { 'use strict'; var window = w || window; var serverUrl = 'https://links.blabber1565.com'; var workApiKey = '4108e3aff26fb82bf074f5d4dfa4757763520b06'; var personalApiKey = '9b80accd3b9b4b91c2a7adc3dcf41621b025329a'; var workUser = 'linkdingsync_tester'; var personalUser = 'linkdingsync_tester_2'; var state = { url: '', apiKey: '', userId: null, results: [] }; function call(method, endpoint, data) { var u = new URL(endpoint, state.url); var r = fetch(u, { method: method, headers: { 'Authorization': 'Token ' + state.apiKey, 'Content-Type': 'application/json' }, body: data ? JSON.stringify(data) : null }); return r.then(function(res) { if (!res.ok) { if (res.status === 404) return { error: '404', status: res.status }; throw new Error(res.status + ': ' + res.statusText); } return res.json(); }); } function createBookmark(url, opts) { var testId = 'test-' + Date.now().toString().slice(-4) + '-' + Math.random().toString(36).slice(2, 4); var base = new URL(url); base.hostname = testId + '.' + base.hostname; var data = { url: base.href, title: opts.title || 'Test: ' + testId, description: 'Test', notes: JSON.stringify({ testId, path: 'Test/' + testId }) }; console.log(' Created: ' + data.url); return call('POST', '/api/bookmarks/', data); } function listBookmarks() { return call('GET', '/api/bookmarks/?limit=100'); } function cleanup() { return listBookmarks().then(function(data) { var tests = data.results.filter(function(b) { return b.testId; }); console.log('[Cleanup] Found ' + tests.length + ' test bookmarks to delete'); if (tests.length > 0) { return Promise.all(tests.map(function(t) { return call('DELETE', '/api/bookmarks/' + t.id + '/'); })); } }).then(function() { console.log('[Cleanup] Done'); }); } // TEST 1 function test1() { console.log('\n=== TEST 1: API Key Isolation ==='); return verify().then(function() { state.url = serverUrl; state.apiKey = workApiKey; state.userId = workUser; return createBookmark('https://t1.example.com', { title: 'T1-Work' }); }).then(function(b1) { state.url = serverUrl; state.apiKey = personalApiKey; state.userId = personalUser; return createBookmark('https://t1.example.com', { title: 'T1-Personal' }); }).then(function(b2) { console.log(' Work ID: ' + b1.id + ' Personal ID: ' + b2.id + ' Same? ' + (b1.id === b2.id)); if (b1.id === b2.id) { console.log(' [TEST 1] ✗ FAIL - API keys do NOT provide isolation'); return { pass: false, reason: 'API keys do not provide isolation' }; } else { console.log(' [TEST 1] ✓ PASS - API keys provide isolation'); return { pass: true, reason: 'API keys provide isolation' }; } }); } // TEST 2 function test2() { console.log('\n=== TEST 2: Cross-User Visibility ==='); return test1().then(function(r1) { state.url = serverUrl; state.apiKey = personalApiKey; state.userId = personalUser; return listBookmarks(); }).then(function(data) { var testCount = 0; if (data.results && Array.isArray(data.results)) { testCount = data.results.filter(function(b) { return b.url && b.url.indexOf('test-') === -1; }).length; } console.log(' Personal sees ' + testCount + ' non-test bookmarks'); var hasWorkBookmark = data.results && Array.isArray(data.results) && data.results.some(function(b) { return b.url === 'https://t2.example.com'; }); if (hasWorkBookmark) { console.log(' [TEST 2] ✗ FAIL - Personal can see work\'s bookmark'); return { pass: false, reason: 'Cross-user visibility' }; } else { console.log(' [TEST 2] ✓ PASS - Proper user isolation'); return { pass: true, reason: 'Proper user isolation' }; } }); } // TEST 3 function test3() { console.log('\n=== TEST 3: Conflict Resolution ==='); return test2().then(function(r2) { state.url = serverUrl; state.apiKey = workApiKey; state.userId = workUser; return createBookmark('https://t3.example.com', { title: 'T3-Work', path: 'Work' }); }).then(function(b1) { state.url = serverUrl; state.apiKey = personalApiKey; state.userId = personalUser; return createBookmark('https://t3.example.com', { title: 'T3-Personal', path: 'Personal' }); }).then(function(b2) { console.log(' Work ID: ' + b1.id + ' Personal ID: ' + b2.id + ' Same? ' + (b1.id === b2.id)); if (b1.id === b2.id) { console.log(' [TEST 3] ✗ FAIL - Server merges by URL'); return { pass: false, reason: 'Server merges by URL' }; } else { console.log(' [TEST 3] ✓ PASS - Separate bookmarks'); return { pass: true, reason: 'Separate bookmarks' }; } }); } // TEST 4 function test4() { console.log('\n=== TEST 4: Field Update Behavior ==='); return test3().then(function(r3) { state.url = serverUrl; state.apiKey = workApiKey; state.userId = workUser; return createBookmark('https://t4.example.com', { title: 'Initial', path: 'Initial' }); }).then(function(bm) { console.log(' Initial ID: ' + bm.id); return call('PUT', '/api/bookmarks/' + bm.id + '/', { title: 'Work Title', description: 'Work', notes: JSON.stringify({ path: 'Work', userNotes: 'Work' }) }); }).then(function(resp) { console.log(' Work update: ' + (resp.error ? resp.error : 'OK')); return call('GET', '/api/bookmarks/' + bm.id + '/'); }).then(function(final) { console.log(' Final title: ' + final.title); if (final.title === 'Work Title') { console.log(' [TEST 4] ✓ Title updated'); return { pass: true, strategy: 'title-update', title: final.title }; } else if (final.title === 'Initial') { console.log(' [TEST 4] ✓ Title NOT updated (notes only)'); return { pass: true, strategy: 'notes-only', title: final.title }; } else { console.log(' [TEST 4] ? UNKNOWN title: ' + final.title); return { pass: null, strategy: 'unknown', title: final.title }; } }); } // TEST 5 function test5() { console.log('\n=== TEST 5: Delete Behavior ==='); return test4().then(function(r4) { state.url = serverUrl; state.apiKey = workApiKey; state.userId = workUser; return createBookmark('https://t5.example.com', { title: 'T5-Work', path: 'Work' }); }).then(function(b1) { state.url = serverUrl; state.apiKey = personalApiKey; state.userId = personalUser; return createBookmark('https://t5.example.com', { title: 'T5-Personal', path: 'Personal' }); }).then(function(b2) { console.log(' Work ID: ' + b1.id + ' Personal ID: ' + b2.id + ' Same? ' + (b1.id === b2.id)); state.url = serverUrl; state.apiKey = workApiKey; state.userId = workUser; return call('DELETE', '/api/bookmarks/' + b1.id + '/'); }).then(function() { state.url = serverUrl; state.apiKey = personalApiKey; state.userId = personalUser; return call('GET', '/api/bookmarks/?limit=100&url=https://t5.example.com'); }).then(function(data) { console.log(' Personal sees ' + (data.count || 0) + ' with that URL'); if (data.count === 0 || !data.results) { console.log(' [TEST 5] ✗ FAIL - Delete propagated'); return { pass: false, reason: 'Delete propagated' }; } else { console.log(' [TEST 5] ✓ PASS - Delete isolated'); return { pass: true, reason: 'Delete isolated' }; } }); } // TEST 6 function test6() { console.log('\n=== TEST 6: Bundle Filtering ==='); return test5().then(function(r5) { state.url = serverUrl; state.apiKey = workApiKey; state.userId = workUser; return Promise.all([createBookmark('https://b6-1.example.com', { title: 'B6-W1', path: 'W1' }), createBookmark('https://b6-2.example.com', { title: 'B6-W2', path: 'W2' })]); }).then(function() { console.log(' Created 2 work bookmarks'); state.url = serverUrl; state.apiKey = workApiKey; state.userId = workUser; return call('GET', '/api/bookmarks/?all=work&limit=100'); }).then(function(data) { var wc = data.count || (data.results ? data.results.length : 0); console.log(' Work bundle: ' + wc + ' bookmarks'); state.url = serverUrl; state.apiKey = personalApiKey; state.userId = personalUser; return call('GET', '/api/bookmarks/?all=personal&limit=100'); }).then(function(data) { var pc = data.count || (data.results ? data.results.length : 0); console.log(' Personal bundle: ' + pc + ' bookmarks'); if (wc > 0) { console.log(' [TEST 6] ✓ PASS - Bundle filtering works'); return { pass: true, work: wc, personal: pc }; } else { console.log(' [TEST 6] ? WARN - Bundle filtering unclear'); return { pass: null, reason: 'Bundle unclear' }; } }); } function verify() { return listBookmarks().then(function(data) { var tests = data.results.filter(function(b) { return b.testId; }); console.log('[Verify] Test bookmarks: ' + tests.length); return tests.length === 0; }); } // MAIN (function main() { console.log(''); console.log('LinkdingSync Verified Test Runner loaded'); console.log(''); console.log('Running tests...'); console.log(''); test1().then(function(r1) { consoleLog(r1); return r1; }); test2().then(function(r2) { consoleLog(r2); return r2; }); test3().then(function(r3) { consoleLog(r3); return r3; }); test4().then(function(r4) { consoleLog(r4); return r4; }); test5().then(function(r5) { consoleLog(r5); return r5; }); test6().then(function(r6) { console.log(''); console.log('='.repeat(60)); console.log(' Summary'); console.log('='.repeat(60)); var passed = 0, failed = 0, warned = 0; [r1, r2, r3, r4, r5, r6].forEach(function(r) { if (r.pass === true) passed++; else if (r.pass === false) failed++; else warned++; }); console.log(' Total: 6, Passed: ' + passed + ', Failed: ' + failed + ', Warning: ' + warned); console.log('='.repeat(60)); return cleanup().then(function() { console.log(''); console.log('LinkdingSyncTests available:'); console.log(' cleanup()'); console.log(' listAll()'); console.log(''); console.log('Done!'); return { r1, r2, r3, r4, r5, r6, passed, failed, warned }; }); }); })(); function consoleLog(r) { if (r.pass === true) console.log(' [PASS]'); else if (r.pass === false) console.log(' [FAIL]'); else console.log(' [WARN]'); console.log(' ' + r.reason); } window.LinkdingSyncTests = { cleanup: cleanup, listAll: listBookmarks, verify: verify }; })(window); console.log(''); console.log('LinkdingSync Verified Test Runner loaded');