mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 18:07:20 +01:00
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
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
162 lines
5.4 KiB
Python
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},
|
|
],
|
|
}
|