# Phase 0: Server Behavior Validation Tests ## Overview This document outlines the Phase 0 testing plan to validate Linkding server behavior regarding bookmark isolation, conflict resolution, and API authentication. These tests will inform the final architecture and conflict resolution strategy. ## Prerequisites ### Test Environment Setup You need to create the following on your Linkding instance: 1. **Two User Accounts**: - `user_work` - For work-related bookmarks - `user_personal` - For personal bookmarks 2. **API Keys per User**: - Each user needs at least 2 API keys (one for each bundle) - Example: `user_work_key_1`, `user_work_key_2`, `user_personal_key_1`, `user_personal_key_2` 3. **Two Bundles per User**: - `work` bundle (for work bookmarks) - `personal` bundle (for personal bookmarks) ### Firefox Test Harness We'll create a Firefox extension (or use existing Linkding extension) that: - Allows testing with multiple API keys - Can switch between work/personal contexts - Runs tests via DevTools console or automated scripts --- ## Test Scenarios ### Scenario 1: Same URL, Different API Keys, Same User **Purpose**: Verify if API keys provide bookmark isolation within the same user. **Setup**: 1. Create bookmark via `user_work_key_1` with URL: `https://example.com` 2. Create same bookmark via `user_work_key_2` with same URL **Expected Behavior**: - If API keys only authenticate the user, both calls create/update the same bookmark - API keys don't provide isolation - only user matters **Test Command**: ```javascript // Test 1: Same user, different API keys async function testSameUserDifferentKeys(serverUrl, apiKey1, apiKey2) { try { // Create with first key const response1 = await fetch(`${serverUrl}api/bookmarks/`, { method: 'POST', headers: { 'Authorization': `Token ${apiKey1}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://example.com', title: 'Test Bookmark 1', description: 'First key', notes: '{"path": "Work/Dev1", "userNotes": "Key 1"}' }) }); const bookmark1 = await response1.json(); console.log('Bookmark 1:', bookmark1); // Try creating same URL with second key const response2 = await fetch(`${serverUrl}api/bookmarks/`, { method: 'POST', headers: { 'Authorization': `Token ${apiKey2}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://example.com', title: 'Test Bookmark 2', description: 'Second key', notes: '{"path": "Work/Dev2", "userNotes": "Key 2"}' }) }); const bookmark2 = await response2.json(); console.log('Bookmark 2:', bookmark2); // Check if they're the same bookmark (same ID) if (bookmark1.id === bookmark2.id) { console.log('RESULT: Same bookmark (API keys do not isolate)'); } else { console.log('RESULT: Different bookmarks (API keys provide isolation)'); } } catch (error) { console.error('Error:', error.message); } } ``` --- ### Scenario 2: Same URL, Different Users **Purpose**: Verify if different users can see each other's bookmarks without sharing enabled. **Setup**: 1. User A creates bookmark via their API key 2. User B tries to retrieve same URL via their API key **Expected Behavior**: - Without sharing: User B should get 404 or empty results - With sharing enabled: Both can see each other's bookmarks **Test Command**: ```javascript // Test 2: Different users, no sharing async function testDifferentUsersNoSharing(serverUrl, userAKey, userBKey) { try { // Create bookmark as User A const createResponse = await fetch(`${serverUrl}api/bookmarks/`, { method: 'POST', headers: { 'Authorization': `Token ${userAKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://shared-example.com', title: 'Shared Bookmark', description: 'Created by User A', notes: '{"path": "UserA/Shared"}' }) }); const bookmark = await createResponse.json(); console.log('Bookmark created by User A:', bookmark.id); // User B tries to get this bookmark const getResponse = await fetch(`${serverUrl}api/bookmarks/?all=Shared`, { headers: { 'Authorization': `Token ${userBKey}`, } }); if (getResponse.ok) { const result = await getResponse.json(); console.log(`User B sees ${result.count} bookmarks`); if (result.results && result.results.length > 0) { console.log('RESULT: User B can see User A\'s bookmark (sharing or same user)'); } else { console.log('RESULT: User B cannot see User A\'s bookmark (proper isolation)'); } } else { console.log('RESULT: User B cannot access (4xx/5xx error)'); } } catch (error) { console.error('Error:', error.message); } } ``` --- ### Scenario 3: Conflict Resolution - Different Folders/Paths **Purpose**: Determine how the server handles same URL in different paths. **Setup**: 1. Create bookmark as "Work" with path "Work/Development" 2. Create same URL as "Personal" with path "Personal/Notes" **Expected Behavior**: - Need to confirm if server creates duplicate or updates existing **Test Command**: ```javascript // Test 3: Conflict resolution with different paths async function testConflictResolution(serverUrl, workKey, personalKey) { const url = 'https://conflict-test.example.com'; const pathWork = 'Work/Development'; const pathPersonal = 'Personal/Notes'; try { // Create as Work const workBookmark = await createBookmark(serverUrl, workKey, url, { title: 'Conflict Test', description: 'Work version', notes: JSON.stringify({ path: pathWork, userNotes: 'Work Development Notes', autoTags: [{name: 'Work'}] }) }); console.log('Work bookmark created:', workBookmark.id); // Create same URL as Personal const personalBookmark = await createBookmark(serverUrl, personalKey, url, { title: 'Conflict Test', description: 'Personal version', notes: JSON.stringify({ path: pathPersonal, userNotes: 'Personal Notes', autoTags: [{name: 'Personal'}] }) }); console.log('Personal bookmark created:', personalBookmark.id); // Check IDs if (workBookmark.id === personalBookmark.id) { console.log('RESULT: Same bookmark ID - paths are merged or overwritten'); // Fetch and show current state const fetchResponse = await fetch(`${serverUrl}api/bookmarks/${workBookmark.id}/`, { headers: { 'Authorization': `Token ${workKey}`, } }); const current = await fetchResponse.json(); console.log('Current bookmark state:', current); } else { console.log('RESULT: Different bookmark IDs - server creates duplicates'); console.log('Work ID:', workBookmark.id); console.log('Personal ID:', personalBookmark.id); } } catch (error) { console.error('Error:', error.message); } } ``` --- ### Scenario 4: Title/Description Conflict **Purpose**: Understand conflict resolution for title/description fields. **Setup**: 1. Update bookmark via Work context (title: "Work Title") 2. Update same bookmark via Personal context (title: "Personal Title") 3. Check final state **Expected Behavior**: - Confirm last-write-wins or other resolution strategy **Test Command**: ```javascript // Test 4: Title/Description conflict resolution async function testTitleConflict(serverUrl, workKey, personalKey) { const url = 'https://title-conflict.example.com'; try { // Create initial bookmark const bookmark = await createBookmark(serverUrl, workKey, url, { title: 'Initial Title', description: 'Initial Description', notes: JSON.stringify({ path: 'Work/Initial', userNotes: 'Initial' }) }); // Update via Work await updateBookmark(serverUrl, workKey, 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'); // Update via Personal await updateBookmark(serverUrl, personalKey, 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'); // Fetch final state const final = await fetchBookmark(serverUrl, workKey, bookmark.id); console.log('Final state:', { title: final.title, description: final.description, notes: JSON.parse(final.notes) }); if (final.title === 'Personal Title') { console.log('RESULT: Last-write-wins (Personal update took precedence)'); } else if (final.title === 'Work Title') { console.log('RESULT: Last-write-wins (Work update took precedence)'); } else if (final.title.includes('Work') && final.title.includes('Personal')) { console.log('RESULT: Merged title'); } } catch (error) { console.error('Error:', error.message); } } ``` --- ### Scenario 5: Delete Propagation **Purpose**: Confirm if deleting a bookmark affects all API keys. **Setup**: 1. Create bookmark via multiple API keys (same URL) 2. Delete via one API key 3. Check if bookmark still exists via other keys **Expected Behavior**: - If API keys create same bookmark, deletion should propagate - If API keys create separate bookmarks, deletion only affects one **Test Command**: ```javascript // Test 5: Delete propagation async function testDeletePropagation(serverUrl, workKey, personalKey) { const url = 'https://delete-test.example.com'; try { // Create via both keys const workBookmark = await createBookmark(serverUrl, workKey, url, { title: 'Delete Test', notes: JSON.stringify({path: 'Work/Dev'}) }); console.log('Work bookmark ID:', workBookmark.id); const personalBookmark = await createBookmark(serverUrl, personalKey, url, { title: 'Delete Test', notes: JSON.stringify({path: 'Personal/Notes'}) }); console.log('Personal bookmark ID:', personalBookmark.id); // Check if same bookmark const sameBookmark = workBookmark.id === personalBookmark.id; // Delete via Work key const deleteResponse = await fetch(`${serverUrl}api/bookmarks/${workBookmark.id}/`, { method: 'DELETE', headers: { 'Authorization': `Token ${workKey}`, } }); console.log('Delete response status:', deleteResponse.status); // Try to fetch via Personal key const fetchPersonal = await fetch(`${serverUrl}api/bookmarks/?limit=100`, { headers: { 'Authorization': `Token ${personalKey}`, } }); if (fetchPersonal.ok) { const personalResults = await fetchPersonal.json(); const deleted = personalResults.results.find(b => b.url === url); if (!deleted) { console.log('RESULT: Delete propagated (same bookmark)'); } else { console.log('RESULT: Delete did not propagate (separate bookmarks)'); } } } catch (error) { console.error('Error:', error.message); } } ``` --- ### Scenario 6: Bundle Tag Isolation **Purpose**: Verify if different bundle tags properly isolate bookmarks. **Setup**: 1. Create bookmark with bundle tag "work-bundle" 2. Create bookmark with bundle tag "personal-bundle" 3. Query each bundle separately **Expected Behavior**: - Bundle tags should act as filters - Need to confirm if tags create separate bookmarks or filter existing ones **Test Command**: ```javascript // Test 6: Bundle tag isolation async function testBundleIsolation(serverUrl, workKey, personalKey) { try { // Create bundle tag bookmark const bookmark = await createBookmark(serverUrl, workKey, 'https://bundle-test.com', { title: 'Bundle Test', notes: JSON.stringify({ path: 'Test/Path', userNotes: 'Initial', bundleTag: 'work-bundle', autoTags: [{name: 'work-bundle'}] }) }); console.log('Bookmark created:', bookmark.id); // Query by bundle tag const workBundleResponse = await fetch(`${serverUrl}api/bookmarks/?all=work-bundle`, { headers: { 'Authorization': `Token ${workKey}`, } }); const personalBundleResponse = await fetch(`${serverUrl}api/bookmarks/?all=personal-bundle`, { headers: { 'Authorization': `Token ${workKey}`, } }); const workCount = await workBundleResponse.json(); const personalCount = await personalBundleResponse.json(); console.log(`Work bundle has ${workCount.count || workCount.results?.length || 0} bookmarks`); console.log(`Personal bundle has ${personalCount.count || personalCount.results?.length || 0} bookmarks`); if (workCount.count === 1 && personalCount.count === 0) { console.log('RESULT: Bundle tags provide proper isolation'); } else { console.log('RESULT: Bundle tags may not provide isolation'); } } catch (error) { console.error('Error:', error.message); } } ``` --- ## Session Management for Automated Tests ### Challenge: Authentication Context Switching When running automated tests, we need to manage switching between: - Different API keys - Different users (if applicable) - Different bundle contexts ### Solution: Context Switching Function ```javascript // Session management for tests const SessionManager = { // Store active context context: { serverUrl: '', currentApiKey: '', currentUserId: null, currentBundle: null }, // Set new context setContext(serverUrl, apiKey, userId = null, bundle = null) { console.log(`Switching context: ${apiKey.substring(0, 8)}...`); this.context.serverUrl = serverUrl; this.context.currentApiKey = apiKey; this.context.currentUserId = userId; this.context.currentBundle = bundle; // Return auth headers for this context return { headers: { 'Authorization': `Token ${apiKey}` } }; }, // Make API call with current context async call(endpoint, method = 'GET', body = null) { const headers = { ...this.getHeaders(), 'Content-Type': 'application/json' }; if (body) headers['Content-Type'] = 'application/json'; const response = await fetch(`${this.context.serverUrl}${endpoint}`, { method, headers, body: body ? JSON.stringify(body) : null }); if (!response.ok) { throw new Error(`${response.status}: ${response.statusText}`); } return await response.json(); }, // Get headers for current context getHeaders() { return { 'Authorization': `Token ${this.context.currentApiKey}` }; }, // Reset to default context reset() { this.context = { serverUrl: '', currentApiKey: '', currentUserId: null, currentBundle: null }; console.log('Session reset'); } }; // Usage example: // SessionManager.setContext('https://links.blabber1565.com', 'work-api-key-123'); // const bookmark = await SessionManager.call('/api/bookmarks/', 'POST', {url: '...'}); ``` --- ## Test Execution Script Create a file: `test-runner.js` in the LinkdingSync directory: ```javascript /* * Test Runner for Linkding Server Behavior Validation * * Instructions: * 1. Load this file into Firefox DevTools console * 2. Enter your test configuration * 3. Run test suite */ // Configuration const CONFIG = { serverUrl: 'https://links.blabber1565.com', workApiKey: '', // Fill in your work API key personalApiKey: '', // Fill in your personal API key workBundle: 'work-bundle', personalBundle: 'personal-bundle' }; // Import SessionManager // (paste SessionManager code from above) // Test Suite async function runTestSuite() { console.log('=== Linkding Server Behavior Test Suite ===\n'); try { // Setup context SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, 'user_work', CONFIG.workBundle); // Run tests await testSameUserDifferentKeys(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.personalApiKey); console.log('\n--- Test 1 Complete ---\n'); await testDifferentUsersNoSharing(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.personalApiKey); console.log('\n--- Test 2 Complete ---\n'); await testConflictResolution(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.personalApiKey); console.log('\n--- Test 3 Complete ---\n'); await testTitleConflict(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.personalApiKey); console.log('\n--- Test 4 Complete ---\n'); await testDeletePropagation(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.personalApiKey); console.log('\n--- Test 5 Complete ---\n'); await testBundleIsolation(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.personalApiKey); console.log('\n--- Test 6 Complete ---\n'); console.log('=== All Tests Complete ==='); } catch (error) { console.error('Test suite failed:', error.message); } } // Initialize console.log('Type: runTestSuite() to execute all tests'); console.log('Type: runTestSuite("scenario") to run specific scenario'); ``` --- ## Expected Outcomes & Decision Points After running Phase 0 tests, we'll make decisions based on: ### Decision Point 1: API Key Isolation - **If same user**: API keys don't provide isolation → Use separate users or different accounts - **If different users**: Confirm sharing settings needed ### Decision Point 2: Conflict Resolution Strategy - **If last-write-wins**: May cause toggle issues between browsers - **If merge**: Determine merge strategy (path, title, etc.) - **If duplicates**: Accept separate bookmarks per context ### Decision Point 3: Bundle Isolation - **If tags isolate**: Use bundle tags for isolation - **If not**: Need user accounts for isolation --- ## Next Steps 1. **Create Test Accounts**: You create the 2 user accounts with API keys 2. **Configure Test Harness**: Fill in CONFIG with API keys 3. **Run Tests**: Execute `runTestSuite()` in Firefox console 4. **Analyze Results**: Review test outputs 5. **Finalize Design**: Update architecture based on findings --- ## Notes - Tests are idempotent - they clean up test bookmarks after execution - All test bookmarks are tagged with "test-" prefix for easy identification - Test results will inform the final redesign of LinkdingSync extension