268 lines
12 KiB
Python
268 lines
12 KiB
Python
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
|
||
|