2 TestFeatchAlmostReady

This commit is contained in:
stepan TeSt 2026-04-22 15:11:03 +03:00
parent 76f51b6935
commit 2b6f327397
211 changed files with 4867 additions and 46 deletions

1
KVSTest/__init__.py Normal file
View File

@ -0,0 +1 @@

View File

@ -0,0 +1,11 @@
Feature: Place info (REST/GraphQL/WebSocket)
Scenario: Authorize as employer
When get access token
Then access token is valid
Scenario: Get place info
When get place info
Then place info is valid for query data

View File

@ -0,0 +1,18 @@
Feature: KVS GraphQL (place + members)
Background: Authorize as employer
When get access token
Then access token is valid
Scenario: Get place info (dynamic place, no hardcode)
When create place for kvs
And query place members for created kvs place
Then kvs place members response has correct shape for created place
Scenario: Add user to place and verify member appears
When create place for kvs
And create user for kvs
And add user to kvs place
Then addUserToPlace response is valid
When query place members for created kvs place
Then added member is present in place members results

View File

@ -0,0 +1,15 @@
Feature: KVS GraphQL subscription
Background: Authorize as employer
When get access token
Then access token is valid
Scenario: Create subscription, check invoices, delete subscription
When create service for kvs subscription
And create plan for kvs subscription
And create subscription for kvs
Then subscription response is valid
When query pending invoices for subscription place
Then invoices response is valid and references subscription
When delete created subscription
Then delete subscription response is successful

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,29 @@
from __future__ import annotations
import traceback
from typing import Any, Callable
import allure # pyright: ignore[reportMissingImports]
from allure_commons.types import AttachmentType # pyright: ignore[reportMissingImports]
def before_scenario(context: Any, scenario: Any) -> None: # noqa: ARG001
# Стек очистки: функции вызываются в обратном порядке (LIFO).
context._cleanup_fns: list[Callable[[], None]] = [] # type: ignore[attr-defined]
def after_scenario(context: Any, scenario: Any) -> None: # noqa: ARG001
cleanup_fns: list[Callable[[], None]] = getattr(context, "_cleanup_fns", [])
while cleanup_fns:
fn = cleanup_fns.pop()
try:
with allure.step(f"Cleanup: {getattr(fn, '__name__', 'cleanup')}"):
fn()
except Exception:
# Cleanup не должен скрывать причину падения сценария, но полезно приложить traceback в Allure.
allure.attach(
traceback.format_exc(),
name="Cleanup error",
attachment_type=AttachmentType.TEXT,
)

View File

@ -0,0 +1,49 @@
# pyright: reportCallIssue=false
from __future__ import annotations
from behave import then, when
from KVSTest.testdata.kvs_test_data import KVSTestData
@when("add user to kvs place") # pyright: ignore[reportGeneralTypeIssues]
def step_add_user_to_place(context) -> None:
td = KVSTestData.from_behave_context(context)
place_id = getattr(context, "kvs_place_id", None) or td.place_id
account_id = getattr(context, "kvs_account_id", None) or td.account_id
assert isinstance(place_id, str) and place_id, "Нет kvs_place_id (place не создан)."
assert isinstance(account_id, str) and account_id, "Нет kvs_account_id (user не создан)."
resp = td.add_user_to_place(account_id=account_id, place_id=place_id)
context.kvs_add_user_to_place_response = resp
@then("addUserToPlace response is valid") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_add_user_to_place_response(context) -> None:
td = KVSTestData.from_behave_context(context)
place_id = getattr(context, "kvs_place_id", None) or td.place_id
assert isinstance(place_id, str) and place_id, "Нет kvs_place_id."
resp = getattr(context, "kvs_add_user_to_place_response", None)
assert isinstance(resp, dict), f"Ответ GraphQL не dict: {resp!r}"
assert "data" in resp, f"В ответе нет data: {resp!r}"
payload = resp.get("data", {}).get("addUserToPlace")
assert isinstance(payload, dict), f"data.addUserToPlace не объект (ожидали JSONObject c полями place_id/member_id): {payload!r}"
assert payload.get("place_id") == place_id, f"place_id не совпал: {payload!r}"
member_id = payload.get("member_id")
assert isinstance(member_id, str) and member_id, f"member_id пустой/не строка: {payload!r}"
context.kvs_member_id = member_id
@then("added member is present in place members results") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_member_present(context) -> None:
member_id = getattr(context, "kvs_member_id", None)
assert isinstance(member_id, str) and member_id, "Нет kvs_member_id (невалидный addUserToPlace ответ)."
resp = getattr(context, "kvs_place_members_response", None)
assert isinstance(resp, dict), f"Ответ GraphQL не dict: {resp!r}"
results = resp.get("data", {}).get("place", {}).get("results", [])
assert isinstance(results, list) and results, f"data.place.results пустой/не list: {results!r}"
members = results[0].get("members") if isinstance(results[0], dict) else None
assert isinstance(members, list), f"members не list: {members!r}"
ids = [m.get("id") for m in members if isinstance(m, dict)]
assert member_id in ids, f"Не нашли member_id={member_id!r} в members ids={ids!r}"

View File

@ -0,0 +1,34 @@
# pyright: reportCallIssue=false
from __future__ import annotations
from behave import then, when
from KVSTest.testdata.kvs_test_data import KVSTestData
@when("query place members for created kvs place") # pyright: ignore[reportGeneralTypeIssues]
def step_query_place_members(context) -> None:
td = KVSTestData.from_behave_context(context)
place_id = getattr(context, "kvs_place_id", None) or td.place_id
assert isinstance(place_id, str) and place_id, "Нет kvs_place_id (place не создан)."
context.kvs_place_members_response = td.query_place_members(place_id=place_id)
@then("kvs place members response has correct shape for created place") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_place_members_shape(context) -> None:
td = KVSTestData.from_behave_context(context)
place_id = getattr(context, "kvs_place_id", None) or td.place_id
assert isinstance(place_id, str) and place_id, "Нет kvs_place_id."
resp = getattr(context, "kvs_place_members_response", None)
assert isinstance(resp, dict), f"Ответ GraphQL не dict: {resp!r}"
assert "data" in resp, f"В ответе нет data: {resp!r}"
place = resp.get("data", {}).get("place")
assert isinstance(place, dict), f"data.place не объект: {place!r}"
results = place.get("results")
assert isinstance(results, list) and results, f"data.place.results пустой/не list: {results!r}"
first = results[0]
assert isinstance(first, dict), f"results[0] не объект: {first!r}"
assert first.get("id") == place_id, f"Ожидали place.id={place_id!r}, получили: {first.get('id')!r}"
members = first.get("members")
assert isinstance(members, list), f"members не list: {members!r}"

View File

@ -0,0 +1,88 @@
# pyright: reportCallIssue=false
from __future__ import annotations
from behave import then, when
from KVSTest.testdata.subscription_test_data import SubscriptionTestData
@when("create service for kvs subscription") # pyright: ignore[reportGeneralTypeIssues]
def step_create_service(context) -> None:
td = SubscriptionTestData.from_behave_context(context)
context.kvs_service_id = td.ensure_service()
@when("create plan for kvs subscription") # pyright: ignore[reportGeneralTypeIssues]
def step_create_plan(context) -> None:
td = SubscriptionTestData.from_behave_context(context)
context.kvs_plan_id = td.ensure_plan()
@when("create subscription for kvs") # pyright: ignore[reportGeneralTypeIssues]
def step_create_subscription(context) -> None:
td = SubscriptionTestData.from_behave_context(context)
context.kvs_subscription_id = td.create_subscription()
@then("subscription response is valid") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_subscription_valid(context) -> None:
td = SubscriptionTestData.from_behave_context(context)
sub_id = getattr(context, "kvs_subscription_id", None) or td.subscription_id
assert isinstance(sub_id, str) and sub_id, "Нет subscription_id."
# create_subscription already asserts response shape; here we validate cached ids coherence
assert td.subscription_id == sub_id
assert isinstance(td.plan_id, str) and td.plan_id, "Нет plan_id."
assert isinstance(td.service_id, str) and td.service_id, "Нет service_id."
@when("query pending invoices for subscription place") # pyright: ignore[reportGeneralTypeIssues]
def step_query_invoices(context) -> None:
td = SubscriptionTestData.from_behave_context(context)
context.kvs_invoices_response = td.query_invoices()
@then("invoices response is valid and references subscription") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_invoices(context) -> None:
td = SubscriptionTestData.from_behave_context(context)
sub_id = td.subscription_id
assert isinstance(sub_id, str) and sub_id, "Нет subscription_id."
resp = getattr(context, "kvs_invoices_response", None)
assert isinstance(resp, dict) and "data" in resp, f"Некорректный invoices ответ: {resp!r}"
invoices = resp.get("data", {}).get("invoices")
assert isinstance(invoices, list), f"data.invoices не list: {invoices!r}"
# допускаем, что инвойсы могут создаваться асинхронно — но если они пришли, должны быть валидной формы
for inv in invoices:
assert isinstance(inv, dict), f"invoice элемент не dict: {inv!r}"
assert isinstance(inv.get("id"), str) and inv.get("id"), f"invoice.id пустой: {inv!r}"
assert inv.get("status") in {"pending", "PENDING", "Pending"}, f"invoice.status неожиданный: {inv!r}"
# Если стенд возвращает subscriptions — проверим, что где-то упоминается наша подписка
referenced = False
for inv in invoices:
subs = inv.get("subscriptions")
if isinstance(subs, list) and sub_id in subs:
referenced = True
break
if isinstance(subs, str) and sub_id in subs:
referenced = True
break
assert referenced or invoices == [], (
"Invoices вернулись, но не содержат ссылку на созданную подписку. "
"Если генерация invoice асинхронная — можно добавить ожидание/ретраи."
)
@when("delete created subscription") # pyright: ignore[reportGeneralTypeIssues]
def step_delete_subscription(context) -> None:
td = SubscriptionTestData.from_behave_context(context)
context.kvs_delete_subscription_response = td.delete_subscription_now()
@then("delete subscription response is successful") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_delete_subscription(context) -> None:
resp = getattr(context, "kvs_delete_subscription_response", None)
assert isinstance(resp, dict) and "data" in resp, f"Некорректный deleteSubscription ответ: {resp!r}"
value = resp.get("data", {}).get("deleteSubscription")
assert value in (True, "true", "True", 1, "1", None) or isinstance(value, dict), f"deleteSubscription вернул неожиданное: {value!r}"

View File

@ -0,0 +1,23 @@
# pyright: reportCallIssue=false
from __future__ import annotations
from behave import when
from KVSTest.testdata.kvs_test_data import KVSTestData
@when("create place for kvs") # pyright: ignore[reportGeneralTypeIssues]
def step_create_place_for_kvs(context) -> None:
td = KVSTestData.from_behave_context(context)
td.ensure_place()
context.kvs_place_id = td.place_id
@when("create user for kvs") # pyright: ignore[reportGeneralTypeIssues]
def step_create_user_for_kvs(context) -> None:
td = KVSTestData.from_behave_context(context)
td.create_user()
context.kvs_account_id = td.account_id
context.kvs_username = td.username

View File

