diff --git a/Claude outputs/main.py b/Claude outputs/main.py index 357951e..dd0b18a 100644 --- a/Claude outputs/main.py +++ b/Claude outputs/main.py @@ -12,6 +12,7 @@ import asyncio import time from datetime import datetime, timedelta from zoneinfo import ZoneInfo +from urllib.parse import urlparse import logging import json @@ -459,6 +460,32 @@ async def delete_event(uid: str) -> dict: return await asyncio.to_thread(_delete_event_sync, uid) +def _deck_base_url() -> str: + host = urlparse(NEXTCLOUD_CALDAV_URL).netloc + return f"https://{host}/index.php/apps/deck/api/v1.0" + + +async def _deck_request(method: str, path: str, json_body: dict = None): + """Generic helper for the Nextcloud Deck REST API - all Deck business + logic goes through this single function so it is the only place tests + need to mock aiohttp directly.""" + url = f"{_deck_base_url()}{path}" + auth = aiohttp.BasicAuth(NEXTCLOUD_USER, NEXTCLOUD_APP_PASSWORD) + headers = {"OCS-APIRequest": "true", "Content-Type": "application/json"} + timeout = aiohttp.ClientTimeout(total=30) + async with aiohttp.ClientSession(timeout=timeout, auth=auth, headers=headers) as session: + async with session.request(method, url, json=json_body) as resp: + if resp.status >= 400: + body = await resp.text() + raise RuntimeError(f"Deck API {method} {path} failed ({resp.status}): {body}") + if resp.status == 204: + return None + return await resp.json() + + +_deck_cache: dict = {} + + def _decode_mime_words(value: str) -> str: parts = decode_header(value) decoded = [] diff --git a/Claude outputs/tests/test_orders.py b/Claude outputs/tests/test_orders.py new file mode 100644 index 0000000..5511445 --- /dev/null +++ b/Claude outputs/tests/test_orders.py @@ -0,0 +1,87 @@ +import os +import sys +import json +from datetime import datetime +from unittest.mock import AsyncMock, MagicMock, patch + +# Mock psycopg2 before importing main (see test_memory.py for rationale). +sys.modules["psycopg2"] = MagicMock() +sys.modules["psycopg2.pool"] = MagicMock() +sys.modules["psycopg2.extras"] = MagicMock() + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import pytest + +import main + +main.NEXTCLOUD_CALDAV_URL = "https://cloud.ffw-onza.de/remote.php/dav/calendars/jonny/ffw-onza-alle/" +main.NEXTCLOUD_USER = "jonny" +main.NEXTCLOUD_APP_PASSWORD = "test-app-password" + + +@pytest.fixture(autouse=True) +def _reset_deck_cache(): + main._deck_cache.clear() + yield + main._deck_cache.clear() + + +class _FakeResponse: + def __init__(self, status, payload): + self.status = status + self._payload = payload + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return False + + async def json(self): + return self._payload + + async def text(self): + return json.dumps(self._payload) + + +class _FakeSession: + def __init__(self, response): + self._response = response + self.request_calls = [] + + def request(self, method, url, **kwargs): + self.request_calls.append((method, url, kwargs)) + return self._response + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return False + + +def test_deck_base_url_derives_host_from_caldav_url(): + assert main._deck_base_url() == "https://cloud.ffw-onza.de/index.php/apps/deck/api/v1.0" + + +@pytest.mark.asyncio +async def test_deck_request_returns_parsed_json_on_success(): + fake_session = _FakeSession(_FakeResponse(200, {"id": 5})) + + with patch.object(main.aiohttp, "ClientSession", return_value=fake_session): + result = await main._deck_request("GET", "/boards") + + assert result == {"id": 5} + method, url, kwargs = fake_session.request_calls[0] + assert method == "GET" + assert url == "https://cloud.ffw-onza.de/index.php/apps/deck/api/v1.0/boards" + + +@pytest.mark.asyncio +async def test_deck_request_raises_on_error_status(): + fake_session = _FakeSession(_FakeResponse(500, {"message": "boom"})) + + with patch.object(main.aiohttp, "ClientSession", return_value=fake_session): + with pytest.raises(RuntimeError): + await main._deck_request("GET", "/boards")