Initial commit: LinkSyncServer and LinkSyncExtension projects with complete documentation, models, API endpoints, tests, and extension implementation
This commit is contained in:
217
Linkding Browser Extension/LinkdingSync/tests/verified-test.js
Normal file
217
Linkding Browser Extension/LinkdingSync/tests/verified-test.js
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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');
|
||||
Reference in New Issue
Block a user