diet-guard/diet_guard/tests/_kuchnia_fakes.py
Krzysztof kuhy Rudnicki e6432d5b9d
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Catering: answer the panel's CSRF 403 at login; kuchnia --log publishes
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 12:42:13 +02:00

162 lines
5.4 KiB
Python

"""Shared fakes for the catering-import tests.
Not a ``test_`` module: these are fixtures-by-hand imported by the
``test_kuchnia_*`` files, and pytest must not collect them as tests. The repo
already excludes this naming pattern from the ``name-tests-test`` hook.
The fake stands in for ``requests`` at the module seam
(``patch.object(_kuchnia_client, "requests", ...)``), which is the same shape
``_estimator_off``'s tests use and the reason both modules resolve the name
through ``sys.modules`` rather than binding it at import.
"""
from __future__ import annotations
import json
from types import SimpleNamespace
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterator
from pathlib import Path
class FakeResponse:
"""The slice of ``requests.Response`` the client actually touches."""
def __init__(
self,
status: int = 200,
payload: object = None,
*,
text: str = "",
cookies: dict[str, str] | None = None,
) -> None:
"""Build a response with ``status`` and either JSON or raw text.
``cookies`` land in the session's jar when the response is replayed,
the way ``requests`` absorbs a ``Set-Cookie`` -- the panel's CSRF 403
hands out its ``XSRF-TOKEN`` that way.
"""
self.status_code = status
self.cookies = cookies or {}
self._payload = payload
self.text = text or (json.dumps(payload) if payload is not None else "")
@property
def ok(self) -> bool:
"""Mirror ``requests``: 2xx and 3xx are OK."""
return self.status_code < 400
def json(self) -> object:
"""Return the decoded payload, raising like ``requests`` on non-JSON."""
if self._payload is None:
msg = "no json"
raise ValueError(msg)
return self._payload
class FakeCookieJar(dict[str, str]):
"""A cookie jar with the two methods the client uses."""
def set(self, name: str, value: str, **_kwargs: object) -> None:
"""Store a cookie, ignoring domain/path keywords."""
self[name] = value
class FakeSession:
"""Records requests and replays queued responses in order."""
def __init__(self, responses: list[FakeResponse] | None = None) -> None:
"""Queue ``responses`` to be returned one per request."""
self.headers: dict[str, str] = {}
self.cookies = FakeCookieJar()
self.calls: list[tuple[str, str]] = []
self.bodies: list[dict[str, str]] = []
# A snapshot per request: the session-level dict mutates afterwards.
self.sent_headers: list[dict[str, str]] = []
self._responses: Iterator[FakeResponse] = iter(responses or [])
self.login_cookie: str | None = "session-abc"
self.raise_on_request: Exception | None = None
def request(
self,
method: str,
url: str,
data: dict[str, str] | None = None,
timeout: float | None = None,
) -> FakeResponse:
"""Return the next queued response, recording the call and body."""
del timeout
if self.raise_on_request is not None:
raise self.raise_on_request
self.calls.append((method, url))
self.sent_headers.append(dict(self.headers))
if data is not None:
self.bodies.append(data)
if method == "POST" and url.endswith("auth/login") and self.login_cookie:
self.cookies.set("SESSION", self.login_cookie)
try:
response = next(self._responses)
except StopIteration:
return FakeResponse(200, {})
self.cookies.update(response.cookies)
return response
def fake_requests(session: FakeSession) -> SimpleNamespace:
"""Return a stand-in for the ``requests`` module itself.
A namespace rather than a class: ``requests.Session`` is capitalised, and
defining a method by that name would need an ``N802`` suppression for no
benefit. ``RequestException`` is included because the client catches it.
"""
return SimpleNamespace(
Session=lambda: session,
RequestException=FakeRequestError,
)
class FakeRequestError(Exception):
"""Stands in for ``requests.RequestException``."""
def write_credentials(path: Path, user: str = "me@example.com", pw: str = "pw") -> None:
"""Write a credentials file in the two-line format the reader expects."""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(f"{user}\n{pw}\n", encoding="utf-8")
#: A menu payload shaped like the live one, macros per portion.
def menu_payload(count: int = 2) -> dict[str, object]:
"""Return a ``menus/delivery/{id}/new`` body with ``count`` valid dishes."""
return {
"deliveryMenuMeal": [
{
"menuMealName": f"Danie {index}",
"mealName": "Obiad",
"mealPriority": index,
"nutrition": {
"weight": 300.0,
"calories": 400.0,
"protein": 25.0,
"carbohydrate": 50.0,
"fat": 11.1,
},
}
for index in range(1, count + 1)
],
}
def order_payload(
delivery_id: int = 111, date: str = "2026-08-22"
) -> dict[str, object]:
"""Return an order body embedding one delivery on ``date``."""
return {
"orderId": 1,
"deliveries": [
{"deliveryId": delivery_id, "date": date, "deleted": False},
],
}