/* * LinkdingSync Final Test Runner * Firefox Console Compatible */ (function(w, eval) { 'use strict'; var window = w || window; var E = eval; // === CONFIG === var SERVER_URL = 'https://links.blabber1565.com'; var WORK_KEY = '4108e3aff26fb82bf074f5d4dfa4757763520b06'; var PERSONAL_KEY = '9b80accd3b9b4b91c2a7adc3dcf41621b025329a'; var WORK_USER = 'linkdingsync_tester'; var PERSONAL_USER = 'linkdingsync_tester_2'; // === HELPERS === var STATE = { URL: '', KEY: '', USER: null }; // Safe URL parsing - handles console-modified strings function parseUrl(str) { try { return new URL(str); } catch(e) { console.log(' [WARN] Invalid URL: ' + str); return null; } } function API(method, endpoint, data) { var url = parseUrl(STATE.URL + endpoint); if (!url) throw new Error('Invalid base URL'); var r = E(function(res) { if (!res.ok && res.status === 404) return { error: '404', status: res.status }; if (!res.ok) throw new Error(res.status + ': ' + res.statusText); return res.json(); })(url, { method: method, headers: { 'Authorization': 'Token ' + STATE.KEY, 'Content-Type': 'application/json' }, body: data ? JSON.stringify(data) : null }); return r; } // === TESTS === var RESULTS = []; function TEST(name, fn) { console.log(''); console.log('=== ' + name + ' ==='); var promise = E(function() { return fn(); }); promise.then(function(r) { console.log(' [PASS/FAIL/' + (r.pass === null ? 'WARN' : 'PASS') + '] ' + r.reason); RESULTS.push(r); return r; }).catch(function(e) { console.log(' [ERROR] ' + e.message); RESULTS.push({ pass: false, reason: 'Error: ' + e.message }); return { pass: false, reason: 'Error: ' + e.message }; }); return promise; } // === MAIN === TEST('API Key Isolation', function() { STATE.URL = SERVER_URL; STATE.KEY = WORK_KEY; STATE.USER = WORK_USER; return API('POST', '/api/bookmarks/', { url: 'https://t1.example.com', title: 'T1-Work', description: 'Test', notes: JSON.stringify({ testId: true }) }) .then(function(b1) { STATE.KEY = PERSONAL_KEY; STATE.USER = PERSONAL_USER; return API('POST', '/api/bookmarks/', { url: 'https://t1.example.com', title: 'T1-Personal', description: 'Test', notes: JSON.stringify({ testId: true }) }); }) .then(function(b2) { console.log(' IDs: ' + b1.id + ' vs ' + b2.id + ' Same? ' + (b1.id === b2.id)); if (b1.id === b2.id) return { pass: false, reason: 'API keys do NOT provide isolation' }; return { pass: true, reason: 'API keys provide isolation' }; }); }).then(function(r1) { console.log(''); console.log('=== Cross-User Visibility ==='); STATE.KEY = PERSONAL_KEY; STATE.USER = PERSONAL_USER; return API('GET', '/api/bookmarks/?limit=100').then(function(data) { var myTests = data.results ? data.results.filter(function(b) { return b.testId; }) : []; console.log(' Personal sees ' + (data.count || data.results.length) + ' bookmarks'); console.log(' My test bookmarks: ' + myTests.length); if (myTests.length === 1 && myTests[0].url === 'https://t1.example.com') { console.log(' [PASS] Personal only sees my test bookmark'); return { pass: true, reason: 'Proper user isolation' }; } console.log(' [WARN] Personal sees ' + myTests.length + ' test bookmarks'); return { pass: null, reason: 'Mixed results - check if sharing enabled' }; }); }).then(function(r2) { console.log(''); console.log('=== Conflict Resolution ==='); STATE.KEY = WORK_KEY; STATE.USER = WORK_USER; return API('POST', '/api/bookmarks/', { url: 'https://t3.example.com', title: 'T3-Work', description: 'Test', notes: JSON.stringify({ testId: true, path: 'Work' }) }) .then(function(b1) { STATE.KEY = PERSONAL_KEY; STATE.USER = PERSONAL_USER; return API('POST', '/api/bookmarks/', { url: 'https://t3.example.com', title: 'T3-Personal', description: 'Test', notes: JSON.stringify({ testId: true, path: 'Personal' }) }); }) .then(function(b2) { console.log(' IDs: ' + b1.id + ' vs ' + b2.id + ' Same? ' + (b1.id === b2.id)); if (b1.id === b2.id) return { pass: false, reason: 'Server merges by URL' }; return { pass: true, reason: 'Server creates separate bookmarks' }; }); }).then(function(r3) { console.log(''); console.log('=== Field Update Behavior ==='); STATE.KEY = WORK_KEY; STATE.USER = WORK_USER; return API('GET', '/api/bookmarks/?limit=1').then(function(data) { var bm = data.results ? data.results[0] : null; if (!bm) return API('POST', '/api/bookmarks/', { url: 'https://t4.example.com', title: 'Initial', description: 'Test', notes: JSON.stringify({ testId: true }) }); return API('PUT', '/api/bookmarks/' + bm.id + '/', { title: 'Work Title', description: 'Work', notes: JSON.stringify({ testId: true, path: 'Work' }) }); }).then(function(resp) { console.log(' Update response: ' + (resp.error ? resp.error : 'OK')); return API('GET', '/api/bookmarks/' + (resp.url || (resp.id ? '/api/bookmarks/' + resp.id + '/' : ''))); }).catch(function() { return API('POST', '/api/bookmarks/', { url: 'https://t4-new.example.com', title: 'Initial', description: 'Test', notes: JSON.stringify({ testId: true }) }); }).then(function(f) { console.log(' Final title: ' + f.title); if (f.title === 'Work Title') return { pass: true, reason: 'Title was updated' }; if (f.title === 'Initial') return { pass: true, reason: 'Title NOT updated (notes only)' }; return { pass: null, reason: 'Unknown title: ' + f.title }; }); }).then(function(r4) { console.log(''); console.log('=== Delete Behavior ==='); STATE.KEY = WORK_KEY; STATE.USER = WORK_USER; return API('POST', '/api/bookmarks/', { url: 'https://t5.example.com', title: 'T5-Work', description: 'Test', notes: JSON.stringify({ testId: true }) }) .then(function(b1) { STATE.KEY = PERSONAL_KEY; STATE.USER = PERSONAL_USER; return API('POST', '/api/bookmarks/', { url: 'https://t5.example.com', title: 'T5-Personal', description: 'Test', notes: JSON.stringify({ testId: true }) }); }) .then(function(b2) { console.log(' IDs: ' + b1.id + ' vs ' + b2.id + ' Same? ' + (b1.id === b2.id)); STATE.KEY = WORK_KEY; STATE.USER = WORK_USER; return API('DELETE', '/api/bookmarks/' + b1.id + '/'); }) .then(function() { STATE.KEY = PERSONAL_KEY; STATE.USER = PERSONAL_USER; return API('GET', '/api/bookmarks/?limit=100&url=https://t5.example.com'); }) .then(function(data) { var count = data.count || data.results ? data.results.length : 0; console.log(' Personal sees ' + count + ' with that URL'); if (count === 0) return { pass: false, reason: 'Delete propagated' }; return { pass: true, reason: 'Delete isolated' }; }); }).then(function(r5) { console.log(''); console.log('=== Bundle Filtering ==='); STATE.KEY = WORK_KEY; STATE.USER = WORK_USER; return API('POST', '/api/bookmarks/', { url: 'https://b6-1.example.com', title: 'B6-W1', description: 'Test', notes: JSON.stringify({ testId: true }) }) .then(function() { return API('POST', '/api/bookmarks/', { url: 'https://b6-2.example.com', title: 'B6-W2', description: 'Test', notes: JSON.stringify({ testId: true }) }); }) .then(function() { console.log(' Created 2 work bookmarks'); STATE.KEY = WORK_KEY; STATE.USER = WORK_USER; return API('GET', '/api/bookmarks/?all=work&limit=100').then(function(wd) { var wc = wd.count || wd.results ? wd.results.length : 0; console.log(' Work bundle: ' + wc + ' bookmarks'); STATE.KEY = PERSONAL_KEY; STATE.USER = PERSONAL_USER; return API('GET', '/api/bookmarks/?all=personal&limit=100').then(function(pd) { var pc = pd.count || pd.results ? pd.results.length : 0; console.log(' Personal bundle: ' + pc + ' bookmarks'); console.log(' [PASS] Bundle filtering works'); return { pass: true, reason: 'Bundle filtering works', work: wc, personal: pc }; }); }); }); }).then(function(r6) { // SUMMARY console.log(''); console.log('='.repeat(60)); console.log(' Summary'); console.log('='.repeat(60)); var passed = RESULTS.filter(function(r) { return r.pass === true; }).length; var failed = RESULTS.filter(function(r) { return r.pass === false; }).length; var warned = RESULTS.filter(function(r) { return r.pass === null; }).length; console.log(' Total: ' + RESULTS.length); console.log(' Passed: ' + passed); console.log(' Failed: ' + failed); console.log(' Warning: ' + warned); console.log('='.repeat(60)); console.log(''); console.log('LinkdingSyncTests available:'); console.log(' cleanup()'); console.log(' listAll()'); console.log(''); console.log('Done!'); return RESULTS; }).catch(function(e) { console.log(''); console.log('Error:', e.message); console.log(''); console.log('Use LinkdingSyncTests.cleanup()'); return RESULTS; }); // === CLEANUP === (function cleanup() { API('GET', '/api/bookmarks/?limit=100').then(function(data) { var tests = data.results ? data.results.filter(function(b) { return b.testId; }) : []; console.log(''); console.log('[Cleanup] ' + tests.length + ' test bookmarks'); if (tests.length) { Promise.all(tests.map(function(t) { return API('DELETE', '/api/bookmarks/' + t.id + '/'); })).then(function() { console.log('[Cleanup] Done'); }); } }); })(); // === EXPORT === window.LinkdingSyncTests = { cleanup: (function() { API('GET', '/api/bookmarks/?limit=100').then(function(data) { var t = data.results ? data.results.filter(function(b) { return b.testId; }) : []; console.log('[Cleanup] ' + t.length + ' bookmarks'); if (t.length) Promise.all(t.map(function(tt) { return API('DELETE', '/api/bookmarks/' + tt.id + '/'); })).then(function() { console.log('[Cleanup] Done'); }); })(); }()) }; })(window, window.eval); console.log(''); console.log('LinkdingSync Final Test Runner loaded'); console.log(''); console.log('Running tests automatically...');