zov-tech/tests/test_manager.py
wasrusgen 02f8dba469 feat: expeditor cabinet, electronic signature (OTP+canvas), invoice room picker
New modules:
- expeditor_dashboard.js: route list (date-grouped) + act detail + signature screen
- invoice.js: 3-col chip room picker, 2500₽ base + 1000₽ extra logic
- act4.js, measurer_dashboard.js, finance_summary.js, client_timeline.js, feedback.js, staff_roster.js

Backend:
- /api/expeditor_inbox: filtered assembly list for expeditor role
- /api/act4_request_otp: 6-digit OTP via Telegram, 10-min expiry
- /api/act4_verify_otp: validates code, marks act as signed
- /api/act4_save_signature: saves base64 canvas signature
- Act4s sheet: added signature_b64, otp_code, otp_expires_at columns

Tests:
- tests/expeditor_scenarios.md: 11 manual test scenarios

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 14:11:20 +03:00

301 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Тест кабинета менеджера — полная проверка всех API-модулей
с реальной Telegram-аутентификацией.
Запуск: python -X utf8 tests/test_manager.py
"""
import hmac
import hashlib
import json
import time
import sys
import urllib.request
import urllib.parse
import urllib.error
from typing import Any
# ─── Конфигурация ───────────────────────────────────────────────────────────
BOT_TOKEN = "8281503057:AAEXmOepY8quH8E3RqOjFbgn7owV1ngnbGA"
ADMIN_TG_ID = 5937498515
ADMIN_USERNAME = "wasrusgen"
ADMIN_NAME = "Руслан"
BASE_URL = "https://api.wasrusgen1.pro"
# ─── Генерация валидного initData ───────────────────────────────────────────
def make_init_data(tg_id: int, username: str, first_name: str) -> str:
user_obj = json.dumps({
"id": tg_id,
"first_name": first_name,
"username": username,
"language_code": "ru",
"allows_write_to_pm": True,
}, separators=(",", ":"))
fields = {
"auth_date": str(int(time.time())),
"user": user_obj,
}
data_check_string = "\n".join(f"{k}={fields[k]}" for k in sorted(fields))
secret_key = hmac.new(b"WebAppData", BOT_TOKEN.encode(), hashlib.sha256).digest()
sig = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
params = {**fields, "hash": sig}
return urllib.parse.urlencode(params)
INIT_DATA = make_init_data(ADMIN_TG_ID, ADMIN_USERNAME, ADMIN_NAME)
# ─── HTTP-хелперы ───────────────────────────────────────────────────────────
def post(path: str, payload: dict, timeout=15) -> tuple[int, Any]:
url = f"{BASE_URL}{path}"
data = json.dumps(payload).encode()
try:
req = urllib.request.Request(
url, data=data,
headers={"Content-Type": "application/json", "User-Agent": "zov-manager-test/1.0"},
method="POST",
)
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status, json.loads(r.read())
except urllib.error.HTTPError as e:
try:
return e.code, json.loads(e.read())
except Exception:
return e.code, {}
except Exception as e:
return None, {"_net_error": str(e)}
# ─── Отчёт ──────────────────────────────────────────────────────────────────
RESULTS: list[tuple[bool, str, str]] = [] # (ok, test_name, detail)
def ok(name: str, detail: str = ""):
RESULTS.append((True, name, detail))
icon = ""
print(f" {icon} {name}" + (f"{detail}" if detail else ""))
def fail(name: str, detail: str = ""):
RESULTS.append((False, name, detail))
icon = ""
print(f" {icon} {name}" + (f"{detail}" if detail else ""))
def section(title: str):
print(f"\n{''*55}")
print(f" {title}")
print(f"{''*55}")
# ─── Тесты ──────────────────────────────────────────────────────────────────
def test_auth():
section("🔐 Аутентификация")
status, data = post("/api/me", {
"initData": INIT_DATA,
"role": "manager",
})
if status != 200 or "error" in data:
fail("POST /api/me — вход менеджера", f"status={status} error={data.get('error','?')}")
return False
role = data.get("role", "?")
name = data.get("name", "?")
ok("POST /api/me — вход менеджера", f"role={role} name={name}")
if role not in ("manager", "admin"):
fail("Роль должна быть manager или admin", f"получили: {role}")
return False
ok("Роль подтверждена", role)
return True
def test_clients():
section("👥 Модуль Клиенты")
# Список клиентов
status, data = post("/api/clients", {"initData": INIT_DATA})
if status != 200 or "error" in data:
fail("POST /api/clients — список", f"status={status} {data.get('error','')}")
else:
clients = data.get("clients", [])
ok("POST /api/clients — список", f"{len(clients)} клиентов")
# Проверяем структуру первого клиента
if clients:
c = clients[0]
required_fields = ["client_name", "client_phone"]
missing = [f for f in required_fields if f not in c]
if missing:
fail("Структура клиента — обязательные поля", f"отсутствуют: {missing}")
else:
ok("Структура клиента — обязательные поля", "client_name, client_phone ✓")
def test_measurements():
section("📐 Модуль Замеры")
# Входящие заявки
status, data = post("/api/measurement_inbox", {"initData": INIT_DATA})
if status != 200 or "error" in data:
fail("POST /api/measurement_inbox", f"status={status} {data.get('error','')}")
else:
items = data.get("requests", data.get("items", []))
ok("POST /api/measurement_inbox", f"{len(items)} заявок")
# Список замеров
status, data = post("/api/measurements", {"initData": INIT_DATA})
if status != 200 or "error" in data:
fail("POST /api/measurements — список", f"status={status} {data.get('error','')}")
else:
items = data.get("measurements", [])
ok("POST /api/measurements — список", f"{len(items)} замеров")
if items:
m = items[0]
ok("Первый замер — ID", m.get("id", "?")[:8] + "")
# Следующий номер
status, data = post("/api/measurement_next_no", {"initData": INIT_DATA})
if status != 200 or "error" in data:
fail("POST /api/measurement_next_no", f"status={status} {data.get('error','')}")
else:
ok("POST /api/measurement_next_no", f"следующий №{data.get('next_no','?')}")
def test_assembly():
section("🔧 Модуль Сборки")
status, data = post("/api/assembly_list", {"initData": INIT_DATA})
if status != 200 or "error" in data:
fail("POST /api/assembly_list", f"status={status} {data.get('error','')}")
else:
items = data.get("assemblies", [])
ok("POST /api/assembly_list", f"{len(items)} сборок")
if items:
a = items[0]
has_status = "status" in a
has_address = "address" in a
if has_status and has_address:
ok("Структура сборки", f"status={a['status']} address={a['address'][:20]}")
else:
fail("Структура сборки — поля status/address", f"has_status={has_status} has_address={has_address}")
def test_proposals():
section("📋 Модуль Предложения")
status, data = post("/api/proposal_list", {"initData": INIT_DATA})
if status != 200 or "error" in data:
fail("POST /api/proposal_list", f"status={status} {data.get('error','')}")
else:
items = data.get("proposals", data.get("items", []))
ok("POST /api/proposal_list", f"{len(items)} предложений")
def test_manager_pending():
section("📬 Менеджер — входящие задачи")
status, data = post("/api/manager_pending", {"initData": INIT_DATA})
if status != 200 or "error" in data:
fail("POST /api/manager_pending", f"status={status} {data.get('error','')}")
else:
count = len(data.get("items", data.get("pending", [])))
ok("POST /api/manager_pending", f"{count} задач в очереди")
def test_staff_list():
section("👷 Сотрудники")
for role in ["measurer", "assembler"]:
status, data = post("/api/staff_list", {"initData": INIT_DATA, "role": role})
if status != 200 or "error" in data:
fail(f"POST /api/staff_list role={role}", f"status={status} {data.get('error','')}")
else:
staff = data.get("staff", [])
ok(f"POST /api/staff_list role={role}", f"{len(staff)} сотрудников")
def test_shipments_arrivals():
section("📦 Отгрузки и поступления")
for endpoint in ["/api/shipments", "/api/arrivals"]:
status, data = post(endpoint, {"initData": INIT_DATA})
if status != 200 or "error" in data:
fail(f"POST {endpoint}", f"status={status} {data.get('error','')}")
else:
key = "shipments" if "shipments" in endpoint else "arrivals"
items = data.get(key, data.get("items", data.get("rows", [])))
ok(f"POST {endpoint}", f"{len(items)} записей")
def test_no_500_on_bad_input():
section("🛡️ Устойчивость — невалидные данные не дают 500")
bad_cases = [
("/api/measurement_detail", {"initData": INIT_DATA, "measurement_id": "nonexistent-000"}),
("/api/assembly_detail", {"initData": INIT_DATA, "assembly_id": "nonexistent-000"}),
("/api/client_create", {"initData": INIT_DATA, "client_name": "", "client_phone": ""}),
("/api/assembly_create", {"initData": INIT_DATA, "client_name": "", "address": "", "scope_of_work": ""}),
]
for path, payload in bad_cases:
status, data = post(path, payload)
if status == 500:
fail(f"POST {path} с плохими данными → 500!", f"ответ: {str(data)[:80]}")
else:
ok(f"POST {path} с плохими данными → не 500", f"status={status} error={data.get('error','?')[:40]}")
# ─── Main ───────────────────────────────────────────────────────────────────
def main():
print(f"\n{'='*55}")
print(f" ТЕСТ КАБИНЕТА МЕНЕДЖЕРА — @wasrusgen1bot")
print(f" Пользователь: @{ADMIN_USERNAME} (id={ADMIN_TG_ID})")
print(f" Сервер: {BASE_URL}")
print(f"{'='*55}")
t0 = time.time()
auth_ok = test_auth()
if not auth_ok:
print("\n🚫 Аутентификация провалена — дальнейшие тесты невозможны.\n")
sys.exit(1)
test_clients()
test_measurements()
test_assembly()
test_proposals()
test_manager_pending()
test_staff_list()
test_shipments_arrivals()
test_no_500_on_bad_input()
elapsed = time.time() - t0
passed = sum(1 for ok_, _, _ in RESULTS if ok_)
failed = len(RESULTS) - passed
print(f"\n{'='*55}")
print(f" ИТОГО: {passed} ✅ / {failed} ❌ ({elapsed:.1f}s)")
print(f"{'='*55}\n")
if failed:
print("📋 ЗАМЕЧАНИЯ К УСТРАНЕНИЮ:\n")
for ok_, name, detail in RESULTS:
if not ok_:
print(f"{name}")
if detail:
print(f"{detail}")
print()
sys.exit(1)
else:
print("✅ Кабинет менеджера работает штатно.\n")
sys.exit(0)
if __name__ == "__main__":
main()