Initial commit: LinkSyncServer and LinkSyncExtension projects with complete documentation, models, API endpoints, tests, and extension implementation

This commit is contained in:
DavidSaylor
2026-05-11 17:37:10 -05:00
parent ad0b12b452
commit aed69afdfd
691 changed files with 181874 additions and 28 deletions

View 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');