Files

334 lines
26 KiB
Python
Raw Permalink 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.
"""Портал: инвентарь и склады, заявки на инструмент, инциденты, суточные задания, карта, карточка сотрудника, отчёт."""
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