from __future__ import annotations import json import time from dataclasses import dataclass from typing import Any, Callable, Optional import allure # pyright: ignore[reportMissingImports] from allure_commons.types import AttachmentType # pyright: ignore[reportMissingImports] from worklib.graphql_client import execute_graphql from KVSTest.testdata.kvs_test_data import KVS_TEST_COMPANY_ID, KVSTestData 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 except RuntimeError as e: # На части стендов Forbidden иногда приходит как GraphQL error со статусом 500. if "Forbidden" in str(e): allure.attach(str(e), name=f"Forbidden (runtime): {op_name}", attachment_type=AttachmentType.TEXT) raise AssertionError( f"Forbidden на операции: {op_name}. Проверьте, что пользователь из env " "AUTH_USERNAME/AUTH_PASSWORD имеет права на subscription операции." ) from e raise @dataclass class SubscriptionTestData: """ Данные для теста подписок: service -> plan -> subscription -> invoices -> delete subscription """ company_id: str = KVS_TEST_COMPANY_ID kvs: KVSTestData | None = None service_id: Optional[str] = None plan_id: Optional[str] = None subscription_id: Optional[str] = None _cleanup_fns: Optional[list[Callable[[], None]]] = None @classmethod def from_behave_context(cls, context: Any, *, company_id: str | None = None) -> "SubscriptionTestData": cid = company_id if company_id is not None else KVS_TEST_COMPANY_ID td: SubscriptionTestData | None = getattr(context, "kvs_subscription_test_data", None) if isinstance(td, cls): if not td._cleanup_fns: td._cleanup_fns = getattr(context, "_cleanup_fns", None) if not td.kvs: td.kvs = KVSTestData.from_behave_context(context, company_id=cid) td.company_id = cid return td td = cls(company_id=cid) td._cleanup_fns = getattr(context, "_cleanup_fns", None) td.kvs = KVSTestData.from_behave_context(context, company_id=cid) setattr(context, "kvs_subscription_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_service(self) -> str: if self.service_id: return self.service_id assert self.kvs is not None token = self.kvs.ensure_token() mutation = """ mutation createservice($title: String!, $type: String!) { createService(dto: { title: $title, type: $type }) { id title type } } """.strip() suffix = str(int(time.time())) # API валидирует type: camera|intercom|access variables = {"title": f"kvs-service-{suffix}", "type": "access"} with allure.step("GraphQL: createService"): resp = _exec_or_fail(op_name="createService(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id) _attach_json("createService response", resp) service = resp.get("data", {}).get("createService") assert isinstance(service, dict) and service.get("id"), f"createService не вернул id. Ответ: {resp}" service_id = service["id"] self.service_id = service_id def _cleanup_delete_service() -> None: delete_mutation = """mutation { deleteService(id: "%s") }""".strip() % service_id _exec_or_fail(op_name="deleteService(mutation)", token=token, query=delete_mutation, variables=None, company_id=self.company_id) self._register_cleanup(_cleanup_delete_service) return service_id def ensure_plan(self) -> str: if self.plan_id: return self.plan_id assert self.kvs is not None token = self.kvs.ensure_token() service_id = self.ensure_service() place_id = self.kvs.ensure_place() mutation = """ mutation ($input: CreatePlanDTO!) { createPlan(dto: $input) { id service_ids bundle_ids place_id place_ids price title discount payment_interval price_without_discount } } """.strip() suffix = str(int(time.time())) variables = { "input": { "services": [service_id], "place_ids": [place_id], "price": 200, "payment_interval": 1, "discount": 0, "title": f"plan-kvs-{suffix}", } } with allure.step("GraphQL: createPlan"): resp = _exec_or_fail(op_name="createPlan(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id) _attach_json("createPlan response", resp) plan = resp.get("data", {}).get("createPlan") assert isinstance(plan, dict) and plan.get("id"), f"createPlan не вернул id. Ответ: {resp}" # Проверяем, что service действительно включён в plan (как просили) service_ids = plan.get("service_ids") assert isinstance(service_ids, list) and service_id in service_ids, ( f"Созданный service_id не попал в plan.service_ids. service_id={service_id!r}, service_ids={service_ids!r}" ) plan_id = plan["id"] self.plan_id = plan_id def _cleanup_delete_plan() -> None: delete_mutation = """mutation { deletePlan(id: "%s") }""".strip() % plan_id _exec_or_fail(op_name="deletePlan(mutation)", token=token, query=delete_mutation, variables=None, company_id=self.company_id) self._register_cleanup(_cleanup_delete_plan) return plan_id def create_subscription(self) -> str: if self.subscription_id: return self.subscription_id assert self.kvs is not None token = self.kvs.ensure_token() plan_id = self.ensure_plan() service_id = self.ensure_service() place_id = self.kvs.ensure_place() subscriber_id = self.kvs.create_user() # Пользователь должен быть участником места (как в addUserToPlace тесте) add_resp = self.kvs.add_user_to_place(account_id=subscriber_id, place_id=place_id) _attach_json("addUserToPlace (for subscription) response", add_resp) payload = add_resp.get("data", {}).get("addUserToPlace") assert isinstance(payload, dict) and payload.get("member_id"), f"addUserToPlace не вернул member_id: {add_resp!r}" members_resp = self.kvs.query_place_members(place_id=place_id) _attach_json("place members (after addUserToPlace) response", members_resp) results = members_resp.get("data", {}).get("place", {}).get("results", []) assert isinstance(results, list) and results, f"place.results пустой: {members_resp!r}" members = results[0].get("members") if isinstance(results[0], dict) else None assert isinstance(members, list), f"members не list: {members!r}" user_ids = [] for m in members: if isinstance(m, dict) and isinstance(m.get("user"), dict): uid = m["user"].get("id") if isinstance(uid, str): user_ids.append(uid) assert subscriber_id in user_ids, f"subscriber_id не найден в place.members.user.id. subscriber_id={subscriber_id!r}, got={user_ids!r}" mutation = """ mutation createsub($plan_id: String!, $subscriber_id: String!, $place_id: String!, $service_id: String!) { createSubscription(dto: { plan_id: $plan_id, subscriber_id: $subscriber_id, place_id: $place_id, service_id: $service_id }) { id services { id title } user { id data { first_name last_name } } plan { id title } place_id } } """.strip() variables = {"plan_id": plan_id, "subscriber_id": subscriber_id, "place_id": place_id, "service_id": service_id} with allure.step("GraphQL: createSubscription"): resp = _exec_or_fail(op_name="createSubscription(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id) _attach_json("createSubscription response", resp) sub = resp.get("data", {}).get("createSubscription") assert isinstance(sub, dict) and sub.get("id"), f"createSubscription не вернул id. Ответ: {resp}" # Проверяем, что service в subscription совпадает с созданным services = sub.get("services") assert isinstance(services, list) and any(isinstance(s, dict) and s.get("id") == service_id for s in services), ( f"service_id не найден в subscription.services. service_id={service_id!r}, services={services!r}" ) plan_obj = sub.get("plan") assert isinstance(plan_obj, dict) and plan_obj.get("id") == plan_id, f"subscription.plan.id не совпал: {plan_obj!r}" assert sub.get("place_id") == place_id, f"subscription.place_id не совпал: {sub!r}" user_obj = sub.get("user") assert isinstance(user_obj, dict) and user_obj.get("id") == subscriber_id, f"subscription.user.id не совпал: {user_obj!r}" subscription_id = sub["id"] self.subscription_id = subscription_id def _cleanup_delete_subscription() -> None: del_mut = """mutation($id: String!) { deleteSubscription(id: $id) }""".strip() _exec_or_fail(op_name="deleteSubscription(mutation)", token=token, query=del_mut, variables={"id": subscription_id}, company_id=self.company_id) self._register_cleanup(_cleanup_delete_subscription) return subscription_id def query_invoices(self) -> dict[str, Any]: assert self.kvs is not None token = self.kvs.ensure_token() place_id = self.kvs.ensure_place() query = """ query invoicesInfo($place_id: String!) { invoices(place_id: $place_id, status: pending) { id price status subscriptions place_id } } """.strip() with allure.step("GraphQL: invoices (pending)"): resp = _exec_or_fail(op_name="invoices(query)", token=token, query=query, variables={"place_id": place_id}, company_id=self.company_id) _attach_json("invoices response", resp) return resp def delete_subscription_now(self) -> dict[str, Any]: assert self.kvs is not None token = self.kvs.ensure_token() sub_id = self.subscription_id assert isinstance(sub_id, str) and sub_id, "Нет subscription_id." mutation = """mutation($id: String!) { deleteSubscription(id: $id) }""".strip() with allure.step("GraphQL: deleteSubscription"): resp = _exec_or_fail(op_name="deleteSubscription(mutation)", token=token, query=mutation, variables={"id": sub_id}, company_id=self.company_id) _attach_json("deleteSubscription response", resp) return resp