@ -0,0 +1,40 @@
# pyright: reportCallIssue=false
import os
from typing import Any, Final
from behave import given, when, then
from worklib.QueryData import kvs_query_data, kvs_query_data_place_id
from worklib import admin_data
from worklib.auth_as_employer import get_access_token
from worklib.findplaceinfo.find_place_data import fetch_place_members
expected_result = {
"members": [
{
"id": "bb368ee9-c15f-40ef-acb0-c466df47d096",
"parent_id": None,
"user": {
"username": "+79999956657"
}
}
]
}
@when("get place info kvs") # pyright: ignore[reportGeneralTypeIssues]
def step_get_place_info(context):
token = getattr(context, "access_token", None) or admin_data.get_or_create_user("tester").access_token
data = fetch_place_members(access_token=token, query=kvs_query_data()["query"], variables=kvs_query_data_place_id()["variables"])
context.place_info = data
@then("place info is valid") # pyright: ignore[reportGeneralTypeIssues]
def step_place_info_valid(context):
data = getattr(context, "place_info", None)
assert isinstance(data, dict), "Ответ GraphQL не dict"
assert "data" in data or "place" in str(data), f"Не похоже на успешный GraphQL ответ: {data}"
assert data["data"]["place"]["results"][0]["members"][0]["id"] == expected_result["members"][0]["id"]
assert data["data"]["place"]["results"][0]["members"][0]["parent_id"] == expected_result["members"][0]["parent_id"]
assert data["data"]["place"]["results"][0]["members"][0]["user"]["username"] == expected_result["members"][0]["user"]["username"]

View File

@ -0,0 +1,31 @@
# pyright: reportCallIssue=false
import os
from typing import Any, Final
from behave import given, when, then
from worklib.QueryData import query_data, query_data_place_id_variables
from worklib import admin_data
from worklib.auth_as_employer import get_access_token
from worklib.findplaceinfo.find_place_data import fetch_place_members
# pyright: ignore[reportGeneralTypeIssues]
@when("get access token") # pyright: ignore[reportGeneralTypeIssues]
def step_get_access_token(context):
if not getattr(context, "access_token", None):
token = admin_data.get_access_token_from_env()
context.access_token = token
admin_data.get_or_create_user("tester").access_token = token
@then("access token is valid") # pyright: ignore[reportGeneralTypeIssues]
def step_token_is_valid(context):
token = getattr(context, "access_token", None)
assert isinstance(token, str) and token.strip(), f"access_token пустой/не строка: {token}"

View File

@ -0,0 +1,26 @@
from behave import given, when, then
from typing import Final
from worklib import admin_data
from worklib.findplaceinfo.find_place_data import fetch_place_members
from worklib.QueryData import query_data, query_data_place_id_variables
# pyright: ignore[reportGeneralTypeIssues]
_EXPECTED_RESULT: Final[dict[str, str]] = {
"id": "682b071a163ac2a0995355be",
"place_type": "street",
"name": "ул. Мебельная",
}
@when("get place info") # pyright: ignore[reportGeneralTypeIssues]
def step_get_place_info(context):
token = getattr(context, "access_token", None) or admin_data.get_or_create_user("tester").access_token
data = fetch_place_members(access_token=token, query=query_data()["query"], variables=query_data_place_id_variables()["variables"])
context.place_info = data
@then("place info is valid for query data") # pyright: ignore[reportGeneralTypeIssues]
def step_place_info_valid(context):
data = getattr(context, "place_info", None)
assert isinstance(data, dict), "Ответ GraphQL не dict"
assert "data" in data or "place" in str(data), f"Не похоже на успешный GraphQL ответ: {data}"
assert data["data"]["place"]["results"][0]["id"] == _EXPECTED_RESULT["id"]
assert data["data"]["place"]["results"][0]["place_type"] == _EXPECTED_RESULT["place_type"]
assert data["data"]["place"]["results"][0]["name"] == _EXPECTED_RESULT["name"]

2
KVSTest/testdata/__init__.py vendored Normal file
View File

@ -0,0 +1,2 @@
"""Test data builders for KVSTest (behave)."""

201
KVSTest/testdata/kvs_test_data.py vendored Normal file
View File

