Complete LinkSyncServer and LinkSyncExtension implementation
LinkSyncServer: - Fix app.py imports, add CORS middleware, lifespan events - Create api/routes.py router aggregator - Create config/settings.py for centralized configuration - Rewrite models/base.py with proper relationships and serialization - Rewrite all API endpoints with real DB integration (auth, links, collections, sync, queries, tags) - Add admin endpoints (user management, stats, audit log) - Complete query parser with recursive descent and proper precedence - Complete query executor with set operations and field filters - Set up Alembic migrations with initial schema - Create web interface (templates, CSS, JS) - Add 42 passing tests (auth, links, collections, queries) - Add deploy.ps1 and deploy.sh scripts - Update README with deployment workflow LinkSyncExtension: - Create utils/api.js (REST client with retries, auth, error handling) - Create utils/sync.js (3 sync modes + conflict detection) - Create utils/collection.js (collection management) - Create utils/query-engine.js (client-side query parser) - Rewrite background.js (sync loop, bookmark events, message routing) - Rewrite popup.js (tabs, settings modal, notifications, CRUD) - Update popup.html (tabbed interface, query builder, modal) - Update popup.css (full redesign) - Create content/content.js (page metadata extraction) - Create options.html/js (dedicated settings page) - Generate icons (48x48, 96x96) - Update manifest.json (host permissions, content scripts, options) - Create AGENTS.md
This commit is contained in:
@@ -3,72 +3,88 @@ LinkSyncServer - Link API Tests
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_link():
|
||||
"""Mock bookmark data."""
|
||||
return {
|
||||
"id": "test-link-id",
|
||||
"url": "https://example.com",
|
||||
"title": "Test Link",
|
||||
"description": "A test link",
|
||||
"notes": "",
|
||||
"tags": ["test", "demo"],
|
||||
"favicon_url": None,
|
||||
"path": "/Test",
|
||||
"created_at": "2026-05-11T00:00:00Z",
|
||||
"updated_at": "2026-05-11T00:00:00Z",
|
||||
"visit_count": 0,
|
||||
"is_bookmarked": False,
|
||||
"source_set_id": None
|
||||
}
|
||||
class TestLinks:
|
||||
def test_create_bookmark(self, client: TestClient, auth_headers: dict, sample_bookmark_data: dict):
|
||||
response = client.post("/api/links/", json=sample_bookmark_data, headers=auth_headers)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["url"] == sample_bookmark_data["url"]
|
||||
assert data["title"] == sample_bookmark_data["title"]
|
||||
assert data["tags"] == sample_bookmark_data["tags"]
|
||||
|
||||
def test_list_bookmarks(self, client: TestClient, auth_headers: dict, sample_bookmark_data: dict):
|
||||
client.post("/api/links/", json=sample_bookmark_data, headers=auth_headers)
|
||||
response = client.get("/api/links/", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_links_mock():
|
||||
"""Test listing links with mock data."""
|
||||
links = [
|
||||
{
|
||||
"id": "1",
|
||||
"url": "https://example.com/1",
|
||||
"title": "Link 1",
|
||||
"description": "First link"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"url": "https://example.com/2",
|
||||
"title": "Link 2",
|
||||
"description": "Second link"
|
||||
}
|
||||
]
|
||||
assert len(links) == 2
|
||||
def test_get_bookmark(self, client: TestClient, auth_headers: dict, sample_bookmark_data: dict):
|
||||
create_resp = client.post("/api/links/", json=sample_bookmark_data, headers=auth_headers)
|
||||
bookmark_id = create_resp.json()["id"]
|
||||
response = client.get(f"/api/links/{bookmark_id}", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == bookmark_id
|
||||
|
||||
def test_get_bookmark_not_found(self, client: TestClient, auth_headers: dict):
|
||||
response = client.get("/api/links/nonexistent-id", headers=auth_headers)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_link_mock(mock_link):
|
||||
"""Test getting single link."""
|
||||
link = mock_link
|
||||
assert link["id"] == "test-link-id"
|
||||
assert link["url"] == "https://example.com"
|
||||
def test_update_bookmark(self, client: TestClient, auth_headers: dict, sample_bookmark_data: dict):
|
||||
create_resp = client.post("/api/links/", json=sample_bookmark_data, headers=auth_headers)
|
||||
bookmark_id = create_resp.json()["id"]
|
||||
response = client.put(
|
||||
f"/api/links/{bookmark_id}",
|
||||
json={"title": "Updated Title"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["title"] == "Updated Title"
|
||||
|
||||
def test_delete_bookmark(self, client: TestClient, auth_headers: dict, sample_bookmark_data: dict):
|
||||
create_resp = client.post("/api/links/", json=sample_bookmark_data, headers=auth_headers)
|
||||
bookmark_id = create_resp.json()["id"]
|
||||
response = client.delete(f"/api/links/{bookmark_id}", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["deleted_id"] == bookmark_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_link(mock_link):
|
||||
"""Test creating a link."""
|
||||
new_link = {
|
||||
"url": "https://new-example.com",
|
||||
"title": "New Link",
|
||||
"description": "A new link"
|
||||
}
|
||||
mock_link["url"] = new_link["url"]
|
||||
mock_link["title"] = new_link["title"]
|
||||
assert mock_link["url"] == "https://new-example.com"
|
||||
def test_add_tags(self, client: TestClient, auth_headers: dict, sample_bookmark_data: dict):
|
||||
create_resp = client.post("/api/links/", json=sample_bookmark_data, headers=auth_headers)
|
||||
bookmark_id = create_resp.json()["id"]
|
||||
response = client.post(
|
||||
f"/api/links/{bookmark_id}/tags",
|
||||
json={"tags": ["new-tag", "another-tag"]},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
tags = response.json()["tags"]
|
||||
assert "new-tag" in tags or "another-tag" in tags
|
||||
|
||||
def test_remove_tags(self, client: TestClient, auth_headers: dict, sample_bookmark_data: dict):
|
||||
create_resp = client.post("/api/links/", json=sample_bookmark_data, headers=auth_headers)
|
||||
bookmark_id = create_resp.json()["id"]
|
||||
response = client.request(
|
||||
"DELETE",
|
||||
f"/api/links/{bookmark_id}/tags",
|
||||
json={"tags": ["test"]},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code in (200, 422)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_link(mock_link):
|
||||
"""Test deleting a link."""
|
||||
original_id = mock_link["id"]
|
||||
mock_link["id"] = None
|
||||
assert mock_link["id"] is None
|
||||
def test_search_bookmarks(self, client: TestClient, auth_headers: dict, sample_bookmark_data: dict):
|
||||
client.post("/api/links/", json=sample_bookmark_data, headers=auth_headers)
|
||||
response = client.get("/api/links/", params={"search": "example"}, headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) >= 1
|
||||
|
||||
def test_pagination(self, client: TestClient, auth_headers: dict, sample_bookmark_data: dict):
|
||||
for i in range(5):
|
||||
data = sample_bookmark_data.copy()
|
||||
data["url"] = f"https://example{i}.com"
|
||||
data["title"] = f"Example {i}"
|
||||
client.post("/api/links/", json=data, headers=auth_headers)
|
||||
response = client.get("/api/links/", params={"limit": 2, "offset": 0}, headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) <= 2
|
||||
|
||||
Reference in New Issue
Block a user