All checks were successful
Deploy Trade-In / test (push) Successful in 4m59s
Deploy Trade-In / build-backend (push) Successful in 1m10s
Deploy Trade-In / deploy (push) Successful in 1m17s
Deploy Trade-In / changes (push) Successful in 14s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
536 lines
22 KiB
Python
536 lines
22 KiB
Python
"""Offline-тесты веб-чата поддержки (#tgsupport-web) —
|
||
POST/GET /api/v1/trade-in/support/{messages,unread,read}.
|
||
|
||
Storage-слой (`app.services.tgbot.web_support_storage`) мокается целиком (как
|
||
mock'ается DB в test_trade_in_lead.py) — эти тесты проверяют РОУТЕР: изоляцию
|
||
тредов (нет параметра, которым можно адресовать чужой тред), валидацию
|
||
входа, поведение при несконфигурированном боте, rate-limit и то, что реплай
|
||
парсится в `bridge.py` в web-ветку (см. tests/services/tgbot/test_bridge.py —
|
||
маршрутизация реплая тестируется ТАМ; здесь — только HTTP-контракт отправки/
|
||
чтения).
|
||
|
||
NEVER touches real DB / real Telegram API.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
|
||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||
|
||
from typing import Any, ClassVar
|
||
from unittest.mock import MagicMock
|
||
|
||
import pytest
|
||
from fastapi import FastAPI
|
||
from fastapi.testclient import TestClient
|
||
|
||
from app.api.v1 import support as support_module
|
||
from app.core.db import get_db
|
||
from app.core.ratelimit import SlidingWindowLimiter
|
||
from app.services.tgbot.client import TelegramApiError
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _bot_configured(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
"""По умолчанию бот считается сконфигурированным — отдельные тесты переопределяют."""
|
||
monkeypatch.setattr(support_module.settings, "telegram_bot_token", "fake-token")
|
||
monkeypatch.setattr(support_module.settings, "telegram_support_chat_id", -100123456789)
|
||
monkeypatch.setattr(support_module.settings, "telegram_support_topic_id", 42)
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _fresh_rate_limiter(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
"""Модульный `_send_limiter` иначе накапливает состояние МЕЖДУ тестами (один
|
||
процесс pytest) — свежий лимитер на каждый тест, щедрый дефолт (rate-limit
|
||
тестируется отдельно на СВОЁМ, явно узком экземпляре)."""
|
||
monkeypatch.setattr(
|
||
support_module, "_send_limiter", SlidingWindowLimiter(limit=1000, window_s=60.0)
|
||
)
|
||
|
||
|
||
class _FakeTelegramClient:
|
||
"""Подменяет `TelegramClient` внутри `support` модуля — никакого httpx/сети."""
|
||
|
||
calls: ClassVar[list[dict[str, Any]]] = []
|
||
_response: ClassVar[dict[str, Any] | Exception] = {"message_id": 555}
|
||
|
||
def __init__(self, _token: str) -> None:
|
||
pass
|
||
|
||
async def send_message(self, **kwargs: Any) -> dict[str, Any]:
|
||
_FakeTelegramClient.calls.append(kwargs)
|
||
if isinstance(_FakeTelegramClient._response, Exception):
|
||
raise _FakeTelegramClient._response
|
||
return _FakeTelegramClient._response
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _fake_telegram_client(monkeypatch: pytest.MonkeyPatch) -> Any:
|
||
_FakeTelegramClient.calls = []
|
||
_FakeTelegramClient._response = {"message_id": 555}
|
||
monkeypatch.setattr(support_module, "TelegramClient", _FakeTelegramClient)
|
||
return _FakeTelegramClient
|
||
|
||
|
||
@pytest.fixture
|
||
def db() -> MagicMock:
|
||
return MagicMock()
|
||
|
||
|
||
@pytest.fixture
|
||
def client(db: MagicMock) -> TestClient:
|
||
app = FastAPI()
|
||
app.include_router(support_module.router, prefix="/api/v1/trade-in")
|
||
|
||
def fake_db() -> Any:
|
||
yield db
|
||
|
||
app.dependency_overrides[get_db] = fake_db
|
||
return TestClient(app)
|
||
|
||
|
||
def _auth(username: str = "alice") -> dict[str, str]:
|
||
return {"x-authenticated-user": username}
|
||
|
||
|
||
# ── auth guard ────────────────────────────────────────────────────────────────
|
||
|
||
|
||
def test_send_message_without_auth_header_401(client: TestClient) -> None:
|
||
r = client.post("/api/v1/trade-in/support/messages", json={"text": "hi"})
|
||
assert r.status_code == 401
|
||
|
||
|
||
def test_list_messages_without_auth_header_401(client: TestClient) -> None:
|
||
r = client.get("/api/v1/trade-in/support/messages")
|
||
assert r.status_code == 401
|
||
|
||
|
||
def test_whitespace_only_auth_header_401(client: TestClient) -> None:
|
||
"""review L4: заголовок из одних пробелов после `.strip()` пуст — не должен
|
||
считаться валидным auth (не проваливается тихо в "" как username)."""
|
||
r = client.get("/api/v1/trade-in/support/messages", headers={"x-authenticated-user": " "})
|
||
assert r.status_code == 401
|
||
|
||
|
||
def test_auth_header_with_surrounding_whitespace_is_stripped(
|
||
client: TestClient, monkeypatch: pytest.MonkeyPatch
|
||
) -> None:
|
||
"""review L4: лишний пробел от прокси не должен заводить ВТОРОЙ тред для, по
|
||
сути, того же пользователя — username резолвится в тред по .strip()-нутому
|
||
значению."""
|
||
seen_usernames = []
|
||
|
||
def fake_find_thread_id(db, username):
|
||
seen_usernames.append(username)
|
||
return None
|
||
|
||
monkeypatch.setattr(support_module.storage, "find_thread_id", fake_find_thread_id)
|
||
|
||
r = client.get(
|
||
"/api/v1/trade-in/support/messages", headers={"x-authenticated-user": " alice "}
|
||
)
|
||
assert r.status_code == 200
|
||
assert seen_usernames == ["alice"]
|
||
|
||
|
||
# ── validation ────────────────────────────────────────────────────────────────
|
||
|
||
|
||
def test_send_message_blank_text_422(client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
|
||
called = []
|
||
monkeypatch.setattr(
|
||
support_module.storage, "get_or_create_thread", lambda *a, **kw: called.append(1) or 1
|
||
)
|
||
r = client.post("/api/v1/trade-in/support/messages", json={"text": " "}, headers=_auth())
|
||
assert r.status_code == 422, r.text
|
||
assert called == [] # ничего не персистится на невалидном вводе
|
||
|
||
|
||
def test_send_message_empty_text_422(client: TestClient) -> None:
|
||
r = client.post("/api/v1/trade-in/support/messages", json={"text": ""}, headers=_auth())
|
||
assert r.status_code == 422
|
||
|
||
|
||
def test_send_message_too_long_422(client: TestClient) -> None:
|
||
too_long = "a" * (support_module.MAX_MESSAGE_LENGTH + 1)
|
||
r = client.post("/api/v1/trade-in/support/messages", json={"text": too_long}, headers=_auth())
|
||
assert r.status_code == 422
|
||
|
||
|
||
def test_send_message_at_max_length_ok(client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
|
||
monkeypatch.setattr(support_module.storage, "get_or_create_thread", lambda *a, **kw: 1)
|
||
monkeypatch.setattr(
|
||
support_module.storage,
|
||
"record_inbound",
|
||
lambda *a, **kw: {
|
||
"id": 1,
|
||
"direction": "in",
|
||
"text_body": kw["text_body"],
|
||
"operator_tg_id": None,
|
||
"created_at": "2026-07-26T00:00:00+00:00",
|
||
},
|
||
)
|
||
at_limit = "a" * support_module.MAX_MESSAGE_LENGTH
|
||
r = client.post("/api/v1/trade-in/support/messages", json={"text": at_limit}, headers=_auth())
|
||
assert r.status_code == 200, r.text
|
||
|
||
|
||
# ── bot not configured ──────────────────────────────────────────────────────
|
||
|
||
|
||
def test_send_message_bot_token_empty_returns_503_not_500(
|
||
client: TestClient, monkeypatch: pytest.MonkeyPatch
|
||
) -> None:
|
||
monkeypatch.setattr(support_module.settings, "telegram_bot_token", "")
|
||
thread_created = []
|
||
monkeypatch.setattr(
|
||
support_module.storage, "get_or_create_thread", lambda *a, **kw: thread_created.append(1)
|
||
)
|
||
|
||
r = client.post("/api/v1/trade-in/support/messages", json={"text": "hi"}, headers=_auth())
|
||
|
||
assert r.status_code == 503
|
||
assert r.status_code != 500
|
||
assert thread_created == [] # ничего не создаём/не пишем, если зеркалировать некуда
|
||
|
||
|
||
def test_send_message_support_chat_id_unset_returns_503(
|
||
client: TestClient, monkeypatch: pytest.MonkeyPatch
|
||
) -> None:
|
||
monkeypatch.setattr(support_module.settings, "telegram_support_chat_id", 0)
|
||
r = client.post("/api/v1/trade-in/support/messages", json={"text": "hi"}, headers=_auth())
|
||
assert r.status_code == 503
|
||
|
||
|
||
# ── happy path + mirror content ──────────────────────────────────────────────
|
||
|
||
|
||
def test_send_message_happy_path_mirrors_with_website_marker(
|
||
client: TestClient, db: MagicMock, monkeypatch: pytest.MonkeyPatch, _fake_telegram_client: Any
|
||
) -> None:
|
||
thread_calls: list[str] = []
|
||
|
||
def fake_get_or_create_thread(db, username):
|
||
thread_calls.append(username)
|
||
return 7
|
||
|
||
monkeypatch.setattr(support_module.storage, "get_or_create_thread", fake_get_or_create_thread)
|
||
recorded = {}
|
||
|
||
def fake_record_inbound(db, *, thread_id, text_body, topic_message_id, support_chat_id):
|
||
recorded.update(
|
||
thread_id=thread_id,
|
||
text_body=text_body,
|
||
topic_message_id=topic_message_id,
|
||
support_chat_id=support_chat_id,
|
||
)
|
||
return {
|
||
"id": 100,
|
||
"direction": "in",
|
||
"text_body": text_body,
|
||
"operator_tg_id": None,
|
||
"created_at": "2026-07-26T00:00:00+00:00",
|
||
}
|
||
|
||
monkeypatch.setattr(support_module.storage, "record_inbound", fake_record_inbound)
|
||
|
||
r = client.post(
|
||
"/api/v1/trade-in/support/messages",
|
||
json={"text": "У меня вопрос про trade-in"},
|
||
headers=_auth("kopylov"),
|
||
)
|
||
|
||
assert r.status_code == 200, r.text
|
||
body = r.json()
|
||
assert body["id"] == 100
|
||
assert body["direction"] == "in"
|
||
|
||
# Зеркало помечено "С САЙТА" + username — оператор не путает с TG-клиентом.
|
||
assert len(_fake_telegram_client.calls) == 1
|
||
mirror_call = _fake_telegram_client.calls[0]
|
||
assert "С САЙТА" in mirror_call["text"]
|
||
assert "kopylov" in mirror_call["text"]
|
||
assert "У меня вопрос про trade-in" in mirror_call["text"]
|
||
assert mirror_call["chat_id"] == support_module.settings.telegram_support_chat_id
|
||
assert mirror_call["message_thread_id"] == 42
|
||
# review H1: интерактивный узкий бюджет ретраев/timeout, не воркерный дефолт.
|
||
assert mirror_call["max_retries"] == support_module._INTERACTIVE_SEND_MAX_RETRIES
|
||
assert mirror_call["timeout"] == support_module._INTERACTIVE_SEND_TIMEOUT_S
|
||
|
||
assert recorded["thread_id"] == 7
|
||
assert recorded["topic_message_id"] == 555 # из FakeTelegramClient.send_message result
|
||
assert recorded["support_chat_id"] == support_module.settings.telegram_support_chat_id
|
||
assert thread_calls == ["kopylov"]
|
||
assert db.commit.called
|
||
|
||
|
||
def test_send_message_telegram_failure_returns_502_and_does_not_persist(
|
||
client: TestClient, monkeypatch: pytest.MonkeyPatch, _fake_telegram_client: Any
|
||
) -> None:
|
||
_fake_telegram_client._response = TelegramApiError("sendMessage", 400, "chat not found")
|
||
thread_created = []
|
||
monkeypatch.setattr(
|
||
support_module.storage,
|
||
"get_or_create_thread",
|
||
lambda db, username: thread_created.append(1),
|
||
)
|
||
record_called = []
|
||
monkeypatch.setattr(
|
||
support_module.storage,
|
||
"record_inbound",
|
||
lambda *a, **kw: record_called.append(1),
|
||
)
|
||
|
||
r = client.post("/api/v1/trade-in/support/messages", json={"text": "hi"}, headers=_auth())
|
||
|
||
assert r.status_code == 502
|
||
assert record_called == [] # неотправленное сообщение не персистится
|
||
# review H1: thread создаётся ПОСЛЕ успешной отправки — на неудаче до БД не доходит вообще.
|
||
assert thread_created == []
|
||
|
||
|
||
# ── rate limit ────────────────────────────────────────────────────────────────
|
||
|
||
|
||
def test_send_message_rate_limited_429(client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
|
||
monkeypatch.setattr(
|
||
support_module, "_send_limiter", SlidingWindowLimiter(limit=1, window_s=60.0)
|
||
)
|
||
monkeypatch.setattr(support_module.storage, "get_or_create_thread", lambda db, username: 1)
|
||
monkeypatch.setattr(
|
||
support_module.storage,
|
||
"record_inbound",
|
||
lambda *a, **kw: {
|
||
"id": 1,
|
||
"direction": "in",
|
||
"text_body": kw["text_body"],
|
||
"operator_tg_id": None,
|
||
"created_at": "2026-07-26T00:00:00+00:00",
|
||
},
|
||
)
|
||
|
||
first = client.post("/api/v1/trade-in/support/messages", json={"text": "one"}, headers=_auth())
|
||
assert first.status_code == 200, first.text
|
||
|
||
second = client.post("/api/v1/trade-in/support/messages", json={"text": "two"}, headers=_auth())
|
||
assert second.status_code == 429
|
||
assert "Retry-After" in second.headers
|
||
|
||
|
||
def test_send_message_failed_attempts_do_not_consume_rate_limit(
|
||
client: TestClient, monkeypatch: pytest.MonkeyPatch, _fake_telegram_client: Any
|
||
) -> None:
|
||
"""review L3: неудачная отправка НЕ должна расходовать rate-limit бюджет —
|
||
иначе клиент, которому не повезло с транзиентной Telegram-ошибкой, терял бы
|
||
попытки, не доставив НИ ОДНОГО сообщения."""
|
||
monkeypatch.setattr(
|
||
support_module, "_send_limiter", SlidingWindowLimiter(limit=1, window_s=60.0)
|
||
)
|
||
_fake_telegram_client._response = TelegramApiError("sendMessage", 500, "boom")
|
||
|
||
for _ in range(3):
|
||
r = client.post("/api/v1/trade-in/support/messages", json={"text": "hi"}, headers=_auth())
|
||
assert r.status_code == 502
|
||
|
||
# "Телеграм" снова работает — бюджет (лимит=1) всё ещё цел, ни одна неудачная
|
||
# попытка выше его не тронула.
|
||
_fake_telegram_client._response = {"message_id": 555}
|
||
monkeypatch.setattr(support_module.storage, "get_or_create_thread", lambda db, username: 1)
|
||
monkeypatch.setattr(
|
||
support_module.storage,
|
||
"record_inbound",
|
||
lambda *a, **kw: {
|
||
"id": 1,
|
||
"direction": "in",
|
||
"text_body": kw["text_body"],
|
||
"operator_tg_id": None,
|
||
"created_at": "2026-07-26T00:00:00+00:00",
|
||
},
|
||
)
|
||
ok = client.post("/api/v1/trade-in/support/messages", json={"text": "ok"}, headers=_auth())
|
||
assert ok.status_code == 200, ok.text
|
||
|
||
|
||
def test_send_message_rate_limit_is_per_user(
|
||
client: TestClient, monkeypatch: pytest.MonkeyPatch
|
||
) -> None:
|
||
monkeypatch.setattr(
|
||
support_module, "_send_limiter", SlidingWindowLimiter(limit=1, window_s=60.0)
|
||
)
|
||
monkeypatch.setattr(support_module.storage, "get_or_create_thread", lambda db, username: 1)
|
||
monkeypatch.setattr(
|
||
support_module.storage,
|
||
"record_inbound",
|
||
lambda *a, **kw: {
|
||
"id": 1,
|
||
"direction": "in",
|
||
"text_body": kw["text_body"],
|
||
"operator_tg_id": None,
|
||
"created_at": "2026-07-26T00:00:00+00:00",
|
||
},
|
||
)
|
||
|
||
alice = client.post(
|
||
"/api/v1/trade-in/support/messages", json={"text": "one"}, headers=_auth("alice")
|
||
)
|
||
assert alice.status_code == 200
|
||
|
||
bob = client.post(
|
||
"/api/v1/trade-in/support/messages", json={"text": "one"}, headers=_auth("bob")
|
||
)
|
||
assert bob.status_code == 200 # свой ключ лимита — не затронут alice-лимитом
|
||
|
||
|
||
# ── thread isolation (own thread only, no thread_id param exists) ───────────
|
||
|
||
|
||
def test_list_messages_resolves_by_own_username_ignores_foreign_params(
|
||
client: TestClient, monkeypatch: pytest.MonkeyPatch
|
||
) -> None:
|
||
"""Никакого способа адресовать чужой тред: даже если запрос несёт посторонний
|
||
`thread_id`/`username` в query — эндпоинт их не читает, резолвит ИСКЛЮЧИТЕЛЬНО
|
||
по X-Authenticated-User."""
|
||
seen_usernames = []
|
||
|
||
def fake_find_thread_id(db, username):
|
||
seen_usernames.append(username)
|
||
return None
|
||
|
||
monkeypatch.setattr(support_module.storage, "find_thread_id", fake_find_thread_id)
|
||
|
||
r = client.get(
|
||
"/api/v1/trade-in/support/messages",
|
||
params={"thread_id": 999, "username": "someone-else", "since": 0},
|
||
headers=_auth("alice"),
|
||
)
|
||
assert r.status_code == 200
|
||
assert r.json() == []
|
||
assert seen_usernames == ["alice"] # username из заголовка — параметры query проигнорированы
|
||
|
||
|
||
def test_list_messages_returns_thread_scoped_rows(
|
||
client: TestClient, monkeypatch: pytest.MonkeyPatch
|
||
) -> None:
|
||
monkeypatch.setattr(support_module.storage, "find_thread_id", lambda db, username: 7)
|
||
monkeypatch.setattr(
|
||
support_module.storage,
|
||
"list_messages",
|
||
lambda db, *, thread_id, since_id, limit: [
|
||
{
|
||
"id": 1,
|
||
"direction": "in",
|
||
"text_body": "hi",
|
||
"operator_tg_id": None,
|
||
"created_at": "2026-07-26T00:00:00+00:00",
|
||
}
|
||
]
|
||
if thread_id == 7
|
||
else [],
|
||
)
|
||
r = client.get("/api/v1/trade-in/support/messages", params={"since": 0}, headers=_auth("alice"))
|
||
assert r.status_code == 200
|
||
body = r.json()
|
||
assert len(body) == 1
|
||
assert body[0]["text_body"] == "hi"
|
||
|
||
|
||
def test_list_messages_passes_bounded_limit_to_storage(
|
||
client: TestClient, monkeypatch: pytest.MonkeyPatch
|
||
) -> None:
|
||
"""review M5: без LIMIT каждое монтирование виджета отдавало бы весь тред."""
|
||
seen_limits = []
|
||
|
||
def fake_list_messages(db, *, thread_id, since_id, limit):
|
||
seen_limits.append(limit)
|
||
return []
|
||
|
||
monkeypatch.setattr(support_module.storage, "find_thread_id", lambda db, username: 7)
|
||
monkeypatch.setattr(support_module.storage, "list_messages", fake_list_messages)
|
||
|
||
r = client.get("/api/v1/trade-in/support/messages", headers=_auth())
|
||
assert r.status_code == 200
|
||
assert seen_limits == [support_module._LIST_MESSAGES_LIMIT]
|
||
|
||
|
||
def test_two_users_get_independent_threads(
|
||
client: TestClient, monkeypatch: pytest.MonkeyPatch
|
||
) -> None:
|
||
thread_by_user = {"alice": 1, "bob": 2}
|
||
monkeypatch.setattr(
|
||
support_module.storage, "find_thread_id", lambda db, username: thread_by_user.get(username)
|
||
)
|
||
monkeypatch.setattr(
|
||
support_module.storage,
|
||
"list_messages",
|
||
lambda db, *, thread_id, since_id, limit: [
|
||
{
|
||
"id": 1,
|
||
"direction": "in",
|
||
"text_body": f"secret-of-thread-{thread_id}",
|
||
"operator_tg_id": None,
|
||
"created_at": "2026-07-26T00:00:00+00:00",
|
||
}
|
||
],
|
||
)
|
||
|
||
alice_resp = client.get("/api/v1/trade-in/support/messages", headers=_auth("alice")).json()
|
||
bob_resp = client.get("/api/v1/trade-in/support/messages", headers=_auth("bob")).json()
|
||
|
||
assert alice_resp[0]["text_body"] == "secret-of-thread-1"
|
||
assert bob_resp[0]["text_body"] == "secret-of-thread-2"
|
||
assert alice_resp != bob_resp
|
||
|
||
|
||
# ── unread / read ─────────────────────────────────────────────────────────────
|
||
|
||
|
||
def test_unread_no_thread_returns_zero_without_querying_count(
|
||
client: TestClient, monkeypatch: pytest.MonkeyPatch
|
||
) -> None:
|
||
monkeypatch.setattr(support_module.storage, "find_thread_id", lambda db, username: None)
|
||
count_called = []
|
||
monkeypatch.setattr(
|
||
support_module.storage, "count_unread", lambda *a, **kw: count_called.append(1)
|
||
)
|
||
|
||
r = client.get("/api/v1/trade-in/support/unread", headers=_auth())
|
||
assert r.status_code == 200
|
||
assert r.json() == {"unread": 0}
|
||
assert count_called == []
|
||
|
||
|
||
def test_unread_delegates_to_storage(client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
|
||
monkeypatch.setattr(support_module.storage, "find_thread_id", lambda db, username: 7)
|
||
monkeypatch.setattr(support_module.storage, "count_unread", lambda db, *, thread_id: 3)
|
||
|
||
r = client.get("/api/v1/trade-in/support/unread", headers=_auth())
|
||
assert r.status_code == 200
|
||
assert r.json() == {"unread": 3}
|
||
|
||
|
||
def test_mark_read_noop_when_no_thread(client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
|
||
monkeypatch.setattr(support_module.storage, "find_thread_id", lambda db, username: None)
|
||
mark_called = []
|
||
monkeypatch.setattr(support_module.storage, "mark_read", lambda *a, **kw: mark_called.append(1))
|
||
|
||
r = client.post("/api/v1/trade-in/support/read", headers=_auth())
|
||
assert r.status_code == 200
|
||
assert r.json() == {"status": "ok"}
|
||
assert mark_called == []
|
||
|
||
|
||
def test_mark_read_calls_storage_when_thread_exists(
|
||
client: TestClient, db: MagicMock, monkeypatch: pytest.MonkeyPatch
|
||
) -> None:
|
||
monkeypatch.setattr(support_module.storage, "find_thread_id", lambda db, username: 7)
|
||
mark_called = []
|
||
monkeypatch.setattr(
|
||
support_module.storage, "mark_read", lambda db, *, thread_id: mark_called.append(thread_id)
|
||
)
|
||
|
||
r = client.post("/api/v1/trade-in/support/read", headers=_auth())
|
||
assert r.status_code == 200
|
||
assert mark_called == [7]
|
||
assert db.commit.called
|