from __future__ import annotations import json from unittest.mock import MagicMock import pytest import pytest_asyncio from aiohttp import web from aiohttp.web import Request from app.features.presets.repository import PresetsRepository from app.features.presets.router import presets_list from app.library.encoder import Encoder from app.library.sqlite_store import SqliteStore from app.tests.helpers import make_in_memory_db_path @pytest_asyncio.fixture async def repo(): PresetsRepository._reset_singleton() SqliteStore._reset_singleton() store = SqliteStore(db_path=make_in_memory_db_path("presets-repository")) await store.get_connection() repository = PresetsRepository.get_instance() yield repository await store.close() PresetsRepository._reset_singleton() SqliteStore._reset_singleton() class TestPresetsRepository: @pytest.mark.asyncio async def test_create_and_get(self, repo): preset = await repo.create({"name": "Custom", "cli": "--format best"}) assert preset.id is not None, "Should generate ID for new preset" assert preset.name == "custom", "Should normalize preset name" assert preset.cli == "--format best", "Should store preset cli" fetched = await repo.get(preset.id) assert fetched is not None, "Should fetch preset by id" assert fetched.name == "custom", "Should return matching preset" @pytest.mark.asyncio async def test_create_normalizes_spaces(self, repo): preset = await repo.create({"name": "My Preset"}) assert preset.name == "my_preset", "Should normalize spaces to underscores" @pytest.mark.asyncio async def test_list_orders_by_priority_then_name(self, repo): await repo.create({"name": "B", "priority": 1}) await repo.create({"name": "A", "priority": 1}) await repo.create({"name": "C", "priority": 2}) items = await repo.list() assert items[0].name == "c", "Highest priority should be first" assert items[1].name == "a", "Same priority should sort by name" assert items[2].name == "b", "Same priority should sort by name" @pytest.mark.asyncio async def test_list_paginated(self, repo): for i in range(5): await repo.create({"name": f"Item {i}", "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" assert page == 1, "Should be on page 1" assert total_pages == 3, "Should have 3 pages total" assert [item.priority for item in items] == [4, 3], "Should keep default priority-desc order" @pytest.mark.asyncio async def test_list_paginated_sorts_by_name_desc(self, repo): await repo.create({"name": "Alpha", "priority": 1}) await repo.create({"name": "Gamma", "priority": 3}) await repo.create({"name": "Beta", "priority": 2}) items, _, _, _ = await repo.list_paginated(page=1, per_page=10, sort="name", order="desc") assert [item.name for item in items] == ["gamma", "beta", "alpha"], "Should sort by requested field" @pytest.mark.asyncio async def test_list_paginated_excludes_defaults(self, repo): await repo.create({"name": "System Default", "default": True, "priority": 10}) await repo.create({"name": "Custom Preset", "priority": 1}) items, total, page, total_pages = await repo.list_paginated(page=1, per_page=10, exclude_defaults=True) assert [item.name for item in items] == ["custom_preset"], "Should exclude default presets" assert total == 1, "Should count only custom presets" assert page == 1, "Should keep current page when filtered results exist" assert total_pages == 1, "Should compute pages from the filtered total" @pytest.mark.asyncio async def test_list_paginated_multi_sort(self, repo): await repo.create({"name": "Charlie", "priority": 2}) await repo.create({"name": "Alpha", "priority": 1}) await repo.create({"name": "Bravo", "priority": 1}) items, _, _, _ = await repo.list_paginated(page=1, per_page=10, sort="priority,name", order="asc,desc") assert [(item.priority, item.name) for item in items] == [ (1, "bravo"), (1, "alpha"), (2, "charlie"), ], "Should support multiple sort fields and directions" @pytest.mark.asyncio async def test_list_paginated_bad_sort(self, repo): with pytest.raises(ValueError, match="sort must use supported fields"): await repo.list_paginated(page=1, per_page=10, sort="cli", order="asc") @pytest.mark.asyncio async def test_list_paginated_bad_order(self, repo): with pytest.raises(ValueError, match="order must be 'asc' or 'desc'"): await repo.list_paginated(page=1, per_page=10, sort="name", order="sideways") @pytest.mark.asyncio async def test_list_paginated_mismatched_order(self, repo): with pytest.raises(ValueError, match="order must provide one direction or match the number of sort fields"): await repo.list_paginated(page=1, per_page=10, sort="priority,name", order="asc,desc,asc") @pytest.mark.asyncio class TestPresetRoutes: async def test_list_route_sort(self, repo): await repo.create({"name": "Alpha", "priority": 1}) await repo.create({"name": "Bravo", "priority": 1}) await repo.create({"name": "Charlie", "priority": 2}) request = MagicMock(spec=Request) request.query = {"page": "1", "per_page": "10", "sort": "priority,name", "order": "asc,desc"} response = await presets_list(request, Encoder(), repo) payload = json.loads(response.text) assert response.status == web.HTTPOk.status_code, "Should return 200 for valid sorting" assert [item["name"] for item in payload["items"]] == ["bravo", "alpha", "charlie"], "Should sort response" async def test_list_route_bad_sort(self, repo): request = MagicMock(spec=Request) request.query = {"sort": "cli", "order": "asc"} response = await presets_list(request, Encoder(), repo) payload = json.loads(response.text) assert response.status == web.HTTPBadRequest.status_code, "Should reject unsupported sort field" assert "sort" in payload["error"], "Should explain invalid sort field" async def test_list_route_bad_order(self, repo): request = MagicMock(spec=Request) request.query = {"sort": "name", "order": "sideways"} response = await presets_list(request, Encoder(), repo) payload = json.loads(response.text) assert response.status == web.HTTPBadRequest.status_code, "Should reject unsupported sort direction" assert "order" in payload["error"], "Should explain invalid sort direction" async def test_list_route_exclude_defaults(self, repo): await repo.create({"name": "System Default", "default": True, "priority": 10}) await repo.create({"name": "Custom Preset", "priority": 1}) request = MagicMock(spec=Request) request.query = {"page": "1", "per_page": "10", "exclude_defaults": "true"} response = await presets_list(request, Encoder(), repo) payload = json.loads(response.text) assert response.status == web.HTTPOk.status_code, "Should return 200 for valid default exclusion" assert [item["name"] for item in payload["items"]] == ["custom_preset"], "Should exclude default presets" assert payload["pagination"]["total"] == 1, "Should report filtered total"