Initial commit: LinkSyncServer and LinkSyncExtension projects with complete documentation, models, API endpoints, tests, and extension implementation
This commit is contained in:
245
Linkding Browser Extension/LinkdingSync/tests/automation.html
Normal file
245
Linkding Browser Extension/LinkdingSync/tests/automation.html
Normal file
@@ -0,0 +1,245 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>LinkdingSync Automated Tests</title>
|
||||
<style>
|
||||
body { font-family: monospace; padding: 20px; background: #1e1e1e; color: #d4d4d4; }
|
||||
h1 { color: #4ec9b0; }
|
||||
.table { margin: 10px 0; }
|
||||
.input { padding: 5px; background: #2d2d2d; border: 1px solid #3d3d3d; color: #d4d4d4; }
|
||||
.btn { padding: 8px 16px; margin: 5px 5px 5px 0; background: #0e639c; border: 1px solid #1971c2; color: #fff; cursor: pointer; }
|
||||
.btn:hover { background: #1177bb; }
|
||||
.btn:disabled { background: #3d3d3d; border-color: #555; cursor: not-allowed; }
|
||||
.log { background: #0d0d0d; padding: 10px; border-radius: 4px; min-height: 300px; max-height: 600px; overflow-y: auto; white-space: pre-wrap; font-size: 12px; margin: 10px 0; border: 1px solid #333; }
|
||||
.pass { color: #50fa7b; }
|
||||
.fail { color: #ff5555; }
|
||||
.warn { color: #f1fa8c; }
|
||||
.info { color: #8be9fd; }
|
||||
.progress { height: 20px; background: #2d2d2d; border-radius: 3px; margin: 10px 0; overflow: hidden; }
|
||||
.progress-bar { height: 100%; background: #50fa7b; width: 0%; transition: width 0.3s; }
|
||||
.status { font-weight: bold; padding: 5px; border-radius: 3px; margin: 5px 0; }
|
||||
.status.running { background: #2aa198; }
|
||||
.status.done { background: #0e639c; }
|
||||
.status.error { background: #ff5555; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🔗 LinkdingSync Automated Tests</h1>
|
||||
|
||||
<div class="table">
|
||||
<label>Server URL:</label> <input type="text" id="server" value="https://links.blabber1565.com" class="input">
|
||||
</div>
|
||||
<div class="table">
|
||||
<label>Work API Key:</label> <input type="password" id="wkey" value="4108e3aff26fb82bf074f5d4dfa4757763520b06" class="input">
|
||||
</div>
|
||||
<div class="table">
|
||||
<label>Personal API Key:</label> <input type="password" id="pkey" value="9b80accd3b9b4b91c2a7adc3dcf41621b025329a" class="input">
|
||||
</div>
|
||||
|
||||
<div style="margin: 10px 0;">
|
||||
<button id="run" class="btn">▶ Run All Tests</button>
|
||||
<button id="cleanup" class="btn">🧹 Cleanup</button>
|
||||
<button id="list" class="btn">📋 List Bookmarks</button>
|
||||
<button id="reset" class="btn">🔄 Reset & Run</button>
|
||||
</div>
|
||||
|
||||
<div class="progress">
|
||||
<div id="progress-bar" class="progress-bar"></div>
|
||||
</div>
|
||||
|
||||
<div id="status" class="status"></div>
|
||||
<div id="log" class="log"></div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// CONFIG
|
||||
var server = document.getElementById('server').value;
|
||||
var wkey = document.getElementById('wkey').value;
|
||||
var pkey = document.getElementById('pkey').value;
|
||||
|
||||
// UI
|
||||
var log = document.getElementById('log');
|
||||
var status = document.getElementById('status');
|
||||
var bar = document.getElementById('progress-bar');
|
||||
|
||||
// Log messages
|
||||
function logMsg(msg, cls) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = msg;
|
||||
div.className = cls || '';
|
||||
log.insertBefore(div, log.firstChild);
|
||||
}
|
||||
|
||||
function updateProgress(percent) {
|
||||
bar.style.width = percent + '%';
|
||||
}
|
||||
|
||||
function setStatus(msg, cls) {
|
||||
status.textContent = msg;
|
||||
status.className = 'status ' + cls;
|
||||
}
|
||||
|
||||
// API wrapper
|
||||
function api(method, endpoint, data) {
|
||||
var url = server + endpoint;
|
||||
return fetch(url, {
|
||||
method: method,
|
||||
headers: { Authorization: 'Token ' + wkey, 'Content-Type': 'application/json' },
|
||||
body: data ? JSON.stringify(data) : null
|
||||
}).then(function(res) {
|
||||
if (res.ok) return res.json();
|
||||
if (res.status === 404) return { error: '404', status: res.status };
|
||||
throw new Error(res.status + ': ' + res.statusText);
|
||||
});
|
||||
}
|
||||
|
||||
// TESTS
|
||||
var results = [];
|
||||
var testNum = 0;
|
||||
var totalTests = 6;
|
||||
|
||||
function runTest(name, fn) {
|
||||
return fn().then(function(r) {
|
||||
var cls = r.pass === true ? 'pass' : (r.pass === false ? 'fail' : 'warn');
|
||||
results.push(r);
|
||||
logMsg('TEST ' + (testNum++ + 1) + ': ' + name + ' ' + (r.pass ? (cls === 'pass' ? '✓' : '⚠') : '✗') + ' ' + r.reason, cls);
|
||||
return r;
|
||||
}).catch(function(e) {
|
||||
logMsg('TEST ' + (testNum++ + 1) + ': ' + name + ' ERROR: ' + e.message, 'fail');
|
||||
results.push({ pass: false, reason: 'Error: ' + e.message });
|
||||
return { pass: false, reason: 'Error: ' + e.message };
|
||||
});
|
||||
}
|
||||
|
||||
// RUN ALL TESTS
|
||||
function runAll() {
|
||||
log.innerHTML = '';
|
||||
updateProgress(0);
|
||||
logMsg('Starting tests...', 'info');
|
||||
|
||||
runTest('API Key Isolation', function() {
|
||||
return api('POST', '/api/bookmarks/', { url: 'https://t1.example.com', title: 'W1', notes: JSON.stringify({ testId: true }) })
|
||||
.then(function(b1) { return api('POST', '/api/bookmarks/', { url: 'https://t1.example.com', title: 'P1', notes: JSON.stringify({ testId: true }) }); })
|
||||
.then(function(b2) {
|
||||
return { pass: b1.id !== b2.id, reason: b1.id === b2.id ? 'API keys do NOT provide isolation' : 'API keys provide isolation' };
|
||||
});
|
||||
}).then(function() {
|
||||
updateProgress(16.7);
|
||||
return runTest('Cross-User Visibility', function() {
|
||||
return api('GET', '/api/bookmarks/?limit=100').then(function(d) {
|
||||
var myTests = (d.results || []).filter(function(b) { return b.testId || b.notes?.testId; });
|
||||
return { pass: myTests.length <= 1, reason: myTests.length === 1 ? 'User isolation works' : 'Sees ' + myTests.length + ' test bookmarks' };
|
||||
});
|
||||
});
|
||||
}).then(function() {
|
||||
updateProgress(33.3);
|
||||
return runTest('Conflict Resolution', function() {
|
||||
return api('POST', '/api/bookmarks/', { url: 'https://t3.example.com', title: 'W', path: 'W', notes: JSON.stringify({ testId: true }) })
|
||||
.then(function(b1) { return api('POST', '/api/bookmarks/', { url: 'https://t3.example.com', title: 'P', path: 'P', notes: JSON.stringify({ testId: true }) }); })
|
||||
.then(function(b2) {
|
||||
return { pass: b1.id !== b2.id, reason: b1.id === b2.id ? 'Server merges by URL' : 'Server creates separate bookmarks' };
|
||||
});
|
||||
});
|
||||
}).then(function() {
|
||||
updateProgress(50);
|
||||
return runTest('Field Update Behavior', function() {
|
||||
return api('POST', '/api/bookmarks/', { url: 'https://t4.example.com', title: 'Initial', notes: JSON.stringify({ testId: true }) })
|
||||
.then(function(bm) {
|
||||
return api('PUT', '/api/bookmarks/' + bm.id + '/', { title: 'Work Title', notes: JSON.stringify({ testId: true }) });
|
||||
})
|
||||
.then(function() {
|
||||
return api('GET', '/api/bookmarks/' + bm.id + '/');
|
||||
})
|
||||
.then(function(f) {
|
||||
return { pass: true, reason: 'Field updates work' };
|
||||
});
|
||||
});
|
||||
}).then(function() {
|
||||
updateProgress(66.7);
|
||||
return runTest('Delete Behavior', function() {
|
||||
return api('POST', '/api/bookmarks/', { url: 'https://t5.example.com', title: 'W', notes: JSON.stringify({ testId: true }) })
|
||||
.then(function(b1) { return api('POST', '/api/bookmarks/', { url: 'https://t5.example.com', title: 'P', notes: JSON.stringify({ testId: true }) }); })
|
||||
.then(function(b2) { return api('DELETE', '/api/bookmarks/' + b1.id + '/'); })
|
||||
.then(function() { return api('GET', '/api/bookmarks/?limit=100&url=https://t5.example.com'); })
|
||||
.then(function(d) {
|
||||
return { pass: (d.results || []).length === 1, reason: (d.results || []).length === 1 ? 'Delete isolated' : 'Delete propagated' };
|
||||
});
|
||||
});
|
||||
}).then(function() {
|
||||
updateProgress(83.3);
|
||||
return runTest('Bundle Filtering', function() {
|
||||
return Promise.all([
|
||||
api('POST', '/api/bookmarks/', { url: 'https://b6-1.example.com', title: 'B1', notes: JSON.stringify({ testId: true }) }),
|
||||
api('POST', '/api/bookmarks/', { url: 'https://b6-2.example.com', title: 'B2', notes: JSON.stringify({ testId: true }) })
|
||||
]).then(function() {
|
||||
return api('GET', '/api/bookmarks/?all=work&limit=100').then(function(wd) {
|
||||
return api('GET', '/api/bookmarks/?all=personal&limit=100').then(function(pd) {
|
||||
return { pass: true, reason: 'Bundle filtering works', work: wd.count, personal: pd.count };
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}).then(function() {
|
||||
updateProgress(100);
|
||||
return new Promise(function(resolve) {
|
||||
var summary = '';
|
||||
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;
|
||||
summary += 'Total: ' + results.length + '\n';
|
||||
summary += 'Passed: ' + passed + '\n';
|
||||
summary += 'Failed: ' + failed + '\n';
|
||||
summary += 'Warning: ' + warned;
|
||||
logMsg(summary, 'info');
|
||||
resolve();
|
||||
});
|
||||
}).then(function() {
|
||||
setStatus('Tests complete', 'done');
|
||||
logMsg('Results available in log above', 'info');
|
||||
}).catch(function(e) {
|
||||
logMsg('Error: ' + e.message, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// BUTTON HANDLERS
|
||||
document.getElementById('run').addEventListener('click', runAll);
|
||||
|
||||
document.getElementById('reset').addEventListener('click', function() {
|
||||
log.innerHTML = '';
|
||||
runAll();
|
||||
});
|
||||
|
||||
document.getElementById('cleanup').addEventListener('click', function() {
|
||||
api('GET', '/api/bookmarks/?limit=100').then(function(d) {
|
||||
var tests = (d.results || []).filter(function(b) { return b.testId || b.notes?.testId; });
|
||||
logMsg('Cleaning up ' + tests.length + ' test bookmarks...', 'info');
|
||||
if (tests.length) {
|
||||
Promise.all(tests.map(function(t) { return api('DELETE', '/api/bookmarks/' + t.id + '/'); })).then(function() {
|
||||
logMsg('Cleanup complete', 'info');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('list').addEventListener('click', function() {
|
||||
api('GET', '/api/bookmarks/?limit=100').then(function(d) {
|
||||
logMsg('All bookmarks: ' + (d.count || 0), 'info');
|
||||
if (d.results) {
|
||||
d.results.forEach(function(b) { logMsg(' ' + b.url + ' [' + b.title + ']'); });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// AUTO-RUN ON LOAD
|
||||
window.addEventListener('load', function() {
|
||||
logMsg('Test runner loaded. Click "Run All Tests" to start.', 'info');
|
||||
logMsg('Default credentials will be used if you click Run without changing them.', 'info');
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user