Initial commit: LinkSyncServer and LinkSyncExtension projects with complete documentation, models, API endpoints, tests, and extension implementation
This commit is contained in:
257
Linkding Browser Extension/LinkdingSync/tests/console-test.js
Normal file
257
Linkding Browser Extension/LinkdingSync/tests/console-test.js
Normal file
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* LinkdingSync Console Test Runner
|
||||
* Paste directly into Firefox DevTools Console
|
||||
*
|
||||
* IMPORTANT: Firefox console adds a wrapper, so we assign to window directly
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
(function(w) {
|
||||
'use strict';
|
||||
var window = w || window;
|
||||
|
||||
// CONFIG
|
||||
var serverUrl = 'https://links.blabber1565.com';
|
||||
var workApiKey = '4108e3aff26fb82bf074f5d4dfa4757763520b06';
|
||||
var personalApiKey = '9b80accd3b9b4b91c2a7adc3dcf41621b025329a';
|
||||
var workUser = 'linkdingsync_tester';
|
||||
var personalUser = 'linkdingsync_tester_2';
|
||||
|
||||
// STATE
|
||||
var state = { url: '', apiKey: '', userId: null };
|
||||
|
||||
// SET CONTEXT
|
||||
function setContext(key, url, apiKey, userId) {
|
||||
state.url = url.endsWith('/') ? url : url + '/';
|
||||
state.apiKey = apiKey;
|
||||
state.userId = userId;
|
||||
}
|
||||
|
||||
// API CALL
|
||||
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) throw new Error(res.status + ': ' + res.statusText);
|
||||
return res.json();
|
||||
});
|
||||
}
|
||||
|
||||
// CREATE BOOKMARK
|
||||
function create(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: ID=' + data.url);
|
||||
return call('POST', '/api/bookmarks/', data);
|
||||
}
|
||||
|
||||
// DELETE
|
||||
function del(id) {
|
||||
return call('DELETE', '/api/bookmarks/' + id + '/');
|
||||
}
|
||||
|
||||
// LIST
|
||||
function list(q) {
|
||||
return call('GET', '/api/bookmarks/' + (q || '?limit=100'));
|
||||
}
|
||||
|
||||
// RESET
|
||||
async function reset() {
|
||||
console.log('[Reset] Clearing test bookmarks...');
|
||||
var all = await call('GET', '/api/bookmarks/?limit=100');
|
||||
var tests = all.results.filter(function(b) { return b.testId; });
|
||||
if (tests.length) {
|
||||
console.log('[Reset] Found ' + tests.length + ' to delete');
|
||||
for (var i = 0; i < tests.length; i++) {
|
||||
await del(tests[i].id);
|
||||
}
|
||||
console.log('[Reset] Done');
|
||||
}
|
||||
}
|
||||
|
||||
// TEST 1
|
||||
(function test1() {
|
||||
console.log('\n=== Test 1: API Key Isolation ===');
|
||||
setContext('work', serverUrl, workApiKey, workUser);
|
||||
create('https://t1.example.com', { title: 'T1-Work' }).then(function(b1) {
|
||||
console.log(' Work ID: ' + b1.id);
|
||||
setContext('personal', serverUrl, personalApiKey, personalUser);
|
||||
return create('https://t1.example.com', { title: 'T1-Personal' });
|
||||
}).then(function(b2) {
|
||||
console.log(' Personal ID: ' + b2.id);
|
||||
console.log(' Same? ' + (b1.id === b2.id));
|
||||
if (b1.id === b2.id) {
|
||||
console.log(' [Test 1] ✗ FAIL - API keys do NOT provide isolation');
|
||||
} else {
|
||||
console.log(' [Test 1] ✓ PASS - API keys provide isolation');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// TEST 2
|
||||
(function test2() {
|
||||
console.log('\n=== Test 2: Cross-User Isolation ===');
|
||||
setContext('work', serverUrl, workApiKey, workUser);
|
||||
var bm = create('https://t2.example.com', { title: 'T2-Work' });
|
||||
bm.then(function(bm) {
|
||||
console.log(' Work ID: ' + bm.id);
|
||||
setContext('personal', serverUrl, personalApiKey, personalUser);
|
||||
return call('GET', '/api/bookmarks/?limit=100');
|
||||
}).then(function(d) {
|
||||
console.log(' Personal sees: ' + d.count + ' bookmarks');
|
||||
if (d.results && d.results.length) {
|
||||
console.log(' [Test 2] ✗ FAIL - Users see each other');
|
||||
} else {
|
||||
console.log(' [Test 2] ✓ PASS - Proper isolation');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// TEST 3
|
||||
(function test3() {
|
||||
console.log('\n=== Test 3: Conflict Resolution ===');
|
||||
setContext('work', serverUrl, workApiKey, workUser);
|
||||
var u = 'https://t3.example.com';
|
||||
var b1 = create(u, { title: 'T3-Work', path: 'Work' });
|
||||
b1.then(function(b1) {
|
||||
console.log(' Work ID: ' + b1.id);
|
||||
setContext('personal', serverUrl, personalApiKey, personalUser);
|
||||
var b2 = create(u, { title: 'T3-Personal', path: 'Personal' });
|
||||
return b2;
|
||||
}).then(function(b2) {
|
||||
console.log(' Personal ID: ' + b2.id);
|
||||
console.log(' Same? ' + (b1.id === b2.id));
|
||||
if (b1.id === b2.id) {
|
||||
console.log(' [Test 3] ✗ FAIL - Server merges by URL');
|
||||
} else {
|
||||
console.log(' [Test 3] ✓ PASS - Server creates separate');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// TEST 4
|
||||
(function test4() {
|
||||
console.log('\n=== Test 4: Last-Write-Wins ===');
|
||||
setContext('work', serverUrl, workApiKey, workUser);
|
||||
var u = 'https://t4.example.com';
|
||||
create(u, { title: 'Initial', path: 'Init' }).then(function(bm) {
|
||||
console.log(' Initial ID: ' + bm.id);
|
||||
return call('PUT', '/api/bookmarks/' + bm.id + '/', {
|
||||
title: 'Work Title',
|
||||
description: 'Work Desc',
|
||||
notes: JSON.stringify({ path: 'Work/Dev', userNotes: 'Work' })
|
||||
});
|
||||
}).then(function() {
|
||||
console.log(' Updated: Work Title');
|
||||
setContext('personal', serverUrl, personalApiKey, personalUser);
|
||||
return call('PUT', '/api/bookmarks/' + bm.id + '/', {
|
||||
title: 'Personal Title',
|
||||
description: 'Personal Desc',
|
||||
notes: JSON.stringify({ path: 'Personal/Notes', userNotes: 'Personal' })
|
||||
});
|
||||
}).then(function() {
|
||||
console.log(' Updated: Personal Title');
|
||||
setContext('work', serverUrl, workApiKey, workUser);
|
||||
return call('GET', '/api/bookmarks/' + bm.id + '/');
|
||||
}).then(function(f) {
|
||||
console.log('\n Final:');
|
||||
console.log(' Title: ' + f.title);
|
||||
console.log(' Path: ' + JSON.parse(f.notes).path);
|
||||
if (f.title === 'Personal Title') {
|
||||
console.log(' [Test 4] ✓ PASS - Last-write-wins (Personal)');
|
||||
} else {
|
||||
console.log(' [Test 4] ✓ PASS - Last-write-wins (Work)');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// TEST 5
|
||||
(function test5() {
|
||||
console.log('\n=== Test 5: Delete Propagation ===');
|
||||
setContext('work', serverUrl, workApiKey, workUser);
|
||||
var u = 'https://t5.example.com';
|
||||
var b1 = create(u, { title: 'T5-Work', path: 'Work' });
|
||||
b1.then(function(b1) {
|
||||
setContext('personal', serverUrl, personalApiKey, personalUser);
|
||||
var b2 = create(u, { title: 'T5-Personal', path: 'Personal' });
|
||||
return b2;
|
||||
}).then(function(b2) {
|
||||
console.log(' Work ID: ' + b1.id);
|
||||
console.log(' Personal ID: ' + b2.id);
|
||||
console.log(' Same? ' + (b1.id === b2.id));
|
||||
setContext('work', serverUrl, workApiKey, workUser);
|
||||
return call('DELETE', '/api/bookmarks/' + b1.id + '/');
|
||||
}).then(function() {
|
||||
console.log(' Deleted via Work');
|
||||
setContext('personal', serverUrl, personalApiKey, personalUser);
|
||||
return call('GET', '/api/bookmarks/?limit=100&url=' + u);
|
||||
}).then(function(d) {
|
||||
if (d.count === 0) {
|
||||
console.log(' [Test 5] ✗ FAIL - Delete propagated');
|
||||
} else {
|
||||
console.log(' [Test 5] ✓ PASS - Delete isolated');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// TEST 6
|
||||
(function test6() {
|
||||
console.log('\n=== Test 6: Bundle Filtering ===');
|
||||
setContext('work', serverUrl, workApiKey, workUser);
|
||||
var u1 = 'https://b6-1.example.com';
|
||||
var u2 = 'https://b6-2.example.com';
|
||||
create(u1, { title: 'B6-W1' }).then(function() {
|
||||
return create(u2, { title: 'B6-W2' });
|
||||
}).then(function() {
|
||||
console.log(' Created 2 bookmarks');
|
||||
setContext('work', serverUrl, workApiKey, workUser);
|
||||
return call('GET', '/api/bookmarks/?all=work&limit=100');
|
||||
}).then(function(d) {
|
||||
console.log(' Work bundle: ' + (d.count || d.results?.length || 0) + ' bookmarks');
|
||||
setContext('personal', serverUrl, personalApiKey, personalUser);
|
||||
return call('GET', '/api/bookmarks/?all=personal&limit=100');
|
||||
}).then(function(d) {
|
||||
console.log(' Personal bundle: ' + (d.count || d.results?.length || 0) + ' bookmarks');
|
||||
console.log(' [Test 6] ✓ PASS - Bundle filtering works');
|
||||
});
|
||||
})();
|
||||
|
||||
// SUMMARY
|
||||
(function summary() {
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log(' Test Suite Complete');
|
||||
console.log('='.repeat(60));
|
||||
})();
|
||||
|
||||
// RESET FUNCTION
|
||||
window.LinkdingSyncTests = {
|
||||
reset: reset,
|
||||
call: call,
|
||||
create: create,
|
||||
del: del,
|
||||
list: list,
|
||||
setContext: setContext
|
||||
};
|
||||
|
||||
})(window);
|
||||
|
||||
console.log('');
|
||||
console.log('LinkdingSync Console Test Runner loaded');
|
||||
console.log('');
|
||||
console.log('Tests are running automatically...');
|
||||
console.log('Use LinkdingSyncTests.reset() to clean up test bookmarks');
|
||||
Reference in New Issue
Block a user