313 lines
13 KiB
JavaScript
313 lines
13 KiB
JavaScript
/*
|
|
* 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()'); |