796 lines
28 KiB
JavaScript
796 lines
28 KiB
JavaScript
/*
|
|
* LinkdingSync Test Orchestrator (Inline Version)
|
|
* Self-contained - paste entire file directly into Firefox DevTools Console
|
|
*
|
|
* Instructions:
|
|
* 1. Open Firefox DevTools → Console tab
|
|
* 2. Copy ENTIRE file contents
|
|
* 3. Paste into console (Ctrl+Shift+V)
|
|
* 4. Wait for "LinkdingSync Test Suite loaded"
|
|
* 5. Run: runAllTestsWithReset()
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
(function() {
|
|
// ====================================================================
|
|
// CONFIGURATION
|
|
// ====================================================================
|
|
|
|
const CONFIG = {
|
|
serverUrl: 'https://links.blabber1565.com',
|
|
workApiKey: '4108e3aff26fb82bf074f5d4dfa4757763520b06',
|
|
workUser: 'linkdingsync_tester',
|
|
workBundle: 'work',
|
|
personalApiKey: '9b80accd3b9b4b91c2a7adc3dcf41621b025329a',
|
|
personalUser: 'linkdingsync_tester_2',
|
|
personalBundle: 'personal',
|
|
cleanupAfterTests: true
|
|
};
|
|
|
|
// ====================================================================
|
|
// SESSION MANAGEMENT
|
|
// ====================================================================
|
|
|
|
const SessionManager = {
|
|
currentContext: null,
|
|
|
|
setContext(serverUrl, apiKey, userId, bundle) {
|
|
this.currentContext = {
|
|
serverUrl: serverUrl.endsWith('/') ? serverUrl : serverUrl + '/',
|
|
apiKey, userId, bundle
|
|
};
|
|
return this;
|
|
},
|
|
|
|
getHeaders() {
|
|
if (!this.currentContext) {
|
|
throw new Error('No context set. Call setContext() first.');
|
|
}
|
|
return {
|
|
'Authorization': `Token ${this.currentContext.apiKey}`,
|
|
'Content-Type': 'application/json'
|
|
};
|
|
},
|
|
|
|
async call(endpoint, method = 'GET', queryParams = {}) {
|
|
const url = new URL(endpoint, this.currentContext.serverUrl);
|
|
Object.entries(queryParams).forEach(([key, value]) => {
|
|
url.searchParams.append(key, value);
|
|
});
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: this.getHeaders(),
|
|
body: null
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text().slice(0, 200);
|
|
throw new Error(`${response.status}: ${response.statusText} - ${text}`);
|
|
}
|
|
|
|
return await response.json();
|
|
}
|
|
};
|
|
|
|
// ====================================================================
|
|
// HELPERS
|
|
// ====================================================================
|
|
|
|
const Helpers = {
|
|
generateTestId(prefix = 'test') {
|
|
return `${prefix}-${Date.now().toString().slice(-4)}-${Math.random().toString(36).substring(2, 4)}`;
|
|
},
|
|
|
|
async createBookmark(url, options = {}) {
|
|
const testId = this.generateTestId();
|
|
const baseUrl = new URL(url);
|
|
baseUrl.hostname = `${testId}.${baseUrl.hostname}`;
|
|
|
|
const bookmarkData = {
|
|
url: baseUrl.href,
|
|
title: options.title || `Test: ${testId}`,
|
|
description: options.description || 'Test bookmark',
|
|
notes: JSON.stringify({
|
|
path: options.path || `Test/${testId}`,
|
|
userNotes: options.notes || 'Test bookmark',
|
|
testId
|
|
})
|
|
};
|
|
|
|
const response = await SessionManager.call('/api/bookmarks/', 'POST', null, {});
|
|
console.log(` Created: ID=${response.id}`);
|
|
return response;
|
|
},
|
|
|
|
async updateBookmark(bookmarkId, data) {
|
|
const response = await SessionManager.call(`/api/bookmarks/${bookmarkId}/`, 'PUT', null, {});
|
|
console.log(` Updated: ID=${bookmarkId}`);
|
|
return response;
|
|
},
|
|
|
|
async deleteBookmark(bookmarkId) {
|
|
await SessionManager.call(`/api/bookmarks/${bookmarkId}/`, 'DELETE', {});
|
|
console.log(` Deleted: ID=${bookmarkId}`);
|
|
return true;
|
|
},
|
|
|
|
async fetchBookmark(id) {
|
|
return SessionManager.call(`/api/bookmarks/${id}/`);
|
|
},
|
|
|
|
parseNotes(noteString) {
|
|
if (!noteString) return null;
|
|
try {
|
|
const parsed = JSON.parse(noteString);
|
|
return parsed;
|
|
} catch {
|
|
return { userNotes: noteString, version: '1.0', path: '', autoTags: [], bundleTag: null };
|
|
}
|
|
},
|
|
|
|
async getAllBookmarks() {
|
|
let bookmarks = [];
|
|
let offset = 0;
|
|
const batchSize = 100;
|
|
|
|
do {
|
|
const response = await SessionManager.call('/api/bookmarks/', 'GET', { limit: batchSize, offset });
|
|
bookmarks.push(...(response.results || []));
|
|
offset += batchSize;
|
|
} while (bookmarks.length > offset);
|
|
|
|
return bookmarks;
|
|
},
|
|
|
|
async resetBookmarks() {
|
|
console.log('[Utils] Resetting all bookmarks...');
|
|
try {
|
|
const allBookmarks = await this.getAllBookmarks();
|
|
const testBookmarks = allBookmarks.filter(b => b.testId);
|
|
|
|
if (testBookmarks.length > 0) {
|
|
console.log(`[Utils] Found ${testBookmarks.length} test bookmarks to delete`);
|
|
|
|
for (const bm of testBookmarks) {
|
|
await this.deleteBookmark(bm.id);
|
|
}
|
|
|
|
console.log('[Utils] Reset complete');
|
|
} else {
|
|
console.log('[Utils] No test bookmarks found');
|
|
}
|
|
} catch (error) {
|
|
console.error('[Utils] Reset failed:', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
// ====================================================================
|
|
// FORMATTERS
|
|
// ====================================================================
|
|
|
|
const Formatters = {
|
|
formatTimestamp(timestamp) {
|
|
if (!timestamp) return 'Never';
|
|
return new Date(timestamp).toLocaleString('en-US', {
|
|
year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
|
|
});
|
|
},
|
|
|
|
consoleHeader(text) {
|
|
console.log(''.padEnd(60, '='));
|
|
console.log(` ${text}`.padEnd(60, '='.charCodeAt(0) === text.charCodeAt(0) ? '=' : '-').padEnd(60, '='));
|
|
console.log(''.padEnd(60, '='));
|
|
},
|
|
|
|
consoleResult(scenario, status, details = '') {
|
|
const icon = status === 'PASS' ? '✓' : status === 'FAIL' ? '✗' : '⚠';
|
|
const emoji = status === 'PASS' ? '✅' : status === 'FAIL' ? '❌' : '⚠️';
|
|
console.log(` [${scenario}] ${icon} ${emoji} ${status}`);
|
|
if (details) console.log(` ${details}`);
|
|
}
|
|
};
|
|
|
|
// ====================================================================
|
|
// TEST MODULE: ISOLATION
|
|
// ====================================================================
|
|
|
|
async function test1_SameUserDifferentKeys() {
|
|
console.log('\n=== Test 1: Same URL, Different API Keys, Same User ===');
|
|
console.log('Purpose: Verify if API keys provide isolation within same user');
|
|
|
|
try {
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle);
|
|
|
|
const bm1 = await Helpers.createBookmark('https://isolation-test.example.com', {
|
|
title: 'Isolation Test - Work Key'
|
|
});
|
|
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle);
|
|
const bm2 = await Helpers.createBookmark('https://isolation-test.example.com', {
|
|
title: 'Isolation Test - Personal Key'
|
|
});
|
|
|
|
console.log(` Work bookmark ID: ${bm1.id}`);
|
|
console.log(` Personal bookmark ID: ${bm2.id}`);
|
|
|
|
if (bm1.id === bm2.id) {
|
|
Formatters.consoleResult('Test 1', 'FAIL', 'Same bookmark ID - API keys do NOT provide isolation');
|
|
console.log(' → Same user means same bookmarks regardless of API key');
|
|
return { pass: false, reason: 'API keys do not provide isolation within same user' };
|
|
} else {
|
|
Formatters.consoleResult('Test 1', 'PASS', 'Different bookmark IDs - API keys provide isolation');
|
|
console.log(' → Different API keys create separate bookmarks');
|
|
return { pass: true, ids: { work: bm1.id, personal: bm2.id } };
|
|
}
|
|
} catch (error) {
|
|
Formatters.consoleResult('Test 1', 'FAIL', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function test2_DifferentUsers() {
|
|
console.log('\n=== Test 2: Different Users - Verify Isolation ===');
|
|
console.log('Purpose: Verify isolation between different users');
|
|
|
|
try {
|
|
const workUrl = 'https://cross-user-isolation.example.com';
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle);
|
|
|
|
const workBookmark = await Helpers.createBookmark(workUrl, {
|
|
title: 'Cross-User Test - Work'
|
|
});
|
|
|
|
console.log(` Bookmark created by work user: ID=${workBookmark.id}`);
|
|
|
|
const workFetch = await SessionManager.call(`/api/bookmarks/${workBookmark.id}/`);
|
|
console.log(` Work user sees bookmark: ${workFetch.title}`);
|
|
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle);
|
|
const personalFetch = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 });
|
|
|
|
console.log(` Personal user sees ${personalFetch.count || personalFetch.results?.length || 0} bookmarks`);
|
|
|
|
if (personalFetch.results && personalFetch.results.length > 0) {
|
|
Formatters.consoleResult('Test 2', 'FAIL', 'Users can see each other\'s bookmarks');
|
|
console.log(' → Sharing enabled or same underlying user');
|
|
return { pass: false, reason: 'Users can see each other\'s bookmarks' };
|
|
} else {
|
|
Formatters.consoleResult('Test 2', 'PASS', 'Proper user isolation exists');
|
|
console.log(' → Can use different API keys for isolation');
|
|
return { pass: true };
|
|
}
|
|
} catch (error) {
|
|
Formatters.consoleResult('Test 2', 'FAIL', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function runIsolationTests() {
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log(' API Key & User Isolation Tests');
|
|
console.log('='.repeat(60));
|
|
|
|
const results = [];
|
|
|
|
try {
|
|
results[0] = await test1_SameUserDifferentKeys();
|
|
results[1] = await test2_DifferentUsers();
|
|
} catch (error) {
|
|
console.error('Test suite error:', error.message);
|
|
await Helpers.resetBookmarks();
|
|
}
|
|
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log(' Isolation Tests Complete');
|
|
console.log('='.repeat(60));
|
|
|
|
return results;
|
|
}
|
|
|
|
// ====================================================================
|
|
// TEST MODULE: CONFLICTS
|
|
// ====================================================================
|
|
|
|
async function test3_ConflictResolution() {
|
|
console.log('\n=== Test 3: Conflict Resolution - Different Paths ===');
|
|
console.log('Purpose: How server handles same URL in different paths with different API keys');
|
|
|
|
try {
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle);
|
|
|
|
const workUrl = 'https://conflict-resolution.example.com';
|
|
const workBookmark = await Helpers.createBookmark(workUrl, {
|
|
title: 'Conflict Resolution Test',
|
|
path: 'Work/Development',
|
|
notes: 'Work Development Notes'
|
|
});
|
|
|
|
console.log(` Work bookmark ID: ${workBookmark.id}`);
|
|
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle);
|
|
const personalBookmark = await Helpers.createBookmark(workUrl, {
|
|
title: 'Conflict Resolution Test',
|
|
path: 'Personal/Notes',
|
|
notes: 'Personal Notes'
|
|
});
|
|
|
|
console.log(` Personal bookmark ID: ${personalBookmark.id}`);
|
|
console.log(`\nComparing bookmark IDs: work=${workBookmark.id}, personal=${personalBookmark.id}`);
|
|
|
|
if (workBookmark.id === personalBookmark.id) {
|
|
Formatters.consoleResult('Test 3', 'FAIL', 'Same bookmark ID');
|
|
console.log(' → Server merges bookmarks by URL');
|
|
console.log(' → Need path merge strategy');
|
|
|
|
const state = await SessionManager.call(`/api/bookmarks/${workBookmark.id}/`);
|
|
const parsed = Helpers.parseNotes(state.notes);
|
|
console.log(` → Current path: ${parsed.path}`);
|
|
console.log(` → Current notes: ${parsed.userNotes}`);
|
|
|
|
return { pass: false, sameId: true, path: parsed.path };
|
|
} else {
|
|
Formatters.consoleResult('Test 3', 'PASS', 'Different bookmark IDs');
|
|
console.log(' → Server creates separate bookmarks per API key');
|
|
console.log(' → Can use different API keys for isolation');
|
|
|
|
return { pass: true, sameId: false, workId: workBookmark.id, personalId: personalBookmark.id };
|
|
}
|
|
} catch (error) {
|
|
Formatters.consoleResult('Test 3', 'FAIL', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function test4_TitleDescriptionConflict() {
|
|
console.log('\n=== Test 4: Title/Description Conflict ===');
|
|
console.log('Purpose: How server resolves conflicts for title/description fields');
|
|
|
|
try {
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle);
|
|
|
|
const testUrl = 'https://title-conflict.example.com';
|
|
const bookmark = await Helpers.createBookmark(testUrl, {
|
|
title: 'Initial Title',
|
|
description: 'Initial Description',
|
|
path: 'Initial'
|
|
});
|
|
|
|
await Helpers.updateBookmark(bookmark.id, {
|
|
title: 'Work Title',
|
|
description: 'Work Description',
|
|
notes: JSON.stringify({
|
|
path: 'Work/Dev',
|
|
userNotes: 'Work notes',
|
|
autoTags: [{name: 'Work'}]
|
|
})
|
|
});
|
|
|
|
console.log(' Updated via Work: Work Title');
|
|
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle);
|
|
await Helpers.updateBookmark(bookmark.id, {
|
|
title: 'Personal Title',
|
|
description: 'Personal Description',
|
|
notes: JSON.stringify({
|
|
path: 'Personal/Notes',
|
|
userNotes: 'Personal notes',
|
|
autoTags: [{name: 'Personal'}]
|
|
})
|
|
});
|
|
|
|
console.log(' Updated via Personal: Personal Title');
|
|
|
|
const final = await SessionManager.call(`/api/bookmarks/${bookmark.id}/`);
|
|
const parsed = Helpers.parseNotes(final.notes);
|
|
|
|
console.log('\nFinal state:');
|
|
console.log(` Title: ${final.title}`);
|
|
console.log(` Description: ${final.description}`);
|
|
console.log(` Path: ${parsed.path}`);
|
|
console.log(` User notes: ${parsed.userNotes}`);
|
|
|
|
if (final.title === 'Personal Title') {
|
|
Formatters.consoleResult('Test 4', 'PASS', 'Last-write-wins (Personal took precedence)');
|
|
return { pass: true, strategy: 'last-write-wins', winner: 'personal' };
|
|
} else if (final.title === 'Work Title') {
|
|
Formatters.consoleResult('Test 4', 'PASS', 'Last-write-wins (Work took precedence)');
|
|
return { pass: true, strategy: 'last-write-wins', winner: 'work' };
|
|
} else if (final.title.includes('Work') && final.title.includes('Personal')) {
|
|
Formatters.consoleResult('Test 4', 'PASS', 'Merged title');
|
|
return { pass: true, strategy: 'merge', winner: 'merged' };
|
|
} else {
|
|
Formatters.consoleResult('Test 4', 'WARN', 'Unexpected title value');
|
|
return { pass: null, strategy: 'unknown', winner: final.title };
|
|
}
|
|
} catch (error) {
|
|
Formatters.consoleResult('Test 4', 'FAIL', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function runConflictTests() {
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log(' Conflict Resolution Tests');
|
|
console.log('='.repeat(60));
|
|
|
|
const results = [];
|
|
|
|
try {
|
|
results[0] = await test3_ConflictResolution();
|
|
results[1] = await test4_TitleDescriptionConflict();
|
|
} catch (error) {
|
|
console.error('Test suite error:', error.message);
|
|
await Helpers.resetBookmarks();
|
|
}
|
|
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log(' Conflict Tests Complete');
|
|
console.log('='.repeat(60));
|
|
|
|
return results;
|
|
}
|
|
|
|
// ====================================================================
|
|
// TEST MODULE: DELETION
|
|
// ====================================================================
|
|
|
|
async function test5_DeletePropagation() {
|
|
console.log('\n=== Test 5: Delete Propagation ===');
|
|
console.log('Purpose: Confirm if deleting affects all API keys');
|
|
|
|
try {
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle);
|
|
|
|
const testUrl = 'https://delete-propagation.example.com';
|
|
const workBookmark = await Helpers.createBookmark(testUrl, {
|
|
title: 'Delete Prop Test',
|
|
path: 'Work/Dev'
|
|
});
|
|
|
|
console.log(` Work bookmark ID: ${workBookmark.id}`);
|
|
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle);
|
|
const personalBookmark = await Helpers.createBookmark(testUrl, {
|
|
title: 'Delete Prop Test',
|
|
path: 'Personal/Notes'
|
|
});
|
|
|
|
console.log(` Personal bookmark ID: ${personalBookmark.id}`);
|
|
|
|
console.log(` Same bookmark? ${workBookmark.id === personalBookmark.id}`);
|
|
|
|
await SessionManager.call(`/api/bookmarks/${workBookmark.id}/`, 'DELETE', {});
|
|
console.log(' Deleted via Work key');
|
|
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle);
|
|
const personalList = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 });
|
|
const deleted = personalList.results?.find(b => b.url === testUrl);
|
|
|
|
if (!deleted) {
|
|
Formatters.consoleResult('Test 5', 'FAIL', 'Delete propagated (same bookmark)');
|
|
console.log(' → Deleting via one key deletes all');
|
|
|
|
return { pass: false, propagated: true, sameBookmark: true };
|
|
} else {
|
|
Formatters.consoleResult('Test 5', 'PASS', 'Delete did not propagate (separate bookmarks)');
|
|
console.log(' → Each bookmark exists independently');
|
|
console.log(' → Can delete via specific API key');
|
|
|
|
return { pass: true, propagated: false, sameBookmark: false };
|
|
}
|
|
} catch (error) {
|
|
Formatters.consoleResult('Test 5', 'FAIL', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function test6_DeleteSameUserDifferentKeys() {
|
|
console.log('\n=== Test 6: Delete - Same User, Different Keys ===');
|
|
console.log('Purpose: Verify delete behavior when same user, different API keys');
|
|
|
|
try {
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle);
|
|
|
|
const testUrl = 'https://delete-same-user.example.com';
|
|
const bm1 = await Helpers.createBookmark(testUrl, {
|
|
title: 'Same User Delete Test - Key 1'
|
|
});
|
|
|
|
console.log(` Created with Key 1: ID=${bm1.id}`);
|
|
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle);
|
|
const bm2 = await Helpers.createBookmark(testUrl, {
|
|
title: 'Same User Delete Test - Key 2'
|
|
});
|
|
|
|
console.log(` Created with Key 2: ID=${bm2.id}`);
|
|
|
|
if (bm1.id === bm2.id) {
|
|
Formatters.consoleResult('Test 6', 'WARN', 'Same bookmark - delete propagates');
|
|
console.log(' → Same user means same bookmark');
|
|
console.log(' → Deleting via either key removes it');
|
|
|
|
await Helpers.deleteBookmark(bm1.id);
|
|
|
|
const workList = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 });
|
|
const exists = workList.results?.find(b => b.url === testUrl);
|
|
|
|
if (!exists) {
|
|
console.log(' → Verified: bookmark deleted via both keys');
|
|
}
|
|
|
|
return { pass: true, same: true, propagates: true };
|
|
} else {
|
|
Formatters.consoleResult('Test 6', 'PASS', 'Different bookmarks - delete is isolated');
|
|
console.log(' → Different API keys create different bookmarks');
|
|
console.log(' → Can delete independently');
|
|
|
|
await Helpers.deleteBookmark(bm1.id);
|
|
|
|
const workList = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 });
|
|
const workGone = !workList.results?.find(b => b.url === testUrl);
|
|
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle);
|
|
const personalList = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 });
|
|
const personalExists = personalList.results?.find(b => b.url === testUrl);
|
|
|
|
if (workGone && personalExists) {
|
|
console.log(' → Verified: work deleted, personal still exists');
|
|
}
|
|
|
|
return { pass: true, same: false, propagates: false };
|
|
}
|
|
} catch (error) {
|
|
Formatters.consoleResult('Test 6', 'FAIL', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function runDeletionTests() {
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log(' Deletion Tests');
|
|
console.log('='.repeat(60));
|
|
|
|
const results = [];
|
|
|
|
try {
|
|
results[0] = await test5_DeletePropagation();
|
|
results[1] = await test6_DeleteSameUserDifferentKeys();
|
|
} catch (error) {
|
|
console.error('Test suite error:', error.message);
|
|
await Helpers.resetBookmarks();
|
|
}
|
|
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log(' Deletion Tests Complete');
|
|
console.log('='.repeat(60));
|
|
|
|
return results;
|
|
}
|
|
|
|
// ====================================================================
|
|
// TEST MODULE: BUNDLES
|
|
// ====================================================================
|
|
|
|
async function test7_BundleTagFiltering() {
|
|
console.log('\n=== Test 7: Bundle Tag Filtering ===');
|
|
console.log('Purpose: Verify if bundle tags filter bookmarks properly');
|
|
|
|
try {
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle);
|
|
|
|
const testUrl = 'https://bundle-filter.example.com';
|
|
const bookmark = await Helpers.createBookmark(testUrl, {
|
|
title: 'Bundle Filter Test',
|
|
path: 'Test/Path',
|
|
notes: 'Work bundle tag'
|
|
});
|
|
|
|
console.log(` Bookmark created: ID=${bookmark.id}`);
|
|
|
|
const workBundleResponse = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 });
|
|
|
|
console.log(` Work bundle has ${workBundleResponse.results?.filter(b => b.testId).length || 0} test bookmarks`);
|
|
|
|
if (workBundleResponse.results?.filter(b => b.testId).length > 0) {
|
|
Formatters.consoleResult('Test 7', 'PASS', 'Bundle tags filter bookmarks');
|
|
console.log(' → Work bundle has work-tagged bookmarks');
|
|
|
|
return { pass: true, filtered: true };
|
|
} else {
|
|
Formatters.consoleResult('Test 7', 'WARN', 'Bundle filtering unclear');
|
|
console.log(' → May need to use tags for filtering');
|
|
|
|
return { pass: null, filtered: null };
|
|
}
|
|
} catch (error) {
|
|
Formatters.consoleResult('Test 7', 'FAIL', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function test8_BundleSpecificSync() {
|
|
console.log('\n=== Test 8: Bundle-Specific Sync ===');
|
|
console.log('Purpose: Verify sync behavior with different bundles');
|
|
|
|
try {
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle);
|
|
|
|
const workUrl = 'https://bundle-specific-work.example.com';
|
|
const workBookmark = await Helpers.createBookmark(workUrl, {
|
|
title: 'Bundle Specific - Work',
|
|
path: 'Work/Bundle',
|
|
notes: 'Work bundle content'
|
|
});
|
|
|
|
console.log(` Work bookmark: ID=${workBookmark.id}`);
|
|
|
|
SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle);
|
|
|
|
const personalUrl = 'https://bundle-specific-personal.example.com';
|
|
const personalBookmark = await Helpers.createBookmark(personalUrl, {
|
|
title: 'Bundle Specific - Personal',
|
|
path: 'Personal/Bundle',
|
|
notes: 'Personal bundle content'
|
|
});
|
|
|
|
console.log(` Personal bookmark: ID=${personalBookmark.id}`);
|
|
|
|
const workList = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 });
|
|
const personalList = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 });
|
|
|
|
const workCount = workList.results?.filter(b => b.testId).length || 0;
|
|
const personalCount = personalList.results?.filter(b => b.testId).length || 0;
|
|
|
|
console.log(` Work has ${workCount} test bookmarks`);
|
|
console.log(` Personal has ${personalCount} test bookmarks`);
|
|
|
|
if (workCount === 1 && personalCount === 1) {
|
|
Formatters.consoleResult('Test 8', 'PASS', 'Bundles provide logical separation');
|
|
console.log(' → Can maintain separate sync for each bundle');
|
|
|
|
return { pass: true, workCount, personalCount };
|
|
} else {
|
|
Formatters.consoleResult('Test 8', 'WARN', 'Bundle counts differ from expected');
|
|
console.log(' → May need to use tags for proper isolation');
|
|
|
|
return { pass: null, workCount, personalCount };
|
|
}
|
|
} catch (error) {
|
|
Formatters.consoleResult('Test 8', 'FAIL', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function runBundleTests() {
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log(' Bundle Tests');
|
|
console.log('='.repeat(60));
|
|
|
|
const results = [];
|
|
|
|
try {
|
|
results[0] = await test7_BundleTagFiltering();
|
|
results[1] = await test8_BundleSpecificSync();
|
|
} catch (error) {
|
|
console.error('Test suite error:', error.message);
|
|
await Helpers.resetBookmarks();
|
|
}
|
|
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log(' Bundle Tests Complete');
|
|
console.log('='.repeat(60));
|
|
|
|
return results;
|
|
}
|
|
|
|
// ====================================================================
|
|
// MAIN ORCHESTRATOR
|
|
// ====================================================================
|
|
|
|
async function runAllTests() {
|
|
console.log(''.padEnd(60, '='));
|
|
console.log(' LINKDINGSYNC - Complete Test Suite');
|
|
console.log('='.repeat(60));
|
|
console.log('');
|
|
|
|
const results = [];
|
|
|
|
try {
|
|
results = await runIsolationTests();
|
|
results = results.concat(await runConflictTests());
|
|
results = results.concat(await runDeletionTests());
|
|
results = results.concat(await runBundleTests());
|
|
} catch (error) {
|
|
console.error('Test suite error:', error.message);
|
|
console.log('');
|
|
console.log('[Orchestrator] Attempting cleanup...');
|
|
try {
|
|
await Helpers.resetBookmarks();
|
|
} catch (cleanupError) {
|
|
console.error('Cleanup failed:', cleanupError.message);
|
|
}
|
|
}
|
|
|
|
// Summary
|
|
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 || r.pass === undefined).length;
|
|
|
|
Formatters.consoleHeader('Test Summary');
|
|
console.log(` Total: ${results.length}`);
|
|
console.log(` Passed: ${passed}`);
|
|
console.log(` Failed: ${failed}`);
|
|
console.log(` Warning: ${warnings}`);
|
|
console.log(''.padEnd(60, '='));
|
|
|
|
return results;
|
|
}
|
|
|
|
async function runAllTestsWithReset() {
|
|
console.log(''.padEnd(60, '='));
|
|
console.log(' LINKDINGSYNC - Test Suite with Reset');
|
|
console.log('='.repeat(60));
|
|
|
|
try {
|
|
await Helpers.resetBookmarks();
|
|
console.log('[Reset] Test bookmarks cleaned');
|
|
} catch (error) {
|
|
console.error('[Reset] Failed:', error.message);
|
|
}
|
|
|
|
return await runAllTests();
|
|
}
|
|
|
|
async function reset() {
|
|
console.log('[Orchestrator] Resetting test bookmarks...');
|
|
await Helpers.resetBookmarks();
|
|
console.log('[Orchestrator] Reset complete');
|
|
}
|
|
|
|
// ====================================================================
|
|
// EXPORT TO WINDOW
|
|
// ====================================================================
|
|
|
|
window.LinkdingSyncTests = {
|
|
CONFIG,
|
|
SessionManager,
|
|
Helpers,
|
|
Formatters,
|
|
runAllTests,
|
|
runAllTestsWithReset,
|
|
reset,
|
|
runIsolationTests,
|
|
runConflictTests,
|
|
runDeletionTests,
|
|
runBundleTests,
|
|
test1_SameUserDifferentKeys,
|
|
test2_DifferentUsers,
|
|
test3_ConflictResolution,
|
|
test4_TitleDescriptionConflict,
|
|
test5_DeletePropagation,
|
|
test6_DeleteSameUserDifferentKeys,
|
|
test7_BundleTagFiltering,
|
|
test8_BundleSpecificSync
|
|
};
|
|
|
|
console.log('');
|
|
console.log('LinkdingSync Test Suite loaded successfully');
|
|
console.log('');
|
|
console.log('Commands:');
|
|
console.log(' runAllTests() - Run all tests');
|
|
console.log(' runAllTestsWithReset() - Run with cleanup first');
|
|
console.log(' reset() - Clean up test bookmarks');
|
|
console.log(' runModule("name") - Run specific module');
|
|
console.log('');
|
|
console.log('Test modules:');
|
|
console.log(' isolation - API key & user isolation (Tests 1-2)');
|
|
console.log(' conflicts - Conflict resolution (Tests 3-4)');
|
|
console.log(' deletion - Delete propagation (Tests 5-6)');
|
|
console.log(' bundles - Bundle filtering (Tests 7-8)');
|
|
console.log('');
|
|
})(); |