Noob_test/Ticket/testdata/ticket_test_data.py
2026-05-15 11:34:24 +03:00

477 lines
21 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.

from __future__ import annotations
import json
import random
import time
from dataclasses import dataclass
from dataclasses import field
from typing import Any, Callable, Optional
import allure # pyright: ignore[reportMissingImports]
from allure_commons.types import AttachmentType # pyright: ignore[reportMissingImports]
from worklib import admin_data
from worklib.graphql_client import DEFAULT_COMPANY_ID, execute_graphql
def _attach_json(name: str, payload: Any) -> None:
allure.attach(
json.dumps(payload, ensure_ascii=False, indent=2),
name=name,
attachment_type=AttachmentType.JSON,
)
def _exec_or_fail(*, op_name: str, token: str, query: str, variables: dict[str, Any] | None = None, company_id: str) -> dict[str, Any]:
try:
return execute_graphql(
query=query,
variables=variables,
company_id=company_id,
access_token=token,
)
except PermissionError as e:
allure.attach(str(e), name=f"Forbidden: {op_name}", attachment_type=AttachmentType.TEXT)
raise AssertionError(f"Forbidden на операции: {op_name}") from e
@dataclass
class TicketTestData:
"""
Хранилище/фабрика тестовых данных для Ticket GraphQL.
- Создаёт сущности (place/category/ticket/user/employee/...) по мере необходимости
- Кеширует полученные id в полях
- Регистрирует cleanup в behave context (если передан)
"""
company_id: str = DEFAULT_COMPANY_ID
parent_place_id: str = "6915dc03462d5aea0adc8cbd"
default_user_first_name: str = "kvstest1"
default_user_last_name: str = "kvstest2"
# Employee that must exist on the stand and be present in multiple groups
fixed_user_id: str = "e47362a9-5354-4b42-97cc-c00dfe1c54f1"
fixed_employee_id: str = "69cbe1d59547f08c1cf556ff"
# cached ids
access_token: Optional[str] = None
place_id: Optional[str] = None
category_id: Optional[str] = None
ticket_id: Optional[str] = None
account_id: Optional[str] = None
employee_id: Optional[str] = None
category_group_id: Optional[str] = None
username: Optional[str] = None
# title -> category_id
category_ids: dict[str, str] = field(default_factory=dict)
# role -> title (последние 3 категории для теста смены категории)
last_category_titles_by_role: dict[str, str] = field(default_factory=dict)
_cleanup_fns: Optional[list[Callable[[], None]]] = None
@classmethod
def from_behave_context(cls, context: Any, *, company_id: str = DEFAULT_COMPANY_ID) -> "TicketTestData":
td: TicketTestData | None = getattr(context, "ticket_test_data", None)
if isinstance(td, cls):
if not td.access_token and getattr(context, "access_token", None):
td.access_token = context.access_token
if not td._cleanup_fns:
td._cleanup_fns = getattr(context, "_cleanup_fns", None)
return td
td = cls(company_id=company_id)
td.access_token = getattr(context, "access_token", None) or None
td._cleanup_fns = getattr(context, "_cleanup_fns", None)
setattr(context, "ticket_test_data", td)
return td
def _register_cleanup(self, fn: Callable[[], None]) -> None:
if self._cleanup_fns is not None:
self._cleanup_fns.append(fn)
def ensure_token(self) -> str:
if self.access_token:
return self.access_token
token = admin_data.get_or_create_user("tester").access_token
assert token, "Нет access_token (ни в context, ни в admin_data.get_or_create_user('tester'))."
self.access_token = token
return token
def ensure_place(self) -> str:
if self.place_id:
return self.place_id
token = self.ensure_token()
mutation = """
mutation ($place_type: PlaceType!, $names: [String!]!, $parent_id: String, $address_id: String) {
createPlaceMultiple(
dto: {place_type: $place_type, names: $names, parent_id: $parent_id, address_id: $address_id}
) {
id
__typename
}
}
""".strip()
variables = {"names": ["test"], "parent_id": self.parent_place_id, "place_type": "flat"}
with allure.step("GraphQL: createPlaceMultiple"):
resp = _exec_or_fail(op_name="createPlaceMultiple(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id)
_attach_json("createPlaceMultiple response", resp)
created = resp.get("data", {}).get("createPlaceMultiple")
if isinstance(created, list):
assert created, f"createPlaceMultiple вернул пустой список. Ответ: {resp}"
created0 = created[0]
else:
created0 = created
assert isinstance(created0, dict), f"createPlaceMultiple вернул не объект: {created0!r}. Ответ: {resp}"
place_id = created0.get("id")
assert place_id, f"createPlaceMultiple не вернул id. Ответ: {resp}"
self.place_id = place_id
def _cleanup_delete_place() -> None:
delete_mutation = """mutation deleteplace($id: String!) { deletePlace(id: $id) }""".strip()
_exec_or_fail(op_name="deletePlace(mutation)", token=token, query=delete_mutation, variables={"id": place_id}, company_id=self.company_id)
self._register_cleanup(_cleanup_delete_place)
return place_id
def ensure_ticket_category(self) -> str:
if self.category_id:
return self.category_id
token = self.ensure_token()
place_id = self.ensure_place()
mutation = """
mutation ($input: CreateTicketCategoryInput!) {
createTicketCategory(dto: $input) {
id
title
place_ids
company_id
}
}
""".strip()
variables = {"input": {"title": "tester1", "place_ids": [place_id], "company_id": self.company_id}}
with allure.step("GraphQL: createTicketCategory"):
resp = _exec_or_fail(op_name="createTicketCategory(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id)
_attach_json("createTicketCategory response", resp)
category = resp.get("data", {}).get("createTicketCategory")
assert isinstance(category, dict) and category.get("id"), f"createTicketCategory не вернул id. Ответ: {resp}"
category_id = category["id"]
self.category_id = category_id
def _cleanup_delete_category() -> None:
delete_mutation = """mutation deleteTicketcategory($id: String!) { deleteTicketCategory(id: $id) }""".strip()
_exec_or_fail(op_name="deleteTicketCategory(mutation)", token=token, query=delete_mutation, variables={"id": category_id}, company_id=self.company_id)
self._register_cleanup(_cleanup_delete_category)
return category_id
def create_ticket_category(self, *, title: str, place_id: Optional[str] = None) -> str:
"""
Создаёт ticket category (не перетирая self.category_id), сохраняет в self.category_ids[title].
"""
if title in self.category_ids:
return self.category_ids[title]
token = self.ensure_token()
pid = place_id or self.ensure_place()
mutation = """
mutation ($input: CreateTicketCategoryInput!) {
createTicketCategory(dto: $input) {
id
title
place_ids
company_id
}
}
""".strip()
variables = {"input": {"title": title, "place_ids": [pid], "company_id": self.company_id}}
with allure.step(f"GraphQL: createTicketCategory ({title})"):
resp = _exec_or_fail(op_name="createTicketCategory(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id)
_attach_json("createTicketCategory response", resp)
category = resp.get("data", {}).get("createTicketCategory")
assert isinstance(category, dict) and category.get("id"), f"createTicketCategory не вернул id. Ответ: {resp}"
category_id = category["id"]
self.category_ids[title] = category_id
def _cleanup_delete_category() -> None:
delete_mutation = """mutation deleteTicketcategory($id: String!) { deleteTicketCategory(id: $id) }""".strip()
_exec_or_fail(op_name="deleteTicketCategory(mutation)", token=token, query=delete_mutation, variables={"id": category_id}, company_id=self.company_id)
self._register_cleanup(_cleanup_delete_category)
return category_id
def ensure_three_ticket_categories(self) -> dict[str, str]:
"""
Создаёт 3 категории с разными названиями под текущий place.
Возвращает dict с ключами: old/in_group/out_group.
"""
pid = self.ensure_place()
suffix = str(int(time.time()))
titles = {
"old": f"cat-old-{suffix}",
"in_group": f"cat-in-group-{suffix}",
"out_group": f"cat-out-group-{suffix}",
}
self.last_category_titles_by_role = titles
ids = {k: self.create_ticket_category(title=v, place_id=pid) for k, v in titles.items()}
return ids
def create_ticket_with_category(self, *, category_id: str, place_id: Optional[str] = None) -> str:
"""
Создаёт ticket с указанными place_id и category_id.
ВАЖНО: если на стенде нет прав на createTicket, тест должен падать (как и требуется).
"""
token = self.ensure_token()
pid = place_id or self.ensure_place()
mutation = """
mutation createticket($category_id: String!, $place_id: String!) {
createTicket(dto: {subject:"my first ticket", body:"try a ticket", category_id:$category_id, place_id:$place_id }) {
id
}
}
""".strip()
variables = {"category_id": category_id, "place_id": pid}
with allure.step("GraphQL: createTicket"):
resp = _exec_or_fail(op_name="createTicket(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id)
_attach_json("createTicket response", resp)
ticket_id = resp.get("data", {}).get("createTicket", {}).get("id")
assert ticket_id, f"createTicket не вернул id. Ответ: {resp}"
self.ticket_id = ticket_id
def _cleanup_delete_ticket() -> None:
# На стенде deleteTicket часто принимается только в форме без variables (как в GraphQL Playground),
# а с $id иногда отдаёт 403. Берём свежий токен из admin_data — тот же, что и для ручного вызова.
fresh = admin_data.get_or_create_user("tester").access_token or self.access_token or token
delete_mutation = """mutation { deleteTicket(id: "%s") }""".strip() % ticket_id
_exec_or_fail(
op_name="deleteTicket(mutation)",
token=fresh,
query=delete_mutation,
variables=None,
company_id=self.company_id,
)
self._register_cleanup(_cleanup_delete_ticket)
return ticket_id
def ensure_ticket(self) -> str:
if self.ticket_id:
return self.ticket_id
token = self.ensure_token()
place_id = self.ensure_place()
category_id = self.ensure_ticket_category()
mutation = """
mutation createticket($category_id: String!, $place_id: String!) {
createTicket(dto: {subject:"my first ticket", body:"try a ticket", category_id:$category_id, place_id:$place_id }) {
id
}
}
""".strip()
variables = {"category_id": category_id, "place_id": place_id}
with allure.step("GraphQL: createTicket"):
resp = _exec_or_fail(op_name="createTicket(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id)
_attach_json("createTicket response", resp)
ticket_id = resp.get("data", {}).get("createTicket", {}).get("id")
assert ticket_id, f"createTicket не вернул id. Ответ: {resp}"
self.ticket_id = ticket_id
def _cleanup_delete_ticket() -> None:
fresh = admin_data.get_or_create_user("tester").access_token or self.access_token or token
delete_mutation = """mutation { deleteTicket(id: "%s") }""".strip() % ticket_id
_exec_or_fail(
op_name="deleteTicket(mutation)",
token=fresh,
query=delete_mutation,
variables=None,
company_id=self.company_id,
)
# Удалять ticket первым (до категории/плейса).
self._register_cleanup(_cleanup_delete_ticket)
return ticket_id
def create_user(self) -> str:
if self.account_id:
return self.account_id
token = self.ensure_token()
username = f"+7999{random.randint(1000000, 9999999)}"
mutation = """
mutation createUser($input: CreateAccountDTO!) {
createUser(dto: $input)
}
""".strip()
variables = {
"input": {
"username": username,
"first_name": self.default_user_first_name,
"last_name": self.default_user_last_name,
"is_demo": True,
"password": "",
}
}
with allure.step("GraphQL: createUser"):
resp = _exec_or_fail(op_name="createUser(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id)
_attach_json("createUser response", resp)
created = resp.get("data", {}).get("createUser")
if isinstance(created, dict):
account_id = created.get("id")
returned_username = created.get("username")
else:
account_id = created
returned_username = None
assert isinstance(account_id, str) and account_id, f"createUser не вернул id. Ответ: {resp}"
self.account_id = account_id
self.username = returned_username or username
def _cleanup_delete_user() -> None:
delete_mutation = """
mutation deleteUser($account_id: String!) { deleteUser(account_id: $account_id) }
""".strip()
_exec_or_fail(
op_name="deleteUser(mutation)",
token=token,
query=delete_mutation,
variables={"account_id": account_id},
company_id=self.company_id,
)
self._register_cleanup(_cleanup_delete_user)
return account_id
def create_employee(self) -> str:
if self.employee_id:
return self.employee_id
token = self.ensure_token()
account_id = self.create_user()
mutation_with_status = """
mutation createEmployee($user_id: String!) {
addEmployee(dto: {user_id: $user_id, attributes: [], role: admin, company_id: "%s"}) {
id
status
}
}
""".strip() % self.company_id
mutation_without_status = """
mutation createEmployee($user_id: String!) {
addEmployee(dto: {user_id: $user_id, attributes: [], role: admin, company_id: "%s"}) {
id
}
}
""".strip() % self.company_id
with allure.step("GraphQL: addEmployee"):
try:
resp = _exec_or_fail(
op_name="addEmployee(mutation, with status)",
token=token,
query=mutation_with_status,
variables={"user_id": account_id},
company_id=self.company_id,
)
except RuntimeError as e:
# На стенде поле EmployeeObject.status иногда ломает GraphQL (500: non-nullable + null).
if "EmployeeObject.status" not in str(e):
raise
allure.attach(
str(e),
name="Skipping employee.status check (API bug)",
attachment_type=AttachmentType.TEXT,
)
resp = _exec_or_fail(
op_name="addEmployee(mutation, without status)",
token=token,
query=mutation_without_status,
variables={"user_id": account_id},
company_id=self.company_id,
)
_attach_json("addEmployee response", resp)
add_employee = resp.get("data", {}).get("addEmployee", {})
employee_id = add_employee.get("id") if isinstance(add_employee, dict) else None
assert employee_id, f"addEmployee не вернул employee_id. Ответ: {resp}"
status = add_employee.get("status") if isinstance(add_employee, dict) else None
if status is not None:
assert status == "active", f"status при создании employee должен быть active, получено: {status!r}. Ответ: {resp}"
self.employee_id = employee_id
return employee_id
def create_category_group(self) -> str:
category_id = self.ensure_ticket_category()
return self.create_category_group_for_categories([category_id])
def create_category_group_for_categories(
self,
category_ids: list[str],
*,
members: Optional[list[dict[str, str]]] = None,
cache: bool = True,
) -> str:
"""
Создаёт CategoryGroup для указанных категорий.
- members: список участников вида {"user_id": "...", "employee_id": "..."}
Если не передан, создаётся один новый user/employee и добавляется в группу.
- cache: если True, сохраняет group_id в self.category_group_id (старое поведение).
Если False, позволяет создавать несколько групп в рамках одного теста.
"""
if cache and self.category_group_id:
return self.category_group_id
assert category_ids and all(isinstance(x, str) and x for x in category_ids), f"category_ids пустой/некорректный: {category_ids!r}"
token = self.ensure_token()
if members is None:
account_id = self.create_user()
employee_id = self.create_employee()
members = [{"user_id": account_id, "employee_id": employee_id}]
assert members and all(isinstance(m, dict) and m.get("user_id") and m.get("employee_id") for m in members), f"members некорректный: {members!r}"
mutation = """
mutation createcategoryGroup($name: String!, $category_ids: [String!]!, $employees: [GroupMemberDto!]!) {
createCategoryGroup(dto: {
name: $name,
employees: $employees,
company_id: "%s",
category_ids: $category_ids
}) { id }
}
""".strip() % self.company_id
variables = {
"name": f"tester-{int(time.time())}",
"category_ids": category_ids,
"employees": members,
}
with allure.step("GraphQL: createCategoryGroup"):
resp = _exec_or_fail(op_name="createCategoryGroup(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id)
_attach_json("createCategoryGroup response", resp)
group_id = resp.get("data", {}).get("createCategoryGroup", {}).get("id")
assert group_id, f"createCategoryGroup не вернул id. Ответ: {resp}"
if cache:
self.category_group_id = group_id
def _cleanup_delete_group() -> None:
delete_mutation = """
mutation deletecategoryGroup($id: String!) { deleteCategoryGroup(id: $id) }
""".strip()
_exec_or_fail(op_name="deleteCategoryGroup(mutation)", token=token, query=delete_mutation, variables={"id": group_id}, company_id=self.company_id)
self._register_cleanup(_cleanup_delete_group)
return group_id
def connect_employee_to_category_group(self) -> None:
token = self.ensure_token()
group_id = self.create_category_group()
employee_id = self.create_employee()
account_id = self.account_id
_attach_json("connectEmployee inputs", {"group_id": group_id, "employee_id": employee_id, "account_id": account_id})
def _looks_like_uuid(value: str) -> bool:
return isinstance(value, str) and value.count("-") == 4 and len(value) >= 32
mutation = """
mutation connectEmployee($input: AddEmployeesToCategoryGroupDTO!) {
addEmployeesToCategoryGroup(dto: $input)
}
""".strip()
employee_ids = [employee_id] if _looks_like_uuid(employee_id) else ([account_id] if account_id else [employee_id])
variables = {"input": {"id": group_id, "employee_ids": employee_ids}}
with allure.step("GraphQL: addEmployeesToCategoryGroup (connectEmployee)"):
resp = _exec_or_fail(op_name="addEmployeesToCategoryGroup(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id)
_attach_json("addEmployeesToCategoryGroup response", resp)