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:
171
LinkSyncServer/tests/test_queries.py
Normal file
171
LinkSyncServer/tests/test_queries.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
LinkSyncServer - Query Engine Tests
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from queries.parser import QueryParser, QuerySyntaxError
|
||||
from queries.executor import execute_query
|
||||
|
||||
|
||||
class TestQueryParser:
|
||||
def test_parse_simple_term(self):
|
||||
parser = QueryParser()
|
||||
result = parser.parse("example")
|
||||
assert result is not None
|
||||
assert result["operation"] == "TERM"
|
||||
assert result["value"] == "example"
|
||||
|
||||
def test_parse_term_set(self):
|
||||
parser = QueryParser()
|
||||
result = parser.parse("term1,term2,term3")
|
||||
assert result is not None
|
||||
assert result["operation"] == "TERM_SET"
|
||||
assert result["value"] == ["term1", "term2", "term3"]
|
||||
|
||||
def test_parse_or(self):
|
||||
parser = QueryParser()
|
||||
result = parser.parse("term1 OR term2")
|
||||
assert result is not None
|
||||
assert result["operation"] == "OR"
|
||||
assert len(result["operands"]) == 2
|
||||
|
||||
def test_parse_and(self):
|
||||
parser = QueryParser()
|
||||
result = parser.parse("term1 AND term2")
|
||||
assert result is not None
|
||||
assert result["operation"] == "AND"
|
||||
|
||||
def test_parse_xor(self):
|
||||
parser = QueryParser()
|
||||
result = parser.parse("term1 XOR term2")
|
||||
assert result is not None
|
||||
assert result["operation"] == "XOR"
|
||||
|
||||
def test_parse_parentheses(self):
|
||||
parser = QueryParser()
|
||||
result = parser.parse("(term1 OR term2) AND term3")
|
||||
assert result is not None
|
||||
assert result["operation"] == "AND"
|
||||
|
||||
def test_parse_field_filter(self):
|
||||
parser = QueryParser()
|
||||
result = parser.parse("url:example.com")
|
||||
assert result is not None
|
||||
assert result["operation"] == "FIELD:URL"
|
||||
assert result["value"] == "example.com"
|
||||
|
||||
def test_parse_tag_filter(self):
|
||||
parser = QueryParser()
|
||||
result = parser.parse("tag:work")
|
||||
assert result is not None
|
||||
assert result["operation"] == "FIELD:TAG"
|
||||
assert result["value"] == "work"
|
||||
|
||||
def test_parse_empty(self):
|
||||
parser = QueryParser()
|
||||
result = parser.parse("")
|
||||
assert result is None
|
||||
|
||||
def test_parse_complex(self):
|
||||
parser = QueryParser()
|
||||
result = parser.parse("term1,term2 OR tag:work AND url:example.com")
|
||||
assert result is not None
|
||||
|
||||
|
||||
class TestQueryExecutor:
|
||||
@pytest.fixture
|
||||
def sample_bookmarks(self):
|
||||
return [
|
||||
{
|
||||
"id": "1",
|
||||
"url": "https://example.com/work",
|
||||
"title": "Work Page",
|
||||
"description": "A work related page",
|
||||
"notes": "",
|
||||
"tags": ["work", "important"],
|
||||
"favicon_url": None,
|
||||
"path": "/Work",
|
||||
"visit_count": 5,
|
||||
"is_bookmarked": True,
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"url": "https://example.com/personal",
|
||||
"title": "Personal Blog",
|
||||
"description": "My personal blog",
|
||||
"notes": "",
|
||||
"tags": ["personal", "blog"],
|
||||
"favicon_url": None,
|
||||
"path": "/Personal",
|
||||
"visit_count": 2,
|
||||
"is_bookmarked": False,
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"url": "https://dev.example.com",
|
||||
"title": "Dev Resources",
|
||||
"description": "Development resources",
|
||||
"notes": "",
|
||||
"tags": ["work", "dev"],
|
||||
"favicon_url": None,
|
||||
"path": "/Dev",
|
||||
"visit_count": 10,
|
||||
"is_bookmarked": True,
|
||||
},
|
||||
]
|
||||
|
||||
def test_execute_term(self, sample_bookmarks):
|
||||
parsed = {"operation": "TERM", "value": "work"}
|
||||
results = execute_query(parsed, sample_bookmarks)
|
||||
assert len(results) >= 1
|
||||
assert any(r["id"] == "1" for r in results)
|
||||
|
||||
def test_execute_field_url(self, sample_bookmarks):
|
||||
parsed = {"operation": "FIELD:URL", "value": "dev"}
|
||||
results = execute_query(parsed, sample_bookmarks)
|
||||
assert len(results) == 1
|
||||
assert results[0]["id"] == "3"
|
||||
|
||||
def test_execute_field_tag(self, sample_bookmarks):
|
||||
parsed = {"operation": "FIELD:TAG", "value": "blog"}
|
||||
results = execute_query(parsed, sample_bookmarks)
|
||||
assert len(results) == 1
|
||||
assert results[0]["id"] == "2"
|
||||
|
||||
def test_execute_or(self, sample_bookmarks):
|
||||
parsed = {
|
||||
"operation": "OR",
|
||||
"operands": [
|
||||
{"operation": "FIELD:TAG", "value": "blog"},
|
||||
{"operation": "FIELD:TAG", "value": "dev"},
|
||||
],
|
||||
}
|
||||
results = execute_query(parsed, sample_bookmarks)
|
||||
assert len(results) == 2
|
||||
|
||||
def test_execute_and(self, sample_bookmarks):
|
||||
parsed = {
|
||||
"operation": "AND",
|
||||
"operands": [
|
||||
{"operation": "TERM", "value": "dev"},
|
||||
{"operation": "FIELD:TAG", "value": "work"},
|
||||
],
|
||||
}
|
||||
results = execute_query(parsed, sample_bookmarks)
|
||||
assert len(results) == 1
|
||||
assert results[0]["id"] == "3"
|
||||
|
||||
def test_execute_empty(self):
|
||||
results = execute_query(None, [])
|
||||
assert results == []
|
||||
|
||||
def test_execute_xor(self, sample_bookmarks):
|
||||
parsed = {
|
||||
"operation": "XOR",
|
||||
"operands": [
|
||||
{"operation": "TERM", "value": "work"},
|
||||
{"operation": "TERM", "value": "personal"},
|
||||
],
|
||||
}
|
||||
results = execute_query(parsed, sample_bookmarks)
|
||||
assert len(results) >= 1
|
||||
Reference in New Issue
Block a user