245 lines
10 KiB
HTML
245 lines
10 KiB
HTML
<!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> |