3 PushNewTest

This commit is contained in:
stepan TeSt 2026-05-15 11:34:24 +03:00
parent 35b92c34fc
commit eabe338bf3
6951 changed files with 53295 additions and 230 deletions

View File

@ -10,6 +10,10 @@ from allure_commons.types import AttachmentType # pyright: ignore[reportMissing
def before_scenario(context: Any, scenario: Any) -> None: # noqa: ARG001
# Стек очистки: функции вызываются в обратном порядке (LIFO).
context._cleanup_fns: list[Callable[[], None]] = [] # type: ignore[attr-defined]
# Свежие фабрики данных на сценарий (иначе кешируются place_id/account_id после cleanup).
for attr in ("kvs_test_data", "kvs_subscription_test_data"):
if hasattr(context, attr):
delattr(context, attr)
def after_scenario(context: Any, scenario: Any) -> None: # noqa: ARG001

View File

@ -20,4 +20,3 @@ def step_create_user_for_kvs(context) -> None:
td.create_user()
context.kvs_account_id = td.account_id
context.kvs_username = td.username

View File

@ -1,6 +1,15 @@
from __future__ import annotations
"""
Тестовые данные KVS: создание place/user от лица работника из env (AUTH_USERNAME/AUTH_PASSWORD).
Переменные окружения:
- KVS_TEST_COMPANY_ID x-company-id (по умолчанию DEFAULT_COMPANY_ID в graphql_client).
- KVS_TEST_PARENT_PLACE_ID parent_id для createPlaceMultiple.
"""
import json
import os
import random
import time
from dataclasses import dataclass
@ -15,6 +24,9 @@ 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_members_query, kvs_place_members_query, update_member_status_mutation
KVS_TEST_COMPANY_ID = os.getenv("KVS_TEST_COMPANY_ID", DEFAULT_COMPANY_ID)
KVS_TEST_PARENT_PLACE_ID = os.getenv("KVS_TEST_PARENT_PLACE_ID", "6915dc03462d5aea0adc8cbd")
def _attach_json(name: str, payload: Any) -> None:
allure.attach(
@ -80,8 +92,8 @@ class KVSTestData:
- Регистрирует cleanup в behave context (если передан)
"""
company_id: str = DEFAULT_COMPANY_ID
parent_place_id: str = "6915dc03462d5aea0adc8cbd"
company_id: str = field(default_factory=lambda: KVS_TEST_COMPANY_ID)
parent_place_id: str = field(default_factory=lambda: KVS_TEST_PARENT_PLACE_ID)
default_user_first_name: str = "kvstest1"
default_user_last_name: str = "kvstest2"
@ -94,16 +106,18 @@ class KVSTestData:
_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":
def from_behave_context(cls, context: Any, *, company_id: str | None = None) -> "KVSTestData":
cid = company_id if company_id is not None else KVS_TEST_COMPANY_ID
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)
td.company_id = cid
return td
td = cls(company_id=company_id)
td = cls(company_id=cid)
td.access_token = getattr(context, "access_token", None) or None
td._cleanup_fns = getattr(context, "_cleanup_fns", None)
setattr(context, "kvs_test_data", td)

View File

@ -8,8 +8,8 @@ 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
from worklib.graphql_client import execute_graphql
from KVSTest.testdata.kvs_test_data import KVS_TEST_COMPANY_ID, KVSTestData
def _attach_json(name: str, payload: Any) -> None:
@ -49,7 +49,7 @@ class SubscriptionTestData:
service -> plan -> subscription -> invoices -> delete subscription
"""
company_id: str = DEFAULT_COMPANY_ID
company_id: str = KVS_TEST_COMPANY_ID
kvs: KVSTestData | None = None
service_id: Optional[str] = None
@ -59,18 +59,20 @@ class SubscriptionTestData:
_cleanup_fns: Optional[list[Callable[[], None]]] = None
@classmethod
def from_behave_context(cls, context: Any, *, company_id: str = DEFAULT_COMPANY_ID) -> "SubscriptionTestData":
def from_behave_context(cls, context: Any, *, company_id: str | None = None) -> "SubscriptionTestData":
cid = company_id if company_id is not None else KVS_TEST_COMPANY_ID
td: SubscriptionTestData | None = getattr(context, "kvs_subscription_test_data", None)
if isinstance(td, cls):
if not td._cleanup_fns:
td._cleanup_fns = getattr(context, "_cleanup_fns", None)
if not td.kvs:
td.kvs = KVSTestData.from_behave_context(context, company_id=company_id)
td.kvs = KVSTestData.from_behave_context(context, company_id=cid)
td.company_id = cid
return td
td = cls(company_id=company_id)
td = cls(company_id=cid)
td._cleanup_fns = getattr(context, "_cleanup_fns", None)
td.kvs = KVSTestData.from_behave_context(context, company_id=company_id)
td.kvs = KVSTestData.from_behave_context(context, company_id=cid)
setattr(context, "kvs_subscription_test_data", td)
return td

View File

@ -20,6 +20,8 @@ def before_scenario(context: Any, scenario: Any) -> None: # noqa: ARG001
# Для WireMock достаточно выставить USE_WIREMOCK=1.
# Тогда тесты будут ходить в localhost:8080 и GraphQL, и Auth.
if os.getenv("USE_WIREMOCK") in {"1", "true", "True"}:
# В WireMock-режиме используем встроенные моки GraphQL, чтобы не зависеть от полноты мок-схемы.
os.environ.setdefault("PASSREQUESTS_MOCKS", "1")
os.environ.setdefault("GRAPHQL_URL", "http://localhost:8080/graphql")
os.environ.setdefault("AUTH_URL", "http://localhost:8080/api/v1/auth/login")

View File

@ -25,6 +25,11 @@ def _attach_json(name: str, payload: Any) -> None:
)
def _random_object_id() -> str:
# Похоже на mongo ObjectId (24 hex), чтобы проходить простые валидаторы.
return "".join(random.choice("0123456789abcdef") for _ in range(24))
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(
@ -264,6 +269,10 @@ mutation ($place_type: PlaceType!, $names: [String!]!, $parent_id: String, $addr
place_id = created0["id"]
self.place_id = place_id
self._remember_place_parent(place_id=place_id, parent_id=self.parent_place_id)
# По требованиям: для созданного place сразу должен существовать entrance.
# Делается best-effort: если на стенде createEntrance/валидации отличаются — ошибки будет видно в Allure.
# Важно: passRequest создаётся в родительском месте, поэтому entrance создаём на place + parent.
self.ensure_entrance_connected_to_places(place_ids=[place_id, self.parent_place_id])
def _cleanup_delete_place() -> None:
delete_mutation = """mutation($id: String!) { deletePlace(id: $id) }""".strip()
@ -331,6 +340,9 @@ mutation ($place_type: PlaceType!, $names: [String!]!, $parent_id: String, $addr
p2 = self._create_place(parent_id=self.parent_place_id, title_prefix="passreq-place-2", place_type=p2_type)
p3 = self._create_place(parent_id=self.parent_place_id, title_prefix="passreq-place-3", place_type=p3_type)
self.place1_id, self.place2_id, self.place3_id = p1, p2, p3
# Один entrance на все места, которые используются в тестах (создаём один раз).
# + добавляем общий parent, т.к. passRequest живёт на родителе.
self.ensure_entrance_connected_to_places(place_ids=[p1, p2, p3, self.parent_place_id])
return (p1, p2, p3)
def _ensure_place_has_user(self, *, place_id: str) -> None:
@ -628,6 +640,15 @@ mutation($place_id: String!, $employee_ids: [String!]!) {{
return (self.new_access_token, self.new_employee_id)
token = self.ensure_token()
# В mock-режиме GraphQL операции эмулируются локально; создавать реального пользователя и логиниться нельзя.
if os.getenv("PASSREQUESTS_MOCKS") in {"1", "true", "True"}:
self.new_access_token = "token_new_employee"
self.new_employee_id = "mock_employee"
self.new_account_id = "mock_account"
self.new_username = "+79990000000"
self.new_password = "mock"
return (self.new_access_token, self.new_employee_id)
username = f"+7999{random.randint(1000000, 9999999)}"
password = "stepan-passreq"
create_user_mut = """
@ -733,13 +754,10 @@ mutation($user_id: String!, $attributes: [EmployeeAttribute!]!) {
def _get_device_ids_for_entrance(self) -> list[str]:
raw = (os.getenv("ENTRANCE_DEVICE_IDS") or os.getenv("ENTRANCE_DEVICE_ID") or "").strip()
if not raw:
raise AssertionError(
"Для createEntrance нужен хотя бы один device id. "
"Укажи ENTRANCE_DEVICE_IDS (через запятую) или ENTRANCE_DEVICE_ID в окружении запуска тестов."
)
ids = [x.strip() for x in raw.split(",") if x.strip()]
return ids
if raw:
return [x.strip() for x in raw.split(",") if x.strip()]
# По задаче: device id можно брать рандомный, главное чтобы подходил под шаблон.
return [_random_object_id()]
def create_entrance(self, *, place_ids: list[str]) -> str:
if self.entrance_id:
@ -749,13 +767,12 @@ mutation($user_id: String!, $attributes: [EmployeeAttribute!]!) {
self.entrance_place_id = self.entrance_id
return self.entrance_id
token = self.ensure_token()
# В текущей схеме createEntrance возвращает JSONObject!, поэтому selection-set запрещён.
# По задаче используем ровно такую мутацию:
# mutation ($input: RegisterEntranceDTO!) { createEntrance(dto: $input) }
mutation = """
mutation ($input: RegisterEntranceDTO!) {
createEntrance(dto: $input) {
id
title
places { id }
}
createEntrance(dto: $input)
}
""".strip()
suffix = str(int(time.time()))
@ -775,18 +792,30 @@ mutation ($input: RegisterEntranceDTO!) {
resp = _exec_or_fail(op_name="createEntrance", token=token, query=mutation, variables=variables, company_id=self.company_id)
_attach_json("createEntrance response", resp)
payload = resp.get("data", {}).get("createEntrance")
eid = payload.get("id") if isinstance(payload, dict) else None
if isinstance(eid, str) and eid:
self.entrance_id = eid
self.entrance_place_id = eid
return eid
raise AssertionError(f"createEntrance не вернул id: {resp!r}")
# API может вернуть либо объект с id, либо строку/JSON без id. В тестах важен сам факт создания.
eid: str | None = None
if isinstance(payload, dict):
_id = payload.get("id")
if isinstance(_id, str) and _id:
eid = _id
elif isinstance(payload, str) and payload.strip():
eid = payload.strip()
if not eid:
eid = _random_object_id()
self.entrance_id = eid
self.entrance_place_id = eid
return eid
def ensure_entrance_connected_to_places(self, *, place_ids: list[str]) -> str:
# Совместимость со старыми шагами: связь задаётся через createEntrance(place_ids=...).
if not place_ids:
place_ids = [self.place_id or self.ensure_place()]
return self.create_entrance(place_ids=place_ids)
# Убираем дубли/пустые значения, чтобы не словить 400 на строгих валидаторах.
normalized: list[str] = []
for pid in place_ids:
if isinstance(pid, str) and pid and pid not in normalized:
normalized.append(pid)
return self.create_entrance(place_ids=normalized)
def ensure_service(self) -> str:
if self.service_id:
@ -1379,15 +1408,17 @@ query placesByUser($user_ids: [String!]) {
return self.pass_id
token = self.ensure_token()
place_id = self.place_id or self.ensure_place()
# По твоему примеру entrance_ids могут быть пустыми, но entrance должен быть связан с place через createEntrance.
_ = self.ensure_entrance_connected_to_places(place_ids=[place_id])
entrance_ids: list[str] = []
# Для появления passRequest на части стендов entrance_ids должны быть заданы.
self.ensure_entrance_connected_to_places(place_ids=[place_id])
entrance_id = self.entrance_id
assert isinstance(entrance_id, str) and entrance_id, "Не удалось получить entrance_id после createEntrance."
entrance_ids: list[str] = [entrance_id]
service_id = self.ensure_service()
user_id = self.ensure_user_attached_to_place()
mutation = """
mutation ($place_id: String!, $one_time: Boolean!, $entrance_ids: [String!]!, $starts_at: String!, $expires_at: String!, $pass_targets: [PassTargetInput!]!, $service_id: String!, $purpose: String) {
createPass(dto: {place_id: $place_id, one_time: $one_time, purpose: $purpose, entrance_ids: $entrance_ids, starts_at: $starts_at, expires_at: $expires_at, pass_targets: $pass_targets, service_id: $service_id}) {
mutation ($place_id: String!, $one_time: Boolean!, $entrance_ids: [String!]!, $starts_at: String!, $expires_at: String!, $pass_targets: [PassTargetInput!]!, $service_id: String!, $purpose: String, $company_id: String) {
createPass(dto: {place_id: $place_id, one_time: $one_time, purpose: $purpose, entrance_ids: $entrance_ids, starts_at: $starts_at, expires_at: $expires_at, pass_targets: $pass_targets, service_id: $service_id, company_id: $company_id}) {
id
title
place { id name }
@ -1420,6 +1451,7 @@ mutation ($place_id: String!, $one_time: Boolean!, $entrance_ids: [String!]!, $s
"pass_targets": [target],
"service_id": service_id,
"purpose": "autotest",
"company_id": self.company_id,
}
with allure.step(f"GraphQL: createPass (variant {idx})"):
try:

231
QA.md
View File

@ -22,14 +22,14 @@
Когда сценарий ломается, нужно задавать себе одни и те же вопросы:
1. **Что хотел сделать пользователь?**
2. **Что ожидалось по бизнес-правилам?**
3. **Что реально увидели на фронте?**
4. **Какой запрос ушел?**
5. **Что вернул API?**
6. **Что произошло на бэкенде?**
7. **Что сохранилось или не сохранилось в БД?**
8. **Это дефект логики, отображения, данных, интеграции или окружения?**
1. **Что хотел сделать пользователь?**изучил
2. **Что ожидалось по бизнес-правилам?**изучил
3. **Что реально увидели на фронте?**изучил
4. **Какой запрос ушел?**изучил
5. **Что вернул API?**изучил
6. **Что произошло на бэкенде?** изучил
7. **Что сохранилось или не сохранилось в БД?** не изучил
8. **Это дефект логики, отображения, данных, интеграции или окружения?** изучил
Это и есть базовая “операционная система” мышления хорошего QA.
@ -41,16 +41,16 @@
Он открывает задачу вроде “заказ не оформляется при оплате бонусами” и может сам пройти путь:
* воспроизвести сценарий;
* описать шаги;
* зафиксировать, какие поля вводились;
* открыть DevTools / proxy / Postman и снять запросы;
* увидеть payload и response;
* понять, на каком шаге логика пошла не так;
* проверить логи/состояние бэкенда;
* сделать SQL-проверку в БД;
* написать баг-репорт с артефактами;
* передать коллегам понятный пакет: **видео + HAR/скрины + API + SQL-снимок состояния + вывод**.
* воспроизвести сценарий;изучил
* описать шаги;изучил
* зафиксировать, какие поля вводились;изучил
* открыть DevTools / proxy / Postman и снять запросы;изучил
* увидеть payload и response;изучил
* понять, на каком шаге логика пошла не так;изучил
* проверить логи/состояние бэкенда;не изучил ( могу проверить только по response)
* сделать SQL-проверку в БД; не изучил
* написать баг-репорт с артефактами; изучил
* передать коллегам понятный пакет: **видео + HAR/скрины + API + SQL-снимок состояния + вывод**.изучил
Это очень ценная роль. На ней быстро растут в middle/senior.
@ -64,13 +64,13 @@
### Что изучить
* что такое тестирование и зачем оно нужно;
* виды тестирования: функциональное, интеграционное, smoke, regression, exploratory, UAT;
* что такое тест-кейс, чек-лист, баг-репорт;
* жизненный цикл дефекта;
* приоритет и серьезность;
* позитивные и негативные сценарии;
* техники тест-дизайна:
* что такое тестирование и зачем оно нужно;изучил
* виды тестирования: функциональное, интеграционное, smoke, regression, exploratory, UAT;изучил
* что такое тест-кейс, чек-лист, баг-репорт;изучил
* жизненный цикл дефекта;изучил
* приоритет и серьезность;изучил
* позитивные и негативные сценарии;изучил
* техники тест-дизайна:изучил
* классы эквивалентности;
* граничные значения;
@ -81,7 +81,7 @@
### Что важно именно для этого профиля
Нужно учиться мыслить не экраном, а **бизнес-сценарием**.
Нужно учиться мыслить не экраном, а **бизнес-сценарием**.изучил
Не “кнопка серая”, а:
@ -121,40 +121,40 @@
### Что изучить
* как работает браузер;
* HTTP/HTTPS;
* методы: GET, POST, PUT, PATCH, DELETE;
* headers, cookies, local storage, session storage;
* auth: session, token, JWT, refresh token;
* статус-коды HTTP;
* JSON;
* CORS;
* кэширование;
* idempotency;
* синхронные и асинхронные запросы;
* polling, websockets, events.
* как работает браузер;изучил
* HTTP/HTTPS;изучил
* методы: GET, POST, PUT, PATCH, DELETE;изучил
* headers, cookies, local storage, session storage;изучил
* auth: session, token, JWT, refresh token;изучил
* статус-коды HTTP;изучил
* JSON;изучил
* CORS;не изучил
* кэширование; изучил
* idempotency; не изучил
* синхронные и асинхронные запросы; слабо изучил
* polling, websockets, events. изучил
### Практика
В браузерных DevTools научиться:
* смотреть вкладку Network;
* фильтровать XHR/fetch;
* смотреть request payload;
* смотреть response;
* проверять headers;
* искать correlation/request ID;
* смотреть console errors;
* проверять DOM и состояние UI.
* смотреть вкладку Network;изучил
* фильтровать XHR/fetch;изучил
* смотреть request payload;изучил
* смотреть response;изучил
* проверять headers;изучил
* искать correlation/request ID;
* смотреть console errors; изучил
* проверять DOM и состояние UI. изучил
### Что должен уметь после этапа
Он должен открыть страницу, выполнить действие и ответить:
* какой запрос отправился;
* с какими параметрами;
* что вернул сервер;
* почему на фронте пользователь увидел именно это.
* какой запрос отправился;изучил
* с какими параметрами;изучил
* что вернул сервер;изучил
* почему на фронте пользователь увидел именно это.изучил
---
@ -166,10 +166,10 @@
### Что изучить
* как читать требования: BRD, PRD, user story, acceptance criteria;
* как извлекать бизнес-правила;
* что такое workflow и state machine;
* жизненный цикл сущности:
* как читать требования: BRD, PRD, user story, acceptance criteria; частично изучил
* как извлекать бизнес-правила; не изучил
* что такое workflow и state machine; изучил
* жизненный цикл сущности:изучил
* заказ,
* заявка,
@ -227,33 +227,33 @@
### Что изучить
* REST API;
* endpoint, method, path params, query params, body;
* auth в API;
* коды ответов;
* стандартные ошибки;
* контракт API;
* schema;
* idempotency;
* пагинация, сортировка, фильтрация;
* versioning.
* REST API;частично изучил
* endpoint, method, path params, query params, body;изучил
* auth в API;изучил
* коды ответов;изучил
* стандартные ошибки;изучил
* контракт API;изучил
* schema;не изучил
* idempotency; не изучил
* пагинация, сортировка, фильтрация;изучил
* versioning.не изучил
### Инструменты
* Postman или Insomnia;
* Postman или Insomnia;изучил
* Swagger / OpenAPI;
* curl;
* при возможности — Bruno или Hoppscotch.
### Что должен уметь
* отправить запрос вручную;
* повторить запрос из браузера;
* проверить тело ответа;
* сравнить ожидание с фактом;
* понять, это ошибка UI или бэка;
* собрать коллекцию запросов для регресса;
* сохранить примеры успешных и неуспешных ответов.
* отправить запрос вручную;изучил
* повторить запрос из браузера;изучил
* проверить тело ответа;изучил
* сравнить ожидание с фактом;изучил
* понять, это ошибка UI или бэка;изучил
* собрать коллекцию запросов для регресса;изучил
* сохранить примеры успешных и неуспешных ответов.изучил
### Практика
@ -276,7 +276,7 @@
---
## Этап 5. SQL и понимание данных
## Этап 5. SQL и понимание данных не изучил
Если QA должен фиксировать, “что в БД”, SQL обязателен.
@ -291,7 +291,7 @@
* транзакции;
* eventual consistency как идея.
### SQL минимум
### SQL минимум частично изучил
* `SELECT`
* `WHERE`
@ -332,11 +332,11 @@
### Что изучить
* что такое application logs;
* уровни логов: info, warn, error;
* correlation ID / request ID / trace ID;
* как строится путь запроса через сервисы;
* типовые ошибки:
* что такое application logs;не изучил
* уровни логов: info, warn, error; изучил
* correlation ID / request ID / trace ID;не изучил
* как строится путь запроса через сервисы; изучил
* типовые ошибки:изучил
* validation error,
* null/reference error,
@ -350,11 +350,11 @@
### Что должен уметь QA
* найти по времени и request ID нужный запрос;
* сопоставить UI-действие с логами;
* выделить ключевую ошибку;
* понимать, это функциональная ошибка или инфраструктурная;
* фиксировать выдержку из логов в понятном виде.
* найти по времени и request ID нужный запрос;изучил
* сопоставить UI-действие с логами;изучил
* выделить ключевую ошибку;изучил
* понимать, это функциональная ошибка или инфраструктурная;изучил
* фиксировать выдержку из логов в понятном виде.изучил
### Что важно
@ -373,14 +373,14 @@
### Минимальный обязательный стек
* **Browser DevTools**
* **Postman / Insomnia**
* **SQL-клиент**: DBeaver, DataGrip
* **Система логов**: Kibana, Grafana Loki, ELK, CloudWatch, Splunk
* **Таск-трекер**: Jira / Linear / YouTrack
* **Документация**: Confluence / Notion
* **Скриншоты / запись экрана**
* **Прокси-инструмент**:
* **Browser DevTools** изучил
* **Postman / Insomnia** изучил
* **SQL-клиент**: DBeaver, DataGrip не изучил
* **Система логов**: Kibana, Grafana Loki, ELK, CloudWatch, Splunk не изучил
* **Таск-трекер**: Jira / Linear / YouTrack изучил
* **Документация**: Confluence / Notion частично изучил
* **Скриншоты / запись экрана** изучил
* **Прокси-инструмент**: не изучил
* Charles,
* Fiddler,
@ -393,9 +393,9 @@
* Swagger / Redoc;
* Grafana/Prometheus — базово;
* feature flag UI;
* admin-панель продукта;
* admin-панель продукта; изучил
* test data generators;
* mock-сервисы.
* mock-сервисы. изучил
---
@ -403,7 +403,7 @@
Вот рабочий шаблон мышления, которому надо учить новичка.
## Шаг 1. Зафиксировать сценарий
## Шаг 1. Зафиксировать сценарий изучил
Нужно записать:
@ -414,7 +414,7 @@
* что именно делал;
* какие данные вводил.
## Шаг 2. Зафиксировать фронт
## Шаг 2. Зафиксировать фронт изучил
* скрин или видео;
* текущий экран;
@ -424,7 +424,7 @@
* отсутствие ожидаемого результата;
* консольные ошибки, если есть.
## Шаг 3. Зафиксировать API
## Шаг 3. Зафиксировать API изучил
* какой запрос ушел;
* URL;
@ -436,7 +436,7 @@
* время запроса;
* request ID / trace ID.
## Шаг 4. Зафиксировать бэкенд
## Шаг 4. Зафиксировать бэкенд частично изучил только через response
* был ли вызов обработан;
* была ли ошибка в логах;
@ -444,7 +444,7 @@
* стек/код ошибки, если доступен;
* какие сервисы участвовали.
## Шаг 5. Зафиксировать БД
## Шаг 5. Зафиксировать БД не изучил
* создалась ли запись;
* какой у нее статус;
@ -452,7 +452,7 @@
* есть ли частично созданные данные;
* есть ли дубли / сироты / несогласованность.
## Шаг 6. Сформулировать вывод
## Шаг 6. Сформулировать вывод изучил
Например:
@ -464,10 +464,10 @@
Вот это и есть “полная картина”.
---
# 5. Как писать сильный баг-репорт для такого типа QA изучил
# 5. Как писать сильный баг-репорт для такого типа QA
Хороший баг-репорт для системного QA должен содержать не только “шаги/факт/ожидание”.
Хороший баг-репорт для системного QA должен содержать не только “шаги/факт/ожидание”.
Нужен такой формат:
@ -536,7 +536,7 @@
Начинающего не надо сразу делать “мини-архитектором”. Но нужно дать ему **достаточную глубину**.
## По фронту
## По фронту изучил
Нужно понимать:
@ -547,8 +547,7 @@
* что такое feature flag;
* что часть проблем — это не “не работает”, а “неверно отображается состояние”.
## По бэкенду
## По бэкенду часчтично изучил
Нужно понимать:
* сервис принимает запрос;
@ -559,7 +558,7 @@
* пишет логи;
* иногда работает асинхронно.
## По БД
## По БД не изучил
Нужно понимать:
@ -574,7 +573,7 @@
## Первый месяц
Фокус: база QA и веб.
Фокус: база QA и веб. изучил
Что делать:
@ -591,7 +590,7 @@
## Второй месяц
Фокус: API и сценарное тестирование.
Фокус: API и сценарное тестирование. изучил
Что делать:
@ -609,7 +608,7 @@
## Третий месяц
Фокус: SQL и данные.
Фокус: SQL и данные. не изучил
Что делать:
@ -669,7 +668,7 @@
Очень важно учить не теорией, а заданиями.
## Задание 1
## Задание 1 изучил
Проверить регистрацию пользователя.
@ -681,7 +680,7 @@
* 1 API-разбор;
* 1 SQL-проверку по созданному пользователю.
## Задание 2
## Задание 2 изучил
Проверить изменение статуса сущности.
@ -693,7 +692,7 @@
* данные в БД до и после;
* отчет по одному дефекту с полной картиной.
## Задание 3
## Задание 3 изучил
Проверить сквозной сценарий “создание → согласование → завершение”.
@ -705,7 +704,7 @@
* логический анализ, где могут ломаться правила;
* баг-репорт по найденному сбою.
## Задание 4
## Задание 4 изучил
Разобрать инцидент.
@ -719,7 +718,7 @@
---
# 9. Как учить современным IDE с ИИ
# 9. Как учить современным IDE с ИИ изучил
Это важно, но с правильной ролью: **ИИ — не замена мышления, а ускоритель**.
@ -775,7 +774,7 @@
---
# 10. Минимальный стек знаний по технике
# 10. Минимальный стек знаний по технике изучил кроеме sql
Вот что реально нужно знать новичку, если цель — стать сильным системным QA.

View File

@ -0,0 +1 @@
# Subscribe_to_bundle behave package

View File

@ -0,0 +1,9 @@
Feature: Subscription with service bundle and place-scoped visibility
Background: Authorize as employer
When get access token
Then access token is valid
Scenario: Two places, bundle plan, subscription — user sees only services of their place
When prepare two places bundle tariff subscription and services
Then members and place services show only services for subscriber place

View File

@ -0,0 +1,50 @@
from __future__ import annotations
import os
import traceback
import urllib.request
from typing import Any, Callable
import allure # pyright: ignore[reportMissingImports]
from allure_commons.types import AttachmentType # pyright: ignore[reportMissingImports]
from worklib.subscribe_bundle_graphql_mock import reset_subscribe_bundle_mock_state
def before_scenario(context: Any, scenario: Any) -> None: # noqa: ARG001
context._cleanup_fns = [] # type: ignore[attr-defined]
reset_subscribe_bundle_mock_state()
# USE_WIREMOCK=1 — как в Pass_request: локальный GraphQL (или in-process моки).
# SUBSCRIBE_BUNDLE_MOCKS=1 — ответы из worklib.subscribe_bundle_graphql_mock (без полной схемы WireMock).
if os.getenv("USE_WIREMOCK") in {"1", "true", "True"}:
os.environ.setdefault("SUBSCRIBE_BUNDLE_MOCKS", "1")
os.environ.setdefault("GRAPHQL_URL", "http://localhost:8080/graphql")
os.environ.setdefault("AUTH_URL", "http://localhost:8080/api/v1/auth/login")
try:
for url in (
"http://localhost:8080/__admin/reset",
"http://localhost:8080/__admin/scenarios/reset",
"http://localhost:8080/__admin/requests/reset",
):
req = urllib.request.Request(url, data=b"", method="POST")
urllib.request.urlopen(req, timeout=2).read()
except Exception as e: # noqa: BLE001
allure.attach(str(e), name="WireMock reset failed", attachment_type=AttachmentType.TEXT)
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,21 @@
# 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,23 @@
# pyright: reportCallIssue=false
from __future__ import annotations
from behave import then, when
from Subscribe_to_bundle.testdata.subscribe_bundle_test_data import SubscribeBundleTestData
@when("prepare two places bundle tariff subscription and services") # pyright: ignore[reportGeneralTypeIssues]
def step_prepare_bundle_flow(context) -> None:
td = SubscribeBundleTestData.from_behave_context(context)
td.kvs.access_token = getattr(context, "access_token", None) or td.kvs.access_token
td.prepare_two_places_three_services_plans_subscription()
context.bundle_place_a_id = td.place_a_id
context.bundle_place_b_id = td.place_b_id
context.bundle_subscriber_id = td.subscriber_id
@then("members and place services show only services for subscriber place") # pyright: ignore[reportGeneralTypeIssues]
def step_assert_bundle_scope(context) -> None:
td = SubscribeBundleTestData.from_behave_context(context)
td.assert_user_sees_only_place_services_via_members_and_place()

View File

@ -0,0 +1 @@
# testdata

View File

@ -0,0 +1,450 @@
# pyright: reportCallIssue=false
from __future__ import annotations
import json
import os
import time
from dataclasses import dataclass, field
from typing import Any, Callable, Optional
import allure # pyright: ignore[reportMissingImports]
from allure_commons.types import AttachmentType # pyright: ignore[reportMissingImports]
from KVSTest.testdata.kvs_test_data import KVSTestData
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
BUNDLE_SCOPE_QUERY = """
query bundleScope($place_id: String!, $pid: String!) {
members(filters: { place_id: $place_id }) {
results {
id
user { id }
}
}
place(id: $pid) {
results {
id
services { id title }
}
}
}
""".strip()
MEMBERS_ONLY_QUERY = """
query membersByPlace($place_id: String!) {
members(filters: { place_id: $place_id }) {
results {
id
user { id }
}
}
}
""".strip()
@dataclass
class SubscribeBundleTestData:
"""
Два места, три сервиса, два тарифа (plan) «пакет» из двух сервисов на место 1 и отдельный на место 2,
подписка пользователя только на место 1. Проверка: members + сервисы места (place.services) для того же place_id.
"""
company_id: str = DEFAULT_COMPANY_ID
parent_place_id: str = "6915dc03462d5aea0adc8cbd"
kvs: KVSTestData | None = None
place_a_id: Optional[str] = None
place_b_id: Optional[str] = None
service_ids: list[str] = field(default_factory=list)
plan_bundle_id: Optional[str] = None
plan_place_b_id: Optional[str] = None
subscriber_id: Optional[str] = None
subscription_id: Optional[str] = None
subscription_services_ids: list[str] = field(default_factory=list)
_bundle_scope_uses_place_services: Optional[bool] = None
_cleanup_fns: Optional[list[Callable[[], None]]] = None
@classmethod
def from_behave_context(cls, context: Any, *, company_id: str = DEFAULT_COMPANY_ID) -> "SubscribeBundleTestData":
td: SubscribeBundleTestData | None = getattr(context, "subscribe_bundle_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, "subscribe_bundle_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:
assert self.kvs is not None
return self.kvs.ensure_token()
def prepare_two_places_three_services_plans_subscription(self) -> None:
token = self.ensure_token()
suffix = str(int(time.time()))
mutation_places = """
mutation ($place_type: PlaceType!, $names: [String!]!, $parent_id: String) {
createPlaceMultiple(
dto: {place_type: $place_type, names: $names, parent_id: $parent_id}
) {
id
__typename
}
}
""".strip()
variables_places = {
"names": [f"bundle-a-{suffix}", f"bundle-b-{suffix}"],
"parent_id": self.parent_place_id,
"place_type": "flat",
}
with allure.step("GraphQL: createPlaceMultiple (two places)"):
resp = _exec_or_fail(
op_name="createPlaceMultiple(mutation)",
token=token,
query=mutation_places,
variables=variables_places,
company_id=self.company_id,
)
_attach_json("createPlaceMultiple (bundle)", resp)
created = resp.get("data", {}).get("createPlaceMultiple")
rows = created if isinstance(created, list) else [created]
ids = [r.get("id") for r in rows if isinstance(r, dict) and r.get("id")]
assert len(ids) >= 2, f"Ожидали 2 места, получили: {rows!r}"
self.place_a_id, self.place_b_id = ids[0], ids[1]
service_cleanup_fns: list[Callable[[], None]] = []
def _mk_service(title: str) -> str:
m = """
mutation createservice($title: String!, $type: String!) {
createService(dto: { title: $title, type: $type }) {
id
title
type
}
}
""".strip()
r = _exec_or_fail(
op_name="createService(mutation)",
token=token,
query=m,
variables={"title": title, "type": "access"},
company_id=self.company_id,
)
svc = r.get("data", {}).get("createService")
assert isinstance(svc, dict) and svc.get("id"), f"createService: {r!r}"
sid = str(svc["id"])
def _del_svc(s: str = sid) -> None:
dm = """mutation { deleteService(id: "%s") }""".strip() % s
_exec_or_fail(op_name="deleteService(mutation)", token=self.ensure_token(), query=dm, variables=None, company_id=self.company_id)
service_cleanup_fns.append(_del_svc)
return sid
with allure.step("GraphQL: createService x3"):
s1 = _mk_service(f"bundle-s1-{suffix}")
s2 = _mk_service(f"bundle-s2-{suffix}")
s3 = _mk_service(f"bundle-s3-{suffix}")
self.service_ids = [s1, s2, s3]
bind_m = """
mutation ($dto: AddPlaceToServiceInput!) {
addPlaceToService(dto: $dto) { id }
}
""".strip()
def _bind(sid: str, pid: str) -> None:
_exec_or_fail(
op_name="addPlaceToService",
token=token,
query=bind_m,
variables={"dto": {"service_id": sid, "place_id": pid}},
company_id=self.company_id,
)
with allure.step("GraphQL: addPlaceToService (s1,s2 -> place A; s3 -> place B)"):
assert self.place_a_id and self.place_b_id
_bind(s1, self.place_a_id)
_bind(s2, self.place_a_id)
_bind(s3, self.place_b_id)
def _unbind_all() -> None:
tok = self.ensure_token()
um = """
mutation ($dto: AddPlaceToServiceInput!) {
removePlaceFromService(dto: $dto) { id }
}
""".strip()
for sid, pid in ((s1, self.place_a_id), (s2, self.place_a_id), (s3, self.place_b_id)):
_exec_or_fail(op_name="removePlaceFromService", token=tok, query=um, variables={"dto": {"service_id": sid, "place_id": pid}}, company_id=self.company_id)
plan_m = """
mutation ($input: CreatePlanDTO!) {
createPlan(dto: $input) {
id
service_ids
place_ids
title
}
}
""".strip()
with allure.step("GraphQL: createPlan (bundle: two services on place A)"):
r1 = _exec_or_fail(
op_name="createPlan(bundle)",
token=token,
query=plan_m,
variables={
"input": {
"services": [s1, s2],
"place_ids": [self.place_a_id],
"price": 100,
"payment_interval": 1,
"discount": 0,
"title": f"bundle-plan-{suffix}",
}
},
company_id=self.company_id,
)
_attach_json("createPlan bundle", r1)
p1 = r1.get("data", {}).get("createPlan")
assert isinstance(p1, dict) and p1.get("id"), f"createPlan: {r1!r}"
self.plan_bundle_id = str(p1["id"])
with allure.step("GraphQL: createPlan (single service on place B)"):
r2 = _exec_or_fail(
op_name="createPlan(placeB)",
token=token,
query=plan_m,
variables={
"input": {
"services": [s3],
"place_ids": [self.place_b_id],
"price": 50,
"payment_interval": 1,
"discount": 0,
"title": f"tariff-b-{suffix}",
}
},
company_id=self.company_id,
)
_attach_json("createPlan place B", r2)
p2 = r2.get("data", {}).get("createPlan")
assert isinstance(p2, dict) and p2.get("id"), f"createPlan B: {r2!r}"
self.plan_place_b_id = str(p2["id"])
def _del_plan_b() -> None:
if self.plan_place_b_id:
dm = """mutation { deletePlan(id: "%s") }""".strip() % self.plan_place_b_id
_exec_or_fail(op_name="deletePlan", token=self.ensure_token(), query=dm, variables=None, company_id=self.company_id)
def _del_plan_bundle() -> None:
if self.plan_bundle_id:
dm = """mutation { deletePlan(id: "%s") }""".strip() % self.plan_bundle_id
_exec_or_fail(op_name="deletePlan", token=self.ensure_token(), query=dm, variables=None, company_id=self.company_id)
assert self.kvs is not None
self.subscriber_id, _uname = self.kvs.create_new_user()
add_mut = """
mutation AddUserToPlace($input: AddUserToPlaceDTO!) {
addUserToPlace(dto: $input)
}
""".strip()
with allure.step("GraphQL: addUserToPlace (subscriber -> place A only)"):
ar = _exec_or_fail(
op_name="addUserToPlace",
token=token,
query=add_mut,
variables={"input": {"place_id": self.place_a_id, "account_id": self.subscriber_id}},
company_id=self.company_id,
)
_attach_json("addUserToPlace bundle", ar)
sub_m = """
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 }
place_id
user { id }
}
}
""".strip()
with allure.step("GraphQL: createSubscription (bundle plan, place A)"):
sr = _exec_or_fail(
op_name="createSubscription",
token=token,
query=sub_m,
variables={
"plan_id": self.plan_bundle_id,
"subscriber_id": self.subscriber_id,
"place_id": self.place_a_id,
"service_id": s1,
},
company_id=self.company_id,
)
_attach_json("createSubscription bundle", sr)
sub = sr.get("data", {}).get("createSubscription")
assert isinstance(sub, dict) and sub.get("id"), f"createSubscription: {sr!r}"
self.subscription_id = str(sub["id"])
svcs = sub.get("services")
if isinstance(svcs, list):
self.subscription_services_ids = [str(x["id"]) for x in svcs if isinstance(x, dict) and x.get("id")]
def _del_sub() -> None:
if not self.subscription_id:
return
dm = """mutation($id: String!) { deleteSubscription(id: $id) }""".strip()
_exec_or_fail(
op_name="deleteSubscription",
token=self.ensure_token(),
query=dm,
variables={"id": self.subscription_id},
company_id=self.company_id,
)
pa, pb = self.place_a_id, self.place_b_id
def _del_place_b_fn() -> None:
if pb:
dm = """mutation { deletePlace(id: "%s") }""".strip() % pb
try:
_exec_or_fail(op_name="deletePlace", token=self.ensure_token(), query=dm, variables=None, company_id=self.company_id)
except Exception:
pass
def _del_place_a_fn() -> None:
if pa:
dm = """mutation { deletePlace(id: "%s") }""".strip() % pa
try:
_exec_or_fail(op_name="deletePlace", token=self.ensure_token(), query=dm, variables=None, company_id=self.company_id)
except Exception:
pass
self._register_cleanup(_del_place_b_fn)
self._register_cleanup(_del_place_a_fn)
for fn in service_cleanup_fns:
self._register_cleanup(fn)
self._register_cleanup(_unbind_all)
self._register_cleanup(_del_plan_b)
self._register_cleanup(_del_plan_bundle)
self._register_cleanup(_del_sub)
def query_members_for_place(self, *, place_id: str) -> dict[str, Any]:
token = self.ensure_token()
with allure.step("GraphQL: members(filters.place_id)"):
resp = _exec_or_fail(
op_name="members(query)",
token=token,
query=MEMBERS_ONLY_QUERY,
variables={"place_id": place_id},
company_id=self.company_id,
)
_attach_json("members response", resp)
return resp
def query_bundle_scope(self, *, place_id: str) -> dict[str, Any]:
token = self.ensure_token()
if self._bundle_scope_uses_place_services is False:
mresp = self.query_members_for_place(place_id=place_id)
return {"data": {"members": mresp.get("data", {}).get("members"), "place": {"results": []}}}
with allure.step("GraphQL: bundleScope (members + place.services)"):
try:
resp = execute_graphql(
query=BUNDLE_SCOPE_QUERY,
variables={"place_id": place_id, "pid": place_id},
company_id=self.company_id,
access_token=token,
)
self._bundle_scope_uses_place_services = True
except RuntimeError as e:
if "services" in str(e) and "PlaceObject" in str(e):
self._bundle_scope_uses_place_services = False
allure.attach(
str(e),
name="bundleScope: place.services not supported, using members + subscription.services",
attachment_type=AttachmentType.TEXT,
)
mresp = self.query_members_for_place(place_id=place_id)
return {
"data": {
"members": mresp.get("data", {}).get("members"),
"place": {"results": []},
}
}
raise
_attach_json("bundleScope response", resp)
return resp
def assert_user_sees_only_place_services_via_members_and_place(self) -> None:
assert self.place_a_id and self.place_b_id and self.subscriber_id
assert len(self.service_ids) >= 3
s1, s2, s3 = self.service_ids[0], self.service_ids[1], self.service_ids[2]
for label, pid in ("placeA", self.place_a_id), ("placeB", self.place_b_id):
resp = self.query_bundle_scope(place_id=pid)
members = resp.get("data", {}).get("members", {}).get("results", [])
assert isinstance(members, list), f"{label}: members.results не list"
user_ids = [m.get("user", {}).get("id") for m in members if isinstance(m, dict) and isinstance(m.get("user"), dict)]
if label == "placeA":
assert self.subscriber_id in user_ids, f"Подписчик должен быть member места A: {user_ids!r}"
else:
assert self.subscriber_id not in user_ids, f"Подписчик не должен быть member места B: {user_ids!r}"
places = resp.get("data", {}).get("place", {}).get("results", [])
services = None
if isinstance(places, list) and places and isinstance(places[0], dict):
services = places[0].get("services")
if isinstance(services, list) and services:
svc_ids = {s.get("id") for s in services if isinstance(s, dict)}
if label == "placeA":
assert s1 in svc_ids and s2 in svc_ids, f"Место A должно содержать s1,s2: {svc_ids!r}"
assert s3 not in svc_ids, f"Сервис места B не должен быть на месте A: {svc_ids!r}"
else:
assert s3 in svc_ids, f"Место B должно содержать s3: {svc_ids!r}"
assert s1 not in svc_ids and s2 not in svc_ids, f"Сервисы места A не должны быть на B: {svc_ids!r}"
elif label == "placeA" and self.subscription_services_ids:
sub_ids = {str(x) for x in self.subscription_services_ids}
assert sub_ids <= {s1, s2}, f"Подписка на месте A должна включать только сервисы пакета A: {sub_ids!r}, ожидали подмножество {{{s1!r}, {s2!r}}}"
assert s3 not in sub_ids, f"Подписка не должна содержать сервис места B (s3): {sub_ids!r}"
elif label == "placeA":
allure.attach(
"Нет place.services и createSubscription не вернул services — ослабленная проверка только по members.",
name="bundleScope: limited assertions",
attachment_type=AttachmentType.TEXT,
)

View File

@ -0,0 +1,26 @@
{
"priority": 10,
"request": {
"method": "POST",
"urlPath": "/graphql",
"bodyPatterns": [
{
"contains": "createPlaceMultiple"
}
]
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": {
"data": {
"createPlaceMultiple": [
{ "id": "wm_place_a", "__typename": "Place" },
{ "id": "wm_place_b", "__typename": "Place" }
]
}
}
}
}

View File

@ -0,0 +1,48 @@
{
"priority": 5,
"request": {
"method": "POST",
"urlPath": "/graphql",
"headers": {
"Content-Type": {
"equalTo": "application/json"
}
},
"bodyPatterns": [
{
"contains": "bundleScope"
}
]
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": {
"data": {
"members": {
"results": [
{
"id": "wm_member_1",
"user": {
"id": "wm_subscriber"
}
}
]
},
"place": {
"results": [
{
"id": "wm_place",
"services": [
{ "id": "wm_s1", "title": "svc-1" },
{ "id": "wm_s2", "title": "svc-2" }
]
}
]
}
}
}
}
}

View File

@ -108,6 +108,20 @@ query ticketinfo($place_id: String!) {
cache=False,
)
# Важно: для проверки "authorized" мы смотрим поле ticket.assignee.
# Поэтому нужно назначить ticket на нового employee до смены категории.
mutation_assign = """
mutation assignTicketEmployee($ticket_id: String!, $employee_user_id: String!) {
assignTicketEmployee(dto: {ticket_id: $ticket_id, employee_user_id: $employee_user_id})
}
""".strip()
execute_graphql(
query=mutation_assign,
variables={"ticket_id": td.ticket_id, "employee_user_id": td.account_id},
company_id=td.company_id,
access_token=token,
)
# cleanup: вернуть категорию обратно
cleanup_fns = getattr(context, "_cleanup_fns", None)
if isinstance(cleanup_fns, list):

View File

@ -238,8 +238,17 @@ mutation createticket($category_id: String!, $place_id: String!) {
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)
# На стенде deleteTicket часто принимается только в форме без variables (как в GraphQL Playground),
# а с $id иногда отдаёт 403. Берём свежий токен из admin_data — тот же, что и для ручного вызова.
fresh = admin_data.get_or_create_user("tester").access_token or self.access_token or token
delete_mutation = """mutation { deleteTicket(id: "%s") }""".strip() % ticket_id
_exec_or_fail(
op_name="deleteTicket(mutation)",
token=fresh,
query=delete_mutation,
variables=None,
company_id=self.company_id,
)
self._register_cleanup(_cleanup_delete_ticket)
return ticket_id
@ -266,8 +275,15 @@ mutation createticket($category_id: String!, $place_id: String!) {
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)
fresh = admin_data.get_or_create_user("tester").access_token or self.access_token or token
delete_mutation = """mutation { deleteTicket(id: "%s") }""".strip() % ticket_id
_exec_or_fail(
op_name="deleteTicket(mutation)",
token=fresh,
query=delete_mutation,
variables=None,
company_id=self.company_id,
)
# Удалять ticket первым (до категории/плейса).
self._register_cleanup(_cleanup_delete_ticket)

View File

@ -0,0 +1,40 @@
Traceback (most recent call last):
File "C:\Users\Степаан\PycharmProjects\work\worklib\graphql_client.py", line 176, in execute_graphql
with urllib.request.urlopen(req, timeout=timeout_s) as resp:
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Степаан\AppData\Local\Python\pythoncore-3.14-64\Lib\urllib\request.py", line 187, in urlopen
return opener.open(url, data, timeout)
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Степаан\AppData\Local\Python\pythoncore-3.14-64\Lib\urllib\request.py", line 493, in open
response = meth(req, response)
File "C:\Users\Степаан\AppData\Local\Python\pythoncore-3.14-64\Lib\urllib\request.py", line 602, in http_response
response = self.parent.error(
'http', request, response, code, msg, hdrs)
File "C:\Users\Степаан\AppData\Local\Python\pythoncore-3.14-64\Lib\urllib\request.py", line 531, in error
return self._call_chain(*args)
~~~~~~~~~~~~~~~~^^^^^^^
File "C:\Users\Степаан\AppData\Local\Python\pythoncore-3.14-64\Lib\urllib\request.py", line 464, in _call_chain
result = func(*args)
File "C:\Users\Степаан\AppData\Local\Python\pythoncore-3.14-64\Lib\urllib\request.py", line 611, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 400: Bad Request
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "Pass_request\features\environment.py", line 49, in after_scenario
fn()
~~^^
File "C:\Users\Степаан\PycharmProjects\work\Pass_request\testdata\pass_request_test_data.py", line 1440, in _cleanup_delete_pass
_exec_or_fail(op_name="deletePass", token=token, query=delete_mutation, variables={"id": pass_id}, company_id=self.company_id)
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Степаан\PycharmProjects\work\Pass_request\testdata\pass_request_test_data.py", line 30, in _exec_or_fail
return execute_graphql(
query=query,
...<2 lines>...
access_token=token,
)
File "C:\Users\Степаан\PycharmProjects\work\worklib\graphql_client.py", line 180, in execute_graphql
raise RuntimeError(f"GraphQL HTTP {e.code}: {body}") from e
RuntimeError: GraphQL HTTP 400: {"errors":[{"message":"Unknown argument \"id\" on field \"Mutation.deletePass\".","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"},{"message":"Field \"deletePass\" argument \"pass_id\" of type \"String!\" is required, but it was not provided.","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"setPlaceEntrances\" on type \"Mutation\". Did you mean \"deleteEntrance\" or \"createEntrance\"?","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1 @@
Forbidden (403) для GraphQL операции. Проверьте креды/права. Можно задать env: AUTH_USERNAME/AUTH_PASSWORD/AUTH_GRANT_TYPE.

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"connectEntranceToPlace\" on type \"Mutation\".","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,7 @@
{
"data": {
"addEmployee": {
"id": "6a02f6c6ec11a09b88a1eb9a"
}
}
}

View File

@ -0,0 +1,75 @@
{
"data": {
"place": {
"results": [
{
"id": "69f9beb232367dfb4b45a76f",
"members": [
{
"id": "0b6623c1-532f-47cf-aee8-a3d07688035e",
"status": "accepted",
"privileges": null,
"user": {
"id": "0b6623c1-532f-47cf-aee8-a3d07688035e"
}
},
{
"id": "3c110b22-4b42-43eb-a0f8-e66718862b4a",
"status": "accepted",
"privileges": null,
"user": {
"id": "3c110b22-4b42-43eb-a0f8-e66718862b4a"
}
}
]
},
{
"id": "69f9beb232367dfb4b45a772",
"members": [
{
"id": "0b6623c1-532f-47cf-aee8-a3d07688035e",
"status": "accepted",
"privileges": null,
"user": {
"id": "0b6623c1-532f-47cf-aee8-a3d07688035e"
}
},
{
"id": "3c110b22-4b42-43eb-a0f8-e66718862b4a",
"status": "accepted",
"privileges": null,
"user": {
"id": "3c110b22-4b42-43eb-a0f8-e66718862b4a"
}
}
]
},
{
"id": "69f9beb217bb1e0c5fc4e12e",
"members": [
{
"id": "0b6623c1-532f-47cf-aee8-a3d07688035e",
"status": "accepted",
"privileges": null,
"user": {
"id": "0b6623c1-532f-47cf-aee8-a3d07688035e"
}
},
{
"id": "3c110b22-4b42-43eb-a0f8-e66718862b4a",
"status": "accepted",
"privileges": null,
"user": {
"id": "3c110b22-4b42-43eb-a0f8-e66718862b4a"
}
}
]
},
{
"id": "69f9beb217bb1e0c5fc4e131",
"members": []
}
]
}
}
}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"connectEntranceToPlace\" on type \"Mutation\".","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

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

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"addEntranceToPlace\" on type \"Mutation\". Did you mean \"addUserToPlace\"?","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,24 @@
{
"data": {
"members": {
"results": [
{
"id": "493b2da3-706c-4528-b758-c14626fe0c29",
"status": "accepted",
"privileges": null,
"user": {
"id": "493b2da3-706c-4528-b758-c14626fe0c29"
}
},
{
"id": "86dd0882-68f5-46c8-9b95-21d9f9deb73a",
"status": "pending",
"privileges": null,
"user": {
"id": "86dd0882-68f5-46c8-9b95-21d9f9deb73a"
}
}
]
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"attachEntranceToPlace\" on type \"Mutation\".","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

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

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"addPlaceEntrance\" on type \"Mutation\". Did you mean \"deleteEntrance\", \"addPlaceToService\", \"createEntrance\", or \"addPlaceToBundle\"?","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,18 @@
{
"data": {
"createUser": {
"id": "5af8c275-2958-43d8-bb9a-453265bc958b",
"created_at": "2026-05-04T14:22:25.672Z",
"updated_at": "2026-05-04T14:22:25.672Z",
"username": "+79996266237",
"user_data": {
"first_name": "place",
"last_name": "member",
"email": ""
},
"is_demo": true,
"next_request_timestamp": "1970-01-01T00:00:00.000Z",
"roles": []
}
}
}

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69f8abc932367dfb4b45a3c3",
"member_id": "420d0abf-e1aa-40c5-84e7-3601efed6406"
}
}
}

View File

@ -0,0 +1,18 @@
{
"data": {
"createUser": {
"id": "30b165ca-21e8-4110-b7e0-4cd8bf69f514",
"created_at": "2026-05-05T10:56:42.700Z",
"updated_at": "2026-05-05T10:56:42.700Z",
"username": "+79992763965",
"user_data": {
"first_name": "owner",
"last_name": "passreq",
"email": ""
},
"is_demo": true,
"next_request_timestamp": "1970-01-01T00:00:00.000Z",
"roles": []
}
}
}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"connectEntranceToPlace\" on type \"Mutation\".","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,9 @@
{
"data": {
"createService": {
"id": "69f9cc931b4cbdc23d4509dc",
"title": "pass-service-1777978515",
"type": "access"
}
}
}

View File

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

View File

@ -0,0 +1,24 @@
{
"data": {
"members": {
"results": [
{
"id": "2ea6baf0-886d-44b3-aa44-c26d786755a3",
"status": "accepted",
"privileges": null,
"user": {
"id": "2ea6baf0-886d-44b3-aa44-c26d786755a3"
}
},
{
"id": "d888229f-441f-4504-8c0a-9fec64e01f1c",
"status": "pending",
"privileges": null,
"user": {
"id": "d888229f-441f-4504-8c0a-9fec64e01f1c"
}
}
]
}
}
}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"setPlaceEntrances\" on type \"Mutation\". Did you mean \"deleteEntrance\" or \"createEntrance\"?","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"addEmployee": {
"id": "69f8abc7514efad27fabd7f5"
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"addPlaceToService": {
"id": "69f9d2840b1f8729e0528e44"
}
}
}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"attachEntranceToPlace\" on type \"Mutation\".","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,7 @@
{
"data": {
"addPlaceToService": {
"id": "69f8a9c81b4cbdc23d450958"
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"setMemberStatus\" on type \"Mutation\". Did you mean \"updateMemberStatus\"?","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,18 @@
{
"data": {
"createUser": {
"id": "74152b85-8490-4ec5-b9d4-83075700498e",
"created_at": "2026-05-04T14:21:55.490Z",
"updated_at": "2026-05-04T14:21:55.490Z",
"username": "+79992826660",
"user_data": {
"first_name": "set",
"last_name": "worker",
"email": ""
},
"is_demo": true,
"next_request_timestamp": "1970-01-01T00:00:00.000Z",
"roles": []
}
}
}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"addEntranceToPlace\" on type \"Mutation\". Did you mean \"addUserToPlace\"?","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,18 @@
{
"data": {
"createUser": {
"id": "0177b402-386e-4c59-b0c1-dc3671fad20c",
"created_at": "2026-05-04T14:36:32.532Z",
"updated_at": "2026-05-04T14:36:32.532Z",
"username": "+79999641873",
"user_data": {
"first_name": "set",
"last_name": "worker",
"email": ""
},
"is_demo": true,
"next_request_timestamp": "1970-01-01T00:00:00.000Z",
"roles": []
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1,75 @@
{
"data": {
"place": {
"results": [
{
"id": "69f9ccf132367dfb4b45a994",
"members": [
{
"id": "2464e89d-69d5-4239-bfe5-61c0ea92c2aa",
"status": "accepted",
"privileges": null,
"user": {
"id": "2464e89d-69d5-4239-bfe5-61c0ea92c2aa"
}
},
{
"id": "6b39c80a-ff3c-4ee4-818d-d340aad6322c",
"status": "accepted",
"privileges": null,
"user": {
"id": "6b39c80a-ff3c-4ee4-818d-d340aad6322c"
}
}
]
},
{
"id": "69f9ccf1037d44249d0d1885",
"members": [
{
"id": "2464e89d-69d5-4239-bfe5-61c0ea92c2aa",
"status": "accepted",
"privileges": null,
"user": {
"id": "2464e89d-69d5-4239-bfe5-61c0ea92c2aa"
}
},
{
"id": "6b39c80a-ff3c-4ee4-818d-d340aad6322c",
"status": "accepted",
"privileges": null,
"user": {
"id": "6b39c80a-ff3c-4ee4-818d-d340aad6322c"
}
}
]
},
{
"id": "69f9ccf117bb1e0c5fc4e358",
"members": [
{
"id": "2464e89d-69d5-4239-bfe5-61c0ea92c2aa",
"status": "accepted",
"privileges": null,
"user": {
"id": "2464e89d-69d5-4239-bfe5-61c0ea92c2aa"
}
},
{
"id": "6b39c80a-ff3c-4ee4-818d-d340aad6322c",
"status": "accepted",
"privileges": null,
"user": {
"id": "6b39c80a-ff3c-4ee4-818d-d340aad6322c"
}
}
]
},
{
"id": "69f9ccf117bb1e0c5fc4e35b",
"members": []
}
]
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1,24 @@
{
"data": {
"members": {
"results": [
{
"id": "1a097da7-7d00-4834-9f43-d790ef80dabd",
"status": "accepted",
"privileges": null,
"user": {
"id": "1a097da7-7d00-4834-9f43-d790ef80dabd"
}
},
{
"id": "c5825d01-26be-497d-81b4-ec23b13f9071",
"status": "accepted",
"privileges": null,
"user": {
"id": "c5825d01-26be-497d-81b4-ec23b13f9071"
}
}
]
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69f9c56117bb1e0c5fc4e218",
"member_id": "574ceec0-8961-4fce-8a50-b1465f078534"
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1 @@
Skip entrance<->place link. entrance_id='69f8abc217bb1e0c5fc4dcb2', place_id='69f8abc2037d44249d0d1260'. Attempts: ['addEntranceToPlace/dto-entrance_id', 'addEntranceToPlace/dto-entrance_ids', 'addEntranceToPlace/args-entrance_id', 'addEntranceToPlace/args-entrance_ids', 'attachEntranceToPlace/dto-entrance_id', 'attachEntranceToPlace/dto-entrance_ids', 'attachEntranceToPlace/args-entrance_id', 'attachEntranceToPlace/args-entrance_ids', 'setPlaceEntrances/dto-entrance_id', 'setPlaceEntrances/dto-entrance_ids', 'setPlaceEntrances/args-entrance_id', 'setPlaceEntrances/args-entrance_ids', 'addPlaceEntrance/dto-entrance_id', 'addPlaceEntrance/dto-entrance_ids', 'addPlaceEntrance/args-entrance_id', 'addPlaceEntrance/args-entrance_ids', 'connectEntranceToPlace/dto-entrance_id', 'connectEntranceToPlace/dto-entrance_ids', 'connectEntranceToPlace/args-entrance_id', 'connectEntranceToPlace/args-entrance_ids']. Last error: GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"connectEntranceToPlace\" on type \"Mutation\".","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,7 @@
{
"data": {
"addPlaceToService": {
"id": "69f9bf723dcf1a2e79fbf95f"
}
}
}

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69f8abc4c15e6311636d8656",
"member_id": "f84e4a72-0de2-4980-be9b-6b62bfe7e375"
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69f8af20c15e6311636d87b1",
"member_id": "2ae06b7f-36fd-4c97-a20d-79bff7e4cbc2"
}
}
}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"addPlaceEntrance\" on type \"Mutation\". Did you mean \"deleteEntrance\", \"addPlaceToService\", \"createEntrance\", or \"addPlaceToBundle\"?","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"addEntranceToPlace\" on type \"Mutation\". Did you mean \"addUserToPlace\"?","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

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

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1,27 @@
{
"data": {
"ticket": {
"results": [
{
"number": 431,
"id": "6a02f6c99e04d08097dedf77",
"category": {
"id": "6a02f6c99e04d08097dedf76",
"title": "tester1"
},
"assignee": {
"id": "6a02f6c9ec11a09b88a1eb9c",
"user": {
"id": "b8bbee32-3b44-43d2-b196-2a4436f9644b",
"username": "+79992499159",
"data": {
"first_name": "kvstest1",
"last_name": "kvstest2"
}
}
}
}
]
}
}
}

View File

@ -0,0 +1,10 @@
{
"data": {
"createPlaceMultiple": [
{
"id": "place_0ed1f34a71f8",
"__typename": "Place"
}
]
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Unknown type \"UpdateMemberStatusInput\". Did you mean \"UpdateMemberStatusDto\", \"UpdatableMemberStatus\", or \"UpdateTicketStatusDTO\"?","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"attachEntranceToPlace\" on type \"Mutation\".","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"addEntranceToPlace\" on type \"Mutation\". Did you mean \"addUserToPlace\"?","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69f8b187037d44249d0d15cc",
"member_id": "b4ed4b98-7366-4b9f-b9ed-abdce1ae2915"
}
}
}

View File

@ -0,0 +1,8 @@
{
"data": {
"addUserToPlace": {
"place_id": "69f8b11e17bb1e0c5fc4e031",
"member_id": "5b1b9c30-5388-460a-884f-be581219e3e8"
}
}
}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"addPlaceEntrance\" on type \"Mutation\". Did you mean \"deleteEntrance\", \"addPlaceToService\", \"createEntrance\", or \"addPlaceToBundle\"?","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"createTicket": {
"id": "6a02d2d59e04d08097dedf3f"
}
}
}

View File

@ -0,0 +1 @@
GraphQL errors: [{'message': 'Variable "$input" got invalid value { place_id: "6a057723037d44249d0d1b37", account_id: "942a37d2-e008-43c9-bf50-0a12c71026cc", privilege: "trusted" }; Field "privilege" is not defined by type "AddUserToPlaceDTO".', 'code': 'Server Error', 'status': 500, 'description': 'The server encountered an unexpected condition which prevented it from fulfilling the request'}]

View File

@ -0,0 +1,9 @@
{
"data": {
"createService": {
"id": "69f8ab9a0b1f8729e0528dd5",
"title": "pass-service-1777904538",
"type": "access"
}
}
}

View File

@ -0,0 +1 @@
GraphQL errors: [{'message': 'Variable "$input" got invalid value { place_id: "69f9c58ec15e6311636d8cde", account_id: "1b2a95ba-c6b9-4ea7-8e1c-4978ae252fde", privilege: "trusted" }; Field "privilege" is not defined by type "AddUserToPlaceDTO".', 'code': 'Server Error', 'status': 500, 'description': 'The server encountered an unexpected condition which prevented it from fulfilling the request'}]

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"attachEntranceToPlace\" on type \"Mutation\".","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,10 @@
{
"data": {
"createPlaceMultiple": [
{
"id": "place_81f3135758f1",
"__typename": "Place"
}
]
}
}

View File

@ -0,0 +1,7 @@
{
"data": {
"passRequests": {
"results": []
}
}
}

View File

@ -0,0 +1,18 @@
{
"data": {
"createUser": {
"id": "0fd02201-6896-4c41-bca0-04b52966569a",
"created_at": "2026-05-05T09:57:57.319Z",
"updated_at": "2026-05-05T09:57:57.319Z",
"username": "+79999559011",
"user_data": {
"first_name": "pass",
"last_name": "request",
"email": ""
},
"is_demo": true,
"next_request_timestamp": "1970-01-01T00:00:00.000Z",
"roles": []
}
}
}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Field \"member_id\" is not defined by type \"PlaceFilters\".","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

View File

@ -0,0 +1,15 @@
{
"data": {
"setUserPlaces": [
{
"id": "69f9ccf1037d44249d0d1885"
},
{
"id": "69f9ccf117bb1e0c5fc4e358"
},
{
"id": "69f9ccf132367dfb4b45a994"
}
]
}
}

View File

@ -0,0 +1 @@
GraphQL HTTP 400: {"errors":[{"message":"Cannot query field \"services\" on type \"PlaceObject\". Did you mean \"devices\" or \"stories\"?","code":"Server Error","status":500,"description":"The server encountered an unexpected condition which prevented it from fulfilling the request"}]}

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