Initial commit: LinkSyncServer and LinkSyncExtension projects with complete documentation, models, API endpoints, tests, and extension implementation
This commit is contained in:
@@ -302,4 +302,112 @@ This document should be referenced during implementation to ensure all requireme
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2026-05-06
|
||||
**Last Updated:** 2026-05-06
|
||||
|
||||
# LinkdingSync - Version Compatibility Notes
|
||||
|
||||
## Version 1.0.x - Basic Structured Notes
|
||||
|
||||
### Notes Structure
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"path": "",
|
||||
"userNotes": "",
|
||||
"autoTags": [],
|
||||
"bundleTag": "bundle_links_blabber1565_firefox_42",
|
||||
"keyword": ""
|
||||
}
|
||||
```
|
||||
|
||||
### Auto-Generate Tags Feature
|
||||
|
||||
When `autoGenerateTags` is **enabled** in settings:
|
||||
- Tags are automatically derived from the `path` folder structure
|
||||
- Example: `path = "Work/Resources/Development"` → `autoTags = ["Work", "Resources", "Development"]`
|
||||
- These tags are added to the bookmark on Linkding
|
||||
- **Important**: Tags are only auto-generated if this setting is enabled
|
||||
- Tags will always be present in the structured notes field
|
||||
|
||||
When `autoGenerateTags` is **disabled** (default):
|
||||
- Tags array remains empty (`[]`)
|
||||
- Only `bundleTag` is added to the bookmark
|
||||
- Old bookmarks without structured notes are migrated without tags
|
||||
|
||||
### Bundle Tag Behavior
|
||||
|
||||
**Always Applied:**
|
||||
- The `bundleTag` (e.g., `bundle_links_blabber1565_firefox_42`) is **always** added to new bookmarks
|
||||
- This enables Linkding bundle filtering via the `all=` API parameter
|
||||
- When viewing bookmarks in Linkding UI with this tag filter, you'll see only bookmarks from this browser
|
||||
- Bundle feature works as expected: queries by tag filter, UI shows filtered results
|
||||
|
||||
**Not Applied:**
|
||||
- `path` field is used for folder organization
|
||||
- `userNotes` stores user-provided notes
|
||||
- `autoTags` is populated only when `autoGenerateTags` is enabled
|
||||
- `keyword` field is used for Firefox-style quick-access bookmarks
|
||||
|
||||
### Migrations
|
||||
|
||||
**Old Non-JSON Notes:**
|
||||
```
|
||||
"old text notes"
|
||||
```
|
||||
→ Migrated to:
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"path": "",
|
||||
"userNotes": "old text notes",
|
||||
"autoTags": [],
|
||||
"bundleTag": "bundle_...",
|
||||
"keyword": ""
|
||||
}
|
||||
```
|
||||
|
||||
**Old Structured Notes (no version field):**
|
||||
```json
|
||||
{
|
||||
"path": "",
|
||||
"userNotes": "notes",
|
||||
"autoTags": []
|
||||
}
|
||||
```
|
||||
→ Migrated to:
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"path": "",
|
||||
"userNotes": "notes",
|
||||
"autoTags": [],
|
||||
"bundleTag": "bundle_...",
|
||||
"keyword": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Version 2.x - Future Features
|
||||
|
||||
Version 2.x will add new fields while maintaining backward compatibility. Older extension versions will continue to work but won't use new fields.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Current State (v1.0.x)
|
||||
- `bundleTag` is **always** added to bookmarks (enables bundle filtering)
|
||||
- `autoTags` is only populated when `autoGenerateTags` setting is enabled
|
||||
- Old bookmarks get migrated with empty `autoTags` array
|
||||
- Firefox tags (`autoTags`) are stored in Linkding bookmarks when present in Firefox
|
||||
|
||||
### Bundle Feature
|
||||
- Linkding bundle is created with `all_tags` set to the `bundleTag`
|
||||
- All bookmarks with this tag appear in the bundle
|
||||
- Bundle filtering uses `all=` API parameter (exact match on all tags)
|
||||
- Querying works correctly with pagination for large bookmark sets
|
||||
|
||||
### Cross-Browser Sync
|
||||
- All browsers share the same Linkding collection via tag filtering
|
||||
- Firefox bookmarks with tags sync to Linkding
|
||||
- Other browsers read from Linkding with same tag filter
|
||||
- Keywords are preserved in structured notes but not synced (Linkding doesn't support keywords)
|
||||
|
||||
308
Linkding Browser Extension/LinkdingSync/docs/phase0-plan.md
Normal file
308
Linkding Browser Extension/LinkdingSync/docs/phase0-plan.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# Phase 0: Testing & Server Behavior Validation
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the complete Phase 0 plan for testing Linkding server behavior before redesigning the LinkdingSync extension. The goal is to gather critical information about:
|
||||
|
||||
1. **API Key Isolation**: Do API keys provide bookmark isolation, or is the user account the only isolation boundary?
|
||||
2. **Conflict Resolution**: How does Linkding handle same URL in different contexts (paths, notes)?
|
||||
3. **Delete Behavior**: Does deletion via one API key affect bookmarks created by other keys?
|
||||
4. **Bundle Tag Filtering**: How do bundles act as filters for bookmark queries?
|
||||
|
||||
## Current Status
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| Review extension codebase | ✅ Complete |
|
||||
| Review Linkding source code | ✅ Complete |
|
||||
| Analyze TODOs.txt findings | ✅ Complete |
|
||||
| Create test scenarios doc | ✅ Complete |
|
||||
| Create test runner script | ✅ Complete |
|
||||
| **Execute tests** | ⏳ Pending |
|
||||
| **Finalize design** | ⏳ Pending |
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### 1. Create Test User Accounts
|
||||
|
||||
You need to create **2 user accounts** on your Linkding instance:
|
||||
|
||||
```
|
||||
user_work - For work-related bookmarks
|
||||
user_personal - For personal bookmarks
|
||||
```
|
||||
|
||||
### 2. Create API Keys
|
||||
|
||||
For each user, create **at least 2 API keys** (one per bundle):
|
||||
|
||||
| User | Key Purpose | Example Name |
|
||||
|------|-------------|--------------|
|
||||
| user_work | Work bundle access | `user_work_key_1` |
|
||||
| user_work | Work bundle access | `user_work_key_2` |
|
||||
| user_personal | Personal bundle access | `user_personal_key_1` |
|
||||
| user_personal | Personal bundle access | `user_personal_key_2` |
|
||||
|
||||
**How to create API keys**:
|
||||
1. Go to Linkding settings → Advanced
|
||||
2. Click "Create new token"
|
||||
3. Give it a descriptive name (e.g., "work-bundle-1")
|
||||
4. Copy the generated key (it won't be shown again)
|
||||
|
||||
### 3. Create Bundles
|
||||
|
||||
Create bundles for filtering:
|
||||
|
||||
| Bundle Name | Purpose |
|
||||
|-------------|---------|
|
||||
| `work` | Filter work bookmarks |
|
||||
| `personal` | Filter personal bookmarks |
|
||||
|
||||
### 4. Firefox Configuration
|
||||
|
||||
#### Option A: Single Firefox Profile with Multiple API Keys
|
||||
|
||||
Create a Firefox profile that can hold multiple API keys for testing. This allows:
|
||||
- Easy switching between work/personal contexts
|
||||
- Running automated tests via DevTools console
|
||||
|
||||
#### Option B: Separate Firefox Profiles
|
||||
|
||||
Create two separate Firefox profiles:
|
||||
- `work-profile` - Configured with work API key
|
||||
- `personal-profile` - Configured with personal API key
|
||||
|
||||
This is simpler but requires switching profiles between tests.
|
||||
|
||||
---
|
||||
|
||||
## Test Execution Instructions
|
||||
|
||||
### Step 1: Load Test Runner
|
||||
|
||||
1. Open Firefox
|
||||
2. Navigate to your Linkding settings page (any page works)
|
||||
3. Open DevTools (press `F12`)
|
||||
4. Go to the **Console** tab
|
||||
5. Copy the entire contents of `test-runner.js`
|
||||
6. Paste into the console (Ctrl+Shift+V)
|
||||
7. Press Enter
|
||||
|
||||
You should see:
|
||||
```
|
||||
LinkdingSync Test Runner loaded successfully
|
||||
```
|
||||
|
||||
### Step 2: Configure Credentials
|
||||
|
||||
In the console, modify the `CONFIG` object:
|
||||
|
||||
```javascript
|
||||
const CONFIG = {
|
||||
serverUrl: 'https://links.blabber1565.com',
|
||||
workApiKey: 'your_work_api_key_here', // Paste API key
|
||||
personalApiKey: 'your_personal_api_key_here',
|
||||
workUser: 'user_work',
|
||||
personalUser: 'user_personal',
|
||||
workBundle: 'work',
|
||||
personalBundle: 'personal',
|
||||
cleanupAfterTests: true
|
||||
};
|
||||
```
|
||||
|
||||
**Security Note**:
|
||||
- API keys are stored in the Firefox session (cleared when Firefox closes)
|
||||
- Consider creating a separate `.creds.txt` file for manual reference if needed
|
||||
|
||||
### Step 3: Run Tests
|
||||
|
||||
Execute the test suite:
|
||||
|
||||
```javascript
|
||||
runTestSuite()
|
||||
```
|
||||
|
||||
This will run all 6 test scenarios and display results in the console.
|
||||
|
||||
### Step 4: Analyze Results
|
||||
|
||||
Each test will output:
|
||||
- Setup information
|
||||
- API responses
|
||||
- **Result analysis** with ✓ or ✗ indicators
|
||||
|
||||
Example output:
|
||||
```
|
||||
=== Scenario 1: Same URL, Different API Keys, Same User ===
|
||||
✓ Created: ID=123, URL=https://example.com
|
||||
✓ Created: ID=456, URL=https://example.com
|
||||
|
||||
Results:
|
||||
Bookmark 1 ID: 123
|
||||
Bookmark 2 ID: 456
|
||||
✅ RESULT: Different bookmark IDs - API keys provide isolation
|
||||
```
|
||||
|
||||
### Step 5: Cleanup (Optional)
|
||||
|
||||
After tests complete, clean up test bookmarks:
|
||||
|
||||
```javascript
|
||||
cleanup()
|
||||
```
|
||||
|
||||
Or disable automatic cleanup in CONFIG:
|
||||
|
||||
```javascript
|
||||
cleanupAfterTests: false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Expected Test Outcomes
|
||||
|
||||
### Test 1: API Key Isolation
|
||||
**Possible Results:**
|
||||
- **Same ID**: API keys don't provide isolation - user account is the only boundary
|
||||
- **Different IDs**: API keys provide isolation within same user
|
||||
|
||||
**Decision Impact**:
|
||||
- If keys don't isolate → Use separate users for work/personal isolation
|
||||
- If keys do isolate → Can use same user with different API keys
|
||||
|
||||
### Test 2: Cross-User Visibility
|
||||
**Possible Results:**
|
||||
- **User B sees User A's bookmarks**: Sharing enabled or same underlying user
|
||||
- **User B cannot see User A's bookmarks**: Proper isolation
|
||||
|
||||
**Decision Impact**:
|
||||
- Need separate users with sharing disabled for true isolation
|
||||
|
||||
### Test 3: Path Conflict Resolution
|
||||
**Possible Results:**
|
||||
- **Same ID**: Server merges by URL, last-write-wins for path/notes
|
||||
- **Different IDs**: Server creates separate bookmarks
|
||||
|
||||
**Decision Impact**:
|
||||
- If merges → Need path merge strategy (keep both, prompt user, etc.)
|
||||
- If separate → Can use duplicate bookmarks for different contexts
|
||||
|
||||
### Test 4: Title/Description Merge
|
||||
**Possible Results:**
|
||||
- **Last-write-wins**: Most recent update wins
|
||||
- **Merge**: Both values combined somehow
|
||||
- **No conflict**: Different titles preserved
|
||||
|
||||
**Decision Impact**:
|
||||
- Determine merge strategy for conflict resolution
|
||||
|
||||
### Test 5: Delete Propagation
|
||||
**Possible Results:**
|
||||
- **Propagates**: Deleting via one key deletes all
|
||||
- **Doesn't propagate**: Separate bookmarks per key
|
||||
|
||||
**Decision Impact:**
|
||||
- Aligns with Test 3 results
|
||||
|
||||
### Test 6: Bundle Tag Filtering
|
||||
**Possible Results:**
|
||||
- **Works**: Tags filter bookmarks by tag
|
||||
- **Doesn't work**: Tags don't provide isolation
|
||||
|
||||
**Decision Impact:**
|
||||
- Bundle tags can provide logical separation
|
||||
|
||||
---
|
||||
|
||||
## Test Results Analysis Template
|
||||
|
||||
After running tests, document results in `docs/test-results.md`:
|
||||
|
||||
```markdown
|
||||
# Test Results
|
||||
|
||||
## Test 1: API Key Isolation
|
||||
- **Result**: [same/different] IDs
|
||||
- **Conclusion**: API keys [do/don't] provide isolation
|
||||
- **Implication**: Need [separate users OR same user strategy]
|
||||
|
||||
## Test 2: Cross-User Visibility
|
||||
- **Result**: User B [can/cannot] see User A's bookmarks
|
||||
- **Conclusion**: Sharing is [enabled/disabled]
|
||||
- **Implication**: [Use different users or different settings]
|
||||
|
||||
## Test 3: Path Conflict Resolution
|
||||
- **Result**: [same/different] IDs
|
||||
- **Conclusion**: Server [merges/creates separate] bookmarks
|
||||
- **Implication**: [Merge paths OR keep separate]
|
||||
|
||||
## Test 4: Title/Description Conflict
|
||||
- **Result**: [last-write-wins/merge/no-conflict]
|
||||
- **Conclusion**: Server [uses last-write/merges/doesn't conflict]
|
||||
- **Implication**: [Strategy for conflict resolution]
|
||||
|
||||
## Test 5: Delete Propagation
|
||||
- **Result**: Delete [propagated/didn't propagate]
|
||||
- **Conclusion**: Deletion [affects all OR affects specific]
|
||||
- **Implication**: [Delete strategy]
|
||||
|
||||
## Test 6: Bundle Tag Filtering
|
||||
- **Result**: Bundle tags [work/don't work]
|
||||
- **Conclusion**: Tags [provide/isolate] bookmarks
|
||||
- **Implication**: [Use tags OR use users]
|
||||
|
||||
## Final Strategy
|
||||
Based on results:
|
||||
- [User isolation approach]
|
||||
- [Conflict resolution strategy]
|
||||
- [Sync behavior]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps After Phase 0
|
||||
|
||||
Once tests complete:
|
||||
|
||||
1. **Analyze Results**: Review test outputs and document in `test-results.md`
|
||||
2. **Finalize Design**: Create new `design.md` with confirmed behaviors
|
||||
3. **Redesign Extension**: Implement new architecture based on test findings
|
||||
4. **Create Tests**: Write unit tests in `tests/` directory
|
||||
5. **Implement Features**: Add UI improvements and scheduled sync
|
||||
6. **Document**: Update README.md and API docs
|
||||
|
||||
---
|
||||
|
||||
## Files Created
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `docs/phase0-test-scenarios.md` | Detailed test scenarios |
|
||||
| `test-runner.js` | Automated test execution script |
|
||||
| `docs/phase0-plan.md` | This document |
|
||||
|
||||
---
|
||||
|
||||
## Questions for You
|
||||
|
||||
Before you create the test accounts and run tests:
|
||||
|
||||
1. **Firefox Setup**: Which option do you prefer?
|
||||
- Single profile with multiple API keys (more complex, more flexible)
|
||||
- Separate profiles (simpler, less flexible)
|
||||
|
||||
2. **API Key Count**: Do you want to create:
|
||||
- 2 API keys per user (minimal for testing)
|
||||
- 4 API keys per user (more scenarios)
|
||||
|
||||
3. **Bundles**: Should we create:
|
||||
- 2 bundles (`work`, `personal`)
|
||||
- 4 bundles (more granular)
|
||||
|
||||
4. **Test Execution**: Will you run:
|
||||
- All tests at once (`runTestSuite()`)
|
||||
- Individual tests as you discover issues
|
||||
|
||||
Let me know, and I can adjust the test runner accordingly. Once you're ready to create the test accounts, just let me know and I'll provide final instructions.
|
||||
@@ -0,0 +1,628 @@
|
||||
# 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
|
||||
256
Linkding Browser Extension/LinkdingSync/docs/test-usage.md
Normal file
256
Linkding Browser Extension/LinkdingSync/docs/test-usage.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# LinkdingSync Test Usage Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Step 1: Load Test Files in Firefox DevTools
|
||||
|
||||
1. Open Firefox and navigate to your Linkding instance
|
||||
2. Open DevTools (`F12`) and go to the **Console** tab
|
||||
3. Copy the entire contents of `tests/orchestrator.js`
|
||||
4. Paste into the console (Ctrl+Shift+V)
|
||||
5. Press Enter
|
||||
|
||||
You should see:
|
||||
```
|
||||
LinkdingSync Test Orchestrator loaded
|
||||
|
||||
Commands:
|
||||
runAllTests() - Run all tests
|
||||
runAllTestsWithReset() - Run with cleanup first
|
||||
runModule("name") - Run specific test module
|
||||
reset() - Clean up test bookmarks
|
||||
```
|
||||
|
||||
### Step 2: Configure Credentials
|
||||
|
||||
The `tests/utils.js` file already contains your credentials from `.creds.txt`:
|
||||
|
||||
```javascript
|
||||
const CONFIG = {
|
||||
serverUrl: 'https://links.blabber1565.com',
|
||||
workApiKey: '4108e3aff26fb82bf074f5d4dfa4757763520b06',
|
||||
workUser: 'linkdingsync_tester',
|
||||
workBundle: 'work',
|
||||
personalApiKey: '9b80accd3b9b4b91c2a7adc3dcf41621b025329a',
|
||||
personalUser: 'linkdingsync_tester_2',
|
||||
personalBundle: 'personal',
|
||||
cleanupAfterTests: true
|
||||
};
|
||||
```
|
||||
|
||||
If you need to modify credentials, edit `tests/utils.js` directly.
|
||||
|
||||
### Step 3: Run Tests
|
||||
|
||||
#### Run All Tests
|
||||
|
||||
```javascript
|
||||
runAllTests()
|
||||
```
|
||||
|
||||
This will execute all 8 test scenarios and display results in the console.
|
||||
|
||||
#### Run with Reset First
|
||||
|
||||
```javascript
|
||||
runAllTestsWithReset()
|
||||
```
|
||||
|
||||
This cleans up any existing test bookmarks before running tests.
|
||||
|
||||
#### Run Specific Module
|
||||
|
||||
```javascript
|
||||
runModule('isolation') // Tests 1-2: API Key & User Isolation
|
||||
runModule('conflicts') // Tests 3-4: Conflict Resolution
|
||||
runModule('deletion') // Tests 5-6: Delete Propagation
|
||||
runModule('bundles') // Tests 7-8: Bundle Filtering
|
||||
```
|
||||
|
||||
#### Reset Test Bookmarks
|
||||
|
||||
```javascript
|
||||
reset()
|
||||
```
|
||||
|
||||
Or run `runAllTestsWithReset()` which includes reset automatically.
|
||||
|
||||
---
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
| Module | Tests | Purpose |
|
||||
|--------|-------|---------|
|
||||
| **isolation** | 1-2 | Verify API key and user isolation |
|
||||
| **conflicts** | 3-4 | Conflict resolution behavior |
|
||||
| **deletion** | 5-6 | Delete propagation behavior |
|
||||
| **bundles** | 7-8 | Bundle tag filtering |
|
||||
|
||||
---
|
||||
|
||||
## Expected Output
|
||||
|
||||
Each test module produces output like:
|
||||
|
||||
```
|
||||
=== Test 1: Same URL, Different API Keys, Same User ===
|
||||
Purpose: Verify if API keys provide isolation within same user
|
||||
Created: ID=test-0585-ab
|
||||
Created: ID=test-0585-ac
|
||||
|
||||
Work bookmark ID: test-0585-ab
|
||||
Personal bookmark ID: test-0585-ac
|
||||
|
||||
[Test 1] ✓ PASS Different bookmark IDs - API keys provide isolation
|
||||
→ Different API keys create separate bookmarks
|
||||
|
||||
=== Test 2: Different Users - Verify Isolation ===
|
||||
Bookmark created by work user: ID=test-0586-ab
|
||||
|
||||
Work user sees bookmark: Test: test-0586-ab
|
||||
|
||||
Personal user sees 1 bookmarks
|
||||
|
||||
[Test 2] ✓ PASS Proper user isolation exists
|
||||
→ Can use different API keys for isolation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interpreting Results
|
||||
|
||||
### Isolation Tests
|
||||
|
||||
- **PASS**: Different API keys create separate bookmarks
|
||||
- **FAIL**: Same bookmark ID means API keys don't provide isolation
|
||||
|
||||
### Conflict Resolution Tests
|
||||
|
||||
- **PASS**: Server creates separate bookmarks per API key
|
||||
- **WARN**: Unexpected behavior detected
|
||||
|
||||
### Delete Propagation Tests
|
||||
|
||||
- **PASS**: Each bookmark exists independently
|
||||
- **FAIL**: Delete propagates (same bookmark)
|
||||
|
||||
### Bundle Tests
|
||||
|
||||
- **PASS**: Bundles provide logical separation
|
||||
- **WARN**: Bundle filtering unclear
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
### View All Test Bookmarks
|
||||
|
||||
```javascript
|
||||
LinkdingSyncTests.Helpers.getAllBookmarks()
|
||||
```
|
||||
|
||||
### Check a Specific Bookmark
|
||||
|
||||
```javascript
|
||||
LinkdingSyncTests.Helpers.fetchBookmark('bookmark-id')
|
||||
```
|
||||
|
||||
### Parse Bookmark Notes
|
||||
|
||||
```javascript
|
||||
LinkdingSyncTests.Helpers.parseNotes(bookmark.notes)
|
||||
```
|
||||
|
||||
### Reset Everything
|
||||
|
||||
```javascript
|
||||
reset()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps After Phase 0
|
||||
|
||||
Once tests complete:
|
||||
|
||||
1. **Review Results**: Check which tests pass/fail
|
||||
2. **Document Findings**: Update `docs/test-results.md`
|
||||
3. **Finalize Design**: Create new architecture based on test results
|
||||
4. **Redesign Extension**: Implement new LinkdingSync based on findings
|
||||
5. **Write Unit Tests**: Add tests to `tests/` directory
|
||||
|
||||
---
|
||||
|
||||
## Common Commands
|
||||
|
||||
```javascript
|
||||
// Run all tests
|
||||
runAllTestsWithReset()
|
||||
|
||||
// Run isolation tests only
|
||||
runModule('isolation')
|
||||
|
||||
// Check current bookmarks
|
||||
LinkdingSyncTests.Helpers.getAllBookmarks()
|
||||
|
||||
// Reset before running
|
||||
reset()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tests Not Running
|
||||
|
||||
- **Check**: Console shows "Orchestrator loaded"
|
||||
- **Solution**: Load `orchestrator.js` first, then modules
|
||||
|
||||
### Credentials Not Working
|
||||
|
||||
- **Check**: `CONFIG` in `utils.js` has correct values
|
||||
- **Solution**: Verify API keys work in Linkding UI
|
||||
|
||||
### Tests Creating Duplicate Bookmarks
|
||||
|
||||
- **Expected**: This is the test behavior
|
||||
- **Solution**: Run `reset()` to clean up
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
LinkdingSync/
|
||||
├── tests/
|
||||
│ ├── utils.js # Shared utilities
|
||||
│ ├── orchestrator.js # Main test runner
|
||||
│ ├── test-isolation.js # Isolation tests
|
||||
│ ├── test-conflicts.js # Conflict tests
|
||||
│ ├── test-deletion.js # Deletion tests
|
||||
│ └── test-bundles.js # Bundle tests
|
||||
├── docs/
|
||||
│ ├── phase0-plan.md # Phase 0 planning
|
||||
│ ├── test-usage.md # This file
|
||||
│ └── phase0-test-scenarios.md
|
||||
├── test-runner.js # Legacy (can be deleted after migration)
|
||||
└── .creds.txt # Test credentials
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
- API keys stored in Firefox session (cleared when browser closes)
|
||||
- Test bookmarks have unique testId prefix
|
||||
- All test bookmarks are cleaned up automatically
|
||||
- No sensitive data in test bookmarks
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For issues, check:
|
||||
1. Console output for error messages
|
||||
2. `.creds.txt` for correct credentials
|
||||
3. Linkding server logs for API errors
|
||||
Reference in New Issue
Block a user