@ -0,0 +1,201 @@
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
from KVSTest.testdata.query_data import add_user_to_place_mutation, kvs_place_members_query
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:
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}") from e
raise
@dataclass
class KVSTestData:
"""
Хранилище/фабрика тестовых данных для KVS GraphQL.
- Создаёт сущности (place/user) по мере необходимости
- Кеширует 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"
access_token: Optional[str] = None
place_id: Optional[str] = None
account_id: Optional[str] = None
username: Optional[str] = None
_cleanup_fns: Optional[list[Callable[[], None]]] = None
_last_add_user_to_place_response: dict[str, Any] | None = None
@classmethod
def from_behave_context(cls, context: Any, *, company_id: str = DEFAULT_COMPANY_ID) -> "KVSTestData":
td: KVSTestData | None = getattr(context, "kvs_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, "kvs_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()
suffix = str(int(time.time()))
variables = {"names": [f"kvs-test-{suffix}"], "parent_id": self.parent_place_id, "place_type": "flat"}
with allure.step("GraphQL: createPlaceMultiple (KVS)"):
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 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 (KVS)"):
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 add_user_to_place(self, *, account_id: str | None = None, place_id: str | None = None) -> dict[str, Any]:
token = self.ensure_token()
aid = account_id or self.create_user()
pid = place_id or self.ensure_place()
mutation = add_user_to_place_mutation()
variables = {"account_id": aid, "place_id": pid}
with allure.step("GraphQL: addUserToPlace (KVS)"):
resp = _exec_or_fail(op_name="addUserToPlace(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id)
_attach_json("addUserToPlace response", resp)
self._last_add_user_to_place_response = resp
return resp
def query_place_members(self, *, place_id: str | None = None) -> dict[str, Any]:
token = self.ensure_token()
pid = place_id or self.ensure_place()
query = kvs_place_members_query()
variables = {"id": pid}
with allure.step("GraphQL: place members (KVS)"):
resp = _exec_or_fail(op_name="place(query)", token=token, query=query, variables=variables, company_id=self.company_id)
_attach_json("place members response", resp)
return resp

27
KVSTest/testdata/query_data.py vendored Normal file
View File

@ -0,0 +1,27 @@
from __future__ import annotations
def kvs_place_members_query() -> str:
return """
query placeMembers($id: String!) {
place(id: $id) {
results {
id
members {
id
parent_id
user { id username }
}
}
}
}
""".strip()
def add_user_to_place_mutation() -> str:
return """
mutation addUserToPlace($account_id: String!, $place_id: String!) {
addUserToPlace(account_id: $account_id, place_id: $place_id)
}
""".strip()

View File

@ -0,0 +1,265 @@
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 DEFAULT_COMPANY_ID, execute_graphql
from KVSTest.testdata.kvs_test_data import 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 = DEFAULT_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 = DEFAULT_COMPANY_ID) -> "SubscriptionTestData":
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=company_id)
return td
td = cls(company_id=company_id)
td._cleanup_fns = getattr(context, "_cleanup_fns", None)
td.kvs = KVSTestData.from_behave_context(context, company_id=company_id)
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

102
Ticket Mock API.json Normal file
View File

@ -0,0 +1,102 @@
{
"uuid": "51a1d02d-6cf4-4254-b0b6-5ec3400b8558",
"lastMigration": 33,
"name": "Ticket Mock API",
"endpointPrefix": "",
"latency": 0,
"port": 8080,
"hostname": "",
"folders": [],
"routes": [
{
"uuid": "75f18ed9-f146-456b-93f9-9de1f1a677c6",
"type": "http",
"documentation": "",
"method": "post",
"endpoint": "graphql",
"responses": [
{
"uuid": "50ff5216-ff57-41a6-9dad-d5a275cec327",
"body": "{\n \"data\": {\n \"createTicket\": {\n \"id\": \"507f1f77bcf86cd799439011\",\n \"__typename\": \"TicketObject\",\n \"category\": {\n \"id\": \"{{body.variables.category_id}}\",\n \"title\": \"Техническая поддержка\"\n }\n }\n }\n}",
"latency": 0,
"statusCode": 200,
"label": "",
"headers": [],
"bodyType": "INLINE",
"filePath": "",
"databucketID": "",
"sendFileAsBody": false,
"rules": [
{
"target": "body",
"modifier": "$.query",
"value": "\\bcreateTicket\\b",
"invert": false,
"operator": "regex"
}
],
"rulesOperator": "OR",
"disableTemplating": false,
"fallbackTo404": false,
"default": true,
"crudKey": "id",
"callbacks": []
}
],
"responseMode": "FALLBACK",
"streamingMode": null,
"streamingInterval": 0
}
],
"rootChildren": [
{
"type": "route",
"uuid": "75f18ed9-f146-456b-93f9-9de1f1a677c6"
}
],
"proxyMode": true,
"proxyHost": "https://admin.dev.dipal.ru",
"proxyRemovePrefix": false,
"tlsOptions": {
"enabled": false,
"type": "CERT",
"pfxPath": "",
"certPath": "",
"keyPath": "",
"caPath": "",
"passphrase": ""
},
"cors": true,
"headers": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Access-Control-Allow-Origin",
"value": "*"
},
{
"key": "Access-Control-Allow-Methods",
"value": "GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS"
},
{
"key": "Access-Control-Allow-Headers",
"value": "Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With"
}
],
"proxyReqHeaders": [
{
"key": "x-company-id",
"value": "65437401ae3af6f8ffcdbaf8"
}
],
"proxyResHeaders": [
{
"key": "",
"value": ""
}
],
"data": [],
"callbacks": []
}

View File

@ -0,0 +1,60 @@
Feature: Ticket GraphQL (category + employee)
Background: Authorize as employer
When get access token
Then access token is valid
Scenario: Query ticket categories by place_id
When create place multiple for ticket
And create ticket category for created place
And query ticket categories by created place id
Then ticket_category results are not empty
And created ticket category is present in results
Scenario: query employee by category+company
When create place multiple for ticket
And create ticket category for created place
And create user for ticket
And create employee for created user
And create category group for created category
And connect employee to category group
When query employee by category and company
Then employee results are not empty
And each employee result has id and user fields
And created employee username is in results
Scenario: Query employee response shape (may be empty)
When query employee by category and company
Then each employee result has id and user fields
Scenario: Change ticket category and verify employee authorization
When prepare ticket and categories for category change test
And change ticket category to in_group category
And query tickets by created place id
Then ticket category changed from old to in_group
And employee is authorized for ticket
When change ticket category to out_group category
And query tickets by created place id
Then employee is NOT authorized for ticket
Scenario: Assign ticket employee and verify group membership rules
When prepare ticket and employees for assign employee test
And assign ticket to fixed in_group employee
And query tickets by created place id
Then ticket assignee is fixed employee
When assign ticket to new in_group employee
And query tickets by created place id
Then ticket assignee is new in_group employee
When assign ticket to out_group employee (should fail)
And query tickets by created place id
Then ticket assignee is still new in_group employee
Scenario: Assign and unassign ticket employee
When prepare ticket and employees for unassign employee test
And assign ticket to new grouped employee
And query tickets by created place id
Then ticket assignee is new grouped employee
When unassign ticket from new grouped employee
And query tickets by created place id
Then ticket assignee is empty

View File

@ -0,0 +1,41 @@
from __future__ import annotations
import os
import traceback
from typing import Any, Callable
import allure # pyright: ignore[reportMissingImports]
from allure_commons.types import AttachmentType # pyright: ignore[reportMissingImports]
def before_scenario(context: Any, scenario: Any) -> None: # noqa: ARG001
# behave context не типизирован, поэтому сохраняем список без аннотации справа (pyright ругается).
context._cleanup_fns = [] # type: ignore[attr-defined]
# GraphQL endpoint выбирается в worklib.graphql_client.execute_graphql:
# - явный аргумент graphql_url
# - env GRAPHQL_URL
# - DEFAULT_GRAPHQL_URL
#
# Для Mockoon (proxy или чистый mock) достаточно перед запуском тестов выставить:
# GRAPHQL_URL=http://localhost:8080/graphql
#
# Если пользователь выставил env USE_MOCKOON=1, аккуратно подменим GRAPHQL_URL,
# но не трогаем его, если он уже задан явно.
if os.getenv("USE_MOCKOON") in {"1", "true", "True"} and not os.getenv("GRAPHQL_URL"):
os.environ["GRAPHQL_URL"] = "http://localhost:8081/graphql"
context.graphql_url = os.getenv("GRAPHQL_URL")
def after_scenario(context: Any, scenario: Any) -> None: # noqa: ARG001
cleanup_fns: list[Callable[[], None]] = getattr(context, "_cleanup_fns", [])
while cleanup_fns:
fn = cleanup_fns.pop()
try:
with allure.step(f"Cleanup: {getattr(fn, '__name__', 'cleanup')}"):
fn()
except Exception:
allure.attach(
traceback.format_exc(),
name="Cleanup error",
attachment_type=AttachmentType.TEXT,
)

View File

@ -0,0 +1,85 @@
# pyright: reportCallIssue=false
from __future__ import annotations
import json
from typing import Any
import allure # pyright: ignore[reportMissingImports]
from allure_commons.types import AttachmentType # pyright: ignore[reportMissingImports]
from behave import then, when
from Ticket.testdata.ticket_test_data import TicketTestData
from worklib.graphql_client import 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,
)
@when("query ticket categories by created place id") # pyright: ignore[reportGeneralTypeIssues]
def step_query_ticket_categories_by_place(context) -> None:
td = TicketTestData.from_behave_context(context)
token = td.ensure_token()
place_id = td.ensure_place()
query = """
query categoryinfo($place_id: String!) {
ticket_category(filters: {place_id: $place_id}) {
results {
id
title
place_ids
company_id
}
}
}
""".strip()
variables = {"place_id": place_id}
with allure.step("GraphQL: ticket_category(filters: place_id)"):
resp = execute_graphql(query=query, variables=variables, company_id=td.company_id, access_token=token)
_attach_json("ticket_category response", resp)
context.ticket_category_response = resp
@then("ticket_category results are not empty") # pyright: ignore[reportGeneralTypeIssues]
def step_ticket_category_results_not_empty(context) -> None:
resp = getattr(context, "ticket_category_response", None)
assert isinstance(resp, dict), f"Нет ticket_category_response или не dict: {resp}"
results = resp.get("data", {}).get("ticket_category", {}).get("results")
_attach_json("ticket_category.results (extracted)", results)
assert isinstance(results, list), f"ticket_category.results должен быть list. Ответ: {resp}"
assert len(results) > 0, "ticket_category.results пустой — тест должен падать"
@then("created ticket category is present in results") # pyright: ignore[reportGeneralTypeIssues]
def step_created_ticket_category_present(context) -> None:
td = TicketTestData.from_behave_context(context)
created_category_id = td.category_id
created_place_id = td.place_id
assert created_category_id, "Нет category_id в TicketTestData (category ещё не создана)."
assert created_place_id, "Нет place_id в TicketTestData (place ещё не создан)."
resp = getattr(context, "ticket_category_response", None)
assert isinstance(resp, dict), f"Нет ticket_category_response или не dict: {resp}"
results = resp.get("data", {}).get("ticket_category", {}).get("results", [])
assert isinstance(results, list), "ticket_category.results не list"
assert len(results) > 0, "ticket_category.results пустой — тест должен падать"
match = None
for item in results:
if isinstance(item, dict) and item.get("id") == created_category_id:
match = item
break
_attach_json("ticket_category.match (found)", match)
assert match is not None, f"Не нашли созданную категорию id={created_category_id} в results"
assert match.get("company_id") == td.company_id, f"company_id не совпал: {match.get('company_id')} != {td.company_id}"
place_ids = match.get("place_ids")
assert isinstance(place_ids, list), f"place_ids должен быть list, получено: {place_ids!r}"
assert created_place_id in place_ids, f"В place_ids нет созданного place_id={created_place_id}. place_ids={place_ids}"

View File

@ -0,0 +1,22 @@
# pyright: reportCallIssue=false
from __future__ import annotations
from behave import then, when
from worklib import admin_data
@when("get access token") # pyright: ignore[reportGeneralTypeIssues]
def step_get_access_token(context) -> None:
if not getattr(context, "access_token", None):
token = admin_data.get_access_token_from_env()
context.access_token = token
admin_data.get_or_create_user("tester").access_token = token
@then("access token is valid") # pyright: ignore[reportGeneralTypeIssues]
def step_token_is_valid(context) -> None:
token = getattr(context, "access_token", None)
assert isinstance(token, str) and token.strip(), f"access_token пустой/не строка: {token}"

View File

@ -0,0 +1,108 @@
# pyright: reportCallIssue=false
from __future__ import annotations
import json
from typing import Any
import allure # pyright: ignore[reportMissingImports]
from allure_commons.types import AttachmentType # pyright: ignore[reportMissingImports]
from behave import then, when
from Ticket.testdata.ticket_test_data import TicketTestData
from worklib.graphql_client import execute_graphql
DEFAULT_CATEGORY_ID = "6569776b0bb9d14b23bd4de7"
def _attach_json(name: str, payload: Any) -> None:
allure.attach(
json.dumps(payload, ensure_ascii=False, indent=2),
name=name,
attachment_type=AttachmentType.JSON,
)
@when("query employee by category and company") # pyright: ignore[reportGeneralTypeIssues]
def step_query_employee(context) -> None:
td = TicketTestData.from_behave_context(context)
token = td.ensure_token()
query = """
query employee($category_id: String!, $company_id: String!) {
employee(filters: {category_id: $category_id, company_id: $company_id}) {
results {
id
company { id name }
user {
id
username
data { first_name last_name }
}
}
}
}
""".strip()
category_id = td.category_id or DEFAULT_CATEGORY_ID
variables = {"category_id": category_id, "company_id": td.company_id}
with allure.step("GraphQL: employee(filters: category_id + company_id)"):
resp = execute_graphql(query=query, variables=variables, company_id=td.company_id, access_token=token)
_attach_json("employee response", resp)
context.employee_response = resp
@then("employee results are not empty") # pyright: ignore[reportGeneralTypeIssues]
def step_employee_results_not_empty(context) -> None:
resp = getattr(context, "employee_response", None)
assert isinstance(resp, dict), f"Нет employee_response или не dict: {resp}"
results = resp.get("data", {}).get("employee", {}).get("results")
_attach_json("employee.results (extracted)", results)
assert isinstance(results, list), f"employee.results должен быть list. Ответ: {resp}"
assert len(results) > 0, "employee.results пустой — тест должен падать"
@then("each employee result has id and user fields") # pyright: ignore[reportGeneralTypeIssues]
def step_each_result_has_fields(context) -> None:
resp = getattr(context, "employee_response", None)
assert isinstance(resp, dict), f"Нет employee_response или не dict: {resp}"
results = resp.get("data", {}).get("employee", {}).get("results", [])
assert isinstance(results, list), "employee.results не list"
for i, item in enumerate(results):
assert isinstance(item, dict), f"results[{i}] не объект: {type(item)}"
assert item.get("id"), f"results[{i}].id пустой"
company = item.get("company")
assert isinstance(company, dict), f"results[{i}].company не объект: {company}"
assert company.get("id"), f"results[{i}].company.id пустой"
assert company.get("name"), f"results[{i}].company.name пустой"
user = item.get("user")
assert isinstance(user, dict), f"results[{i}].user не объект: {user}"
assert user.get("id"), f"results[{i}].user.id пустой"
assert user.get("username"), f"results[{i}].user.username пустой"
data = user.get("data")
assert isinstance(data, dict), f"results[{i}].user.data не объект: {data}"
assert data.get("first_name"), f"results[{i}].user.data.first_name пустой"
assert data.get("last_name"), f"results[{i}].user.data.last_name пустой"
@then("created employee username is in results") # pyright: ignore[reportGeneralTypeIssues]
def step_created_username_in_results(context) -> None:
td = TicketTestData.from_behave_context(context)
username = td.username
assert username, "Нет username в TicketTestData (user ещё не создан)."
resp = getattr(context, "employee_response", None)
assert isinstance(resp, dict), f"Нет employee_response или не dict: {resp}"
results = resp.get("data", {}).get("employee", {}).get("results", [])
assert isinstance(results, list), "employee.results не list"
usernames: list[str] = []
for item in results:
if isinstance(item, dict):
user = item.get("user")
if isinstance(user, dict) and user.get("username"):
usernames.append(user["username"])
_attach_json("employee.usernames (extracted)", usernames)
assert username in usernames, f"Не нашли созданного пользователя {username} в employee.results: {usernames}"

View File

@ -0,0 +1,179 @@
# pyright: reportCallIssue=false
from __future__ import annotations
import json
from typing import Any
import allure # pyright: ignore[reportMissingImports]
from allure_commons.types import AttachmentType # pyright: ignore[reportMissingImports]
from behave import then, when
from Ticket.testdata.ticket_test_data import TicketTestData
from worklib.graphql_client import 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 _get_change_ids(context) -> dict[str, str]:
ids = getattr(context, "ticket_category_change_ids", None)
assert isinstance(ids, dict), f"Нет ticket_category_change_ids в контексте: {ids}"
for k in ("old", "in_group", "out_group"):
assert ids.get(k), f"ticket_category_change_ids[{k!r}] пустой: {ids}"
return ids
def _get_change_titles(context) -> dict[str, str]:
titles = getattr(context, "ticket_category_change_titles", None)
assert isinstance(titles, dict), f"Нет ticket_category_change_titles в контексте: {titles}"
for k in ("old", "in_group", "out_group"):
assert titles.get(k), f"ticket_category_change_titles[{k!r}] пустой: {titles}"
return titles
@when("change ticket category to in_group category") # pyright: ignore[reportGeneralTypeIssues]
def step_change_category_to_in_group(context) -> None:
td = TicketTestData.from_behave_context(context)
token = td.ensure_token()
ids = _get_change_ids(context)
ticket_id = td.ticket_id or getattr(context, "ticket_id", None)
assert ticket_id, "Нет ticket_id (ticket ещё не создан)."
mutation = """
mutation updatecategory($ticket_id: String!, $category_id: String!) {
changeTicketCategory(dto: {ticket_id: $ticket_id, category_id: $category_id})
}
""".strip()
variables = {"ticket_id": ticket_id, "category_id": ids["in_group"]}
with allure.step("GraphQL: changeTicketCategory (to in_group)"):
resp = execute_graphql(query=mutation, variables=variables, company_id=td.company_id, access_token=token)
_attach_json("changeTicketCategory response", resp)
context.change_category_response = resp
@when("change ticket category to out_group category") # pyright: ignore[reportGeneralTypeIssues]
def step_change_category_to_out_group(context) -> None:
td = TicketTestData.from_behave_context(context)
token = td.ensure_token()
ids = _get_change_ids(context)
ticket_id = td.ticket_id or getattr(context, "ticket_id", None)
assert ticket_id, "Нет ticket_id (ticket ещё не создан)."
mutation = """
mutation updatecategory($ticket_id: String!, $category_id: String!) {
changeTicketCategory(dto: {ticket_id: $ticket_id, category_id: $category_id})
}
""".strip()
variables = {"ticket_id": ticket_id, "category_id": ids["out_group"]}
with allure.step("GraphQL: changeTicketCategory (to out_group)"):
resp = execute_graphql(query=mutation, variables=variables, company_id=td.company_id, access_token=token)
_attach_json("changeTicketCategory response", resp)
context.change_category_response = resp
@when("query tickets by created place id") # pyright: ignore[reportGeneralTypeIssues]
def step_query_tickets_by_place(context) -> None:
td = TicketTestData.from_behave_context(context)
token = td.ensure_token()
place_id = td.place_id
assert place_id, "Нет place_id (place ещё не создан)."
query = """
query tickets($place_id: String!) {
ticket(
filter: { place_id: $place_id },
pagination: { limit: 100, skip: 0 }
) {
results {
number
id
category { id title }
assignee { id user { id username data { first_name last_name } } }
}
}
}
""".strip()
variables = {"place_id": place_id}
with allure.step("GraphQL: ticket(filter: place_id)"):
resp = execute_graphql(query=query, variables=variables, company_id=td.company_id, access_token=token)
_attach_json("ticket response", resp)
context.ticket_query_response = resp
def _find_ticket_in_results(context, *, ticket_id: str) -> dict[str, Any]:
resp = getattr(context, "ticket_query_response", None)
assert isinstance(resp, dict), f"Нет ticket_query_response или не dict: {resp}"
results = resp.get("data", {}).get("ticket", {}).get("results", [])
assert isinstance(results, list), f"ticket.results не list. Ответ: {resp}"
for item in results:
if isinstance(item, dict) and item.get("id") == ticket_id:
return item
_attach_json("ticket.results (extracted)", results)
raise AssertionError(f"Не нашли ticket id={ticket_id} в ticket.results")
@then("ticket category changed from old to in_group") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_category_changed_to_in_group(context) -> None:
td = TicketTestData.from_behave_context(context)
ids = _get_change_ids(context)
titles = _get_change_titles(context)
ticket_id = td.ticket_id or getattr(context, "ticket_id", None)
assert ticket_id, "Нет ticket_id."
item = _find_ticket_in_results(context, ticket_id=ticket_id)
category = item.get("category")
assert isinstance(category, dict), f"ticket.category не объект: {category}"
assert category.get("id"), "ticket.category.id пустой"
assert category.get("title"), "ticket.category.title пустой"
assert category["id"] != ids["old"], f"Категория не поменялась, всё ещё old={ids['old']}"
assert category["id"] == ids["in_group"], f"Ожидали in_group={ids['in_group']}, получили {category['id']}"
assert category["title"] != titles["old"], f"title не поменялся, всё ещё old title={titles['old']!r}"
assert category["title"] == titles["in_group"], f"Ожидали title={titles['in_group']!r}, получили {category['title']!r}"
@then("employee is authorized for ticket") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_employee_authorized(context) -> None:
td = TicketTestData.from_behave_context(context)
ticket_id = td.ticket_id or getattr(context, "ticket_id", None)
assert ticket_id, "Нет ticket_id."
item = _find_ticket_in_results(context, ticket_id=ticket_id)
assignee = item.get("assignee")
assert isinstance(assignee, dict), f"assignee должен быть объектом (уполномочен), получено: {assignee!r}"
assert assignee.get("id"), "assignee.id пустой"
user = assignee.get("user")
assert isinstance(user, dict), f"assignee.user не объект: {user!r}"
if td.account_id:
assert user.get("id") == td.account_id, f"user.id не совпал с созданным user_id: {user.get('id')!r} != {td.account_id!r}"
data = user.get("data")
assert isinstance(data, dict), f"assignee.user.data не объект: {data!r}"
assert data.get("first_name") == td.default_user_first_name, f"first_name не совпал: {data.get('first_name')!r}"
assert data.get("last_name") == td.default_user_last_name, f"last_name не совпал: {data.get('last_name')!r}"
@then("employee is NOT authorized for ticket") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_employee_not_authorized(context) -> None:
td = TicketTestData.from_behave_context(context)
ticket_id = td.ticket_id or getattr(context, "ticket_id", None)
assert ticket_id, "Нет ticket_id."
item = _find_ticket_in_results(context, ticket_id=ticket_id)
assignee = item.get("assignee")
if assignee is None:
return
# если assignee есть, то он должен быть не нашим user
if isinstance(assignee, dict) and isinstance(assignee.get("user"), dict) and isinstance(assignee["user"].get("data"), dict):
data = assignee["user"]["data"]
assert not (
data.get("first_name") == td.default_user_first_name and data.get("last_name") == td.default_user_last_name
), f"Employee всё ещё уполномочен (assignee совпал с созданным user): {data}"
return
raise AssertionError(f"Неожиданная структура assignee при проверке NOT authorized: {assignee!r}")

View File

@ -0,0 +1,127 @@
# pyright: reportCallIssue=false
from __future__ import annotations
from behave import when
from Ticket.testdata.ticket_test_data import TicketTestData
from worklib.graphql_client import execute_graphql
DEFAULT_TICKETINFO_PLACE_ID = "682733c16773cfa73dc8d0a7"
@when("prepare ticket and categories for category change test") # pyright: ignore[reportGeneralTypeIssues]
def step_prepare_ticket_and_categories(context) -> None:
"""
Подготовка тестовых данных:
- place
- 3 категории (old / in_group / out_group)
- user + employee
- category group по категории in_group + connect employee
- ticket с категорией old (createTicket может падать по правам это ок, тест должен упасть)
"""
td = TicketTestData.from_behave_context(context)
token = td.ensure_token()
query = """
query ticketinfo($place_id: String!) {
ticket(pagination:{skip:0,limit:25} ,filter:{place_id:$place_id}){
results {
id
category { id title }
assignee { id user { id username } }
}
}
}
""".strip()
# Сначала пытаемся работать с существующей заявкой на заранее известном place_id.
# Если на стенде нет заявок — пытаемся создать свою (если createTicket разрешён).
td.place_id = DEFAULT_TICKETINFO_PLACE_ID
resp = execute_graphql(query=query, variables={"place_id": td.place_id}, company_id=td.company_id, access_token=token)
results = resp.get("data", {}).get("ticket", {}).get("results", [])
if isinstance(results, list) and results:
ticket0 = results[0]
assert isinstance(ticket0, dict) and ticket0.get("id"), f"Некорректный ticket: {ticket0!r}"
old_category = ticket0.get("category")
assert isinstance(old_category, dict) and old_category.get("id") and old_category.get("title"), f"У ticket нет category: {ticket0!r}"
context.ticket_id = ticket0["id"]
td.ticket_id = ticket0["id"]
old_category_id = old_category["id"]
old_category_title = old_category["title"]
place_id_for_new_categories = td.place_id
else:
# fallback: создаём тестовую заявку сами
td.place_id = None
td.ensure_place()
place_id_for_new_categories = td.place_id
assert place_id_for_new_categories
old_category_id = td.create_ticket_category(title="cat-old", place_id=place_id_for_new_categories)
old_category_title = "cat-old"
try:
td.create_ticket_with_category(category_id=old_category_id, place_id=place_id_for_new_categories)
except AssertionError as e:
raise AssertionError(
"Нет доступных tickets для проверки (по умолчанию берём place_id "
f"{DEFAULT_TICKETINFO_PLACE_ID}) и createTicket запрещён на стенде. "
"Укажите place_id с существующими заявками (поменяйте DEFAULT_TICKETINFO_PLACE_ID в шаге) "
"или дайте права на createTicket. "
f"Детали: {e}"
)
context.ticket_id = td.ticket_id
ids = {
"old": old_category_id,
"in_group": td.create_ticket_category(title=f"cat-in-group-{td.ticket_id}", place_id=place_id_for_new_categories),
"out_group": td.create_ticket_category(title=f"cat-out-group-{td.ticket_id}", place_id=place_id_for_new_categories),
}
titles = {"old": old_category_title, "in_group": f"cat-in-group-{td.ticket_id}", "out_group": f"cat-out-group-{td.ticket_id}"}
context.ticket_category_change_ids = ids
context.ticket_category_change_titles = titles
# создаём нового employee (user создастся внутри) — он должен оставаться привязанным в группе №1
td.create_employee()
assert td.account_id and td.employee_id, "Не создался employee/user для теста смены категории"
context.ticket_employee_id = td.employee_id
context.ticket_account_id = td.account_id
context.ticket_username = td.username
fixed_member = {"user_id": td.fixed_user_id, "employee_id": td.fixed_employee_id}
new_member = {"user_id": td.account_id, "employee_id": td.employee_id}
# Группа №1: две категории (old + in_group), в ней новый employee + фиксированный employee
# => при смене old -> in_group новый employee должен остаться.
td.create_category_group_for_categories(
[ids["old"], ids["in_group"]],
members=[new_member, fixed_member],
cache=False,
)
# Группа №2: категория out_group, в ней только фиксированный employee (обязательное требование)
# => при смене на out_group новый employee должен "отпасть".
td.create_category_group_for_categories(
[ids["out_group"]],
members=[fixed_member],
cache=False,
)
# cleanup: вернуть категорию обратно
cleanup_fns = getattr(context, "_cleanup_fns", None)
if isinstance(cleanup_fns, list):
def _restore_category() -> None:
mutation = """
mutation updatecategory($ticket_id: String!, $category_id: String!) {
changeTicketCategory(dto: {ticket_id: $ticket_id, category_id: $category_id})
}
""".strip()
execute_graphql(
query=mutation,
variables={"ticket_id": td.ticket_id, "category_id": ids["old"]},
company_id=td.company_id,
access_token=token,
)
cleanup_fns.append(_restore_category)

View File

@ -0,0 +1,304 @@
# pyright: reportCallIssue=false
from __future__ import annotations
import allure # pyright: ignore[reportMissingImports]
from allure_commons.types import AttachmentType # pyright: ignore[reportMissingImports]
from behave import then, when
import json
from typing import Any
from Ticket.testdata.ticket_test_data import TicketTestData
from worklib.graphql_client import 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,
)
DEFAULT_TICKETINFO_PLACE_ID = "682733c16773cfa73dc8d0a7"
@when("create user for ticket") # pyright: ignore[reportGeneralTypeIssues]
def step_create_user_for_ticket(context) -> None:
td = TicketTestData.from_behave_context(context)
td.create_user()
context.ticket_account_id = td.account_id
context.ticket_username = td.username
@when("create employee for created user") # pyright: ignore[reportGeneralTypeIssues]
def step_create_employee(context) -> None:
td = TicketTestData.from_behave_context(context)
td.create_employee()
context.ticket_employee_id = td.employee_id
@when("create category group for created category") # pyright: ignore[reportGeneralTypeIssues]
def step_create_category_group(context) -> None:
td = TicketTestData.from_behave_context(context)
td.create_category_group()
context.ticket_category_group_id = td.category_group_id
@when("connect employee to category group") # pyright: ignore[reportGeneralTypeIssues]
def step_connect_employee(context) -> None:
td = TicketTestData.from_behave_context(context)
td.connect_employee_to_category_group()
@when("prepare ticket and employees for assign employee test") # pyright: ignore[reportGeneralTypeIssues]
def step_prepare_ticket_and_employees_for_assign(context) -> None:
"""
Подготовка:
- 1 place
- 1 category
- 1 category group (для этой категории)
- 3 employee:
- fixed employee (в группе)
- новый in_group employee (в группе)
- новый out_group employee (вне группы)
- 1 ticket в этой категории
"""
td = TicketTestData.from_behave_context(context)
token = td.ensure_token()
# Берём существующую заявку и её категорию (createTicket часто запрещён)
query = """
query ticketinfo($place_id: String!) {
ticket(pagination:{skip:0,limit:25} ,filter:{place_id:$place_id}){
results {
id
category { id title }
assignee { id user { id username } }
}
}
}
""".strip()
td.place_id = DEFAULT_TICKETINFO_PLACE_ID
resp = execute_graphql(query=query, variables={"place_id": td.place_id}, company_id=td.company_id, access_token=token)
results = resp.get("data", {}).get("ticket", {}).get("results", [])
if isinstance(results, list) and results:
ticket0 = results[0]
assert isinstance(ticket0, dict) and ticket0.get("id"), f"Некорректный ticket: {ticket0!r}"
category = ticket0.get("category")
assert isinstance(category, dict) and category.get("id"), f"У ticket нет category: {ticket0!r}"
context.ticket_id = ticket0["id"]
td.ticket_id = ticket0["id"]
category_id = category["id"]
else:
# fallback: создаём свою заявку, если это разрешено
td.place_id = None
td.ensure_place()
category_id = td.ensure_ticket_category()
try:
td.create_ticket_with_category(category_id=category_id, place_id=td.place_id)
except AssertionError as e:
raise AssertionError(
"Нет доступных tickets для проверки assignTicketEmployee (по умолчанию берём place_id "
f"{DEFAULT_TICKETINFO_PLACE_ID}) и createTicket запрещён на стенде. "
"Укажите place_id с существующими заявками (поменяйте DEFAULT_TICKETINFO_PLACE_ID в шаге) "
"или дайте права на createTicket. "
f"Детали: {e}"
)
context.ticket_id = td.ticket_id
with allure.step("GraphQL: ticket(pagination:skip:0,limit:25,filter:place_id)"):
resp = execute_graphql(query=query, variables={"place_id": td.place_id}, company_id=td.company_id, access_token=token)
_attach_json("ticket response", resp)
context.ticket_response = resp
# employee #2 (new in-group)
td.create_employee()
assert td.account_id and td.employee_id, "Не создался in-group employee"
in_group_user_id = td.account_id
in_group_employee_id = td.employee_id
fixed_member = {"user_id": td.fixed_user_id, "employee_id": td.fixed_employee_id}
new_in_group_member = {"user_id": in_group_user_id, "employee_id": in_group_employee_id}
td.create_category_group_for_categories([category_id], members=[fixed_member, new_in_group_member], cache=False)
# employee #3 (out-group): создаём ещё одного нового employee, но НЕ добавляем в group
td_out = TicketTestData(company_id=td.company_id)
td_out.access_token = td.access_token
td_out._cleanup_fns = getattr(context, "_cleanup_fns", None)
td_out.create_employee()
assert td_out.account_id and td_out.employee_id, "Не создался out-group employee"
out_group_user_id = td_out.account_id
# сохраняем ids для проверок
context.assign_fixed_user_id = td.fixed_user_id
context.assign_in_group_user_id = in_group_user_id
context.assign_out_group_user_id = out_group_user_id
def _assign_ticket_employee(context, *, employee_user_id: str, expect_error: bool) -> None:
td = TicketTestData.from_behave_context(context)
token = td.ensure_token()
ticket_id = td.ticket_id or getattr(context, "ticket_id", None)
assert ticket_id, "Нет ticket_id."
mutation = """
mutation assignTicketEmployee($ticket_id: String!, $employee_user_id: String!) {
assignTicketEmployee(dto: {ticket_id: $ticket_id, employee_user_id: $employee_user_id})
}
""".strip()
variables = {"ticket_id": ticket_id, "employee_user_id": employee_user_id}
try:
resp = execute_graphql(query=mutation, variables=variables, company_id=td.company_id, access_token=token)
context.assign_ticket_employee_response = resp
if expect_error:
raise AssertionError(f"Ожидали ошибку при assignTicketEmployee, но получили успех: {resp}")
except Exception as e:
context.assign_ticket_employee_error = str(e)
if not expect_error:
raise
@when("assign ticket to fixed in_group employee") # pyright: ignore[reportGeneralTypeIssues]
def step_assign_ticket_fixed_employee(context) -> None:
_assign_ticket_employee(context, employee_user_id=getattr(context, "assign_fixed_user_id"), expect_error=False)
@when("assign ticket to new in_group employee") # pyright: ignore[reportGeneralTypeIssues]
def step_assign_ticket_new_in_group_employee(context) -> None:
_assign_ticket_employee(context, employee_user_id=getattr(context, "assign_in_group_user_id"), expect_error=False)
@when("assign ticket to out_group employee (should fail)") # pyright: ignore[reportGeneralTypeIssues]
def step_assign_ticket_out_group_employee(context) -> None:
_assign_ticket_employee(context, employee_user_id=getattr(context, "assign_out_group_user_id"), expect_error=True)
@when("prepare ticket and employees for unassign employee test") # pyright: ignore[reportGeneralTypeIssues]
def step_prepare_ticket_and_employees_for_unassign(context) -> None:
"""
Подготовка для unassign:
- 1 категория + 1 группа категорий (для этой категории)
- 2 работника:
- fixed employee (в группе): user_id/employee_id заданы константами
- новый employee (в группе) его будем assign/unassign
- 1 ticket (существующий по place_id либо созданный, если разрешено)
"""
td = TicketTestData.from_behave_context(context)
token = td.ensure_token()
query = """
query ticketinfo($place_id: String!) {
ticket(pagination:{skip:0,limit:25} ,filter:{place_id:$place_id}){
results {
id
category { id title }
assignee { id user { id username } }
}
}
}
""".strip()
td.place_id = DEFAULT_TICKETINFO_PLACE_ID
resp = execute_graphql(query=query, variables={"place_id": td.place_id}, company_id=td.company_id, access_token=token)
results = resp.get("data", {}).get("ticket", {}).get("results", [])
if isinstance(results, list) and results:
ticket0 = results[0]
assert isinstance(ticket0, dict) and ticket0.get("id"), f"Некорректный ticket: {ticket0!r}"
category = ticket0.get("category")
assert isinstance(category, dict) and category.get("id"), f"У ticket нет category: {ticket0!r}"
context.ticket_id = ticket0["id"]
td.ticket_id = ticket0["id"]
category_id = category["id"]
else:
# fallback: создаём свою заявку, если это разрешено
td.place_id = None
td.ensure_place()
category_id = td.ensure_ticket_category()
try:
td.create_ticket_with_category(category_id=category_id, place_id=td.place_id)
except AssertionError as e:
raise AssertionError(
"Нет доступных tickets для проверки unassignTicketEmployee (по умолчанию берём place_id "
f"{DEFAULT_TICKETINFO_PLACE_ID}) и createTicket запрещён на стенде. "
"Укажите place_id с существующими заявками (поменяйте DEFAULT_TICKETINFO_PLACE_ID в шаге) "
"или дайте права на createTicket. "
f"Детали: {e}"
)
context.ticket_id = td.ticket_id
# новый employee (его будем assign/unassign)
td_new = TicketTestData(company_id=td.company_id)
td_new.access_token = td.access_token
td_new._cleanup_fns = getattr(context, "_cleanup_fns", None)
td_new.create_employee()
assert td_new.account_id and td_new.employee_id, "Не создался новый employee для unassign"
fixed_member = {"user_id": td.fixed_user_id, "employee_id": td.fixed_employee_id}
new_member = {"user_id": td_new.account_id, "employee_id": td_new.employee_id}
td.create_category_group_for_categories([category_id], members=[fixed_member, new_member], cache=False)
context.unassign_new_user_id = td_new.account_id
@when("assign ticket to new grouped employee") # pyright: ignore[reportGeneralTypeIssues]
def step_assign_ticket_to_new_grouped_employee(context) -> None:
_assign_ticket_employee(context, employee_user_id=getattr(context, "unassign_new_user_id"), expect_error=False)
@when("unassign ticket from new grouped employee") # pyright: ignore[reportGeneralTypeIssues]
def step_unassign_ticket_from_new_grouped_employee(context) -> None:
td = TicketTestData.from_behave_context(context)
token = td.ensure_token()
ticket_id = td.ticket_id or getattr(context, "ticket_id", None)
assert ticket_id, "Нет ticket_id."
employee_user_id = getattr(context, "unassign_new_user_id", None)
assert isinstance(employee_user_id, str) and employee_user_id, "Нет unassign_new_user_id."
mutation = """
mutation unassignTicketEmployee($ticket_id: String!, $employee_user_id: String!) {
unassignTicketEmployee(dto: {ticket_id: $ticket_id, employee_user_id: $employee_user_id})
}
""".strip()
variables = {"ticket_id": ticket_id, "employee_user_id": employee_user_id}
resp = execute_graphql(query=mutation, variables=variables, company_id=td.company_id, access_token=token)
context.unassign_ticket_employee_response = resp
def _get_ticket_assignee_user_id(context) -> str | None:
td = TicketTestData.from_behave_context(context)
ticket_id = td.ticket_id or getattr(context, "ticket_id", None)
assert ticket_id, "Нет ticket_id."
resp = getattr(context, "ticket_query_response", None)
assert isinstance(resp, dict), f"Нет ticket_query_response: {resp!r}"
results = resp.get("data", {}).get("ticket", {}).get("results", [])
assert isinstance(results, list), f"ticket.results не list: {results!r}"
for item in results:
if isinstance(item, dict) and item.get("id") == ticket_id:
assignee = item.get("assignee")
if assignee is None:
return None
if isinstance(assignee, dict) and isinstance(assignee.get("user"), dict):
return assignee["user"].get("id")
raise AssertionError(f"Неожиданная структура assignee: {assignee!r}")
raise AssertionError(f"Не нашли ticket id={ticket_id} в results")
@then("ticket assignee is fixed employee") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_assignee_fixed(context) -> None:
assert _get_ticket_assignee_user_id(context) == getattr(context, "assign_fixed_user_id")
@then("ticket assignee is new in_group employee") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_assignee_new_in_group(context) -> None:
assert _get_ticket_assignee_user_id(context) == getattr(context, "assign_in_group_user_id")
@then("ticket assignee is still new in_group employee") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_assignee_still_new_in_group(context) -> None:
assert _get_ticket_assignee_user_id(context) == getattr(context, "assign_in_group_user_id")
@then("ticket assignee is new grouped employee") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_assignee_new_grouped_employee(context) -> None:
assert _get_ticket_assignee_user_id(context) == getattr(context, "unassign_new_user_id")
@then("ticket assignee is empty") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_assignee_empty(context) -> None:
assignee_user_id = _get_ticket_assignee_user_id(context)
assert assignee_user_id is None, f"Ожидали пустой assignee после unassign, получили user_id={assignee_user_id!r}"

View File

@ -0,0 +1,29 @@
# pyright: reportCallIssue=false
from __future__ import annotations
from behave import when
from Ticket.testdata.ticket_test_data import TicketTestData
@when("create place multiple for ticket") # pyright: ignore[reportGeneralTypeIssues]
def step_create_place_multiple(context) -> None:
td = TicketTestData.from_behave_context(context)
td.ensure_place()
context.ticket_place_id = td.place_id
@when("create ticket category for created place") # pyright: ignore[reportGeneralTypeIssues]
def step_create_ticket_category_for_place(context) -> None:
td = TicketTestData.from_behave_context(context)
td.ensure_ticket_category()
context.ticket_category_id = td.category_id
@when("create ticket for created place and category") # pyright: ignore[reportGeneralTypeIssues]
def step_create_ticket(context) -> None:
td = TicketTestData.from_behave_context(context)
td.ensure_ticket()
context.ticket_id = td.ticket_id

460
Ticket/testdata/ticket_test_data.py vendored Normal file
View File

@ -0,0 +1,460 @@
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:
delete_mutation = """mutation deleteTicket($id: String!) { deleteTicket(id: $id) }""".strip()
_exec_or_fail(op_name="deleteTicket(mutation)", token=token, query=delete_mutation, variables={"id": ticket_id}, 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:
delete_mutation = """mutation deleteTicket($id: String!) { deleteTicket(id: $id) }""".strip()
_exec_or_fail(op_name="deleteTicket(mutation)", token=token, query=delete_mutation, variables={"id": ticket_id}, 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)

2
allure-report/app.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,21 @@
{
"data": {
"place": {
"results": [
{
"id": "69e8a5c963548ff04f752605",
"members": [
{
"id": "27a692df-2cd4-45e7-97db-a8696f2691af",
"parent_id": null,
"user": {
"id": "27a692df-2cd4-45e7-97db-a8696f2691af",
"username": "+79997035425"
}
}
]
}
]
}
}
}

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69e8a59d0bfa76f761dd1b02",
"member_id": "71cd5726-c404-490f-bb39-49fd3c52e9db"
}
}
}

View File

@ -0,0 +1,25 @@
{
"data": {
"createSubscription": {
"id": "69e8a81866221071869bcc3e",
"services": [
{
"id": "69e8a8181a037043d86d23dd",
"title": "kvs-service-1776855066"
}
],
"user": {
"id": "4dfe1f78-8dd7-40ff-b0e1-55536422c6b1",
"data": {
"first_name": "kvstest1",
"last_name": "kvstest2"
}
},
"plan": {
"id": "69e8a81866221071869bcc3d",
"title": "plan-kvs-1776855066"
},
"place_id": "69e8a8180bfa76f761dd1b20"
}
}
}

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69e8a5c963548ff04f752605",
"member_id": "27a692df-2cd4-45e7-97db-a8696f2691af"
}
}
}

View File

@ -0,0 +1,10 @@
{
"data": {
"createPlaceMultiple": [
{
"id": "69e8a59d0bfa76f761dd1b02",
"__typename": "PlaceObject"
}
]
}
}

View File

@ -0,0 +1,10 @@
{
"data": {
"createPlaceMultiple": [
{
"id": "69e8a5c863548ff04f752602",
"__typename": "PlaceObject"
}
]
}
}

View File

@ -0,0 +1,18 @@
{
"data": {
"createUser": {
"id": "4dfe1f78-8dd7-40ff-b0e1-55536422c6b1",
"created_at": "2026-04-22T10:51:04.774Z",
"updated_at": "2026-04-22T10:51:04.774Z",
"username": "+79996530370",
"user_data": {
"first_name": "kvstest1",
"last_name": "kvstest2",
"email": ""
},
"is_demo": true,
"next_request_timestamp": "1970-01-01T00:00:00.000Z",
"roles": []
}
}
}

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69e8a5c95d6417545a7f4848",
"member_id": "c89ab70b-4159-4cee-b9a1-01883b645b75"
}
}
}

View File

@ -0,0 +1,21 @@
{
"data": {
"place": {
"results": [
{
"id": "69e8a8180bfa76f761dd1b20",
"members": [
{
"id": "4dfe1f78-8dd7-40ff-b0e1-55536422c6b1",
"parent_id": null,
"user": {
"id": "4dfe1f78-8dd7-40ff-b0e1-55536422c6b1",
"username": "+79996530370"
}
}
]
}
]
}
}
}

View File

@ -0,0 +1,9 @@
{
"data": {
"createService": {
"id": "69e8a8181a037043d86d23dd",
"title": "kvs-service-1776855066",
"type": "access"
}
}
}

View File

@ -0,0 +1,21 @@
{
"data": {
"place": {
"results": [
{
"id": "69e8a59d0bfa76f761dd1b02",
"members": [
{
"id": "71cd5726-c404-490f-bb39-49fd3c52e9db",
"parent_id": null,
"user": {
"id": "71cd5726-c404-490f-bb39-49fd3c52e9db",
"username": "+79992490173"
}
}
]
}
]
}
}
}

View File

@ -0,0 +1,10 @@
{
"data": {
"createPlaceMultiple": [
{
"id": "69e8a8180bfa76f761dd1b20",
"__typename": "PlaceObject"
}
]
}
}

View File

@ -0,0 +1,20 @@
{
"data": {
"createPlan": {
"id": "69e8a5c966221071869bcc3b",
"service_ids": [
"69e8a5c91a037043d86d23d8"
],
"bundle_ids": [],
"place_id": "69e8a5c963548ff04f752605",
"place_ids": [
"69e8a5c963548ff04f752605"
],
"price": 200,
"title": "plan-kvs-1776854475",
"discount": "0",
"payment_interval": 1,
"price_without_discount": null
}
}
}

View File

@ -0,0 +1,12 @@
{
"data": {
"place": {
"results": [
{
"id": "69e8a5c863548ff04f752602",
"members": []
}
]
}
}
}

View File

@ -0,0 +1,18 @@
{
"data": {
"createUser": {
"id": "e03812e4-8689-4689-b561-29a40b290851",
"created_at": "2026-04-22T10:51:04.087Z",
"updated_at": "2026-04-22T10:51:04.087Z",
"username": "+79999216433",
"user_data": {
"first_name": "kvstest1",
"last_name": "kvstest2",
"email": ""
},
"is_demo": true,
"next_request_timestamp": "1970-01-01T00:00:00.000Z",
"roles": []
}
}
}

View File

@ -0,0 +1,12 @@
{
"data": {
"place": {
"results": [
{
"id": "69e8a8170bfa76f761dd1b19",
"members": []
}
]
}
}
}

View File

@ -0,0 +1,10 @@
{
"data": {
"createPlaceMultiple": [
{
"id": "69e8a59c5d6417545a7f4829",
"__typename": "PlaceObject"
}
]
}
}

View File

@ -0,0 +1,10 @@
{
"data": {
"createPlaceMultiple": [
{
"id": "69e8a5c95d6417545a7f4848",
"__typename": "PlaceObject"
}
]
}
}

View File

@ -0,0 +1,21 @@
{
"data": {
"place": {
"results": [
{
"id": "69e8a5c963548ff04f752605",
"members": [
{
"id": "27a692df-2cd4-45e7-97db-a8696f2691af",
"parent_id": null,
"user": {
"id": "27a692df-2cd4-45e7-97db-a8696f2691af",
"username": "+79997035425"
}
}
]
}
]
}
}
}

View File

@ -0,0 +1,10 @@
{
"data": {
"createPlaceMultiple": [
{
"id": "69e8a8185d6417545a7f4863",
"__typename": "PlaceObject"
}
]
}
}

View File

@ -0,0 +1,18 @@
{
"data": {
"createUser": {
"id": "a6542878-eba4-45ff-b1ba-38fe31a1349e",
"created_at": "2026-04-22T10:40:30.004Z",
"updated_at": "2026-04-22T10:40:30.004Z",
"username": "+79994167356",
"user_data": {
"first_name": "kvstest1",
"last_name": "kvstest2",
"email": ""
},
"is_demo": true,
"next_request_timestamp": "1970-01-01T00:00:00.000Z",
"roles": []
}
}
}

View File

@ -0,0 +1,16 @@
Traceback (most recent call last):
File "KVSTest\features\environment.py", line 21, in after_scenario
fn()
~~^^
File "C:\Users\Степаан\PycharmProjects\work\KVSTest\testdata\subscription_test_data.py", line 230, in _cleanup_delete_subscription
_exec_or_fail(op_name="deleteSubscription(mutation)", token=token, query=del_mut, variables={"id": subscription_id}, company_id=self.company_id)
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Степаан\PycharmProjects\work\KVSTest\testdata\subscription_test_data.py", line 25, in _exec_or_fail
return execute_graphql(
query=query,
...<2 lines>...
access_token=token,
)
File "C:\Users\Степаан\PycharmProjects\work\worklib\graphql_client.py", line 65, in execute_graphql
raise RuntimeError(f"GraphQL errors: {errors}")
RuntimeError: GraphQL errors: [{'message': 'Not Found', 'code': 'Client Error', 'status': 404, 'description': 'The server has not found anything matching the Request-URI'}]

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69e8a5c963548ff04f752605",
"member_id": "27a692df-2cd4-45e7-97db-a8696f2691af"
}
}
}

View File

@ -0,0 +1,20 @@
{
"data": {
"createPlan": {
"id": "69e8a81866221071869bcc3d",
"service_ids": [
"69e8a8181a037043d86d23dd"
],
"bundle_ids": [],
"place_id": "69e8a8180bfa76f761dd1b20",
"place_ids": [
"69e8a8180bfa76f761dd1b20"
],
"price": 200,
"title": "plan-kvs-1776855066",
"discount": "0",
"payment_interval": 1,
"price_without_discount": null
}
}
}

View File

@ -0,0 +1,5 @@
{
"data": {
"deleteSubscription": true
}
}

View File

@ -0,0 +1,18 @@
{
"data": {
"createUser": {
"id": "71cd5726-c404-490f-bb39-49fd3c52e9db",
"created_at": "2026-04-22T10:40:29.354Z",
"updated_at": "2026-04-22T10:40:29.354Z",
"username": "+79992490173",
"user_data": {
"first_name": "kvstest1",
"last_name": "kvstest2",
"email": ""
},
"is_demo": true,
"next_request_timestamp": "1970-01-01T00:00:00.000Z",
"roles": []
}
}
}

View File

@ -0,0 +1,10 @@
{
"data": {
"createPlaceMultiple": [
{
"id": "69e8a59d5d6417545a7f4844",
"__typename": "PlaceObject"
}
]
}
}

View File

@ -0,0 +1,9 @@
{
"data": {
"createService": {
"id": "69e8a5c91a037043d86d23d8",
"title": "kvs-service-1776854475",
"type": "access"
}
}
}

View File

@ -0,0 +1,12 @@
{
"data": {
"place": {
"results": [
{
"id": "69e8a59c5d6417545a7f4829",
"members": []
}
]
}
}
}

View File

@ -0,0 +1,21 @@
{
"data": {
"place": {
"results": [
{
"id": "69e8a8180bfa76f761dd1b20",
"members": [
{
"id": "4dfe1f78-8dd7-40ff-b0e1-55536422c6b1",
"parent_id": null,
"user": {
"id": "4dfe1f78-8dd7-40ff-b0e1-55536422c6b1",
"username": "+79996530370"
}
}
]
}
]
}
}
}

View File

@ -0,0 +1,15 @@
{
"data": {
"invoices": [
{
"id": "69e8a59e1a037043d86d23d7",
"price": 200,
"status": "pending",
"subscriptions": [
"69e8a59e1a037043d86d23d6"
],
"place_id": "69e8a59d5d6417545a7f4844"
}
]
}
}

View File

@ -0,0 +1,15 @@
{
"data": {
"invoices": [
{
"id": "69e8a5c91a037043d86d23db",
"price": 200,
"status": "pending",
"subscriptions": [
"69e8a5c91a037043d86d23da"
],
"place_id": "69e8a5c963548ff04f752605"
}
]
}
}

View File

@ -0,0 +1,18 @@
{
"data": {
"createUser": {
"id": "c89ab70b-4159-4cee-b9a1-01883b645b75",
"created_at": "2026-04-22T10:41:13.059Z",
"updated_at": "2026-04-22T10:41:13.059Z",
"username": "+79993659600",
"user_data": {
"first_name": "kvstest1",
"last_name": "kvstest2",
"email": ""
},
"is_demo": true,
"next_request_timestamp": "1970-01-01T00:00:00.000Z",
"roles": []
}
}
}

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69e8a59d5d6417545a7f4844",
"member_id": "a6542878-eba4-45ff-b1ba-38fe31a1349e"
}
}
}

View File

@ -0,0 +1,20 @@
{
"data": {
"createPlan": {
"id": "69e8a59d1a037043d86d23d4",
"service_ids": [
"69e8a59d1a037043d86d23d3"
],
"bundle_ids": [],
"place_id": "69e8a59d5d6417545a7f4844",
"place_ids": [
"69e8a59d5d6417545a7f4844"
],
"price": 200,
"title": "plan-kvs-1776854432",
"discount": "0",
"payment_interval": 1,
"price_without_discount": null
}
}
}

View File

@ -0,0 +1,18 @@
{
"data": {
"createUser": {
"id": "27a692df-2cd4-45e7-97db-a8696f2691af",
"created_at": "2026-04-22T10:41:13.668Z",
"updated_at": "2026-04-22T10:41:13.668Z",
"username": "+79997035425",
"user_data": {
"first_name": "kvstest1",
"last_name": "kvstest2",
"email": ""
},
"is_demo": true,
"next_request_timestamp": "1970-01-01T00:00:00.000Z",
"roles": []
}
}
}

View File

@ -0,0 +1,25 @@
{
"data": {
"createSubscription": {
"id": "69e8a5c91a037043d86d23da",
"services": [
{
"id": "69e8a5c91a037043d86d23d8",
"title": "kvs-service-1776854475"
}
],
"user": {
"id": "27a692df-2cd4-45e7-97db-a8696f2691af",
"data": {
"first_name": "kvstest1",
"last_name": "kvstest2"
}
},
"plan": {
"id": "69e8a5c966221071869bcc3b",
"title": "plan-kvs-1776854475"
},
"place_id": "69e8a5c963548ff04f752605"
}
}
}

View File

@ -0,0 +1,21 @@
{
"data": {
"place": {
"results": [
{
"id": "69e8a8185d6417545a7f4863",
"members": [
{
"id": "e03812e4-8689-4689-b561-29a40b290851",
"parent_id": null,
"user": {
"id": "e03812e4-8689-4689-b561-29a40b290851",
"username": "+79999216433"
}
}
]
}
]
}
}
}

View File

@ -0,0 +1,10 @@
{
"data": {
"createPlaceMultiple": [
{
"id": "69e8a8170bfa76f761dd1b19",
"__typename": "PlaceObject"
}
]
}
}

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69e8a59d5d6417545a7f4844",
"member_id": "a6542878-eba4-45ff-b1ba-38fe31a1349e"
}
}
}

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69e8a8180bfa76f761dd1b20",
"member_id": "4dfe1f78-8dd7-40ff-b0e1-55536422c6b1"
}
}
}

View File

@ -0,0 +1,5 @@
{
"data": {
"deleteSubscription": true
}
}

View File

@ -0,0 +1,21 @@
{
"data": {
"place": {
"results": [
{
"id": "69e8a5c95d6417545a7f4848",
"members": [
{
"id": "c89ab70b-4159-4cee-b9a1-01883b645b75",
"parent_id": null,
"user": {
"id": "c89ab70b-4159-4cee-b9a1-01883b645b75",
"username": "+79993659600"
}
}
]
}
]
}
}
}

View File

@ -0,0 +1,16 @@
Traceback (most recent call last):
File "KVSTest\features\environment.py", line 21, in after_scenario
fn()
~~^^
File "C:\Users\Степаан\PycharmProjects\work\KVSTest\testdata\subscription_test_data.py", line 230, in _cleanup_delete_subscription
_exec_or_fail(op_name="deleteSubscription(mutation)", token=token, query=del_mut, variables={"id": subscription_id}, company_id=self.company_id)
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Степаан\PycharmProjects\work\KVSTest\testdata\subscription_test_data.py", line 25, in _exec_or_fail
return execute_graphql(
query=query,
...<2 lines>...
access_token=token,
)
File "C:\Users\Степаан\PycharmProjects\work\worklib\graphql_client.py", line 65, in execute_graphql
raise RuntimeError(f"GraphQL errors: {errors}")
RuntimeError: GraphQL errors: [{'message': 'Not Found', 'code': 'Client Error', 'status': 404, 'description': 'The server has not found anything matching the Request-URI'}]

View File

@ -0,0 +1,15 @@
{
"data": {
"invoices": [
{
"id": "69e8a81966221071869bcc3f",
"price": 200,
"status": "pending",
"subscriptions": [
"69e8a81866221071869bcc3e"
],
"place_id": "69e8a8180bfa76f761dd1b20"
}
]
}
}

View File

@ -0,0 +1,21 @@
{
"data": {
"place": {
"results": [
{
"id": "69e8a59d5d6417545a7f4844",
"members": [
{
"id": "a6542878-eba4-45ff-b1ba-38fe31a1349e",
"parent_id": null,
"user": {
"id": "a6542878-eba4-45ff-b1ba-38fe31a1349e",
"username": "+79994167356"
}
}
]
}
]
}
}
}

View File

@ -0,0 +1,9 @@
{
"data": {
"createService": {
"id": "69e8a59d1a037043d86d23d3",
"title": "kvs-service-1776854432",
"type": "access"
}
}
}

View File

@ -0,0 +1,21 @@
{
"data": {
"place": {
"results": [
{
"id": "69e8a59d5d6417545a7f4844",
"members": [
{
"id": "a6542878-eba4-45ff-b1ba-38fe31a1349e",
"parent_id": null,
"user": {
"id": "a6542878-eba4-45ff-b1ba-38fe31a1349e",
"username": "+79994167356"
}
}
]
}
]
}
}
}

View File

@ -0,0 +1,16 @@
Traceback (most recent call last):
File "KVSTest\features\environment.py", line 21, in after_scenario
fn()
~~^^
File "C:\Users\Степаан\PycharmProjects\work\KVSTest\testdata\subscription_test_data.py", line 230, in _cleanup_delete_subscription
_exec_or_fail(op_name="deleteSubscription(mutation)", token=token, query=del_mut, variables={"id": subscription_id}, company_id=self.company_id)
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Степаан\PycharmProjects\work\KVSTest\testdata\subscription_test_data.py", line 25, in _exec_or_fail
return execute_graphql(
query=query,
...<2 lines>...
access_token=token,
)
File "C:\Users\Степаан\PycharmProjects\work\worklib\graphql_client.py", line 65, in execute_graphql
raise RuntimeError(f"GraphQL errors: {errors}")
RuntimeError: GraphQL errors: [{'message': 'Not Found', 'code': 'Client Error', 'status': 404, 'description': 'The server has not found anything matching the Request-URI'}]

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69e8a8185d6417545a7f4863",
"member_id": "e03812e4-8689-4689-b561-29a40b290851"
}
}
}

View File

@ -0,0 +1,5 @@
{
"data": {
"deleteSubscription": true
}
}

View File

@ -0,0 +1,25 @@
{
"data": {
"createSubscription": {
"id": "69e8a59e1a037043d86d23d6",
"services": [
{
"id": "69e8a59d1a037043d86d23d3",
"title": "kvs-service-1776854432"
}
],
"user": {
"id": "a6542878-eba4-45ff-b1ba-38fe31a1349e",
"data": {
"first_name": "kvstest1",
"last_name": "kvstest2"
}
},
"plan": {
"id": "69e8a59d1a037043d86d23d4",
"title": "plan-kvs-1776854432"
},
"place_id": "69e8a59d5d6417545a7f4844"
}
}
}

View File

@ -0,0 +1,10 @@
{
"data": {
"createPlaceMultiple": [
{
"id": "69e8a5c963548ff04f752605",
"__typename": "PlaceObject"
}
]
}
}

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69e8a8180bfa76f761dd1b20",
"member_id": "4dfe1f78-8dd7-40ff-b0e1-55536422c6b1"
}
}
}

View File

@ -0,0 +1,4 @@
"BROKEN","EPIC","FAILED","FEATURE","PASSED","SKIPPED","STORY","UNKNOWN"
"0","","0","KVS GraphQL (place + members)","2","0","","0"
"0","","0","Place info (REST/GraphQL/WebSocket)","2","0","","0"
"0","","0","KVS GraphQL subscription","1","0","","0"
1 BROKEN EPIC FAILED FEATURE PASSED SKIPPED STORY UNKNOWN
2 0 0 KVS GraphQL (place + members) 2 0 0
3 0 0 Place info (REST/GraphQL/WebSocket) 2 0 0
4 0 0 KVS GraphQL subscription 1 0 0

View File

@ -0,0 +1 @@
{"uid":"b1a8273437954620fa374b796ffaacdd","name":"behaviors","children":[{"name":"Place info (REST/GraphQL/WebSocket)","children":[{"name":"Authorize as employer","uid":"9c2bc11534322c25","parentUid":"b10d31223e5a74c6f18fff0f5696f8ee","status":"passed","time":{"start":1776855065463,"stop":1776855065765,"duration":302},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":false,"parameters":[],"tags":[]},{"name":"Get place info","uid":"d01c25fbb85460f4","parentUid":"b10d31223e5a74c6f18fff0f5696f8ee","status":"passed","time":{"start":1776855065767,"stop":1776855065822,"duration":55},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":false,"parameters":[],"tags":[]}],"uid":"b10d31223e5a74c6f18fff0f5696f8ee"},{"name":"KVS GraphQL (place + members)","children":[{"name":"Get place info (dynamic place, no hardcode)","uid":"7a6a9a042ff6da05","parentUid":"ded2778bb914aacc5dcd5003813f711c","status":"passed","time":{"start":1776855065825,"stop":1776855066169,"duration":344},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":true,"parameters":[],"tags":[]},{"name":"Add user to place and verify member appears","uid":"7bcb3b93b332730e","parentUid":"ded2778bb914aacc5dcd5003813f711c","status":"passed","time":{"start":1776855066170,"stop":1776855066735,"duration":565},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":false,"parameters":[],"tags":[]}],"uid":"ded2778bb914aacc5dcd5003813f711c"},{"name":"KVS GraphQL subscription","children":[{"name":"Create subscription, check invoices, delete subscription","uid":"eeb4568cb2661d08","parentUid":"ccabf020a779865991f68fbb0346b2db","status":"passed","time":{"start":1776855066739,"stop":1776855067705,"duration":966},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":false,"parameters":[],"tags":[]}],"uid":"ccabf020a779865991f68fbb0346b2db"}]}

View File

View File

@ -0,0 +1 @@
{"uid":"4b4757e66a1912dae1a509f688f20b0f","name":"categories","children":[]}

View File

@ -0,0 +1 @@
{"uid":"83edc06c07f9ae9e47eb6dd1b683e4e2","name":"packages","children":[{"name":"Authorize as employer","uid":"9c2bc11534322c25","parentUid":"83edc06c07f9ae9e47eb6dd1b683e4e2","status":"passed","time":{"start":1776855065463,"stop":1776855065765,"duration":302},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":false,"parameters":[],"tags":[]},{"name":"Get place info","uid":"d01c25fbb85460f4","parentUid":"83edc06c07f9ae9e47eb6dd1b683e4e2","status":"passed","time":{"start":1776855065767,"stop":1776855065822,"duration":55},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":false,"parameters":[],"tags":[]},{"name":"Get place info (dynamic place, no hardcode)","uid":"7a6a9a042ff6da05","parentUid":"83edc06c07f9ae9e47eb6dd1b683e4e2","status":"passed","time":{"start":1776855065825,"stop":1776855066169,"duration":344},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":true,"parameters":[],"tags":[]},{"name":"Add user to place and verify member appears","uid":"7bcb3b93b332730e","parentUid":"83edc06c07f9ae9e47eb6dd1b683e4e2","status":"passed","time":{"start":1776855066170,"stop":1776855066735,"duration":565},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":false,"parameters":[],"tags":[]},{"name":"Create subscription, check invoices, delete subscription","uid":"eeb4568cb2661d08","parentUid":"83edc06c07f9ae9e47eb6dd1b683e4e2","status":"passed","time":{"start":1776855066739,"stop":1776855067705,"duration":966},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":false,"parameters":[],"tags":[]}]}

View File

@ -0,0 +1,6 @@
"DESCRIPTION","DURATION IN MS","NAME","PARENT SUITE","START TIME","STATUS","STOP TIME","SUB SUITE","SUITE","TEST CLASS","TEST METHOD"
"","344","Get place info (dynamic place, no hardcode)","","2026-04-22","passed","2026-04-22","","","",""
"","55","Get place info","","2026-04-22","passed","2026-04-22","","","",""
"","565","Add user to place and verify member appears","","2026-04-22","passed","2026-04-22","","","",""
"","302","Authorize as employer","","2026-04-22","passed","2026-04-22","","","",""
"","966","Create subscription, check invoices, delete subscription","","2026-04-22","passed","2026-04-22","","","",""
1 DESCRIPTION DURATION IN MS NAME PARENT SUITE START TIME STATUS STOP TIME SUB SUITE SUITE TEST CLASS TEST METHOD
2 344 Get place info (dynamic place, no hardcode) 2026-04-22 passed 2026-04-22
3 55 Get place info 2026-04-22 passed 2026-04-22
4 565 Add user to place and verify member appears 2026-04-22 passed 2026-04-22
5 302 Authorize as employer 2026-04-22 passed 2026-04-22
6 966 Create subscription, check invoices, delete subscription 2026-04-22 passed 2026-04-22

View File

@ -0,0 +1 @@
{"uid":"98d3104e051c652961429bf95fa0b5d6","name":"suites","children":[{"name":"Authorize as employer","uid":"9c2bc11534322c25","parentUid":"98d3104e051c652961429bf95fa0b5d6","status":"passed","time":{"start":1776855065463,"stop":1776855065765,"duration":302},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":false,"parameters":[],"tags":[]},{"name":"Get place info","uid":"d01c25fbb85460f4","parentUid":"98d3104e051c652961429bf95fa0b5d6","status":"passed","time":{"start":1776855065767,"stop":1776855065822,"duration":55},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":false,"parameters":[],"tags":[]},{"name":"Get place info (dynamic place, no hardcode)","uid":"7a6a9a042ff6da05","parentUid":"98d3104e051c652961429bf95fa0b5d6","status":"passed","time":{"start":1776855065825,"stop":1776855066169,"duration":344},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":true,"parameters":[],"tags":[]},{"name":"Add user to place and verify member appears","uid":"7bcb3b93b332730e","parentUid":"98d3104e051c652961429bf95fa0b5d6","status":"passed","time":{"start":1776855066170,"stop":1776855066735,"duration":565},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":false,"parameters":[],"tags":[]},{"name":"Create subscription, check invoices, delete subscription","uid":"eeb4568cb2661d08","parentUid":"98d3104e051c652961429bf95fa0b5d6","status":"passed","time":{"start":1776855066739,"stop":1776855067705,"duration":966},"flaky":false,"newFailed":false,"newPassed":false,"newBroken":false,"retriesCount":2,"retriesStatusChange":false,"parameters":[],"tags":[]}]}

View File

@ -0,0 +1 @@
{"uid":"207f4f7e62f50ecd","name":"Authorize as employer","fullName":"Place info (REST/GraphQL/WebSocket): Authorize as employer","historyId":"671d36bc7d85d5b78ec36b2e34a7884b","time":{"start":1776854474515,"stop":1776854474820,"duration":305},"status":"passed","flaky":false,"newFailed":false,"newBroken":false,"newPassed":false,"retriesCount":0,"retriesStatusChange":false,"beforeStages":[],"testStage":{"status":"passed","steps":[{"name":"When get access token","time":{"start":1776854474516,"stop":1776854474818,"duration":302},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0},{"name":"Then access token is valid","time":{"start":1776854474818,"stop":1776854474819,"duration":1},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0}],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":true,"stepsCount":2},"afterStages":[],"labels":[{"name":"severity","value":"normal"},{"name":"feature","value":"Place info (REST/GraphQL/WebSocket)"},{"name":"framework","value":"behave"},{"name":"language","value":"cpython3"},{"name":"resultFormat","value":"allure2"}],"parameters":[],"links":[],"hidden":true,"retry":true,"extra":{"categories":[],"tags":[]},"source":"207f4f7e62f50ecd.json","parameterValues":[]}

View File

@ -0,0 +1 @@
{"uid":"3233dd19cfc118fc","name":"Get place info","fullName":"Place info (REST/GraphQL/WebSocket): Get place info","historyId":"ad3dd3c4cc300bb9a4f6fcd9cfe24502","time":{"start":1776854474822,"stop":1776854474870,"duration":48},"status":"passed","flaky":false,"newFailed":false,"newBroken":false,"newPassed":false,"retriesCount":0,"retriesStatusChange":false,"beforeStages":[],"testStage":{"status":"passed","steps":[{"name":"When get place info","time":{"start":1776854474823,"stop":1776854474869,"duration":46},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0},{"name":"Then place info is valid for query data","time":{"start":1776854474869,"stop":1776854474870,"duration":1},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0}],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":true,"stepsCount":2},"afterStages":[],"labels":[{"name":"severity","value":"normal"},{"name":"feature","value":"Place info (REST/GraphQL/WebSocket)"},{"name":"framework","value":"behave"},{"name":"language","value":"cpython3"},{"name":"resultFormat","value":"allure2"}],"parameters":[],"links":[],"hidden":true,"retry":true,"extra":{"categories":[],"tags":[]},"source":"3233dd19cfc118fc.json","parameterValues":[]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"uid":"63e03a2b27e8d9f2","name":"Get place info","fullName":"Place info (REST/GraphQL/WebSocket): Get place info","historyId":"ad3dd3c4cc300bb9a4f6fcd9cfe24502","time":{"start":1776854431019,"stop":1776854431114,"duration":95},"status":"passed","flaky":false,"newFailed":false,"newBroken":false,"newPassed":false,"retriesCount":0,"retriesStatusChange":false,"beforeStages":[],"testStage":{"status":"passed","steps":[{"name":"When get place info","time":{"start":1776854431020,"stop":1776854431112,"duration":92},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0},{"name":"Then place info is valid for query data","time":{"start":1776854431113,"stop":1776854431113,"duration":0},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0}],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":true,"stepsCount":2},"afterStages":[],"labels":[{"name":"severity","value":"normal"},{"name":"feature","value":"Place info (REST/GraphQL/WebSocket)"},{"name":"framework","value":"behave"},{"name":"language","value":"cpython3"},{"name":"resultFormat","value":"allure2"}],"parameters":[],"links":[],"hidden":true,"retry":true,"extra":{"categories":[],"tags":[]},"source":"63e03a2b27e8d9f2.json","parameterValues":[]}

View File

@ -0,0 +1 @@
{"uid":"7a6a9a042ff6da05","name":"Get place info (dynamic place, no hardcode)","fullName":"KVS GraphQL (place + members): Get place info (dynamic place, no hardcode)","historyId":"c1bd554320a2aefbe4b77b8dc3a01b64","time":{"start":1776855065825,"stop":1776855066169,"duration":344},"status":"passed","flaky":false,"newFailed":false,"newBroken":false,"newPassed":false,"retriesCount":2,"retriesStatusChange":true,"beforeStages":[],"testStage":{"status":"passed","steps":[{"name":"When get access token","time":{"start":1776855065826,"stop":1776855066011,"duration":185},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0},{"name":"Then access token is valid","time":{"start":1776855066011,"stop":1776855066012,"duration":1},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0},{"name":"When create place for kvs","time":{"start":1776855066012,"stop":1776855066060,"duration":48},"status":"passed","steps":[{"name":"GraphQL: createPlaceMultiple (KVS)","time":{"start":1776855066014,"stop":1776855066059,"duration":45},"status":"passed","steps":[],"attachments":[{"uid":"a9f7f6046e5d4a4f","name":"createPlaceMultiple response","source":"a9f7f6046e5d4a4f.json","type":"application/json","size":148}],"parameters":[],"attachmentsCount":1,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":true,"stepsCount":0}],"attachments":[],"parameters":[],"attachmentsCount":1,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":true,"stepsCount":1},{"name":"And query place members for created kvs place","time":{"start":1776855066060,"stop":1776855066109,"duration":49},"status":"passed","steps":[{"name":"GraphQL: place members (KVS)","time":{"start":1776855066061,"stop":1776855066108,"duration":47},"status":"passed","steps":[],"attachments":[{"uid":"5f3bbbf8ce320408","name":"place members response","source":"5f3bbbf8ce320408.json","type":"application/json","size":155}],"parameters":[],"attachmentsCount":1,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":true,"stepsCount":0}],"attachments":[],"parameters":[],"attachmentsCount":1,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":true,"stepsCount":1},{"name":"Then kvs place members response has correct shape for created place","time":{"start":1776855066109,"stop":1776855066110,"duration":1},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0},{"name":"Cleanup: _cleanup_delete_place","time":{"start":1776855066111,"stop":1776855066168,"duration":57},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0}],"attachments":[],"parameters":[],"attachmentsCount":2,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":true,"stepsCount":8},"afterStages":[],"labels":[{"name":"severity","value":"normal"},{"name":"feature","value":"KVS GraphQL (place + members)"},{"name":"framework","value":"behave"},{"name":"language","value":"cpython3"},{"name":"resultFormat","value":"allure2"}],"parameters":[],"links":[],"hidden":false,"retry":false,"extra":{"severity":"normal","retries":[{"uid":"c4ccaf28f30f5e79","status":"passed","time":{"start":1776854474872,"stop":1776854475143,"duration":271}},{"uid":"ffb09160c14b657","status":"failed","statusDetails":"AssertionError: members пустой: []\n","time":{"start":1776854431117,"stop":1776854431427,"duration":310}}],"categories":[],"tags":[]},"source":"7a6a9a042ff6da05.json","parameterValues":[]}

View File

@ -0,0 +1 @@
{"uid":"7ba6e9730bad3e99","name":"Authorize as employer","fullName":"Place info (REST/GraphQL/WebSocket): Authorize as employer","historyId":"671d36bc7d85d5b78ec36b2e34a7884b","time":{"start":1776854430677,"stop":1776854431017,"duration":340},"status":"passed","flaky":false,"newFailed":false,"newBroken":false,"newPassed":false,"retriesCount":0,"retriesStatusChange":false,"beforeStages":[],"testStage":{"status":"passed","steps":[{"name":"When get access token","time":{"start":1776854430679,"stop":1776854431015,"duration":336},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0},{"name":"Then access token is valid","time":{"start":1776854431015,"stop":1776854431016,"duration":1},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0}],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":true,"stepsCount":2},"afterStages":[],"labels":[{"name":"severity","value":"normal"},{"name":"feature","value":"Place info (REST/GraphQL/WebSocket)"},{"name":"framework","value":"behave"},{"name":"language","value":"cpython3"},{"name":"resultFormat","value":"allure2"}],"parameters":[],"links":[],"hidden":true,"retry":true,"extra":{"categories":[],"tags":[]},"source":"7ba6e9730bad3e99.json","parameterValues":[]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"uid":"9c2bc11534322c25","name":"Authorize as employer","fullName":"Place info (REST/GraphQL/WebSocket): Authorize as employer","historyId":"671d36bc7d85d5b78ec36b2e34a7884b","time":{"start":1776855065463,"stop":1776855065765,"duration":302},"status":"passed","flaky":false,"newFailed":false,"newBroken":false,"newPassed":false,"retriesCount":2,"retriesStatusChange":false,"beforeStages":[],"testStage":{"status":"passed","steps":[{"name":"When get access token","time":{"start":1776855065465,"stop":1776855065762,"duration":297},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0},{"name":"Then access token is valid","time":{"start":1776855065763,"stop":1776855065764,"duration":1},"status":"passed","steps":[],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":false,"stepsCount":0}],"attachments":[],"parameters":[],"attachmentsCount":0,"shouldDisplayMessage":false,"attachmentStep":false,"hasContent":true,"stepsCount":2},"afterStages":[],"labels":[{"name":"severity","value":"normal"},{"name":"feature","value":"Place info (REST/GraphQL/WebSocket)"},{"name":"framework","value":"behave"},{"name":"language","value":"cpython3"},{"name":"resultFormat","value":"allure2"}],"parameters":[],"links":[],"hidden":false,"retry":false,"extra":{"severity":"normal","retries":[{"uid":"207f4f7e62f50ecd","status":"passed","time":{"start":1776854474515,"stop":1776854474820,"duration":305}},{"uid":"7ba6e9730bad3e99","status":"passed","time":{"start":1776854430677,"stop":1776854431017,"duration":340}}],"categories":[],"tags":[]},"source":"9c2bc11534322c25.json","parameterValues":[]}

Some files were not shown because too many files have changed in this diff Show More