246 lines
9 KiB
Python
246 lines
9 KiB
Python
"""Tests for ConditionsRepository."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from types import SimpleNamespace
|
|
import pytest
|
|
from typing import Any
|
|
from aiohttp import web
|
|
from aiohttp.test_utils import make_mocked_request
|
|
|
|
from app.features.conditions.router import conditions_test
|
|
from app.library.config import Config
|
|
from app.library.encoder import Encoder
|
|
from app.features.conditions.repository import ConditionsRepository
|
|
from app.library.sqlite_store import SqliteStore
|
|
from app.tests.helpers import make_in_memory_db_path
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def repo():
|
|
ConditionsRepository._reset_singleton()
|
|
SqliteStore._reset_singleton()
|
|
|
|
store = SqliteStore(db_path=make_in_memory_db_path("conditions-repository"))
|
|
await store.get_connection()
|
|
|
|
# Create repository
|
|
repository = ConditionsRepository.get_instance()
|
|
|
|
yield repository
|
|
|
|
# Cleanup - close connections properly
|
|
await store.close()
|
|
|
|
# Reset singletons
|
|
ConditionsRepository._reset_singleton()
|
|
SqliteStore._reset_singleton()
|
|
|
|
|
|
def _json_request(path: str, payload: object) -> web.Request:
|
|
mock_request: Any = make_mocked_request("POST", path)
|
|
|
|
async def _json() -> object:
|
|
return payload
|
|
|
|
mock_request.json = _json
|
|
return mock_request
|
|
|
|
|
|
class TestAllowInternalUrlsScope:
|
|
def setup_method(self) -> None:
|
|
Config._reset_singleton()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rejects_internal_url(self) -> None:
|
|
config = Config.get_instance()
|
|
config.allow_internal_urls = False
|
|
encoder = Encoder()
|
|
cache = SimpleNamespace(hash=lambda value: value, has=lambda _key: False, set=lambda **_kwargs: None)
|
|
|
|
request = _json_request(
|
|
"/api/conditions/test/",
|
|
{"url": "http://127.0.0.1/test", "condition": "title", "preset": config.default_preset},
|
|
)
|
|
|
|
response = await conditions_test(request, encoder, cache, config)
|
|
|
|
assert response.status == web.HTTPBadRequest.status_code
|
|
body = response.body
|
|
assert isinstance(body, bytes)
|
|
assert b"internal urls" in body.lower()
|
|
|
|
|
|
class TestConditionsRepository:
|
|
"""Test suite for ConditionsRepository database operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_condition(self, repo):
|
|
"""Create condition with valid data."""
|
|
data = {
|
|
"name": "Test Condition",
|
|
"filter": "duration > 60",
|
|
"cli": "--format best",
|
|
"enabled": True,
|
|
"priority": 10,
|
|
"description": "Test description",
|
|
"extras": {"key": "value"},
|
|
}
|
|
|
|
model = await repo.create(data)
|
|
|
|
assert model.id is not None, "Should generate ID for new condition"
|
|
assert model.name == "Test Condition", "Should store name correctly"
|
|
assert model.filter == "duration > 60", "Should store filter correctly"
|
|
assert model.cli == "--format best", "Should store CLI correctly"
|
|
assert model.enabled is True, "Should store enabled flag correctly"
|
|
assert model.priority == 10, "Should store priority correctly"
|
|
assert model.description == "Test description", "Should store description correctly"
|
|
assert model.extras == {"key": "value"}, "Should store extras as dict"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_with_defaults(self, repo):
|
|
"""Create condition with minimal data uses defaults."""
|
|
data = {
|
|
"name": "Minimal",
|
|
"filter": "duration > 30",
|
|
}
|
|
|
|
model = await repo.create(data)
|
|
|
|
assert model.cli == "", "Should default CLI to empty string"
|
|
assert model.enabled is True, "Should default enabled to True"
|
|
assert model.priority == 0, "Should default priority to 0"
|
|
assert model.description == "", "Should default description to empty"
|
|
assert model.extras == {}, "Should default extras to empty dict"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_by_id(self, repo):
|
|
"""Get condition by integer ID."""
|
|
created = await repo.create({"name": "Get Test", "filter": "duration > 40"})
|
|
|
|
retrieved = await repo.get(created.id)
|
|
|
|
assert retrieved is not None, "Should retrieve created condition"
|
|
assert retrieved.id == created.id, "Should retrieve correct condition by ID"
|
|
assert retrieved.name == "Get Test", "Should match created condition name"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_by_name(self, repo):
|
|
"""Get condition by string name."""
|
|
await repo.create({"name": "Named Test", "filter": "duration > 50"})
|
|
|
|
retrieved = await repo.get("Named Test")
|
|
|
|
assert retrieved is not None, "Should retrieve by name"
|
|
assert retrieved.name == "Named Test", "Should match condition name"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_nonexistent(self, repo):
|
|
"""Get nonexistent condition returns None."""
|
|
result = await repo.get(99999)
|
|
assert result is None, "Should return None for nonexistent ID"
|
|
|
|
result = await repo.get("nonexistent")
|
|
assert result is None, "Should return None for nonexistent name"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_condition(self, repo):
|
|
"""Update existing condition."""
|
|
created = await repo.create({"name": "Update Test", "filter": "duration > 60"})
|
|
|
|
updated = await repo.update(
|
|
created.id,
|
|
{
|
|
"name": "Updated Name",
|
|
"priority": 5,
|
|
"extras": {"updated": True},
|
|
},
|
|
)
|
|
|
|
assert updated.name == "Updated Name", "Should update name"
|
|
assert updated.priority == 5, "Should update priority"
|
|
assert updated.extras == {"updated": True}, "Should update extras"
|
|
assert updated.filter == "duration > 60", "Should preserve unchanged filter"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_nonexistent_raises(self, repo):
|
|
"""Update nonexistent condition raises KeyError."""
|
|
with pytest.raises(KeyError, match="not found"):
|
|
await repo.update(99999, {"name": "Should Fail"})
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_condition(self, repo):
|
|
"""Delete existing condition."""
|
|
created = await repo.create({"name": "Delete Test", "filter": "duration > 70"})
|
|
|
|
deleted = await repo.delete(created.id)
|
|
|
|
assert deleted.id == created.id, "Should return deleted condition"
|
|
|
|
result = await repo.get(created.id)
|
|
assert result is None, "Deleted condition should not be retrievable"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_nonexistent_raises(self, repo):
|
|
"""Delete nonexistent condition raises KeyError."""
|
|
with pytest.raises(KeyError, match="not found"):
|
|
await repo.delete(99999)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_paginated(self, repo):
|
|
"""List paginated returns correct subset."""
|
|
for i in range(5):
|
|
await repo.create({"name": f"Item {i}", "filter": "duration > 10", "priority": i})
|
|
|
|
items, total, page, total_pages = await repo.list_paginated(page=1, per_page=2)
|
|
|
|
assert len(items) == 2, "Should return 2 items per page"
|
|
assert total == 5, "Should report total count of 5"
|
|
assert page == 1, "Should be on page 1"
|
|
assert total_pages == 3, "Should have 3 pages total"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_ordering(self, repo):
|
|
"""List orders by priority desc then name asc."""
|
|
await repo.create({"name": "B", "filter": "test", "priority": 1})
|
|
await repo.create({"name": "A", "filter": "test", "priority": 1})
|
|
await repo.create({"name": "C", "filter": "test", "priority": 2})
|
|
|
|
items = await repo.all()
|
|
|
|
assert items[0].name == "C", "Highest priority should be first"
|
|
assert items[1].name == "A", "Same priority sorted alphabetically"
|
|
assert items[2].name == "B", "Same priority sorted alphabetically"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_by_name_excludes_id(self, repo):
|
|
"""Get by name can exclude specific ID."""
|
|
first = await repo.create({"name": "Duplicate", "filter": "test"})
|
|
|
|
result = await repo.get_by_name("Duplicate", exclude_id=first.id)
|
|
assert result is None, "Should not find when excluding only match"
|
|
|
|
result = await repo.get_by_name("Duplicate", exclude_id=None)
|
|
assert result is not None, "Should find without exclusion"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_replace_all(self, repo):
|
|
"""Replace all conditions atomically."""
|
|
await repo.create({"name": "Old 1", "filter": "test"})
|
|
await repo.create({"name": "Old 2", "filter": "test"})
|
|
|
|
new_items = [
|
|
{"name": "New 1", "filter": "duration > 10"},
|
|
{"name": "New 2", "filter": "duration > 20"},
|
|
]
|
|
|
|
result = await repo.replace_all(new_items)
|
|
|
|
assert len(result) == 2, "Should create 2 new conditions"
|
|
|
|
all_items = await repo.all()
|
|
assert len(all_items) == 2, "Should only have new conditions"
|
|
assert all_items[0].name in ["New 1", "New 2"], "Should only have new items"
|