- crm/tests: 27 тестов через настоящий server.main на sqlite-бэкенде (валидация заказов, цена только из каталога, honeypot, дедупликация, трекинг и отмена гостем, стоп-лист, аутентификация, гигиена текста, гейт расписания бара) - .gitea/workflows/ci.yml: test-джоба + deploy (только push в main, после тестов): rsync кода -> pre-flight docker compose config -> pg_dump бэкап БД (ротация 7) -> up -d --build -> health-check - деплой на neon-bar86.ru идёт деплой-ключом из secrets репозитория
This commit is contained in:
1 parent
da5b259d06
commit
881ef45be5
8 files changed
+469
No files matched your search
@@ -0,0 +1,119 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Публичное оформление заказа с сайта: валидация состава, honeypot,
|
||||
цена только из каталога, трекинг и отмена гостем.
|
||||
|
||||
Барное время зафиксировано на пятнице 21:00 (см. tests/_server_main.py),
|
||||
поэтому гейт «бар закрыт» не мешает проверять валидацию в любое время суток.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def table(api):
|
||||
status, body = api.request("GET", "/api/halls/names")
|
||||
assert status == 200 and body.get("tables")
|
||||
return body["tables"][0]
|
||||
|
||||
|
||||
_ORDER_SEQ = [0]
|
||||
|
||||
|
||||
def _order(api, table, items, **extra):
|
||||
# уникальный guest_phone на каждый заказ: дедупликация (тот же состав за
|
||||
# минуту → 409) не должна превращать быстрые тесты в ложные конфликты
|
||||
_ORDER_SEQ[0] += 1
|
||||
payload = {
|
||||
"table_name": table,
|
||||
"items": items,
|
||||
"guest_phone": f"+7900000{_ORDER_SEQ[0]:04d}",
|
||||
}
|
||||
payload.update(extra)
|
||||
return api.request("POST", "/api/orders", payload)
|
||||
|
||||
|
||||
def test_order_requires_items(api, table):
|
||||
status, body = api.request("POST", "/api/orders", {"table_name": table})
|
||||
assert status == 400
|
||||
assert "items" in body["error"]
|
||||
|
||||
|
||||
def test_order_unknown_item_rejected(api, table):
|
||||
# состав заказа валидируется строго по каталогу CRM — клиентские
|
||||
# name/price игнорируются, неизвестные позиции отклоняют заказ
|
||||
status, body = _order(api, table, [{"id": "no-such-item", "qty": 1}])
|
||||
assert status == 400
|
||||
|
||||
|
||||
def test_order_unknown_table_rejected(api):
|
||||
status, body = _order(api, "несуществующий-стол-12345", [{"id": "blackberry-mint", "qty": 1}])
|
||||
assert status == 400
|
||||
|
||||
|
||||
def test_honeypot_swallows_bot_order(api, table):
|
||||
# скрытое поле hp_check: отвечаем боту успехом, заказ не создаём
|
||||
status, body = _order(api, table, [{"id": "blackberry-mint", "qty": 1}], hp_check="spam")
|
||||
assert status == 200 and body.get("ok") is True
|
||||
|
||||
|
||||
def test_order_price_comes_from_catalog(api, table):
|
||||
# 2 × 350 ₽ из crm/seed/menu.json: подмена цены клиентом невозможна
|
||||
status, order = _order(api, table, [{"id": "blackberry-mint", "qty": 2, "price": 1}])
|
||||
assert status == 201
|
||||
assert order["status"] == "new"
|
||||
assert order["price"] == 700
|
||||
assert order["code"]
|
||||
|
||||
|
||||
def test_qty_clamped_to_50(api, table):
|
||||
status, order = _order(api, table, [{"id": "blackberry-mint", "qty": 999}])
|
||||
assert status == 201
|
||||
item = next(i for i in order["items"] if i["id"] == "blackberry-mint")
|
||||
assert item["qty"] == 50
|
||||
|
||||
|
||||
def test_track_then_guest_cancel(api, table):
|
||||
status, order = _order(api, table, [{"id": "blackberry-mint", "qty": 1}])
|
||||
assert status == 201
|
||||
code = order["code"]
|
||||
|
||||
status, tracked = api.request("GET", f"/api/track/{code}")
|
||||
assert status == 200
|
||||
assert tracked["status"] == "new"
|
||||
|
||||
# гость отменяет случайный заказ, пока он «new»
|
||||
status, _ = api.request("POST", f"/api/track/{code}/cancel")
|
||||
assert status in (200, 201)
|
||||
|
||||
status, tracked = api.request("GET", f"/api/track/{code}")
|
||||
assert status == 200
|
||||
assert tracked["status"] == "cancelled"
|
||||
|
||||
|
||||
def test_double_cancel_conflict(api, table):
|
||||
status, order = _order(api, table, [{"id": "blackberry-mint", "qty": 1}])
|
||||
assert status == 201
|
||||
code = order["code"]
|
||||
|
||||
status, _ = api.request("POST", f"/api/track/{code}/cancel")
|
||||
assert status in (200, 201)
|
||||
status, body = api.request("POST", f"/api/track/{code}/cancel")
|
||||
assert status == 409
|
||||
|
||||
|
||||
def test_stoplisted_item_rejects_order(api, table, owner_token):
|
||||
# владелец уводит позицию в стоп-лист → заказ с ней отклоняется целиком
|
||||
status, _ = api.request(
|
||||
"PATCH", "/api/menu/blackberry-mint",
|
||||
{"stopped": True}, token=owner_token,
|
||||
)
|
||||
assert status in (200, 201)
|
||||
try:
|
||||
status, body = _order(api, table, [{"id": "blackberry-mint", "qty": 1}])
|
||||
assert status == 400
|
||||
assert "стоп" in body["error"]
|
||||
finally:
|
||||
status, _ = api.request(
|
||||
"PATCH", "/api/menu/blackberry-mint",
|
||||
{"stopped": False}, token=owner_token,
|
||||
)
|
||||
assert status in (200, 201)
|
||||
Reference in new issue
Block a user