Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android

This commit is contained in:
darkorion
2026-10-13 09:09:53 +03:00
commit c298f5a830
247 changed files with 23200 additions and 0 deletions
+333
View File
@@ -0,0 +1,333 @@
"""Портал: инвентарь и склады, заявки на инструмент, инциденты, суточные задания, карта, карточка сотрудника, отчёт."""
import os
import tempfile
from datetime import timedelta
import pytest
os.environ.setdefault("DATABASE_URL", os.environ.get("TEST_DATABASE_URL") or "sqlite:///" + os.path.join(tempfile.mkdtemp(), "portal.db").replace("\\", "/"))
os.environ.setdefault("SCHEDULER_INTERVAL", "3600")
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-pytest-0123456789abcdef")
os.environ.setdefault("DATA_DIR", tempfile.mkdtemp())
from fastapi.testclient import TestClient # noqa: E402
from app import config # noqa: E402
from app.main import app # noqa: E402
V1 = "/api/v1"
PNG = b"\x89PNG\r\n\x1a\n" + b"\0" * 64
PW = "Pass1234567"
@pytest.fixture(scope="module")
def env():
with TestClient(app) as c:
def login(u, p=PW):
r = c.post(f"{V1}/auth/login", json={"username": u, "password": p})
assert r.status_code == 200, r.text
return {"Authorization": "Bearer " + r.json()["access_token"]}
adm = login("admin", "admin")
tech_pos = c.post(f"{V1}/positions", headers=adm, json={"name": "П-Слесарь", "kind": "technician"}).json()
eng_pos = c.post(f"{V1}/positions", headers=adm, json={"name": "П-Инженер", "kind": "engineer"}).json()
ids = {}
for name, pos, extra in (("p_tech", tech_pos, {}), ("p_tech2", tech_pos, {}), ("p_eng", eng_pos, {}),
("p_store", tech_pos, {"is_storekeeper": True})):
r = c.post(f"{V1}/users", headers=adm, json={"username": name, "password": PW, "full_name": name.upper(),
"position_id": pos["id"], **extra})
assert r.status_code == 201, r.text
ids[name] = r.json()["id"]
eq = c.post(f"{V1}/equipment", headers=adm, json={"name": "П-Насос", "icon": "builtin:pump"}).json()
unit = c.post(f"{V1}/equipment/{eq['id']}/service-units", headers=adm,
json={"name": "Подшипник", "mode": "hours", "interval_hours": 100}).json()
yield {"c": c, "adm": adm, "tech": login("p_tech"), "tech2": login("p_tech2"), "eng": login("p_eng"),
"store": login("p_store"), "ids": ids, "eq": eq, "unit": unit}
def test_inventory_roles_and_moves(env):
c, store, tech, adm = env["c"], env["store"], env["tech"], env["adm"]
tid = env["ids"]["p_tech"]
assert c.get(f"{V1}/me", headers=store).json()["is_storekeeper"] is True
# сотрудник не может править каталог и склады
assert c.post(f"{V1}/warehouses", headers=tech, json={"name": "Х"}).status_code == 403
assert c.post(f"{V1}/inventory/items", headers=tech, json={"name": "Х"}).status_code == 403
w1 = c.post(f"{V1}/warehouses", headers=store, json={"name": "Склад №1"}).json()
w2 = c.post(f"{V1}/warehouses", headers=store, json={"name": "Склад №2"}).json()
drill = c.post(f"{V1}/inventory/items", headers=store, json={"name": "Дрель", "category": "Электроинструмент",
"inventory_no": "ИН-1"}).json()
keys = c.post(f"{V1}/inventory/items", headers=store, json={"name": "Набор ключей"}).json()
mv = lambda body, h=store: c.post(f"{V1}/inventory/moves", headers=h, json=body)
assert mv({"kind": "receive", "item_id": drill["id"], "qty": 2, "to": {"warehouse_id": w1["id"]}}).status_code == 201
assert mv({"kind": "receive", "item_id": keys["id"], "qty": 5, "to": {"warehouse_id": w1["id"]}}).status_code == 201
# сотрудник не может выдавать
assert mv({"kind": "move", "item_id": drill["id"], "from": {"warehouse_id": w1["id"]}, "to": {"user_id": tid}}, tech).status_code == 403
# выдача лично сотруднику
r = mv({"kind": "move", "item_id": drill["id"], "qty": 1, "from": {"warehouse_id": w1["id"]}, "to": {"user_id": tid}})
assert r.status_code == 201, r.text
# нельзя выдать больше, чем есть
assert mv({"kind": "move", "item_id": drill["id"], "qty": 5, "from": {"warehouse_id": w1["id"]}, "to": {"user_id": tid}}).status_code == 422
# перемещение между складами, списание
assert mv({"kind": "move", "item_id": keys["id"], "qty": 2, "from": {"warehouse_id": w1["id"]}, "to": {"warehouse_id": w2["id"]}}).status_code == 201
assert mv({"kind": "writeoff", "item_id": keys["id"], "qty": 1, "from": {"warehouse_id": w2["id"]}, "note": "сломан"}).status_code == 201
# «откуда» должно быть ровно одно
assert mv({"kind": "move", "item_id": keys["id"], "from": {"warehouse_id": w1["id"], "user_id": tid}, "to": {"warehouse_id": w2["id"]}}).status_code == 422
# сотрудник видит только своё
mine = c.get(f"{V1}/me/inventory", headers=tech).json()
assert [(x["item_name"], x["qty"]) for x in mine] == [("Дрель", 1)]
assert c.get(f"{V1}/inventory/stock", headers=tech, params={"warehouse_id": w1["id"]}).status_code == 403
assert c.get(f"{V1}/inventory/stock", headers=tech).json() == mine
st = {x["item_name"]: x["qty"] for x in c.get(f"{V1}/inventory/stock", headers=store, params={"warehouse_id": w1["id"]}).json()}
assert st == {"Дрель": 1, "Набор ключей": 3}
items = {x["name"]: x for x in c.get(f"{V1}/inventory/items", headers=store).json()}
assert items["Дрель"]["in_stock"] == 1 and items["Дрель"]["issued"] == 1
whs = {x["name"]: x for x in c.get(f"{V1}/warehouses", headers=tech).json()}
assert whs["Склад №2"]["total_qty"] == 1
moves = c.get(f"{V1}/inventory/moves", headers=store, params={"item_id": drill["id"]}).json()
assert [m["kind"] for m in moves] == ["issue", "receive"]
assert [m["kind"] for m in c.get(f"{V1}/inventory/moves", headers=tech).json()] == ["issue"]
# возврат на склад
assert mv({"kind": "move", "item_id": drill["id"], "from": {"user_id": tid}, "to": {"warehouse_id": w1["id"]}}).status_code == 201
assert c.get(f"{V1}/me/inventory", headers=tech).json() == []
# склад с инвентарём не удаляется; зона на карте
assert c.delete(f"{V1}/warehouses/{w1['id']}", headers=store).status_code == 409
assert c.put(f"{V1}/warehouses/{w1['id']}/zone", headers=store, json={"zone": {"x": 10, "y": 10, "w": 20, "h": 15}}).status_code == 200
assert c.put(f"{V1}/warehouses/{w1['id']}/zone", headers=store, json={"zone": {"x": 90, "y": 10, "w": 20, "h": 15}}).status_code == 422
assert c.put(f"{V1}/warehouses/{w1['id']}/zone", headers=tech, json={"zone": None}).status_code == 403
# обычный администратор тоже может всё
assert c.post(f"{V1}/warehouses", headers=adm, json={"name": "Склад админа"}).status_code == 201
def test_tool_requests(env):
c, tech, tech2, store = env["c"], env["tech"], env["tech2"], env["store"]
bad = c.post(f"{V1}/tool-requests", headers=tech, json={"what": "Мультиметр", "why": "Старый сломан", "link": "javascript:alert(1)"})
assert bad.status_code == 422
assert c.post(f"{V1}/tool-requests", headers=tech, json={"what": "Мультиметр"}).status_code == 422 # «зачем» обязательно
r = c.post(f"{V1}/tool-requests", headers=tech, json={"what": "Мультиметр Fluke", "why": "Старый сломан",
"link": "https://example.com/item/1", "qty": 1}).json()
assert r["status"] == "new" and r["author_name"] == "P_TECH"
c.post(f"{V1}/tool-requests", headers=tech2, json={"what": "Перчатки", "why": "Износились"})
assert [x["what"] for x in c.get(f"{V1}/tool-requests", headers=tech).json()] == ["Мультиметр Fluke"]
assert len(c.get(f"{V1}/tool-requests", headers=store).json()) >= 2
assert c.post(f"{V1}/tool-requests/{r['id']}/answer", headers=tech, json={"status": "approved"}).status_code == 403
assert c.post(f"{V1}/tool-requests/{r['id']}/answer", headers=store, json={"status": "rejected"}).status_code == 422
a = c.post(f"{V1}/tool-requests/{r['id']}/answer", headers=store, json={"status": "ordered", "answer": "Заказан"}).json()
assert a["status"] == "ordered" and a["handled_by_name"] == "P_STORE"
assert c.delete(f"{V1}/tool-requests/{r['id']}", headers=tech).status_code == 403 # уже не новая
def test_incidents_affect_state(env):
c, tech, eng, adm, eq, unit = env["c"], env["tech"], env["eng"], env["adm"], env["eq"], env["unit"]
c.post(f"{V1}/equipment/{eq['id']}/meter", headers=eng, json={"hours": 95}) # узел «скоро» (осталось 5 из 100)
h = c.get(f"{V1}/equipment-health", headers=tech).json()[str(eq["id"])]
assert h["state"] == "soon" and h["soon"] == 1
other = c.post(f"{V1}/equipment", headers=adm, json={"name": "П-Другая"}).json()
other_unit = c.post(f"{V1}/equipment/{other['id']}/service-units", headers=adm, json={"name": "X", "mode": "hours", "interval_hours": 10}).json()
assert c.post(f"{V1}/incidents", headers=tech, json={"equipment_id": eq["id"], "title": "Течь",
"units": [{"unit_id": other_unit["id"]}]}).status_code == 422
r = c.post(f"{V1}/incidents", headers=tech, json={"equipment_id": eq["id"], "title": "Заклинил подшипник",
"description": "Шум, нагрев", "units": [{"unit_id": unit["id"], "replaced": True}],
"replacement_note": "Поставлен новый"})
assert r.status_code == 201, r.text
inc = r.json()
assert inc["replaced"] is True and inc["units"][0]["unit_name"] == "Подшипник" and inc["status"] == "open"
# замена сбросила узел, открытый инцидент — «Авария»
u = c.get(f"{V1}/equipment/{eq['id']}/service-units", headers=tech).json()[0]
assert u["state"] == "ok" and u["used_hours"] == 0
assert c.get(f"{V1}/service-units/{unit['id']}/events", headers=tech).json()[0]["kind"] == "incident"
m = {x["id"]: x for x in c.get(f"{V1}/map", headers=tech).json()["equipment"]}
assert m[eq["id"]]["state"] == "alarm" and m[eq["id"]]["open_incidents"] == 1
assert c.post(f"{V1}/incidents/{inc['id']}/close", headers=env["tech2"], json={}).status_code == 403
assert c.post(f"{V1}/incidents/{inc['id']}/close", headers=tech, json={"note": "Устранено"}).json()["status"] == "closed"
assert c.get(f"{V1}/equipment-health", headers=tech).json()[str(eq["id"])]["state"] == "ok"
assert len(c.get(f"{V1}/incidents", headers=tech, params={"equipment_id": eq["id"]}).json()) == 1
def test_daily_tasks(env):
c, tech, tech2, eng, eq, unit = env["c"], env["tech"], env["tech2"], env["eng"], env["eq"], env["unit"]
ids = env["ids"]
# техник не может ставить задания другим, но может себе
assert c.post(f"{V1}/tasks", headers=tech, json={"title": "Чужое", "assignee_id": ids["p_tech2"]}).status_code == 403
own = c.post(f"{V1}/tasks", headers=tech, json={"title": "Себе задание"}).json()
assert own["assignee_id"] == ids["p_tech"] and own["status"] == "assigned"
t = c.post(f"{V1}/tasks", headers=eng, json={"title": "Проверить насос", "assignee_id": ids["p_tech"],
"equipment_id": eq["id"], "unit_id": unit["id"]}).json()
assert t["equipment_name"] == "П-Насос" and t["unit_name"] == "Подшипник"
st = lambda s, cm="", h=tech: c.post(f"{V1}/tasks/{t['id']}/status", headers=h, json={"status": s, "comment": cm})
assert st("in_progress").status_code == 200
assert st("paused").status_code == 422 # без комментария нельзя
assert st("paused", "Жду запчасть").status_code == 200
assert st("done").status_code == 422
assert st("in_progress").status_code == 200
# чужой техник не видит задание
assert c.get(f"{V1}/tasks/{t['id']}", headers=tech2).status_code == 404
# фото
r = c.post(f"{V1}/tasks/{t['id']}/photos", headers=tech, content=PNG)
assert r.status_code == 201 and len(r.json()["photos"]) == 1
assert c.get(f"{V1}/task-photos/{r.json()['photos'][0]}", headers=eng).status_code == 200
assert c.post(f"{V1}/tasks/{t['id']}/photos", headers=tech, content=b"not an image").status_code == 415
done = st("done", "Подшипник смазан").json()
assert done["status"] == "done" and done["finished_at"] and done["last_comment"] == "Подшипник смазан"
assert [e["to_status"] for e in done["events"]] == ["assigned", "in_progress", "paused", "in_progress", "done"]
assert st("in_progress").status_code == 403 # завершённое техник уже не меняет
assert st("in_progress", "x", eng).status_code == 422 # из «выполнен» — только вернуть в «назначен»
assert st("assigned", "Переделать").status_code == 403 # техник не возвращает завершённое
assert st("assigned", "Переделать", eng).status_code == 200
mine = c.get(f"{V1}/tasks", headers=tech, params={"mine": True}).json()
assert {x["title"] for x in mine} >= {"Себе задание", "Проверить насос"}
assert all(x["assignee_id"] == ids["p_tech"] for x in c.get(f"{V1}/tasks", headers=tech).json())
def test_icons_map_card(env):
c, adm, tech, store, eng, eq = env["c"], env["adm"], env["tech"], env["store"], env["eng"], env["eq"]
icons = c.get(f"{V1}/icons", headers=tech).json()
assert len(icons) >= 12 and all(i["id"].startswith("builtin:") for i in icons)
assert c.get(icons[0]["url"]).status_code == 200
assert c.patch(f"{V1}/equipment/{eq['id']}", headers=adm, json={"icon": "file:../../etc/passwd"}).status_code == 422
assert c.patch(f"{V1}/equipment/{eq['id']}", headers=adm, json={"icon": "builtin:motor"}).json()["icon"] == "builtin:motor"
r = c.put(f"{V1}/equipment/{eq['id']}/icon", headers=eng, content=PNG)
assert r.status_code == 200 and r.json()["icon"].startswith("file:")
assert c.get(f"{V1}/equipment/{eq['id']}/icon", headers=tech).content == PNG
# карта: картинку и иконки ставит админ, зоны складов — кладовщик
assert c.put(f"{V1}/map/image", headers=store, content=PNG).status_code == 403
assert c.put(f"{V1}/map/image", headers=adm, content=PNG).status_code == 204
assert c.get(f"{V1}/map/image", headers=tech).content == PNG
assert c.put(f"{V1}/equipment/{eq['id']}/map", headers=store, json={"x": 10, "y": 20}).status_code == 403
assert c.put(f"{V1}/equipment/{eq['id']}/map", headers=adm, json={"x": 10, "y": None}).status_code == 422
assert c.put(f"{V1}/equipment/{eq['id']}/map", headers=adm, json={"x": 10.5, "y": 20}).json()["map_x"] == 10.5
m = c.get(f"{V1}/map", headers=store).json()
assert m["has_image"] and m["can_edit_warehouses"] and not m["can_edit_equipment"]
assert any(e["id"] == eq["id"] and e["x"] == 10.5 for e in m["equipment"])
# карточка сотрудника
tid = env["ids"]["p_tech"]
assert c.patch(f"{V1}/users/{tid}/profile", headers=tech, json={"skills": "Сварка, электрика", "phone": "+7 900"}).status_code == 200
assert c.patch(f"{V1}/users/{env['ids']['p_tech2']}/profile", headers=tech, json={"skills": "x"}).status_code == 403
assert c.put(f"{V1}/users/{tid}/photo", headers=tech, content=PNG).json()["has_photo"] is True
card = c.get(f"{V1}/users/{tid}/card", headers=env["tech2"]).json()
assert card["user"]["skills"] == "Сварка, электрика" and card["user"]["position"]["name"] == "П-Слесарь"
assert any(t["title"] == "Проверить насос" for t in card["tasks"])
assert card["inventory"] is None and card["can_edit"] is False # чужой инвентарь коллеге не показываем
assert c.get(f"{V1}/users/{tid}/card", headers=store).json()["inventory"] == []
assert c.get(f"{V1}/users/{tid}/photo", headers=env["tech2"]).content == PNG
def test_execution_report(env):
c, adm, eng, tech, eq = env["c"], env["adm"], env["eng"], env["tech"], env["eq"]
cl = c.post(f"{V1}/checklists", headers=adm, json={"name": "П-Осмотр", "items": [
{"text": "Давление", "answer_type": "number", "min_value": 1, "max_value": 5, "unit": "бар"}]}).json()
o = c.post(f"{V1}/orders", headers=eng, json={"title": "Осмотр насоса", "checklist_id": cl["id"],
"equipment_id": eq["id"], "assignee_user_id": env["ids"]["p_tech"]}).json()
item = c.get(f"{V1}/orders/{o['id']}", headers=tech).json()["items"][0]
c.put(f"{V1}/orders/{o['id']}/items/{item['id']}", headers=tech, json={"value": "9"})
assert c.post(f"{V1}/orders/{o['id']}/submit", headers=tech).json()["status"] == "completed"
o2 = c.post(f"{V1}/orders", headers=eng, json={"title": "Второй осмотр", "checklist_id": cl["id"], "equipment_id": eq["id"]}).json()
c.post(f"{V1}/orders/{o2['id']}/cancel", headers=eng)
now = config.now()
q = {"from": (now - timedelta(days=1)).isoformat(), "to": (now + timedelta(days=1)).isoformat(), "equipment_id": eq["id"]}
assert c.get(f"{V1}/reports/execution", headers=tech, params=q).status_code == 403
rep = c.get(f"{V1}/reports/execution", headers=eng, params=q).json()
row = rep["rows"][0]
assert row["equipment"] == "П-Насос"
assert row["cats"]["inspection"] == {"planned": 2, "done": 1, "percent": 50}
assert row["cats"]["tasks"]["planned"] >= 1
texts = [d["text"] for d in row["deviations"]]
assert any("Давление: 9 бар" in t for t in texts) and any("Наряд отменён" in t for t in texts)
assert any("Жду запчасть" in t for t in texts)
pdf = c.get(f"{V1}/reports/execution", headers=eng, params={**q, "format": "pdf"})
assert pdf.status_code == 200 and pdf.content[:5] == b"%PDF-"
csv = c.get(f"{V1}/reports/execution", headers=eng, params={**q, "format": "csv"})
assert csv.status_code == 200 and "Суточные осмотры" in csv.content.decode("utf-8-sig")
def test_task_transfer(env):
c, tech, tech2, eng, ids = env["c"], env["tech"], env["tech2"], env["eng"], env["ids"]
t = c.post(f"{V1}/tasks", headers=eng, json={"title": "Передаваемое", "assignee_id": ids["p_tech"]}).json()
assert t["can_transfer"] is False or True
c.post(f"{V1}/tasks/{t['id']}/status", headers=tech, json={"status": "in_progress"})
tr = lambda h, to, cm="Занят на аварии": c.post(f"{V1}/tasks/{t['id']}/transfer", headers=h, json={"assignee_id": to, "comment": cm})
assert tr(tech2, ids["p_tech2"]).status_code == 404 # чужое задание не видно
assert tr(tech, ids["p_tech2"], "").status_code == 422 # причина обязательна
r = tr(tech, ids["p_tech2"]) # исполнитель передаёт
assert r.status_code == 200, r.text
d = r.json()
assert d["assignee_id"] == ids["p_tech2"] and d["status"] == "assigned"
ev = d["events"][-1]
assert ev["kind"] == "transfer" and ev["from_user_name"] == "P_TECH" and ev["to_user_name"] == "P_TECH2"
assert tr(tech2, ids["p_tech2"]).status_code == 409
assert tr(eng, ids["p_tech"]).status_code == 200 # заказчик (поставивший) возвращает
c.post(f"{V1}/tasks/{t['id']}/status", headers=tech, json={"status": "done", "comment": "ok"})
assert tr(eng, ids["p_tech2"]).status_code == 403 # завершённое не передаётся
def test_public_and_external_incidents(env):
c, adm, tech, eq = env["c"], env["adm"], env["tech"], env["eq"]
loc = c.post(f"{V1}/locations", headers=adm, json={"name": "П-Цех"}).json()
sub = c.post(f"{V1}/locations", headers=adm, json={"name": "П-Участок", "parent_id": loc["id"]}).json()
other = c.post(f"{V1}/locations", headers=adm, json={"name": "П-Склад"}).json()
c.patch(f"{V1}/equipment/{eq['id']}", headers=adm, json={"location_id": sub["id"], "inventory_no": "П-ИНВ-1"})
o = c.get(f"{V1}/public/report-options").json() # без входа
assert o["enabled"] and any(e["id"] == eq["id"] and e["location_id"] == sub["id"] for e in o["equipment"])
base = {"location_id": loc["id"], "equipment_id": eq["id"], "description": "Сильная течь масла под машиной",
"reporter_name": "Подрядчик Петров", "reporter_contact": "+7 900 123-45-67"}
assert c.post(f"{V1}/public/incidents", json={**base, "reporter_contact": ""}).status_code == 422
assert c.post(f"{V1}/public/incidents", json={**base, "description": ""}).status_code == 422
assert c.post(f"{V1}/public/incidents", json={**base, "location_id": other["id"]}).status_code == 422 # машина не в этом месте
r = c.post(f"{V1}/public/incidents", json=base) # родительское место включает вложенное
assert r.status_code == 201 and r.json()["number"]
inc = c.get(f"{V1}/incidents/{r.json()['number']}", headers=tech).json()
assert inc["source"] == "public" and inc["reporter_contact"] == "+7 900 123-45-67" and inc["title"].startswith("Сильная течь")
assert c.get("/report").status_code == 200
# выключение формы
c.put(f"{V1}/settings", headers=adm, json={"public_incidents": False})
assert c.get(f"{V1}/public/report-options").json()["enabled"] is False
assert c.post(f"{V1}/public/incidents", json=base).status_code == 403
c.put(f"{V1}/settings", headers=adm, json={"public_incidents": True})
# внешняя система по ключу
assert c.post(f"{V1}/api-keys", headers=tech, json={"name": "x"}).status_code == 403
k = c.post(f"{V1}/api-keys", headers=adm, json={"name": "SCADA"}).json()
assert k["key"].startswith("cl_")
H = {"X-API-Key": k["key"]}
body = {"inventory_no": "П-ИНВ-1", "title": "Перегрев двигателя", "external_id": "SCADA-77"}
assert c.post(f"{V1}/external/incidents", json=body).status_code == 401
assert c.post(f"{V1}/external/incidents", headers={"X-API-Key": "cl_wrong"}, json=body).status_code == 401
r1 = c.post(f"{V1}/external/incidents", headers=H, json=body)
assert r1.status_code == 201 and r1.json()["source"] == "api" and r1.json()["reporter_name"] == "SCADA"
assert c.post(f"{V1}/external/incidents", headers=H, json=body).json()["id"] == r1.json()["id"] # повтор — без дубликата
assert c.post(f"{V1}/external/incidents", headers=H, json={"inventory_no": "нет", "title": "x1"}).status_code == 404
assert any(e["inventory_no"] == "П-ИНВ-1" for e in c.get(f"{V1}/external/equipment", headers=H).json())
assert c.get(f"{V1}/api-keys", headers=adm).json()[0]["last_used_at"]
c.delete(f"{V1}/api-keys/{k['id']}", headers=adm)
assert c.post(f"{V1}/external/incidents", headers=H, json=body).status_code == 401
def test_equipment_history(env):
c, tech, eq = env["c"], env["tech"], env["eq"]
h = c.get(f"{V1}/equipment/{eq['id']}/history", headers=tech).json()
kinds = {r["type"] for r in h["rows"]}
assert {"incident", "service", "meter", "order", "task"} <= kinds
assert h["rows"] == sorted(h["rows"], key=lambda r: r["at"], reverse=True)
only = c.get(f"{V1}/equipment/{eq['id']}/history", headers=tech, params={"types": "incident"}).json()
assert only["rows"] and all(r["type"] == "incident" for r in only["rows"])
assert c.get(f"{V1}/equipment/{eq['id']}/history", headers=tech, params={"types": ""}).json()["total"] == 0
srch = c.get(f"{V1}/equipment/{eq['id']}/history", headers=tech, params={"q": "заклинил"}).json()
assert srch["rows"] and all("аклинил" in r["title"] + r["text"] for r in srch["rows"])
old = c.get(f"{V1}/equipment/{eq['id']}/history", headers=tech, params={"to": "2000-01-01"}).json()
assert old["total"] == 0
assert any("заменён при отказе" in r["title"] for r in h["rows"])
def test_departments(env):
c, adm, tech, ids = env["c"], env["adm"], env["tech"], env["ids"]
assert c.post(f"{V1}/departments", headers=tech, json={"name": "Х"}).status_code == 403
top = c.post(f"{V1}/departments", headers=adm, json={"name": "П-Служба ГИ"}).json()
mech = c.post(f"{V1}/departments", headers=adm, json={"name": "П-Мехслужба", "parent_id": top["id"], "head_user_id": ids["p_eng"]}).json()
assert c.patch(f"{V1}/departments/{top['id']}", headers=adm, json={"parent_id": mech["id"]}).status_code == 422 # цикл
assert c.post(f"{V1}/departments", headers=adm, json={"name": "П-Мехслужба"}).status_code == 409 # дубль
assert c.patch(f"{V1}/users/{ids['p_tech']}", headers=adm, json={"department_id": 99999}).status_code == 422
u = c.patch(f"{V1}/users/{ids['p_tech']}", headers=adm, json={"department_id": mech["id"]}).json()
assert u["department"]["name"] == "П-Мехслужба"
deps = {d["name"]: d for d in c.get(f"{V1}/departments", headers=tech).json()}
assert deps["П-Мехслужба"]["members"] == 1 and deps["П-Мехслужба"]["parent_id"] == top["id"]
assert c.get(f"{V1}/users/{ids['p_tech']}/card", headers=tech).json()["user"]["department"]["id"] == mech["id"]
assert c.delete(f"{V1}/departments/{mech['id']}", headers=adm).status_code == 204
assert c.get(f"{V1}/me", headers=tech).json()["department_id"] is None