diff --git a/API.md b/API.md index 9fac609e..1d408c44 100644 --- a/API.md +++ b/API.md @@ -72,6 +72,7 @@ This document describes the available endpoints and their usage. All endpoints r - [POST /api/system/pause](#post-apisystempause) - [POST /api/system/resume](#post-apisystemresume) - [POST /api/system/shutdown](#post-apisystemshutdown) + - [POST /api/system/check-updates](#post-apisystemcheck-updates) - [GET /api/dev/loop](#get-apidevloop) - [GET /api/dev/pip](#get-apidevpip) - [GET /api/docs/{file}](#get-apidocsfile) @@ -102,9 +103,7 @@ This document describes the available endpoints and their usage. All endpoints r - [`cli_post` (Client → Server)](#cli_post-client--server) - [`cli_output` (Server → Client)](#cli_output-server--client) - [`cli_close` (Server → Client)](#cli_close-server--client) - - [Configuration Events](#configuration-events) - - [`presets_update` (Server → Client)](#presets_update-server--client) - - [`dlfields_update` (Server → Client)](#dlfields_update-server--client) + - [Error Responses](#error-responses) --- @@ -2087,19 +2086,6 @@ Emitted when CLI command execution completes. **Data Fields**: - `exitcode`: Command exit code (0 = success, non-zero = error) -### Configuration Events - -#### `presets_update` (Server → Client) -Emitted when download presets are updated or created. - -**Data**: Array of preset objects - -#### `dlfields_update` (Server → Client) -Emitted when download fields configuration is updated. - -**Data**: Array of field objects -``` - --- ## Error Responses diff --git a/app/features/conditions/router.py b/app/features/conditions/router.py index 4934a864..b6fbf4f5 100644 --- a/app/features/conditions/router.py +++ b/app/features/conditions/router.py @@ -8,11 +8,12 @@ from pydantic import ValidationError from app.features.conditions.schemas import Condition, ConditionList, ConditionPatch from app.features.conditions.service import Conditions -from app.features.core.schemas import Pagination +from app.features.core.schemas import CEAction, CEFeature, ConfigEvent, Pagination from app.features.core.utils import build_pagination, format_validation_errors, normalize_pagination from app.library.cache import Cache from app.library.config import Config from app.library.encoder import Encoder +from app.library.Events import EventBus, Events from app.library.router import route LOG: logging.Logger = logging.getLogger(__name__) @@ -136,7 +137,7 @@ async def conditions_test(request: Request, encoder: Encoder, cache: Cache, conf @route("POST", "api/conditions/", name="condition_add") -async def conditions_add(request: Request, encoder: Encoder) -> Response: +async def conditions_add(request: Request, encoder: Encoder, notify: EventBus) -> Response: """ Add Condition. @@ -166,15 +167,14 @@ async def conditions_add(request: Request, encoder: Encoder) -> Response: ) try: - data = _serialize(await Conditions.get_instance().save(item=item.model_dump())) + saved = _serialize(await Conditions.get_instance().save(item=item.model_dump())) except ValueError as exc: return web.json_response({"error": str(exc)}, status=web.HTTPBadRequest.status_code) - return web.json_response( - data=data, - status=web.HTTPOk.status_code, - dumps=encoder.encode, + notify.emit( + Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.CONDITIONS, action=CEAction.CREATE, data=saved) ) + return web.json_response(data=saved, status=web.HTTPOk.status_code, dumps=encoder.encode) @route("GET", "api/conditions/{id}", name="condition_get") @@ -200,13 +200,14 @@ async def conditions_get(request: Request, encoder: Encoder) -> Response: @route("DELETE", "api/conditions/{id}", name="condition_delete") -async def conditions_delete(request: Request, encoder: Encoder) -> Response: +async def conditions_delete(request: Request, encoder: Encoder, notify: EventBus) -> Response: """ Delete Condition. Args: request (Request): The request object. encoder (Encoder): The encoder instance. + notify (EventBus): The event bus instance. Returns: Response: The response object. @@ -216,23 +217,24 @@ async def conditions_delete(request: Request, encoder: Encoder) -> Response: return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code) try: - return web.json_response( - data=_serialize(await Conditions.get_instance()._repo.delete(id)), - status=web.HTTPOk.status_code, - dumps=encoder.encode, + deleted = _serialize(await Conditions.get_instance()._repo.delete(id)) + notify.emit( + Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.CONDITIONS, action=CEAction.DELETE, data=deleted) ) + return web.json_response(data=deleted, status=web.HTTPOk.status_code, dumps=encoder.encode) except KeyError as exc: return web.json_response({"error": str(exc)}, status=web.HTTPNotFound.status_code) @route("PATCH", "api/conditions/{id}", name="condition_patch") -async def conditions_patch(request: Request, encoder: Encoder) -> Response: +async def conditions_patch(request: Request, encoder: Encoder, notify: EventBus) -> Response: """ Patch Condition. Args: request (Request): The request object. encoder (Encoder): The encoder instance. + notify (EventBus): The event bus instance. Returns: Response: The response object. @@ -268,23 +270,22 @@ async def conditions_patch(request: Request, encoder: Encoder) -> Response: status=web.HTTPConflict.status_code, ) - dct = validated.model_dump(exclude_unset=True) - - return web.json_response( - data=_serialize(await service._repo.update(model.id, dct)), - status=web.HTTPOk.status_code, - dumps=encoder.encode, + updated = _serialize(await service._repo.update(model.id, validated.model_dump(exclude_unset=True))) + notify.emit( + Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.CONDITIONS, action=CEAction.UPDATE, data=updated) ) + return web.json_response(data=updated, status=web.HTTPOk.status_code, dumps=encoder.encode) @route("PUT", "api/conditions/{id}", name="condition_update") -async def conditions_update(request: Request, encoder: Encoder) -> Response: +async def conditions_update(request: Request, encoder: Encoder, notify: EventBus) -> Response: """ Update Condition. Args: request (Request): The request object. encoder (Encoder): The encoder instance. + notify (EventBus): The event bus instance. Returns: Response: The response object. @@ -320,8 +321,8 @@ async def conditions_update(request: Request, encoder: Encoder) -> Response: status=web.HTTPConflict.status_code, ) - return web.json_response( - data=_serialize(await service._repo.update(model.id, validated.model_dump(exclude_unset=True))), - status=web.HTTPOk.status_code, - dumps=encoder.encode, + updated = _serialize(await service._repo.update(model.id, validated.model_dump(exclude_unset=True))) + notify.emit( + Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.CONDITIONS, action=CEAction.UPDATE, data=updated) ) + return web.json_response(data=updated, status=web.HTTPOk.status_code, dumps=encoder.encode) diff --git a/app/features/core/schemas.py b/app/features/core/schemas.py index 4fbe8d9b..9fa1235d 100644 --- a/app/features/core/schemas.py +++ b/app/features/core/schemas.py @@ -1,6 +1,9 @@ from __future__ import annotations -from pydantic import BaseModel +from enum import Enum +from typing import Any + +from pydantic import BaseModel, Field class Pagination(BaseModel): @@ -10,3 +13,29 @@ class Pagination(BaseModel): total_pages: int has_next: bool has_prev: bool + + +class CEFeature(str, Enum): + PRESETS = "presets" + DL_FIELDS = "dl_fields" + CONDITIONS = "conditions" + + def __str__(self) -> str: + return self.value + + +class CEAction(str, Enum): + CREATE = "create" + UPDATE = "update" + DELETE = "delete" + REPLACE = "replace" + + def __str__(self) -> str: + return self.value + + +class ConfigEvent(BaseModel): + feature: CEFeature + action: CEAction + data: dict[str, Any] | list[dict[str, Any]] + extras: dict[str, Any] = Field(default_factory=dict) diff --git a/app/features/dl_fields/router.py b/app/features/dl_fields/router.py index ae5c484a..41c8a7c6 100644 --- a/app/features/dl_fields/router.py +++ b/app/features/dl_fields/router.py @@ -5,7 +5,7 @@ from aiohttp import web from aiohttp.web import Request, Response from pydantic import ValidationError -from app.features.core.schemas import Pagination +from app.features.core.schemas import CEAction, CEFeature, ConfigEvent, Pagination from app.features.core.utils import build_pagination, format_validation_errors, normalize_pagination from app.features.dl_fields.schemas import DLField, DLFieldList, DLFieldPatch from app.features.dl_fields.service import DLFields @@ -62,7 +62,7 @@ async def dl_fields_add(request: Request, encoder: Encoder, notify: EventBus) -> except ValueError as exc: return web.json_response({"error": str(exc)}, status=web.HTTPBadRequest.status_code) - notify.emit(Events.DLFIELDS_UPDATE, data=[saved]) + notify.emit(Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.DL_FIELDS, action=CEAction.CREATE, data=saved)) return web.json_response(data=saved, status=web.HTTPOk.status_code, dumps=encoder.encode) @@ -79,13 +79,17 @@ async def dl_fields_get(request: Request, encoder: Encoder) -> Response: @route("DELETE", "api/dl_fields/{id}", "dl_fields_delete") -async def dl_fields_delete(request: Request, encoder: Encoder) -> Response: +async def dl_fields_delete(request: Request, encoder: Encoder, notify: EventBus) -> Response: if not (identifier := request.match_info.get("id")): return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code) try: + deleted = _serialize(await DLFields.get_instance()._repo.delete(identifier)) + notify.emit( + Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.DL_FIELDS, action=CEAction.DELETE, data=deleted) + ) return web.json_response( - data=_serialize(await DLFields.get_instance()._repo.delete(identifier)), + data=deleted, status=web.HTTPOk.status_code, dumps=encoder.encode, ) @@ -94,7 +98,7 @@ async def dl_fields_delete(request: Request, encoder: Encoder) -> Response: @route("PATCH", "api/dl_fields/{id}", "dl_fields_patch") -async def dl_fields_patch(request: Request, encoder: Encoder) -> Response: +async def dl_fields_patch(request: Request, encoder: Encoder, notify: EventBus) -> Response: if not (identifier := request.match_info.get("id")): return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code) @@ -123,17 +127,19 @@ async def dl_fields_patch(request: Request, encoder: Encoder) -> Response: status=web.HTTPConflict.status_code, ) - dct = validated.model_dump(exclude_unset=True) - + updated = _serialize(await DLFields.get_instance()._repo.update(model.id, validated.model_dump(exclude_unset=True))) + notify.emit( + Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.DL_FIELDS, action=CEAction.UPDATE, data=updated) + ) return web.json_response( - data=_serialize(await DLFields.get_instance()._repo.update(model.id, dct)), + data=updated, status=web.HTTPOk.status_code, dumps=encoder.encode, ) @route("PUT", "api/dl_fields/{id}", "dl_fields_update") -async def dl_fields_update(request: Request, encoder: Encoder) -> Response: +async def dl_fields_update(request: Request, encoder: Encoder, notify: EventBus) -> Response: if not (identifier := request.match_info.get("id")): return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code) @@ -162,8 +168,9 @@ async def dl_fields_update(request: Request, encoder: Encoder) -> Response: status=web.HTTPConflict.status_code, ) - return web.json_response( - data=_serialize(await DLFields.get_instance()._repo.update(model.id, validated.model_dump(exclude_unset=True))), - status=web.HTTPOk.status_code, - dumps=encoder.encode, + updated = _serialize(await DLFields.get_instance()._repo.update(model.id, validated.model_dump(exclude_unset=True))) + notify.emit( + Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.DL_FIELDS, action=CEAction.UPDATE, data=updated) ) + + return web.json_response(data=updated, status=web.HTTPOk.status_code, dumps=encoder.encode) diff --git a/app/library/Events.py b/app/library/Events.py index ff1b07b1..72f9c691 100644 --- a/app/library/Events.py +++ b/app/library/Events.py @@ -25,6 +25,7 @@ class Events: CONNECTED: str = "connected" CONFIGURATION: str = "configuration" + CONFIG_UPDATE: str = "config_update" ACTIVE_QUEUE: str = "active_queue" LOG_INFO: str = "log_info" @@ -58,17 +59,8 @@ class Events: TASK_FINISHED: str = "task_finished" TASK_ERROR: str = "task_error" - PRESETS_ADD: str = "presets_add" - PRESETS_UPDATE: str = "presets_update" - - DLFIELDS_ADD: str = "dlfields_add" - DLFIELDS_UPDATE: str = "dlfields_update" - SCHEDULE_ADD: str = "schedule_add" - CONDITIONS_ADD: str = "conditions_add" - CONDITIONS_UPDATE: str = "conditions_update" - SUBSCRIBED: str = "subscribed" UNSUBSCRIBED: str = "unsubscribed" @@ -94,6 +86,7 @@ class Events: """ return [ Events.CONFIGURATION, + Events.CONFIG_UPDATE, Events.CONNECTED, Events.ACTIVE_QUEUE, Events.LOG_INFO, @@ -110,8 +103,6 @@ class Events: Events.RESUMED, Events.CLI_CLOSE, Events.CLI_OUTPUT, - Events.PRESETS_UPDATE, - Events.DLFIELDS_UPDATE, ] def only_debug() -> list: diff --git a/app/library/Presets.py b/app/library/Presets.py index 7f68346c..9d5e4252 100644 --- a/app/library/Presets.py +++ b/app/library/Presets.py @@ -8,7 +8,6 @@ from typing import Any from aiohttp import web from .config import Config -from .Events import EventBus, Events from .Singleton import Singleton from .Utils import arg_converter, create_cookies_file, init_class @@ -214,12 +213,6 @@ class Presets(metaclass=Singleton): LOG.error(f"Default preset '{self._config.default_preset}' not found, using 'default' preset.") self._config.default_preset = "default" - async def event_handler(_, __): - msg = "Not implemented" - raise Exception(msg) - - EventBus.get_instance().subscribe(Events.PRESETS_ADD, event_handler, f"{__class__.__name__}.add") - def get_all(self) -> list[Preset]: """Return the items.""" return sorted(self._default + self._items, key=lambda x: x.priority, reverse=True) diff --git a/app/routes/api/presets.py b/app/routes/api/presets.py index 69eecac7..e729cbe4 100644 --- a/app/routes/api/presets.py +++ b/app/routes/api/presets.py @@ -4,6 +4,7 @@ import uuid from aiohttp import web from aiohttp.web import Request, Response +from app.features.core.schemas import CEAction, CEFeature, ConfigEvent from app.library.encoder import Encoder from app.library.Events import EventBus, Events from app.library.Presets import Preset, Presets @@ -96,5 +97,12 @@ async def presets_add(request: Request, encoder: Encoder, notify: EventBus) -> R status=web.HTTPInternalServerError.status_code, ) - notify.emit(Events.PRESETS_UPDATE, data=presets) + notify.emit( + Events.CONFIG_UPDATE, + data=ConfigEvent( + feature=CEFeature.PRESETS, + action=CEAction.REPLACE, + data=[preset.serialize() for preset in presets], + ), + ) return web.json_response(data=presets, status=web.HTTPOk.status_code, dumps=encoder.encode) diff --git a/app/tests/test_events.py b/app/tests/test_events.py index 53434952..563dd6f6 100644 --- a/app/tests/test_events.py +++ b/app/tests/test_events.py @@ -20,6 +20,7 @@ class TestEvents: # Connection events assert Events.CONNECTED == "connected" + assert Events.CONFIG_UPDATE == "config_update" # Log events assert Events.LOG_INFO == "log_info" @@ -48,6 +49,7 @@ class TestEvents: "started", "shutdown", "connected", + "config_update", "log_info", "log_warning", "log_error", @@ -85,6 +87,7 @@ class TestEvents: # Check some expected frontend events expected_frontend = [ Events.CONNECTED, + Events.CONFIG_UPDATE, Events.LOG_INFO, Events.LOG_WARNING, Events.LOG_ERROR, diff --git a/app/tests/test_presets.py b/app/tests/test_presets.py index 65395ff5..987c1750 100644 --- a/app/tests/test_presets.py +++ b/app/tests/test_presets.py @@ -103,11 +103,9 @@ class TestPresets: Presets._reset_singleton() @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_singleton(self, mock_eventbus, mock_config): + def test_presets_singleton(self, mock_config): """Test that Presets follows singleton pattern.""" mock_config.get_instance.return_value = Mock(config_path="/tmp", default_preset="default") - mock_eventbus.get_instance.return_value = Mock() instance1 = Presets.get_instance() instance2 = Presets.get_instance() @@ -116,13 +114,10 @@ class TestPresets: assert isinstance(instance1, Presets) @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_initialization(self, mock_eventbus, mock_config): + def test_presets_initialization(self, mock_config): """Test Presets initialization with default presets.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus_instance = Mock() - mock_eventbus.get_instance.return_value = mock_eventbus_instance with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -138,12 +133,10 @@ class TestPresets: assert preset.default is True @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_load_empty_file(self, mock_eventbus, mock_config): + def test_presets_load_empty_file(self, mock_config): """Test loading presets from empty or non-existent file.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -157,13 +150,11 @@ class TestPresets: assert len(presets._default) > 0 # Should still have default presets @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") @patch("app.library.Presets.arg_converter") - def test_presets_load_valid_file(self, mock_arg_converter, mock_eventbus, mock_config): + def test_presets_load_valid_file(self, mock_arg_converter, mock_config): """Test loading presets from valid JSON file.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() mock_arg_converter.return_value = [] # Mock successful CLI parsing test_presets = [ @@ -184,13 +175,11 @@ class TestPresets: assert presets._items[0].name == "test_preset_1" assert presets._items[1].name == "test_preset_2" - @patch("app.library.Presets.EventBus") @patch("app.library.Presets.Config") - def test_presets_load_invalid_json(self, mock_eventbus, mock_config): + def test_presets_load_invalid_json(self, mock_config): """Test loading presets from invalid JSON file.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -204,13 +193,11 @@ class TestPresets: assert len(presets._items) == 0 @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") @patch("app.library.Presets.arg_converter") - def test_presets_load_with_format_migration(self, mock_arg_converter, mock_eventbus, mock_config): + def test_presets_load_with_format_migration(self, mock_arg_converter, mock_config): """Test loading presets with old format that needs migration.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() mock_arg_converter.return_value = [] # Mock successful CLI parsing # Old format preset with 'format' field instead of 'cli' @@ -233,12 +220,10 @@ class TestPresets: assert loaded_preset.id is not None @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_validate_valid_preset(self, mock_eventbus, mock_config): + def test_presets_validate_valid_preset(self, mock_config): """Test validating a valid preset.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -250,12 +235,10 @@ class TestPresets: assert result is True @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_validate_invalid_preset(self, mock_eventbus, mock_config): + def test_presets_validate_invalid_preset(self, mock_config): """Test validating invalid presets.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -274,13 +257,11 @@ class TestPresets: presets.validate("invalid_type") @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") @patch("app.library.Presets.arg_converter") - def test_presets_save(self, mock_arg_converter, mock_eventbus, mock_config): + def test_presets_save(self, mock_arg_converter, mock_config): """Test saving presets to file.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() mock_arg_converter.return_value = [] # Mock successful CLI parsing test_presets = [ @@ -303,12 +284,10 @@ class TestPresets: assert saved_data[0]["name"] == "test1" @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_get_all(self, mock_eventbus, mock_config): + def test_presets_get_all(self, mock_config): """Test getting all presets (default + custom).""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -325,12 +304,10 @@ class TestPresets: assert any(p.default is True for p in all_presets) @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_get_by_id(self, mock_eventbus, mock_config): + def test_presets_get_by_id(self, mock_config): """Test getting preset by ID.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -347,12 +324,10 @@ class TestPresets: assert found_preset.name == "test_preset" @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_get_by_name(self, mock_eventbus, mock_config): + def test_presets_get_by_name(self, mock_config): """Test getting preset by name.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -368,12 +343,10 @@ class TestPresets: assert found_preset.cli == "--test" @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_get_nonexistent(self, mock_eventbus, mock_config): + def test_presets_get_nonexistent(self, mock_config): """Test getting non-existent preset returns None.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -384,12 +357,10 @@ class TestPresets: assert found_preset is None @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_get_empty_parameter(self, mock_eventbus, mock_config): + def test_presets_get_empty_parameter(self, mock_config): """Test getting preset with empty string returns None.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -399,12 +370,10 @@ class TestPresets: assert presets.get(None) is None @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_has_method(self, mock_eventbus, mock_config): + def test_presets_has_method(self, mock_config): """Test has method for checking preset existence.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -418,12 +387,10 @@ class TestPresets: assert presets.has("nonexistent") is False @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_clear(self, mock_eventbus, mock_config): + def test_presets_clear(self, mock_config): """Test clearing all custom presets.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -441,12 +408,10 @@ class TestPresets: assert len(presets._items) == 0 @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_file_permissions(self, mock_eventbus, mock_config): + def test_presets_file_permissions(self, mock_config): """Test that presets file gets correct permissions.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -483,12 +448,10 @@ class TestPresets: assert isinstance(preset, Preset) @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_attach_method(self, mock_eventbus, mock_config): + def test_presets_attach_method(self, mock_config): """Test the attach method for aiohttp integration.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() mock_app = Mock() with tempfile.TemporaryDirectory() as temp_dir: @@ -501,12 +464,10 @@ class TestPresets: mock_get.assert_called_once_with("default") @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_presets_attach_default_preset_not_found(self, mock_eventbus, mock_config): + def test_presets_attach_default_preset_not_found(self, mock_config): """Test attach method when default preset is not found.""" mock_config_instance = Mock(config_path="/tmp", default_preset="nonexistent") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() mock_app = Mock() with tempfile.TemporaryDirectory() as temp_dir: @@ -520,12 +481,10 @@ class TestPresets: @pytest.mark.asyncio @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - async def test_presets_on_shutdown(self, mock_eventbus, mock_config): + async def test_presets_on_shutdown(self, mock_config): """Test the on_shutdown method.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() mock_app = Mock() with tempfile.TemporaryDirectory() as temp_dir: @@ -536,14 +495,12 @@ class TestPresets: await presets.on_shutdown(mock_app) @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") @patch("app.library.Presets.LOG") @patch("app.library.Presets.arg_converter") - def test_presets_load_invalid_preset_in_file(self, mock_arg_converter, mock_log, mock_eventbus, mock_config): + def test_presets_load_invalid_preset_in_file(self, mock_arg_converter, mock_log, mock_config): """Test loading file with some invalid presets.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() mock_arg_converter.return_value = [] # Mock successful CLI parsing # Mix of valid and invalid presets @@ -570,13 +527,11 @@ class TestPresets: mock_log.error.assert_called() @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") @patch("app.library.Presets.LOG") - def test_presets_save_with_invalid_preset(self, mock_log, mock_eventbus, mock_config): + def test_presets_save_with_invalid_preset(self, mock_log, mock_config): """Test saving presets with some invalid ones.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() # Create preset with invalid CLI that will fail validation valid_preset = Preset(name="valid", cli="--valid-option") @@ -596,12 +551,10 @@ class TestPresets: mock_log.error.assert_called() @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_preset_priority_validation_integer(self, mock_eventbus, mock_config): + def test_preset_priority_validation_integer(self, mock_config): """Test that priority must be an integer.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -614,12 +567,10 @@ class TestPresets: presets.validate(invalid_preset) @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_preset_priority_validation_negative(self, mock_eventbus, mock_config): + def test_preset_priority_validation_negative(self, mock_config): """Test that priority must be >= 0.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -632,12 +583,10 @@ class TestPresets: presets.validate(invalid_preset) @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_preset_priority_sorting(self, mock_eventbus, mock_config): + def test_preset_priority_sorting(self, mock_config): """Test that presets are sorted by priority in descending order.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" @@ -661,19 +610,16 @@ class TestPresets: assert non_default[2].priority == 1 @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_preset_priority_default_zero(self, mock_eventbus, mock_config): + def test_preset_priority_default_zero(self, mock_config): """Test that priority defaults to 0 if not specified.""" preset = Preset(name="test") assert preset.priority == 0 @patch("app.library.Presets.Config") - @patch("app.library.Presets.EventBus") - def test_preset_priority_migration(self, mock_eventbus, mock_config): + def test_preset_priority_migration(self, mock_config): """Test that old presets without priority get it added on load.""" mock_config_instance = Mock(config_path="/tmp", default_preset="default") mock_config.get_instance.return_value = mock_config_instance - mock_eventbus.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: presets_file = Path(temp_dir) / "presets.json" diff --git a/ui/app/components/History.vue b/ui/app/components/History.vue index 2f0c0ee6..2978ba0a 100644 --- a/ui/app/components/History.vue +++ b/ui/app/components/History.vue @@ -452,7 +452,7 @@
source_name:task_name - items added by the specified task.Download history is empty.