4918 lines
206 KiB
Python
4918 lines
206 KiB
Python
from pathlib import Path
|
|
import re
|
|
import time
|
|
from uuid import uuid4
|
|
import zipfile
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from api_server import main
|
|
from api_server.main import app
|
|
from one_c_normalizer import ConfigurationRoot, MetadataGroup, MetadataObject, Module, NormalizedProject
|
|
|
|
|
|
def create_authoring_session(client: TestClient, project_id: str, task_id: str, session_id: str, user_id: str = "dev.ivan") -> None:
|
|
user = client.post("/collaboration/users", json={"user_id": user_id, "display_name": user_id})
|
|
assert user.status_code == 200
|
|
grant = client.post(f"/security/users/{user_id}/roles/developer")
|
|
assert grant.status_code == 200
|
|
task = client.post(
|
|
"/collaboration/tasks",
|
|
json={"task_id": task_id, "project_id": project_id, "title": f"Authoring {task_id}", "assignee_user_id": user_id},
|
|
)
|
|
assert task.status_code == 200
|
|
session = client.post(
|
|
"/collaboration/sessions",
|
|
json={"session": {"session_id": session_id, "task_id": task_id, "user_id": user_id}},
|
|
)
|
|
assert session.status_code == 200
|
|
|
|
|
|
def assert_html5_contract(text: str, *markers: str, full_page: bool = False) -> None:
|
|
assert "__next" not in text
|
|
assert "unpkg.com" not in text
|
|
assert 'hx-trigger="every' not in text
|
|
if full_page:
|
|
assert "<!doctype html>" in text
|
|
assert "/html5/assets/htmx.min.js" in text
|
|
assert "/html5/assets/htmx-ext-sse.js" in text
|
|
else:
|
|
assert "<html" not in text
|
|
for marker in markers:
|
|
assert marker in text
|
|
|
|
|
|
def test_cors_allows_lan_panel_origin():
|
|
client = TestClient(app)
|
|
response = client.options(
|
|
"/health",
|
|
headers={
|
|
"Origin": "http://192.168.200.60:3000",
|
|
"Access-Control-Request-Method": "GET",
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.headers["access-control-allow-origin"] == "http://192.168.200.60:3000"
|
|
|
|
|
|
def test_metadata_catalog_exposes_1c_tree_structure():
|
|
client = TestClient(app)
|
|
response = client.get("/metadata/catalog")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert "8.3" in payload["platform_family"]
|
|
assert "Общие модули" in payload["common_branch_children"]
|
|
document = next(item for item in payload["types"] if item["code"] == "DOCUMENT")
|
|
common_module = next(item for item in payload["types"] if item["code"] == "COMMON_MODULE")
|
|
accumulation_register = next(item for item in payload["types"] if item["code"] == "ACCUMULATION_REGISTER")
|
|
http_service = next(item for item in payload["types"] if item["code"] == "HTTP_SERVICE")
|
|
report = next(item for item in payload["types"] if item["code"] == "REPORT")
|
|
catalog = next(item for item in payload["types"] if item["code"] == "CATALOG")
|
|
url_template = next(item for item in payload["child_object_types"] if item["code"] == "URL_TEMPLATE")
|
|
data_composition_schema = next(item for item in payload["child_object_types"] if item["code"] == "DATA_COMPOSITION_SCHEMA")
|
|
assert document["tree_branch"] == "Документы"
|
|
assert "Реквизиты" in document["child_groups"]
|
|
assert "Табличные части" in document["child_groups"]
|
|
assert "Движения" in document["child_groups"]
|
|
assert "Проведение" in document["properties"]
|
|
assert "Открыть модуль объекта" in document["context_actions"]
|
|
assert common_module["tree_branch"] == "Общие модули"
|
|
assert "Экспортные методы" in common_module["child_groups"]
|
|
assert "Клиент" in common_module["properties"]
|
|
assert "Найти вызовы" in common_module["context_actions"]
|
|
assert accumulation_register["module_kinds"] == ["Модуль набора записей", "Модуль менеджера"]
|
|
assert "Ресурсы" in accumulation_register["child_groups"]
|
|
assert "Показать чтение/запись" in accumulation_register["context_actions"]
|
|
assert http_service["child_groups"] == ["Шаблоны URL", "Методы", "Модуль"]
|
|
assert "HTTP-методы" in http_service["properties"]
|
|
assert "Показать URL-шаблоны" in http_service["context_actions"]
|
|
assert "СКД" in report["child_groups"]
|
|
assert "Табличные документы" in report["child_groups"]
|
|
assert "Варианты отчета" in report["child_groups"]
|
|
assert "Настройки" in report["child_groups"]
|
|
assert "Справочник" in catalog["description"]
|
|
assert "HTTP-сервис" in http_service["description"]
|
|
assert "Шаблоны URL" in url_template["parent_groups"]
|
|
assert data_composition_schema["parent_groups"] == ["СКД"]
|
|
|
|
|
|
def test_html5_server_rendered_project_editor(tmp_path: Path):
|
|
client = TestClient(app)
|
|
project_id = f"html5-editor-{uuid4()}"
|
|
module = tmp_path / "demo_module.bsl"
|
|
module.write_text(
|
|
"Процедура Проверить()\n"
|
|
" Сообщить(\"HTML5\");\n"
|
|
"КонецПроцедуры\n",
|
|
encoding="utf-8",
|
|
)
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
|
|
index = client.get("/html5")
|
|
assert index.status_code == 200
|
|
assert "text/html" in index.headers["content-type"]
|
|
assert 'data-html5-page="projects"' in index.text
|
|
assert project_id in index.text
|
|
assert "__next" not in index.text
|
|
|
|
editor = client.get(f"/html5/projects/{project_id}/editor", params={"q": "Проверить"})
|
|
assert editor.status_code == 200
|
|
assert 'data-html5-page="editor"' in editor.text
|
|
assert "data-html5-editor" in editor.text
|
|
assert "data-html5-symbol-results" in editor.text
|
|
assert "data-html5-symbol-detail" in editor.text
|
|
assert "data-html5-flowchart" in editor.text
|
|
assert 'sse-swap="project-flowchart"' in editor.text
|
|
assert f'hx-get="/html5/projects/{project_id}/flowchart?depth=1"' in editor.text
|
|
assert "data-html5-project-report" in editor.text
|
|
assert 'sse-swap="project-report"' in editor.text
|
|
assert f'hx-get="/html5/projects/{project_id}/report"' in editor.text
|
|
assert "data-html5-review" in editor.text
|
|
assert 'sse-swap="project-review"' in editor.text
|
|
assert f'hx-get="/html5/projects/{project_id}/review"' in editor.text
|
|
assert "data-html5-authoring-preview" in editor.text
|
|
assert "data-html5-authoring-preview-form" in editor.text
|
|
assert f'hx-post="/html5/projects/{project_id}/authoring/completion-preview"' in editor.text
|
|
assert "data-html5-authoring-diff-form" in editor.text
|
|
assert f'hx-post="/html5/projects/{project_id}/authoring/semantic-diff-preview"' in editor.text
|
|
assert "data-html5-metadata-authoring" in editor.text
|
|
assert "data-html5-metadata-preview-form" in editor.text
|
|
assert f'hx-post="/html5/projects/{project_id}/authoring/metadata-object-preview"' in editor.text
|
|
assert "data-html5-authoring-changes" in editor.text
|
|
assert f'hx-get="/html5/projects/{project_id}/authoring/changes"' in editor.text
|
|
assert 'sse-swap="authoring-changes"' in editor.text
|
|
assert 'hx-get="/html5/projects/' in editor.text
|
|
assert 'hx-target="[data-html5-symbol-results]"' in editor.text
|
|
assert 'hx-target="[data-html5-symbol-detail]"' in editor.text
|
|
assert 'hx-target="[data-html5-source]"' in editor.text
|
|
assert 'hx-swap="outerHTML"' in editor.text
|
|
assert "hx-ext=\"sse\"" in editor.text
|
|
assert f"sse-connect=\"/html5/projects/{project_id}/events\"" in editor.text
|
|
assert '/html5/assets/htmx.min.js' in editor.text
|
|
assert '/html5/assets/htmx-ext-sse.js' in editor.text
|
|
assert "unpkg.com" not in editor.text
|
|
assert "client-js: htmx+sse only" in editor.text
|
|
assert ".object-actions .button[data-html5-object-action-active" in editor.text
|
|
assert ".inline-actions" in editor.text
|
|
assert ".object-breadcrumb" in editor.text
|
|
assert ".object-summary" in editor.text
|
|
assert "Проверить" in editor.text
|
|
assert "__next" not in editor.text
|
|
|
|
with client.stream("GET", f"/html5/projects/{project_id}/events?once=1") as events:
|
|
first_chunk = "".join(events.iter_text())
|
|
assert ": project " in first_chunk
|
|
assert "retry: 5000" in first_chunk
|
|
assert "event: status" in first_chunk
|
|
assert "event: authoring-changes" in first_chunk
|
|
assert "event: project-report" in first_chunk
|
|
assert "event: project-review" in first_chunk
|
|
assert "event: project-flowchart" in first_chunk
|
|
assert "data-html5-authoring-changes" in first_chunk
|
|
assert "data-html5-project-report" in first_chunk
|
|
assert "data-html5-review" in first_chunk
|
|
assert "data-html5-flowchart" in first_chunk
|
|
assert "data:" in first_chunk
|
|
assert project_id in first_chunk
|
|
|
|
symbols = client.get(f"/html5/projects/{project_id}/symbols", params={"q": "Проверить"})
|
|
assert symbols.status_code == 200
|
|
assert "text/html" in symbols.headers["content-type"]
|
|
assert 'data-html5-symbol' in symbols.text
|
|
assert 'data-html5-lineage-id' in symbols.text
|
|
assert 'hx-target="[data-html5-symbol-detail]"' in symbols.text
|
|
assert "Проверить" in symbols.text
|
|
assert "<html" not in symbols.text
|
|
|
|
snapshot = client.get(f"/projects/{project_id}/snapshot/export").json()
|
|
module_node = next(node for node in snapshot["nodes"] if node["kind"] == "MODULE")
|
|
procedure_node = next(node for node in snapshot["nodes"] if node["kind"] == "PROCEDURE")
|
|
source = client.get(f"/html5/projects/{project_id}/source/{module_node['lineage_id']}")
|
|
assert source.status_code == 200
|
|
assert "text/html" in source.headers["content-type"]
|
|
assert "data-html5-source" in source.text
|
|
assert "data-html5-source-name" in source.text
|
|
assert "data-html5-lineage-id" in source.text
|
|
assert "data-html5-source-summary" in source.text
|
|
assert "Lines" in source.text
|
|
assert "chars" in source.text
|
|
assert "Проверить" in source.text
|
|
assert "<html" not in source.text
|
|
|
|
source_by_path = client.get(
|
|
f"/html5/projects/{project_id}/source/by-path",
|
|
params={"path": module_node["source_ref"]["source_path"]},
|
|
)
|
|
assert source_by_path.status_code == 200
|
|
assert "data-html5-source-summary" in source_by_path.text
|
|
assert "Проверить" in source_by_path.text
|
|
assert "<html" not in source_by_path.text
|
|
|
|
detail = client.get(f"/html5/projects/{project_id}/symbols/{procedure_node['lineage_id']}/detail")
|
|
assert detail.status_code == 200
|
|
assert "text/html" in detail.headers["content-type"]
|
|
assert "data-html5-symbol-detail" in detail.text
|
|
assert "data-html5-symbol-summary" in detail.text
|
|
assert "references" in detail.text
|
|
assert "data-html5-symbol-source" in detail.text
|
|
assert 'hx-target="[data-html5-source]"' in detail.text
|
|
assert "Проверить" in detail.text
|
|
assert "<html" not in detail.text
|
|
|
|
report = client.get(f"/html5/projects/{project_id}/report")
|
|
assert report.status_code == 200
|
|
assert "text/html" in report.headers["content-type"]
|
|
assert "data-html5-project-report" in report.text
|
|
assert "data-html5-project-summary" in report.text
|
|
assert "risk signals" in report.text
|
|
assert "Objects" in report.text
|
|
assert "<html" not in report.text
|
|
|
|
review = client.get(f"/html5/projects/{project_id}/review")
|
|
assert review.status_code == 200
|
|
assert "text/html" in review.headers["content-type"]
|
|
assert "data-html5-review" in review.text
|
|
assert "data-html5-review-summary" in review.text
|
|
assert "findings" in review.text
|
|
assert "data-html5-review-source" in review.text or "Findings не найдены" in review.text
|
|
assert "<html" not in review.text
|
|
|
|
authoring = client.get(f"/html5/projects/{project_id}/authoring/changes")
|
|
assert authoring.status_code == 200
|
|
assert "text/html" in authoring.headers["content-type"]
|
|
assert "data-html5-authoring-changes" in authoring.text
|
|
assert "data-html5-authoring-summary" in authoring.text
|
|
assert "changes" in authoring.text
|
|
assert "data-html5-authoring-detail" in authoring.text
|
|
assert "Изменений пока нет" in authoring.text
|
|
assert "<html" not in authoring.text
|
|
|
|
htmx_asset = client.get("/html5/assets/htmx.min.js")
|
|
assert htmx_asset.status_code == 200
|
|
assert "javascript" in htmx_asset.headers["content-type"]
|
|
assert "htmx" in htmx_asset.text
|
|
sse_asset = client.get("/html5/assets/htmx-ext-sse.js")
|
|
assert sse_asset.status_code == 200
|
|
assert "javascript" in sse_asset.headers["content-type"]
|
|
assert "sse" in sse_asset.text
|
|
|
|
|
|
def test_html5_contracts_are_server_rendered_and_stable(tmp_path: Path):
|
|
client = TestClient(app)
|
|
project_id = f"html5-contract-{uuid4()}"
|
|
module = tmp_path / "contract_module.bsl"
|
|
module.write_text(
|
|
"Процедура ПроверитьКонтракт()\n"
|
|
" Сообщить(\"contract\");\n"
|
|
"КонецПроцедуры\n",
|
|
encoding="utf-8",
|
|
)
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
settings = client.post(
|
|
f"/projects/{project_id}/settings",
|
|
json={"name": "HTML5 Contract", "structure_source": "XML_DUMP"},
|
|
)
|
|
assert settings.status_code == 200
|
|
|
|
full_pages = [
|
|
("/html5", ("data-html5-page=\"projects\"", "data-html5-projects")),
|
|
(f"/html5/projects/{project_id}/editor", ("data-html5-page=\"editor\"", "data-html5-editor")),
|
|
(f"/html5/projects/{project_id}/setup", ("data-html5-page=\"setup\"", "data-html5-setup-summary")),
|
|
("/html5/operations", ("data-html5-page=\"operations\"", "data-html5-operations-filter")),
|
|
]
|
|
for path, markers in full_pages:
|
|
response = client.get(path)
|
|
assert response.status_code == 200
|
|
assert_html5_contract(response.text, *markers, full_page=True)
|
|
|
|
partials = [
|
|
(f"/html5/projects/{project_id}/symbols", {"q": "Проверить"}, ("data-html5-symbol",)),
|
|
(f"/html5/projects/{project_id}/report", {}, ("data-html5-project-report", "data-html5-project-summary")),
|
|
(f"/html5/projects/{project_id}/review", {}, ("data-html5-review", "data-html5-review-summary")),
|
|
(f"/html5/projects/{project_id}/flowchart", {}, ("data-html5-flowchart",)),
|
|
(f"/html5/projects/{project_id}/authoring/changes", {}, ("data-html5-authoring-changes", "data-html5-authoring-summary")),
|
|
(f"/html5/projects/{project_id}/setup/summary", {}, ("data-html5-setup-summary",)),
|
|
("/html5/operations/jobs", {}, ("data-html5-operation",)),
|
|
("/html5/operations/summary", {}, ("data-html5-operations-summary",)),
|
|
]
|
|
for path, params, markers in partials:
|
|
response = client.get(path, params=params)
|
|
assert response.status_code == 200
|
|
assert_html5_contract(response.text, *markers)
|
|
|
|
|
|
def test_html5_project_index_creates_project_with_fragment():
|
|
client = TestClient(app)
|
|
project_id = f"html5-created-{uuid4()}"
|
|
|
|
index = client.get("/html5")
|
|
assert index.status_code == 200
|
|
assert 'data-html5-project-create' in index.text
|
|
assert 'data-html5-projects-body' in index.text
|
|
assert 'hx-post="/html5/projects"' in index.text
|
|
assert 'href="/html5/operations"' in index.text
|
|
|
|
created = client.post("/html5/projects", data={"project_id": project_id, "name": "HTML5 Created"})
|
|
assert created.status_code == 200
|
|
assert "text/html" in created.headers["content-type"]
|
|
assert f'data-html5-project="{project_id}"' in created.text
|
|
assert "HTML5 Created" in created.text
|
|
assert f"/html5/projects/{project_id}/setup" in created.text
|
|
assert f'hx-post="/html5/projects/{project_id}/delete"' in created.text
|
|
assert "<html" not in created.text
|
|
|
|
setup = client.get(f"/html5/projects/{project_id}/setup")
|
|
assert setup.status_code == 200
|
|
assert "HTML5 Created" in setup.text
|
|
|
|
deleted = client.post(f"/html5/projects/{project_id}/delete", data={"confirmation": project_id})
|
|
assert deleted.status_code == 200
|
|
assert "text/html" in deleted.headers["content-type"]
|
|
assert f'data-html5-project="{project_id}"' not in deleted.text
|
|
assert "<html" not in deleted.text
|
|
|
|
deleted_setup = client.get(f"/projects/{project_id}/setup")
|
|
assert deleted_setup.status_code == 200
|
|
assert deleted_setup.json()["status"] == "NOT_CONFIGURED"
|
|
|
|
|
|
def test_html5_object_context_fragment(tmp_path: Path):
|
|
project_id = f"html5-object-context-{uuid4()}"
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<Attribute name="Контрагент" qualifiedName="Документ.ЗаказПокупателя.Контрагент" />
|
|
<TabularSection name="Товары" qualifiedName="Документ.ЗаказПокупателя.Товары">
|
|
<Attribute name="Количество" qualifiedName="Документ.ЗаказПокупателя.Товары.Количество" />
|
|
</TabularSection>
|
|
<Form name="ФормаДокумента" qualifiedName="Документ.ЗаказПокупателя.ФормаДокумента">
|
|
<Command name="Провести" qualifiedName="Документ.ЗаказПокупателя.ФормаДокумента.Провести" action="ПровестиКоманда" />
|
|
</Form>
|
|
</Document>
|
|
<Role name="Менеджер" qualifiedName="Роль.Менеджер">
|
|
<Right object="Документ.ЗаказПокупателя" read="true" write="true" post="true" />
|
|
</Role>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
module = tmp_path / "Documents" / "ЗаказПокупателя" / "Ext" / "ObjectModule.bsl"
|
|
module.parent.mkdir(parents=True)
|
|
module.write_text(
|
|
"""
|
|
Процедура ПровестиКоманда()
|
|
ПроверитьКонтрагента();
|
|
Соединение = Новый HTTPСоединение("api.example.local");
|
|
Адрес = "https://api.example.local/orders";
|
|
КонецПроцедуры
|
|
|
|
Процедура ПроверитьКонтрагента()
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
snapshot = client.get(f"/projects/{project_id}/snapshot/export").json()
|
|
handler = next(node for node in snapshot["nodes"] if node["name"] == "ПровестиКоманда")
|
|
attribute = next(node for node in snapshot["nodes"] if node["name"] == "Контрагент")
|
|
signal = client.post(
|
|
f"/projects/{project_id}/runtime/signals",
|
|
json={
|
|
"signal": {
|
|
"signal_id": "html5-runtime.1",
|
|
"lineage_id": handler["lineage_id"],
|
|
"kind": "ERROR",
|
|
"duration_ms": 125.0,
|
|
}
|
|
},
|
|
)
|
|
assert signal.status_code == 200
|
|
knowledge = client.post(
|
|
"/knowledge",
|
|
json={
|
|
"record_id": f"knowledge.html5.object.{uuid4()}",
|
|
"scope": "PROJECT",
|
|
"title": "Правила проведения HTML5",
|
|
"body": "Контекст проведения заказа для HTML5 inspector.",
|
|
"related_lineages": [handler["lineage_id"]],
|
|
},
|
|
)
|
|
assert knowledge.status_code == 200
|
|
marker = client.post(
|
|
f"/projects/{project_id}/privacy/markers",
|
|
json={
|
|
"target_id": attribute["lineage_id"],
|
|
"classification": "PERSONAL_DATA",
|
|
"reason": "Контрагент содержит персональные данные",
|
|
},
|
|
)
|
|
assert marker.status_code == 200
|
|
|
|
editor = client.get(f"/html5/projects/{project_id}/editor")
|
|
assert editor.status_code == 200
|
|
assert "data-html5-object-context" in editor.text
|
|
assert f'hx-get="/html5/projects/{project_id}/objects/context/%D0%94%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82.%D0%97%D0%B0%D0%BA%D0%B0%D0%B7%D0%9F%D0%BE%D0%BA%D1%83%D0%BF%D0%B0%D1%82%D0%B5%D0%BB%D1%8F"' in editor.text
|
|
assert 'hx-target="[data-html5-object-context]"' in editor.text
|
|
|
|
context = client.get(f"/html5/projects/{project_id}/objects/context/Документ.ЗаказПокупателя")
|
|
assert context.status_code == 200
|
|
assert "text/html" in context.headers["content-type"]
|
|
assert "data-html5-object-context" in context.text
|
|
assert "Документ.ЗаказПокупателя" in context.text
|
|
assert "data-html5-object-actions" in context.text
|
|
assert f"/html5/projects/{project_id}/objects/context/%D0%94%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82.%D0%97%D0%B0%D0%BA%D0%B0%D0%B7%D0%9F%D0%BE%D0%BA%D1%83%D0%BF%D0%B0%D1%82%D0%B5%D0%BB%D1%8F" in context.text
|
|
assert f"/projects/{project_id}/objects/schema/%D0%94%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82.%D0%97%D0%B0%D0%BA%D0%B0%D0%B7%D0%9F%D0%BE%D0%BA%D1%83%D0%BF%D0%B0%D1%82%D0%B5%D0%BB%D1%8F" in context.text
|
|
assert f"/projects/{project_id}/objects/impact/%D0%94%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82.%D0%97%D0%B0%D0%BA%D0%B0%D0%B7%D0%9F%D0%BE%D0%BA%D1%83%D0%BF%D0%B0%D1%82%D0%B5%D0%BB%D1%8F" in context.text
|
|
assert "mode=schema" in context.text
|
|
assert "mode=impact" in context.text
|
|
assert "mode=privacy" in context.text
|
|
assert 'data-html5-object-mode="overview"' in context.text
|
|
assert "data-html5-object-breadcrumb" in context.text
|
|
assert "<span>Документ</span>" in context.text
|
|
assert "<span>ЗаказПокупателя</span>" in context.text
|
|
assert "data-html5-object-summary" in context.text
|
|
assert "1 attrs" in context.text
|
|
assert "1 tables" in context.text
|
|
assert "1 commands" in context.text
|
|
assert "1 access rules" in context.text
|
|
assert "1 runtime signals" in context.text
|
|
assert "1 privacy markers" in context.text
|
|
assert "Object context · overview" in context.text
|
|
assert 'data-html5-object-action-active="true"' in context.text
|
|
assert 'aria-current="page"' in context.text
|
|
assert 'hx-target="[data-html5-object-context]"' in context.text
|
|
assert 'hx-target="[data-html5-flowchart]"' in context.text
|
|
assert f"/html5/projects/{project_id}/flowchart?focus=%D0%94%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82.%D0%97%D0%B0%D0%BA%D0%B0%D0%B7%D0%9F%D0%BE%D0%BA%D1%83%D0%BF%D0%B0%D1%82%D0%B5%D0%BB%D1%8F&depth=1" in context.text
|
|
assert 'hx-target="[data-html5-source]"' in context.text
|
|
assert 'hx-target="[data-html5-symbol-detail]"' in context.text
|
|
assert "Контрагент" in context.text
|
|
assert "Товары" in context.text
|
|
assert "ФормаДокумента" in context.text
|
|
assert "Провести" in context.text
|
|
assert "ПровестиКоманда" in context.text
|
|
assert "ПроверитьКонтрагента" in context.text
|
|
assert "HTTPConnection" in context.text
|
|
assert "https://api.example.local/orders" in context.text
|
|
assert "OUTBOUND" in context.text
|
|
assert "data-html5-object-context-item=\"flow-edge\"" in context.text
|
|
assert "data-html5-flowchart-focus" in context.text
|
|
assert "data-html5-flowchart-context" in context.text
|
|
assert "data-html5-flowchart" in context.text
|
|
assert 'hx-swap-oob="outerHTML"' in context.text
|
|
assert "Карта связей · focus" in context.text
|
|
assert "data-html5-source" in context.text
|
|
assert "data-html5-source-summary" in context.text
|
|
assert "ObjectModule.bsl" in context.text
|
|
assert "Соединение = Новый HTTPСоединение" in context.text
|
|
assert "data-html5-symbol-detail" in context.text
|
|
assert "data-html5-symbol-summary" in context.text
|
|
assert "data-html5-symbol-source" in context.text
|
|
assert "Символ · DOCUMENT" in context.text
|
|
assert "HAS_ATTRIBUTE" in context.text
|
|
assert "data-html5-project-report" in context.text
|
|
assert "Отчет объекта" in context.text
|
|
assert "server focused summary" in context.text
|
|
assert "data-html5-object-report-summary" in context.text
|
|
assert "data links" in context.text
|
|
assert "data-html5-review" in context.text
|
|
assert "data-html5-review-summary" in context.text
|
|
assert "data-html5-review-source" in context.text
|
|
assert "Review объекта" in context.text
|
|
assert "External integration endpoint" in context.text
|
|
assert "1 signals" in context.text
|
|
assert "1 errors" in context.text
|
|
assert "125.0 ms" in context.text
|
|
assert "Правила проведения HTML5" in context.text
|
|
assert "Контекст проведения заказа" in context.text
|
|
assert "PERSONAL_DATA" in context.text
|
|
assert "Контрагент содержит персональные данные" in context.text
|
|
assert "Роль.Менеджер" in context.text
|
|
assert "read, write, post" in context.text or "post, read, write" in context.text
|
|
assert "<html" not in context.text
|
|
|
|
schema_context = client.get(
|
|
f"/html5/projects/{project_id}/objects/context/Документ.ЗаказПокупателя",
|
|
params={"mode": "schema"},
|
|
)
|
|
assert schema_context.status_code == 200
|
|
assert 'data-html5-object-mode="schema"' in schema_context.text
|
|
assert "Object context · schema" in schema_context.text
|
|
assert 'data-html5-object-action-active="true"' in schema_context.text
|
|
assert "Контрагент" in schema_context.text
|
|
|
|
impact_context = client.get(
|
|
f"/html5/projects/{project_id}/objects/context/Документ.ЗаказПокупателя",
|
|
params={"mode": "impact"},
|
|
)
|
|
assert impact_context.status_code == 200
|
|
assert 'data-html5-object-mode="impact"' in impact_context.text
|
|
assert "Object context · impact" in impact_context.text
|
|
assert "HTTPConnection" in impact_context.text
|
|
|
|
privacy_context = client.get(
|
|
f"/html5/projects/{project_id}/objects/context/Документ.ЗаказПокупателя",
|
|
params={"mode": "privacy"},
|
|
)
|
|
assert privacy_context.status_code == 200
|
|
assert 'data-html5-object-mode="privacy"' in privacy_context.text
|
|
assert "Object context · privacy" in privacy_context.text
|
|
assert "PERSONAL_DATA" in privacy_context.text
|
|
|
|
|
|
def test_html5_flowchart_fragment(tmp_path: Path):
|
|
client = TestClient(app)
|
|
project_id = f"html5-flowchart-{uuid4()}"
|
|
(tmp_path / "module.bsl").write_text(
|
|
"""
|
|
Процедура ПровестиЗаказ()
|
|
ПроверитьОстатки();
|
|
Движения.ОстаткиТоваров.Записать();
|
|
КонецПроцедуры
|
|
|
|
Процедура ПроверитьОстатки()
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
|
|
flowchart = client.get(
|
|
f"/html5/projects/{project_id}/flowchart",
|
|
params={"focus": "ПровестиЗаказ"},
|
|
)
|
|
assert flowchart.status_code == 200
|
|
assert "text/html" in flowchart.headers["content-type"]
|
|
assert "data-html5-flowchart" in flowchart.text
|
|
assert "data-html5-flowchart-actions" in flowchart.text
|
|
assert "Depth 1" in flowchart.text
|
|
assert "Depth 2" in flowchart.text
|
|
assert "Depth 3" in flowchart.text
|
|
assert "depth=2" in flowchart.text
|
|
assert 'hx-target="[data-html5-flowchart]"' in flowchart.text
|
|
assert "data-html5-flowchart-focus" in flowchart.text
|
|
assert 'sse-swap="project-flowchart"' not in flowchart.text
|
|
assert 'hx-swap-oob="outerHTML"' not in flowchart.text
|
|
assert "Карта связей" in flowchart.text
|
|
assert "Nodes" in flowchart.text
|
|
assert "Edges" in flowchart.text
|
|
assert "ПровестиЗаказ" in flowchart.text or "ПроверитьОстатки" in flowchart.text
|
|
assert "<html" not in flowchart.text
|
|
|
|
deep_flowchart = client.get(
|
|
f"/html5/projects/{project_id}/flowchart",
|
|
params={"focus": "ПровестиЗаказ", "depth": 2},
|
|
)
|
|
assert deep_flowchart.status_code == 200
|
|
assert "depth=2" in deep_flowchart.text
|
|
assert 'data-html5-object-action-active="true"' in deep_flowchart.text
|
|
|
|
|
|
def test_html5_project_setup_renders_server_fragments():
|
|
client = TestClient(app)
|
|
project_id = f"html5-setup-{uuid4()}"
|
|
|
|
saved = client.post(
|
|
f"/projects/{project_id}/settings",
|
|
json={
|
|
"name": "HTML5 Setup Demo",
|
|
"structure_source": "XML_DUMP",
|
|
"platform_version": "8.3.24",
|
|
"compatibility_mode": "8.3.20",
|
|
},
|
|
)
|
|
assert saved.status_code == 200
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/XML_DUMP",
|
|
json={
|
|
"source": "XML_DUMP",
|
|
"metadata": {"platform_version": "8.3.24", "compatibility_mode": "8.3.20"},
|
|
},
|
|
)
|
|
assert imported.status_code == 200
|
|
|
|
setup = client.get(f"/html5/projects/{project_id}/setup")
|
|
assert setup.status_code == 200
|
|
assert "text/html" in setup.headers["content-type"]
|
|
assert 'data-html5-page="setup"' in setup.text
|
|
assert "HTML5 Setup Demo" in setup.text
|
|
assert "data-html5-settings-panel" in setup.text
|
|
assert "data-html5-setup-summary" in setup.text
|
|
assert 'hx-ext="sse"' in setup.text
|
|
assert f'sse-connect="/html5/projects/{project_id}/setup/events"' in setup.text
|
|
assert 'sse-swap="setup-summary"' in setup.text
|
|
assert 'sse-swap="setup-import-job"' in setup.text
|
|
assert 'hx-trigger="every 5s"' not in setup.text
|
|
assert f'hx-get="/html5/projects/{project_id}/setup/summary"' in setup.text
|
|
assert f'hx-post="/html5/projects/{project_id}/setup/settings"' in setup.text
|
|
assert f'hx-post="/html5/projects/{project_id}/setup/source"' in setup.text
|
|
assert f'hx-post="/html5/projects/{project_id}/setup/check"' in setup.text
|
|
assert f'hx-post="/html5/projects/{project_id}/setup/import-job"' in setup.text
|
|
assert f'hx-post="/html5/projects/{project_id}/setup/import"' in setup.text
|
|
assert f'hx-post="/html5/projects/{project_id}/setup/reindex"' in setup.text
|
|
assert "data-html5-import-check" in setup.text
|
|
assert "data-html5-import-job" in setup.text
|
|
assert "XML_DUMP" in setup.text
|
|
assert "__next" not in setup.text
|
|
|
|
settings = client.post(
|
|
f"/html5/projects/{project_id}/setup/settings",
|
|
data={
|
|
"name": "HTML5 Renamed",
|
|
"platform_version": "8.3.25",
|
|
"compatibility_mode": "8.3.21",
|
|
},
|
|
)
|
|
assert settings.status_code == 200
|
|
assert "data-html5-settings-panel" in settings.text
|
|
assert "HTML5 Renamed" in settings.text
|
|
assert "8.3.25" in settings.text
|
|
assert "Сохранено" in settings.text
|
|
assert "<html" not in settings.text
|
|
saved_setup = client.get(f"/projects/{project_id}/setup").json()
|
|
assert saved_setup["settings"]["name"] == "HTML5 Renamed"
|
|
assert saved_setup["settings"]["platform_version"] == "8.3.25"
|
|
assert saved_setup["settings"]["compatibility_mode"] == "8.3.21"
|
|
|
|
source = client.post(f"/html5/projects/{project_id}/setup/source", data={"source": "EDT_PROJECT"})
|
|
assert source.status_code == 200
|
|
assert "data-html5-setup-summary" in source.text
|
|
assert "EDT_PROJECT" in source.text
|
|
assert "<html" not in source.text
|
|
|
|
check = client.post(f"/html5/projects/{project_id}/setup/check")
|
|
assert check.status_code == 200
|
|
assert "data-html5-import-check" in check.text
|
|
assert "data-html5-preflight-check" in check.text
|
|
assert "WARNING" in check.text
|
|
assert "<html" not in check.text
|
|
|
|
import_job = client.post(f"/html5/projects/{project_id}/setup/import-job")
|
|
assert import_job.status_code == 200
|
|
assert "data-html5-import-job" in import_job.text
|
|
assert "SERVER_IMPORT" not in import_job.text
|
|
assert "hx-get" in import_job.text
|
|
assert 'sse-swap="setup-import-job"' in import_job.text
|
|
assert 'hx-trigger="every 2s"' not in import_job.text
|
|
assert "<html" not in import_job.text
|
|
|
|
jobs = client.get("/operations/jobs", params={"project_id": project_id, "kind": "SERVER_IMPORT"})
|
|
assert jobs.status_code == 200
|
|
job_id = jobs.json()[0]["job_id"]
|
|
job_fragment = client.get(f"/html5/projects/{project_id}/setup/jobs/{job_id}")
|
|
assert job_fragment.status_code == 200
|
|
assert "data-html5-import-job" in job_fragment.text
|
|
assert job_id in job_fragment.text
|
|
assert 'sse-swap="setup-import-job"' in job_fragment.text
|
|
assert 'hx-trigger="every 2s"' not in job_fragment.text
|
|
assert "<html" not in job_fragment.text
|
|
|
|
html5_import = client.post(f"/html5/projects/{project_id}/setup/import")
|
|
assert html5_import.status_code == 200
|
|
assert "data-html5-setup-summary" in html5_import.text
|
|
assert "mock_indexed" in html5_import.text
|
|
assert "<html" not in html5_import.text
|
|
|
|
reindex = client.post(f"/html5/projects/{project_id}/setup/reindex")
|
|
assert reindex.status_code == 200
|
|
assert "data-html5-setup-summary" in reindex.text
|
|
assert "reindexed" in reindex.text
|
|
assert "<html" not in reindex.text
|
|
|
|
summary = client.get(f"/html5/projects/{project_id}/setup/summary")
|
|
assert summary.status_code == 200
|
|
assert "text/html" in summary.headers["content-type"]
|
|
assert "data-html5-setup-summary" in summary.text
|
|
assert "INDEXED" in summary.text
|
|
assert "mock_indexed" in summary.text
|
|
assert 'sse-swap="setup-summary"' in summary.text
|
|
assert 'hx-trigger="every 5s"' not in summary.text
|
|
assert "<html" not in summary.text
|
|
|
|
with client.stream("GET", f"/html5/projects/{project_id}/setup/events?once=1") as events:
|
|
first_chunk = "".join(events.iter_text())
|
|
assert ": setup " in first_chunk
|
|
assert "retry: 5000" in first_chunk
|
|
assert "event: setup-summary" in first_chunk
|
|
assert "event: setup-import-job" in first_chunk
|
|
assert "data-html5-setup-summary" in first_chunk
|
|
assert "data-html5-import-job" in first_chunk
|
|
assert project_id in first_chunk
|
|
|
|
|
|
def test_html5_operations_renders_job_monitor_fragments():
|
|
client = TestClient(app)
|
|
project_id = f"html5-ops-{uuid4()}"
|
|
saved = client.post(
|
|
f"/projects/{project_id}/settings",
|
|
json={"name": "HTML5 Ops", "structure_source": "XML_DUMP"},
|
|
)
|
|
assert saved.status_code == 200
|
|
|
|
job = client.post(f"/html5/projects/{project_id}/setup/import-job")
|
|
assert job.status_code == 200
|
|
|
|
page = client.get("/html5/operations")
|
|
assert page.status_code == 200
|
|
assert "text/html" in page.headers["content-type"]
|
|
assert 'data-html5-page="operations"' in page.text
|
|
assert "data-html5-operations-body" in page.text
|
|
assert "data-html5-operations-summary" in page.text
|
|
assert "data-html5-operations-filter" in page.text
|
|
assert "data-html5-operation-detail" in page.text
|
|
assert 'hx-ext="sse"' in page.text
|
|
assert 'sse-connect="/html5/operations/events"' in page.text
|
|
assert 'sse-swap="operations-summary"' in page.text
|
|
assert 'sse-swap="operations-jobs"' in page.text
|
|
assert 'hx-trigger="every 3s"' not in page.text
|
|
assert project_id in page.text
|
|
assert "__next" not in page.text
|
|
|
|
with client.stream("GET", "/html5/operations/events?once=1") as events:
|
|
first_chunk = "".join(events.iter_text())
|
|
assert ": operations heartbeat" in first_chunk
|
|
assert "retry: 5000" in first_chunk
|
|
assert "event: operations-summary" in first_chunk
|
|
assert "event: operations-jobs" in first_chunk
|
|
assert "data-html5-operations-summary" in first_chunk
|
|
assert "data-html5-operation" in first_chunk
|
|
assert project_id in first_chunk
|
|
|
|
rows = client.get("/html5/operations/jobs")
|
|
assert rows.status_code == 200
|
|
assert "text/html" in rows.headers["content-type"]
|
|
assert "data-html5-operation" in rows.text
|
|
assert 'hx-target="[data-html5-operation-detail]"' in rows.text
|
|
assert project_id in rows.text
|
|
assert "<html" not in rows.text
|
|
|
|
operation_job_id = client.get("/operations/jobs", params={"project_id": project_id, "kind": "SERVER_IMPORT"}).json()[0]["job_id"]
|
|
detail = client.get(f"/html5/operations/jobs/{operation_job_id}/detail")
|
|
assert detail.status_code == 200
|
|
assert "text/html" in detail.headers["content-type"]
|
|
assert "data-html5-operation-detail" in detail.text
|
|
assert operation_job_id in detail.text
|
|
assert "SERVER_IMPORT" in detail.text
|
|
assert project_id in detail.text
|
|
assert "<html" not in detail.text
|
|
|
|
summary = client.get("/html5/operations/summary")
|
|
assert summary.status_code == 200
|
|
assert "text/html" in summary.headers["content-type"]
|
|
assert "data-html5-operations-summary" in summary.text
|
|
assert "Всего" in summary.text
|
|
assert "<html" not in summary.text
|
|
|
|
job_status = client.get("/operations/jobs", params={"project_id": project_id, "kind": "SERVER_IMPORT"}).json()[0]["status"]
|
|
filtered = client.get("/html5/operations", params={"project_id": project_id, "status": job_status, "kind": "SERVER_IMPORT"})
|
|
assert filtered.status_code == 200
|
|
assert f'sse-connect="/html5/operations/events?project_id={project_id}&status={job_status}&kind=SERVER_IMPORT"' in filtered.text
|
|
assert f'value="{project_id}"' in filtered.text
|
|
assert f'value="{job_status}"' in filtered.text
|
|
assert 'value="SERVER_IMPORT"' in filtered.text
|
|
assert project_id in filtered.text
|
|
|
|
filtered_rows = client.get("/html5/operations/jobs", params={"project_id": project_id, "status": job_status, "kind": "SERVER_IMPORT"})
|
|
assert filtered_rows.status_code == 200
|
|
assert project_id in filtered_rows.text
|
|
|
|
with client.stream(
|
|
"GET",
|
|
f"/html5/operations/events?once=1&project_id={project_id}&status={job_status}&kind=SERVER_IMPORT",
|
|
) as events:
|
|
filtered_chunk = "".join(events.iter_text())
|
|
assert "event: operations-summary" in filtered_chunk
|
|
assert "event: operations-jobs" in filtered_chunk
|
|
assert project_id in filtered_chunk
|
|
|
|
|
|
def test_project_setup_mock_import_indexes_project():
|
|
client = TestClient(app)
|
|
project_id = f"setup-import-{uuid4()}"
|
|
|
|
initial = client.get(f"/projects/{project_id}/setup")
|
|
assert initial.status_code == 200
|
|
assert initial.json()["status"] == "NOT_CONFIGURED"
|
|
assert "Проект не проиндексирован" in initial.json()["message"]
|
|
|
|
settings = client.post(
|
|
f"/projects/{project_id}/settings",
|
|
json={
|
|
"name": "Demo",
|
|
"structure_source": "XML_DUMP",
|
|
"platform_version": "8.3.24",
|
|
"compatibility_mode": "8.3.20",
|
|
},
|
|
)
|
|
assert settings.status_code == 200
|
|
assert settings.json()["status"] == "IMPORT_REQUIRED"
|
|
assert settings.json()["settings"]["structure_source"] == "XML_DUMP"
|
|
assert settings.json()["settings"]["platform_version"] == "8.3.24"
|
|
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/XML_DUMP",
|
|
json={
|
|
"source": "XML_DUMP",
|
|
"metadata": {"platform_version": "8.3.24", "compatibility_mode": "8.3.20"},
|
|
},
|
|
)
|
|
assert imported.status_code == 200
|
|
payload = imported.json()
|
|
assert payload["status"] == "mock_indexed"
|
|
assert payload["snapshot"]["project_id"] == project_id
|
|
assert payload["object_count"] >= 5
|
|
assert payload["form_count"] >= 2
|
|
assert payload["role_count"] >= 1
|
|
|
|
setup = client.get(f"/projects/{project_id}/setup")
|
|
assert setup.status_code == 200
|
|
assert setup.json()["status"] == "INDEXED"
|
|
assert setup.json()["last_import"]["source_path"]
|
|
assert setup.json()["import_history"][0]["status"] == "mock_indexed"
|
|
|
|
history = client.get(f"/projects/{project_id}/imports/history")
|
|
assert history.status_code == 200
|
|
assert history.json()[0]["source"] == "XML_DUMP"
|
|
|
|
tree = client.get(f"/projects/{project_id}/metadata/tree")
|
|
assert tree.status_code == 200
|
|
assert tree.json()["root"]["kind"] == "PROJECT"
|
|
|
|
reindexed = client.post(f"/projects/{project_id}/reindex")
|
|
assert reindexed.status_code == 200
|
|
assert reindexed.json()["status"] == "reindexed"
|
|
|
|
|
|
def test_windows_agent_import_job_protocol_applies_server_result(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Catalog name="Контрагенты" qualifiedName="Справочник.Контрагенты" />
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"agent-import-{uuid4()}"
|
|
agent_id = f"win-agent-{uuid4()}"
|
|
client.post("/agent/heartbeat", json={"agent_id": agent_id, "hostname": "test-host"})
|
|
|
|
created = client.post(
|
|
f"/projects/{project_id}/imports/EDT_PROJECT/agent-jobs",
|
|
json={
|
|
"agent_id": agent_id,
|
|
"source": "EDT_PROJECT",
|
|
"local_path": r"D:\edt\upo",
|
|
"bin_path": r"C:\Program Files\1cv8\8.3.24.0000\bin\1cv8.exe",
|
|
"mode": "FULL_REPLACE",
|
|
},
|
|
)
|
|
assert created.status_code == 200
|
|
job_id = created.json()["job_id"]
|
|
assert created.json()["status"] == "QUEUED"
|
|
|
|
claimed = client.get("/agent/jobs/next", params={"agent_id": agent_id})
|
|
assert claimed.status_code == 200
|
|
assert claimed.json()["job_id"] == job_id
|
|
assert claimed.json()["status"] == "RUNNING"
|
|
assert claimed.json()["local_path"] == r"D:\edt\upo"
|
|
|
|
completed = client.post(
|
|
f"/agent/jobs/{job_id}/result",
|
|
json={
|
|
"status": "SUCCEEDED",
|
|
"server_path": str(tmp_path),
|
|
"logs": ["EDT exported and uploaded."],
|
|
},
|
|
)
|
|
assert completed.status_code == 200
|
|
deadline = time.monotonic() + 10
|
|
payload = completed.json()
|
|
while time.monotonic() < deadline and payload["status"] == "RUNNING":
|
|
time.sleep(0.05)
|
|
jobs = client.get(f"/projects/{project_id}/imports/agent-jobs")
|
|
assert jobs.status_code == 200
|
|
payload = next(item for item in jobs.json() if item["job_id"] == job_id)
|
|
|
|
assert payload["status"] == "SUCCEEDED"
|
|
assert payload["import_summary"]["status"] == "structure_indexed"
|
|
assert payload["import_summary"]["object_count"] == 1
|
|
|
|
setup = client.get(f"/projects/{project_id}/setup")
|
|
assert setup.status_code == 200
|
|
assert setup.json()["last_import"]["source"] == "EDT_PROJECT"
|
|
|
|
|
|
def test_windows_agent_import_job_accepts_uploaded_zip(tmp_path: Path):
|
|
payload_root = tmp_path / "payload"
|
|
payload_root.mkdir()
|
|
(payload_root / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Catalog name="Номенклатура" qualifiedName="Справочник.Номенклатура" />
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
archive_path = tmp_path / "edt.zip"
|
|
with zipfile.ZipFile(archive_path, "w") as archive:
|
|
archive.write(payload_root / "metadata.xml", "metadata.xml")
|
|
|
|
client = TestClient(app)
|
|
project_id = f"agent-upload-{uuid4()}"
|
|
agent_id = f"win-agent-{uuid4()}"
|
|
client.post("/agent/heartbeat", json={"agent_id": agent_id, "hostname": "test-host"})
|
|
created = client.post(
|
|
f"/projects/{project_id}/imports/EDT_PROJECT/agent-jobs",
|
|
json={"agent_id": agent_id, "source": "EDT_PROJECT", "local_path": r"D:\edt\upo", "mode": "FULL_REPLACE"},
|
|
)
|
|
assert created.status_code == 200
|
|
job_id = created.json()["job_id"]
|
|
|
|
uploaded = client.post(
|
|
f"/agent/jobs/{job_id}/upload",
|
|
params={"filename": "edt.zip"},
|
|
content=archive_path.read_bytes(),
|
|
headers={"Content-Type": "application/octet-stream"},
|
|
)
|
|
assert uploaded.status_code == 200
|
|
assert uploaded.json()["server_path"]
|
|
|
|
deadline = time.monotonic() + 10
|
|
payload = uploaded.json()
|
|
while time.monotonic() < deadline and payload["status"] == "RUNNING":
|
|
time.sleep(0.05)
|
|
jobs = client.get(f"/projects/{project_id}/imports/agent-jobs")
|
|
assert jobs.status_code == 200
|
|
payload = next(item for item in jobs.json() if item["job_id"] == job_id)
|
|
|
|
assert payload["status"] == "SUCCEEDED"
|
|
assert payload["import_summary"]["object_count"] == 1
|
|
|
|
|
|
def test_windows_agent_cf_export_job_accepts_infobase_settings_without_local_path():
|
|
client = TestClient(app)
|
|
project_id = f"agent-cf-{uuid4()}"
|
|
agent_id = f"win-agent-{uuid4()}"
|
|
client.post("/agent/heartbeat", json={"agent_id": agent_id, "hostname": "test-host"})
|
|
|
|
created = client.post(
|
|
f"/projects/{project_id}/imports/CF_FILE/agent-jobs",
|
|
json={
|
|
"agent_id": agent_id,
|
|
"source": "CF_FILE",
|
|
"mode": "FULL_REPLACE",
|
|
"metadata": {
|
|
"one_c_server": "192.168.200.95",
|
|
"one_c_infobase": "upo",
|
|
"one_c_user": "import-user",
|
|
"one_c_password": "secret",
|
|
},
|
|
},
|
|
)
|
|
assert created.status_code == 200
|
|
payload = created.json()
|
|
assert payload["source"] == "CF_FILE"
|
|
assert payload["local_path"] is None
|
|
assert payload["metadata"]["one_c_infobase"] == "upo"
|
|
|
|
|
|
def test_windows_agent_browse_request_protocol():
|
|
client = TestClient(app)
|
|
agent_id = f"win-agent-{uuid4()}"
|
|
|
|
created = client.post("/agent/browse-requests", json={"agent_id": agent_id, "path": r"D:\EDT"})
|
|
assert created.status_code == 200
|
|
request_id = created.json()["request_id"]
|
|
assert created.json()["status"] == "QUEUED"
|
|
|
|
claimed = client.get("/agent/browse/next", params={"agent_id": agent_id})
|
|
assert claimed.status_code == 200
|
|
assert claimed.json()["request_id"] == request_id
|
|
assert claimed.json()["status"] == "RUNNING"
|
|
|
|
completed = client.post(
|
|
f"/agent/browse/{request_id}/result",
|
|
json={
|
|
"status": "SUCCEEDED",
|
|
"parent_path": r"D:\\",
|
|
"entries": [{"name": "UPO", "path": r"D:\EDT\UPO", "is_directory": True}],
|
|
},
|
|
)
|
|
assert completed.status_code == 200
|
|
assert completed.json()["status"] == "SUCCEEDED"
|
|
assert completed.json()["entries"][0]["path"] == r"D:\EDT\UPO"
|
|
|
|
|
|
def test_windows_agent_heartbeat_status():
|
|
client = TestClient(app)
|
|
agent_id = f"win-agent-{uuid4()}"
|
|
|
|
offline = client.get(f"/agent/status/{agent_id}")
|
|
assert offline.status_code == 200
|
|
assert offline.json()["status"] == "offline"
|
|
|
|
heartbeat = client.post(
|
|
"/agent/heartbeat",
|
|
json={
|
|
"agent_id": agent_id,
|
|
"host": "workstation-1",
|
|
"user": "svc-sfera",
|
|
"version": "dev",
|
|
"network_roots": [r"\\server\share"],
|
|
},
|
|
)
|
|
assert heartbeat.status_code == 200
|
|
assert heartbeat.json()["status"] == "online"
|
|
|
|
online = client.get(f"/agent/status/{agent_id}")
|
|
assert online.status_code == 200
|
|
assert online.json()["status"] == "online"
|
|
assert online.json()["host"] == "workstation-1"
|
|
assert online.json()["network_roots"] == [r"\\server\share"]
|
|
|
|
|
|
def test_server_browse_lists_directories(tmp_path: Path):
|
|
(tmp_path / "edt").mkdir()
|
|
(tmp_path / "file.txt").write_text("not a dir", encoding="utf-8")
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/server/browse", params={"path": str(tmp_path)})
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["path"]
|
|
assert payload["parent_path"]
|
|
assert payload["entries"] == [{"name": "edt", "path": (tmp_path / "edt").as_posix(), "is_directory": True}]
|
|
|
|
|
|
def test_server_smb_browse_validates_unc_path():
|
|
client = TestClient(app)
|
|
|
|
response = client.post("/server/smb/browse", json={"path": "192.168.200.200", "username": "user", "password": "secret"})
|
|
|
|
assert response.status_code == 200
|
|
assert "UNC путь" in response.json()["error"]
|
|
|
|
|
|
def test_project_setup_recovers_indexed_status_from_stored_snapshot(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Catalog name="Контрагенты" qualifiedName="Справочник.Контрагенты" />
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"setup-recover-{uuid4()}"
|
|
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/XML_DUMP",
|
|
json={"source": "XML_DUMP", "path": str(tmp_path)},
|
|
)
|
|
assert imported.status_code == 200
|
|
|
|
main._snapshots.pop(project_id, None)
|
|
main._project_setup.pop(project_id, None)
|
|
|
|
recovered = client.get(f"/projects/{project_id}/setup")
|
|
|
|
assert recovered.status_code == 200
|
|
payload = recovered.json()
|
|
assert payload["status"] == "INDEXED"
|
|
assert payload["last_import"]["source"] == "XML_DUMP"
|
|
|
|
|
|
def test_project_create_and_delete_lifecycle():
|
|
client = TestClient(app)
|
|
project_id = f"lifecycle-{uuid4()}"
|
|
|
|
created = client.post("/projects", json={"project_id": project_id, "name": "Lifecycle"})
|
|
assert created.status_code == 200
|
|
assert created.json()["project_id"] == project_id
|
|
assert created.json()["settings"]["name"] == "Lifecycle"
|
|
|
|
duplicate = client.post("/projects", json={"project_id": project_id})
|
|
assert duplicate.status_code == 409
|
|
|
|
wrong_confirmation = client.request("DELETE", f"/projects/{project_id}", json={"confirmation": "wrong"})
|
|
assert wrong_confirmation.status_code == 400
|
|
|
|
deleted = client.request("DELETE", f"/projects/{project_id}", json={"confirmation": project_id})
|
|
assert deleted.status_code == 200
|
|
assert deleted.json()["project_id"] == project_id
|
|
|
|
|
|
def test_project_settings_persist_wizard_sections():
|
|
client = TestClient(app)
|
|
project_id = f"setup-sections-{uuid4()}"
|
|
|
|
settings = client.post(
|
|
f"/projects/{project_id}/settings",
|
|
json={
|
|
"name": "Wizard Sections",
|
|
"configuration_source": "EDT local project",
|
|
"structure_source": "EDT_PROJECT",
|
|
"platform_version": "8.5.1.1150",
|
|
"compatibility_mode": "8.3",
|
|
"extensions": ["UPO_Ext"],
|
|
"environments": {
|
|
"dev": {
|
|
"url": "http://192.168.200.95/upo/ru_RU/",
|
|
"infobase": 'Srvr="192.168.200.95";Ref="upo";',
|
|
"runtime": "mock",
|
|
},
|
|
"test": {"runtime": "remote_worker"},
|
|
},
|
|
"agent": {"mode": "mock", "endpoint": "https://agent.local/snapshot", "allow_snapshots": True},
|
|
"privacy_mode": "LOCAL_ONLY",
|
|
"knowledge_sources": ["wiki", "incidents"],
|
|
"task_session_policy": {
|
|
"mode": "required",
|
|
"default_task_id": "task.default",
|
|
"session_ttl": "8h",
|
|
"audit_privacy": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert settings.status_code == 200
|
|
payload = settings.json()["settings"]
|
|
assert payload["environments"]["dev"]["runtime"] == "mock"
|
|
assert payload["agent"]["allow_snapshots"] is True
|
|
assert payload["knowledge_sources"] == ["wiki", "incidents"]
|
|
assert payload["task_session_policy"]["default_task_id"] == "task.default"
|
|
|
|
|
|
def test_import_source_check_reports_preflight_requirements(tmp_path: Path):
|
|
client = TestClient(app)
|
|
project_id = f"source-check-{uuid4()}"
|
|
xml_file = tmp_path / "metadata.xml"
|
|
xml_file.write_text("<Configuration />", encoding="utf-8")
|
|
|
|
xml_check = client.post(
|
|
f"/projects/{project_id}/imports/XML_DUMP/check",
|
|
json={"source": "XML_DUMP", "path": str(xml_file)},
|
|
)
|
|
assert xml_check.status_code == 200
|
|
assert xml_check.json()["ready"] is True
|
|
assert any(check["code"] == "path" and check["status"] == "OK" for check in xml_check.json()["checks"])
|
|
|
|
live_check = client.post(
|
|
f"/projects/{project_id}/imports/LIVE_INFOBASE/check",
|
|
json={"source": "LIVE_INFOBASE"},
|
|
)
|
|
assert live_check.status_code == 200
|
|
assert live_check.json()["ready"] is False
|
|
assert any(check["code"] == "credentials" and check["status"] == "BLOCKED" for check in live_check.json()["checks"])
|
|
|
|
|
|
def test_runtime_required_import_uses_api_mock_when_adapter_unavailable(monkeypatch):
|
|
monkeypatch.setenv("RUNTIME_ADAPTER_MODE", "mock")
|
|
monkeypatch.setenv("RUNTIME_ADAPTER_URL", "http://127.0.0.1:1")
|
|
client = TestClient(app)
|
|
project_id = f"runtime-import-{uuid4()}"
|
|
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/CF_FILE",
|
|
json={"source": "CF_FILE", "metadata": {"platform_version": "8.3.24"}},
|
|
)
|
|
|
|
assert imported.status_code == 200
|
|
payload = imported.json()
|
|
assert payload["status"] == "mock_indexed"
|
|
assert payload["runtime_mode"] == "mock"
|
|
assert payload["runtime_diagnostics"]
|
|
assert payload["snapshot"]["project_id"] == project_id
|
|
|
|
platform = client.get("/runtime/platform")
|
|
assert platform.status_code == 200
|
|
assert platform.json()["mode"] == "mock"
|
|
assert platform.json()["platform_found"] is False
|
|
|
|
|
|
def test_runtime_required_import_does_not_index_cf_file_directly(monkeypatch, tmp_path: Path):
|
|
monkeypatch.setenv("RUNTIME_ADAPTER_MODE", "mock")
|
|
monkeypatch.setenv("RUNTIME_ADAPTER_URL", "http://127.0.0.1:1")
|
|
cf_file = tmp_path / "demo.cf"
|
|
cf_file.write_text("not xml", encoding="utf-8")
|
|
client = TestClient(app)
|
|
project_id = f"cf-import-{uuid4()}"
|
|
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/CF_FILE",
|
|
json={"source": "CF_FILE", "path": str(cf_file), "metadata": {"platform_version": "8.3.24"}},
|
|
)
|
|
|
|
assert imported.status_code == 200
|
|
payload = imported.json()
|
|
assert payload["status"] == "mock_indexed"
|
|
assert payload["source_path"] != cf_file.as_posix()
|
|
assert payload["normalized_summary"]["object_count"] >= 1
|
|
|
|
|
|
def test_import_supports_structure_only_indexing(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<HTTPService name="ПубличныйAPI" qualifiedName="HTTPСервис.ПубличныйAPI" />
|
|
<Subsystem name="Продажи" qualifiedName="Подсистема.Продажи" />
|
|
<Role name="Менеджер" qualifiedName="Роль.Менеджер">
|
|
<Right object="HTTPСервис.ПубличныйAPI" read="true" />
|
|
</Role>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"structure-only-{uuid4()}"
|
|
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/EDT_PROJECT",
|
|
json={"source": "EDT_PROJECT", "path": str(tmp_path), "structure_only": True},
|
|
)
|
|
|
|
assert imported.status_code == 200
|
|
payload = imported.json()
|
|
assert payload["status"] == "structure_indexed"
|
|
assert payload["snapshot"]["project_id"] == project_id
|
|
assert payload["object_count"] >= 2
|
|
assert payload["normalized_summary"]["group_count"] >= 3
|
|
assert payload["normalized_summary"]["rights_count"] == 1
|
|
|
|
setup = client.get(f"/projects/{project_id}/setup")
|
|
assert setup.status_code == 200
|
|
assert setup.json()["status"] == "STRUCTURE_INDEXED"
|
|
assert "Запустите индексацию" in setup.json()["message"]
|
|
|
|
summary = client.get(f"/projects/{project_id}/normalized/summary")
|
|
assert summary.status_code == 200
|
|
assert summary.json()["rights_count"] == 1
|
|
|
|
quality = client.get(f"/projects/{project_id}/imports/quality")
|
|
assert quality.status_code == 200
|
|
quality_payload = quality.json()
|
|
assert quality_payload["project_id"] == project_id
|
|
assert quality_payload["score"] > 0
|
|
assert any(check["code"] == "rights" and check["passed"] for check in quality_payload["checks"])
|
|
|
|
normalized = client.get(f"/projects/{project_id}/normalized")
|
|
assert normalized.status_code == 200
|
|
groups = {group["name"] for group in normalized.json()["configuration"]["groups"]}
|
|
assert {"HTTP-сервисы", "Подсистемы", "Роли"}.issubset(groups)
|
|
|
|
detail = client.get(
|
|
f"/projects/{project_id}/normalized/object",
|
|
params={"qualified_name": "Роль.Менеджер"},
|
|
)
|
|
assert detail.status_code == 200
|
|
assert detail.json()["group_name"] == "Роли"
|
|
assert detail.json()["object"]["rights"][0]["target"] == "HTTPСервис.ПубличныйAPI"
|
|
|
|
tree = client.get(f"/projects/{project_id}/metadata/tree")
|
|
assert tree.status_code == 200
|
|
root = tree.json()["root"]
|
|
assert root["label"] == "Проект"
|
|
assert [item["label"] for item in root["children"][:4]] == [
|
|
"Основная конфигурация",
|
|
"Расширение: <Имя>",
|
|
"SFERA",
|
|
"Среды",
|
|
]
|
|
main_configuration = root["children"][0]
|
|
common = next(item for item in main_configuration["children"] if item["label"] == "Общие")
|
|
assert any(item["label"] == "HTTP-сервисы" for item in common["children"])
|
|
|
|
|
|
def test_import_full_replace_replaces_current_normalized_project(tmp_path: Path):
|
|
first = tmp_path / "first"
|
|
second = tmp_path / "second"
|
|
first.mkdir()
|
|
second.mkdir()
|
|
(first / "Контрагенты.mdo").write_text(
|
|
"""
|
|
<mdclass:Catalog xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>Контрагенты</name>
|
|
<synonym>Контрагенты</synonym>
|
|
</mdclass:Catalog>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(second / "Номенклатура.mdo").write_text(
|
|
"""
|
|
<mdclass:Catalog xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>Номенклатура</name>
|
|
<synonym>Номенклатура</synonym>
|
|
</mdclass:Catalog>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"full-replace-{uuid4()}"
|
|
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/EDT_PROJECT",
|
|
json={"source": "EDT_PROJECT", "path": str(first), "mode": "FULL_REPLACE"},
|
|
)
|
|
assert imported.status_code == 200
|
|
assert imported.json()["mode"] == "FULL_REPLACE"
|
|
assert imported.json()["applied"] is True
|
|
|
|
replaced = client.post(
|
|
f"/projects/{project_id}/imports/EDT_PROJECT",
|
|
json={"source": "EDT_PROJECT", "path": str(second), "mode": "FULL_REPLACE"},
|
|
)
|
|
assert replaced.status_code == 200
|
|
assert replaced.json()["mode"] == "FULL_REPLACE"
|
|
assert replaced.json()["normalized_summary"]["object_count"] == 1
|
|
|
|
normalized = client.get(f"/projects/{project_id}/normalized")
|
|
assert normalized.status_code == 200
|
|
payload = normalized.json()
|
|
objects = [
|
|
item["qualified_name"]
|
|
for group in payload["configuration"]["groups"]
|
|
for item in group["objects"]
|
|
]
|
|
assert objects == ["Справочник.Номенклатура"]
|
|
|
|
|
|
def test_import_sync_preview_does_not_replace_current_project(tmp_path: Path):
|
|
current = tmp_path / "current"
|
|
incoming = tmp_path / "incoming"
|
|
current.mkdir()
|
|
incoming.mkdir()
|
|
(current / "Контрагенты.mdo").write_text(
|
|
"""
|
|
<mdclass:Catalog xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>Контрагенты</name>
|
|
</mdclass:Catalog>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(incoming / "Номенклатура.mdo").write_text(
|
|
"""
|
|
<mdclass:Catalog xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>Номенклатура</name>
|
|
</mdclass:Catalog>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"sync-preview-{uuid4()}"
|
|
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/EDT_PROJECT",
|
|
json={"source": "EDT_PROJECT", "path": str(current)},
|
|
)
|
|
assert imported.status_code == 200
|
|
|
|
preview = client.post(
|
|
f"/projects/{project_id}/imports/EDT_PROJECT",
|
|
json={"source": "EDT_PROJECT", "path": str(incoming), "mode": "SYNC_PREVIEW"},
|
|
)
|
|
assert preview.status_code == 200
|
|
payload = preview.json()
|
|
assert payload["status"] == "sync_preview"
|
|
assert payload["mode"] == "SYNC_PREVIEW"
|
|
assert payload["applied"] is False
|
|
assert payload["sync_preview"]["added_count"] == 1
|
|
assert payload["sync_preview"]["removed_count"] == 1
|
|
assert {item["change_kind"] for item in payload["sync_preview"]["items"]} == {"ADD", "REMOVE"}
|
|
|
|
normalized = client.get(f"/projects/{project_id}/normalized")
|
|
assert normalized.status_code == 200
|
|
objects = [
|
|
item["qualified_name"]
|
|
for group in normalized.json()["configuration"]["groups"]
|
|
for item in group["objects"]
|
|
]
|
|
assert objects == ["Справочник.Контрагенты"]
|
|
|
|
|
|
def test_import_full_replace_keeps_valid_edt_objects_when_one_xml_is_broken(tmp_path: Path):
|
|
(tmp_path / "valid.mdo").write_text(
|
|
"""
|
|
<mdclass:Catalog xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>Контрагенты</name>
|
|
<synonym>Контрагенты</synonym>
|
|
</mdclass:Catalog>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(tmp_path / "broken.mdo").write_text("<mdclass:Catalog xmlns:mdclass=", encoding="utf-8")
|
|
client = TestClient(app)
|
|
project_id = f"edt-broken-xml-{uuid4()}"
|
|
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/EDT_PROJECT",
|
|
json={"source": "EDT_PROJECT", "path": str(tmp_path), "mode": "FULL_REPLACE"},
|
|
)
|
|
|
|
assert imported.status_code == 200
|
|
payload = imported.json()
|
|
assert payload["status"] == "indexed"
|
|
assert payload["object_count"] == 1
|
|
assert payload["diagnostics_count"] == 1
|
|
assert "XML_PARSE_ERROR" in payload["diagnostics"][0]
|
|
snapshot = client.get(f"/projects/{project_id}/snapshot")
|
|
assert snapshot.status_code == 200
|
|
assert snapshot.json()["diagnostics_count"] == 1
|
|
normalized = client.get(f"/projects/{project_id}/normalized")
|
|
assert normalized.status_code == 200
|
|
assert normalized.json()["configuration"]["groups"][0]["objects"][0]["qualified_name"] == "Справочник.Контрагенты"
|
|
|
|
|
|
def test_import_edt_full_replace_includes_object_bsl_modules_in_normalized_model(tmp_path: Path):
|
|
catalog_dir = tmp_path / "Catalogs" / "Контрагенты"
|
|
module_dir = catalog_dir / "Ext"
|
|
module_dir.mkdir(parents=True)
|
|
(catalog_dir / "Контрагенты.mdo").write_text(
|
|
"""
|
|
<mdclass:Catalog xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>Контрагенты</name>
|
|
</mdclass:Catalog>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(module_dir / "ObjectModule.bsl").write_text(
|
|
"""
|
|
Процедура ПроверитьКонтрагента() Экспорт
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(module_dir / "ManagerModule.bsl").write_text(
|
|
"""
|
|
Процедура Создать() Экспорт
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"edt-modules-{uuid4()}"
|
|
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/EDT_PROJECT",
|
|
json={"source": "EDT_PROJECT", "path": str(tmp_path), "mode": "FULL_REPLACE"},
|
|
)
|
|
|
|
assert imported.status_code == 200
|
|
assert imported.json()["normalized_summary"]["module_count"] == 2
|
|
detail = client.get(
|
|
f"/projects/{project_id}/normalized/object",
|
|
params={"qualified_name": "Справочник.Контрагенты"},
|
|
)
|
|
assert detail.status_code == 200
|
|
modules = detail.json()["object"]["modules"]
|
|
assert [module["module_kind"] for module in modules] == ["MANAGER_MODULE", "OBJECT_MODULE"]
|
|
assert all(module["attributes"]["source_hash"] for module in modules)
|
|
|
|
|
|
def test_import_edt_full_replace_keeps_tabular_section_columns_nested(tmp_path: Path):
|
|
(tmp_path / "ЗаказПокупателя.mdo").write_text(
|
|
"""
|
|
<mdclass:Document xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>ЗаказПокупателя</name>
|
|
<tabularSections>
|
|
<name>Товары</name>
|
|
<attributes>
|
|
<name>Номенклатура</name>
|
|
</attributes>
|
|
<attributes>
|
|
<name>Количество</name>
|
|
</attributes>
|
|
</tabularSections>
|
|
</mdclass:Document>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"edt-tabular-{uuid4()}"
|
|
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/EDT_PROJECT",
|
|
json={"source": "EDT_PROJECT", "path": str(tmp_path), "mode": "FULL_REPLACE"},
|
|
)
|
|
|
|
assert imported.status_code == 200
|
|
assert imported.json()["normalized_summary"]["attribute_count"] == 2
|
|
detail = client.get(
|
|
f"/projects/{project_id}/normalized/object",
|
|
params={"qualified_name": "Документ.ЗаказПокупателя"},
|
|
)
|
|
assert detail.status_code == 200
|
|
document = detail.json()["object"]
|
|
assert document["attributes"] == []
|
|
assert [child["name"] for child in document["tabular_sections"][0]["children"]] == ["Номенклатура", "Количество"]
|
|
|
|
|
|
def test_import_full_replace_preserves_configuration_root_metadata(tmp_path: Path):
|
|
(tmp_path / "configuration.xml").write_text(
|
|
"""
|
|
<Configuration name="УправлениеТорговлей" synonym="Управление торговлей" platformVersion="8.3.24" compatibilityMode="8.3.20">
|
|
<Catalog name="Контрагенты" qualifiedName="Справочник.Контрагенты" />
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"configuration-root-{uuid4()}"
|
|
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/XML_DUMP",
|
|
json={"source": "XML_DUMP", "path": str(tmp_path), "mode": "FULL_REPLACE"},
|
|
)
|
|
|
|
assert imported.status_code == 200
|
|
assert imported.json()["normalized_summary"]["object_count"] == 1
|
|
normalized = client.get(f"/projects/{project_id}/normalized")
|
|
assert normalized.status_code == 200
|
|
configuration = normalized.json()["configuration"]
|
|
assert configuration["name"] == "УправлениеТорговлей"
|
|
assert configuration["metadata"]["synonym"] == "Управление торговлей"
|
|
assert configuration["metadata"]["platformVersion"] == "8.3.24"
|
|
|
|
|
|
def test_project_metadata_tree_uses_catalog_structure(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Catalog name="Контрагенты" qualifiedName="Справочник.Контрагенты">
|
|
<Attribute name="НаименованиеПолное" qualifiedName="Справочник.Контрагенты.НаименованиеПолное" />
|
|
</Catalog>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя" />
|
|
<MetadataObject type="AccumulationRegister">
|
|
<Name>ОстаткиТоваров</Name>
|
|
<QualifiedName>РегистрНакопления.ОстаткиТоваров</QualifiedName>
|
|
</MetadataObject>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"metadata-tree-{uuid4()}"
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get(f"/projects/{project_id}/metadata/tree")
|
|
assert response.status_code == 200
|
|
root = response.json()["root"]
|
|
main_configuration = next(item for item in root["children"] if item["label"] == "Основная конфигурация")
|
|
assert any(item["label"].startswith("Расширение:") for item in root["children"])
|
|
assert any(item["label"] == "SFERA" for item in root["children"])
|
|
assert any(item["label"] == "Среды" for item in root["children"])
|
|
common = next(item for item in main_configuration["children"] if item["label"] == "Общие")
|
|
catalogs = next(item for item in main_configuration["children"] if item["label"] == "Справочники")
|
|
documents = next(item for item in main_configuration["children"] if item["label"] == "Документы")
|
|
accumulation_registers = next(item for item in main_configuration["children"] if item["label"] == "Регистры накопления")
|
|
|
|
assert any(item["label"] == "Общие модули" for item in common["children"])
|
|
assert any(item["label"] == "Общие формы" for item in common["children"])
|
|
assert not any(item["label"] == "Общие формы" for item in main_configuration["children"])
|
|
assert catalogs["children"][0]["qualified_name"] == "Справочник.Контрагенты"
|
|
assert any(item["label"] == "Реквизиты" for item in catalogs["children"][0]["children"])
|
|
assert documents["children"][0]["qualified_name"] == "Документ.ЗаказПокупателя"
|
|
assert accumulation_registers["children"][0]["qualified_name"] == "РегистрНакопления.ОстаткиТоваров"
|
|
|
|
lazy = client.get(f"/projects/{project_id}/metadata/tree", params={"object_limit_per_branch": 0})
|
|
assert lazy.status_code == 200
|
|
lazy_main_configuration = next(item for item in lazy.json()["root"]["children"] if item["label"] == "Основная конфигурация")
|
|
lazy_catalogs = next(item for item in lazy_main_configuration["children"] if item["label"] == "Справочники")
|
|
assert lazy_catalogs["children"] == []
|
|
assert lazy_catalogs["has_more"] is True
|
|
assert lazy_catalogs["count"] == 1
|
|
|
|
children = client.get(
|
|
f"/projects/{project_id}/metadata/tree/children",
|
|
params={"node_id": "branch.CATALOG", "offset": 0, "limit": 1},
|
|
)
|
|
assert children.status_code == 200
|
|
assert children.json()["total"] == 1
|
|
assert children.json()["children"][0]["qualified_name"] == "Справочник.Контрагенты"
|
|
|
|
search = client.get(f"/projects/{project_id}/metadata/tree/search", params={"q": "Контраг", "limit": 5})
|
|
assert search.status_code == 200
|
|
assert search.json()["total"] >= 1
|
|
assert search.json()["results"][0]["qualified_name"] == "Справочник.Контрагенты"
|
|
|
|
path = client.get(
|
|
f"/projects/{project_id}/metadata/tree/path",
|
|
params={"node_id": search.json()["results"][0]["id"]},
|
|
)
|
|
assert path.status_code == 200
|
|
assert path.json()["path"] == ["main-configuration", "branch.CATALOG", search.json()["results"][0]["id"]]
|
|
assert path.json()["steps"][-1] == {
|
|
"parent_id": "branch.CATALOG",
|
|
"child_id": search.json()["results"][0]["id"],
|
|
"offset": 0,
|
|
}
|
|
|
|
|
|
def test_project_metadata_tree_loads_normalized_lazy_children(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Catalog name="Контрагенты" qualifiedName="Справочник.Контрагенты">
|
|
<Attribute name="ИНН" qualifiedName="Справочник.Контрагенты.ИНН" />
|
|
<Form name="ФормаЭлемента" qualifiedName="Справочник.Контрагенты.ФормаЭлемента" />
|
|
</Catalog>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"normalized-tree-{uuid4()}"
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/XML_DUMP",
|
|
json={"source": "XML_DUMP", "path": str(tmp_path), "mode": "FULL_REPLACE"},
|
|
)
|
|
assert imported.status_code == 200
|
|
|
|
lazy = client.get(f"/projects/{project_id}/metadata/tree", params={"object_limit_per_branch": 0})
|
|
assert lazy.status_code == 200
|
|
main_configuration = next(item for item in lazy.json()["root"]["children"] if item["label"] == "Основная конфигурация")
|
|
catalogs = next(item for item in main_configuration["children"] if item["label"] == "Справочники")
|
|
assert catalogs["id"].startswith("normalized.branch.")
|
|
assert catalogs["children"] == []
|
|
assert catalogs["has_more"] is True
|
|
|
|
objects = client.get(
|
|
f"/projects/{project_id}/metadata/tree/children",
|
|
params={"node_id": catalogs["id"], "offset": 0, "limit": 80},
|
|
)
|
|
assert objects.status_code == 200
|
|
catalog = objects.json()["children"][0]
|
|
assert catalog["qualified_name"] == "Справочник.Контрагенты"
|
|
attributes = next(item for item in catalog["children"] if item["label"] == "Реквизиты")
|
|
|
|
parts = client.get(
|
|
f"/projects/{project_id}/metadata/tree/children",
|
|
params={"node_id": attributes["id"], "offset": 0, "limit": 80},
|
|
)
|
|
assert parts.status_code == 200
|
|
assert parts.json()["children"][0]["qualified_name"] == "Справочник.Контрагенты.ИНН"
|
|
|
|
|
|
def test_project_metadata_tree_shows_http_service_documented_structure(tmp_path: Path):
|
|
(tmp_path / "ПубличныйAPI.mdo").write_text(
|
|
"""
|
|
<mdclass:HTTPService xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>ПубличныйAPI</name>
|
|
<rootURL>api</rootURL>
|
|
<urlTemplates>
|
|
<name>Orders</name>
|
|
<template>/orders/{id}</template>
|
|
<methods>
|
|
<httpMethod>GET</httpMethod>
|
|
<handler>ПолучитьЗаказ</handler>
|
|
</methods>
|
|
</urlTemplates>
|
|
</mdclass:HTTPService>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"http-service-tree-{uuid4()}"
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/EDT_PROJECT",
|
|
json={"source": "EDT_PROJECT", "path": str(tmp_path), "mode": "FULL_REPLACE"},
|
|
)
|
|
assert imported.status_code == 200
|
|
|
|
tree = client.get(f"/projects/{project_id}/metadata/tree")
|
|
assert tree.status_code == 200
|
|
main_configuration = next(item for item in tree.json()["root"]["children"] if item["label"] == "Основная конфигурация")
|
|
common = next(item for item in main_configuration["children"] if item["label"] == "Общие")
|
|
http_services = next(item for item in common["children"] if item["label"] == "HTTP-сервисы")
|
|
service = http_services["children"][0]
|
|
|
|
assert service["qualified_name"] == "HTTPСервис.ПубличныйAPI"
|
|
assert [item["label"] for item in service["children"]] == ["Шаблоны URL"]
|
|
templates = client.get(
|
|
f"/projects/{project_id}/metadata/tree/children",
|
|
params={"node_id": service["children"][0]["id"], "offset": 0, "limit": 80},
|
|
)
|
|
assert templates.status_code == 200
|
|
assert templates.json()["children"][0]["label"] == "Orders"
|
|
|
|
|
|
def test_project_metadata_tree_shows_report_documented_structure(tmp_path: Path):
|
|
(tmp_path / "АнализПродаж.mdo").write_text(
|
|
"""
|
|
<mdclass:Report xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>АнализПродаж</name>
|
|
<attributes>
|
|
<name>Период</name>
|
|
</attributes>
|
|
<tabularSections>
|
|
<name>Показатели</name>
|
|
</tabularSections>
|
|
<forms>
|
|
<name>ФормаОтчета</name>
|
|
</forms>
|
|
<templates>
|
|
<name>ПечатнаяФорма</name>
|
|
</templates>
|
|
<tabularDocuments>
|
|
<name>ТабличныйДокумент</name>
|
|
</tabularDocuments>
|
|
<mainDataCompositionSchema>
|
|
<name>ОсновнаяСхемаКомпоновкиДанных</name>
|
|
</mainDataCompositionSchema>
|
|
<reportVariants>
|
|
<name>Основной</name>
|
|
</reportVariants>
|
|
<settings>
|
|
<name>НастройкиПоУмолчанию</name>
|
|
</settings>
|
|
</mdclass:Report>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"report-tree-{uuid4()}"
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/EDT_PROJECT",
|
|
json={"source": "EDT_PROJECT", "path": str(tmp_path), "mode": "FULL_REPLACE"},
|
|
)
|
|
assert imported.status_code == 200
|
|
|
|
tree = client.get(f"/projects/{project_id}/metadata/tree")
|
|
assert tree.status_code == 200
|
|
main_configuration = next(item for item in tree.json()["root"]["children"] if item["label"] == "Основная конфигурация")
|
|
reports = next(item for item in main_configuration["children"] if item["label"] == "Отчеты")
|
|
report = reports["children"][0]
|
|
|
|
assert report["qualified_name"] == "Отчет.АнализПродаж"
|
|
assert [item["label"] for item in report["children"]] == [
|
|
"Реквизиты",
|
|
"Табличные части",
|
|
"Формы",
|
|
"Макеты",
|
|
"Табличные документы",
|
|
"СКД",
|
|
"Варианты отчета",
|
|
"Настройки",
|
|
]
|
|
dcs = next(item for item in report["children"] if item["label"] == "СКД")
|
|
dcs_children = client.get(
|
|
f"/projects/{project_id}/metadata/tree/children",
|
|
params={"node_id": dcs["id"], "offset": 0, "limit": 80},
|
|
)
|
|
assert dcs_children.status_code == 200
|
|
assert dcs_children.json()["children"][0]["label"] == "ОсновнаяСхемаКомпоновкиДанных"
|
|
|
|
|
|
def test_project_metadata_tree_shows_extension_as_configuration_structure(tmp_path: Path):
|
|
(tmp_path / "РасширениеCRM.mdo").write_text(
|
|
"""
|
|
<mdclass:ConfigurationExtension xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass/extension">
|
|
<name>CRM</name>
|
|
<version>1.0</version>
|
|
<catalogs>
|
|
<name>КонтрагентыCRM</name>
|
|
<attributes>
|
|
<name>ВнешнийКод</name>
|
|
</attributes>
|
|
</catalogs>
|
|
<commonModules>
|
|
<name>CRMСервер</name>
|
|
</commonModules>
|
|
</mdclass:ConfigurationExtension>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"extension-tree-{uuid4()}"
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/EDT_PROJECT",
|
|
json={"source": "EDT_PROJECT", "path": str(tmp_path), "mode": "FULL_REPLACE"},
|
|
)
|
|
assert imported.status_code == 200
|
|
assert imported.json()["normalized_summary"]["extension_count"] == 1
|
|
|
|
tree = client.get(f"/projects/{project_id}/metadata/tree")
|
|
assert tree.status_code == 200
|
|
root_children = tree.json()["root"]["children"]
|
|
extension = next(item for item in root_children if item["label"] == "Расширение: CRM")
|
|
assert extension["kind"] == "EXTENSION"
|
|
assert [item["label"] for item in extension["children"][:2]] == ["Сведения (1.0)", "Общие"]
|
|
|
|
common = next(item for item in extension["children"] if item["label"] == "Общие")
|
|
common_modules = next(item for item in common["children"] if item["label"] == "Общие модули")
|
|
assert common_modules["children"][0]["label"] == "CRMСервер"
|
|
|
|
catalogs = next(item for item in extension["children"] if item["label"] == "Справочники")
|
|
catalog = catalogs["children"][0]
|
|
assert catalog["qualified_name"].endswith("Справочник.КонтрагентыCRM")
|
|
attributes = next(item for item in catalog["children"] if item["label"] == "Реквизиты")
|
|
parts = client.get(
|
|
f"/projects/{project_id}/metadata/tree/children",
|
|
params={"node_id": attributes["id"], "offset": 0, "limit": 80},
|
|
)
|
|
assert parts.status_code == 200
|
|
assert parts.json()["children"][0]["label"] == "ВнешнийКод"
|
|
|
|
lazy_tree = client.get(f"/projects/{project_id}/metadata/tree", params={"object_limit_per_branch": 0})
|
|
lazy_extension = next(item for item in lazy_tree.json()["root"]["children"] if item["label"] == "Расширение: CRM")
|
|
lazy_catalogs = next(item for item in lazy_extension["children"] if item["label"] == "Справочники")
|
|
lazy_catalog_children = client.get(
|
|
f"/projects/{project_id}/metadata/tree/children",
|
|
params={"node_id": lazy_catalogs["id"], "offset": 0, "limit": 80},
|
|
)
|
|
assert lazy_catalog_children.status_code == 200
|
|
assert lazy_catalog_children.json()["children"][0]["label"] == "КонтрагентыCRM"
|
|
|
|
|
|
def test_project_metadata_tree_adds_reference_configuration_root():
|
|
client = TestClient(app)
|
|
project_id = f"reference-tree-{uuid4()}"
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/REFERENCE_CONFIGURATION",
|
|
json={
|
|
"source": "REFERENCE_CONFIGURATION",
|
|
"metadata": {"context_description": "Reference baseline for comparison"},
|
|
},
|
|
)
|
|
assert imported.status_code == 200
|
|
|
|
tree = client.get(f"/projects/{project_id}/metadata/tree")
|
|
assert tree.status_code == 200
|
|
root = tree.json()["root"]
|
|
reference = next(item for item in root["children"] if item["kind"] == "REFERENCE_CONFIGURATION")
|
|
assert reference["label"] == "Reference configuration"
|
|
assert [item["label"] for item in reference["children"][:3]] == ["Сведения", "Общие", "Константы"]
|
|
lazy_tree = client.get(f"/projects/{project_id}/metadata/tree", params={"object_limit_per_branch": 0})
|
|
assert lazy_tree.status_code == 200
|
|
lazy_root = lazy_tree.json()["root"]
|
|
lazy_reference = next(item for item in lazy_root["children"] if item["kind"] == "REFERENCE_CONFIGURATION")
|
|
assert lazy_reference["label"] == "Reference configuration"
|
|
|
|
|
|
def test_project_metadata_tree_adds_context_configuration_root():
|
|
client = TestClient(app)
|
|
project_id = f"context-tree-{uuid4()}"
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/CONTEXT_ONLY",
|
|
json={
|
|
"source": "CONTEXT_ONLY",
|
|
"metadata": {"context_description": "Context-only outline"},
|
|
},
|
|
)
|
|
assert imported.status_code == 200
|
|
|
|
tree = client.get(f"/projects/{project_id}/metadata/tree")
|
|
assert tree.status_code == 200
|
|
root = tree.json()["root"]
|
|
context_root = next(item for item in root["children"] if item["kind"] == "CONTEXT_CONFIGURATION")
|
|
assert context_root["label"] == "Context-only configuration"
|
|
assert [item["label"] for item in context_root["children"][:3]] == ["Сведения", "Общие", "Константы"]
|
|
lazy_tree = client.get(f"/projects/{project_id}/metadata/tree", params={"object_limit_per_branch": 0})
|
|
assert lazy_tree.status_code == 200
|
|
lazy_root = lazy_tree.json()["root"]
|
|
lazy_context_root = next(item for item in lazy_root["children"] if item["kind"] == "CONTEXT_CONFIGURATION")
|
|
assert lazy_context_root["label"] == "Context-only configuration"
|
|
|
|
|
|
def test_project_metadata_tree_does_not_add_context_or_reference_roots_for_regular_sources():
|
|
client = TestClient(app)
|
|
project_id = f"regular-tree-{uuid4()}"
|
|
imported = client.post(
|
|
f"/projects/{project_id}/imports/XML_DUMP",
|
|
json={
|
|
"source": "XML_DUMP",
|
|
"metadata": {"platform_version": "8.3.24", "compatibility_mode": "8.3.20"},
|
|
},
|
|
)
|
|
assert imported.status_code == 200
|
|
|
|
tree = client.get(f"/projects/{project_id}/metadata/tree")
|
|
assert tree.status_code == 200
|
|
root = tree.json()["root"]
|
|
kinds = {item["kind"] for item in root["children"]}
|
|
assert "CONTEXT_CONFIGURATION" not in kinds
|
|
assert "REFERENCE_CONFIGURATION" not in kinds
|
|
lazy_tree = client.get(f"/projects/{project_id}/metadata/tree", params={"object_limit_per_branch": 0})
|
|
assert lazy_tree.status_code == 200
|
|
lazy_root = lazy_tree.json()["root"]
|
|
lazy_kinds = {item["kind"] for item in lazy_root["children"]}
|
|
assert "CONTEXT_CONFIGURATION" not in lazy_kinds
|
|
assert "REFERENCE_CONFIGURATION" not in lazy_kinds
|
|
|
|
|
|
def test_project_metadata_tree_places_conditional_configuration_roots_before_sfera():
|
|
client = TestClient(app)
|
|
|
|
context_project = f"context-order-{uuid4()}"
|
|
context_import = client.post(
|
|
f"/projects/{context_project}/imports/CONTEXT_ONLY",
|
|
json={"source": "CONTEXT_ONLY", "metadata": {"context_description": "Context order check"}},
|
|
)
|
|
assert context_import.status_code == 200
|
|
context_tree = client.get(f"/projects/{context_project}/metadata/tree")
|
|
assert context_tree.status_code == 200
|
|
context_children = context_tree.json()["root"]["children"]
|
|
context_index = next(i for i, item in enumerate(context_children) if item["kind"] == "CONTEXT_CONFIGURATION")
|
|
context_sfera_index = next(i for i, item in enumerate(context_children) if item["label"] == "SFERA")
|
|
assert context_index < context_sfera_index
|
|
context_lazy_tree = client.get(f"/projects/{context_project}/metadata/tree", params={"object_limit_per_branch": 0})
|
|
assert context_lazy_tree.status_code == 200
|
|
context_lazy_children = context_lazy_tree.json()["root"]["children"]
|
|
context_lazy_index = next(i for i, item in enumerate(context_lazy_children) if item["kind"] == "CONTEXT_CONFIGURATION")
|
|
context_lazy_sfera_index = next(i for i, item in enumerate(context_lazy_children) if item["label"] == "SFERA")
|
|
assert context_lazy_index < context_lazy_sfera_index
|
|
|
|
reference_project = f"reference-order-{uuid4()}"
|
|
reference_import = client.post(
|
|
f"/projects/{reference_project}/imports/REFERENCE_CONFIGURATION",
|
|
json={"source": "REFERENCE_CONFIGURATION", "metadata": {"context_description": "Reference order check"}},
|
|
)
|
|
assert reference_import.status_code == 200
|
|
reference_tree = client.get(f"/projects/{reference_project}/metadata/tree")
|
|
assert reference_tree.status_code == 200
|
|
reference_children = reference_tree.json()["root"]["children"]
|
|
reference_index = next(i for i, item in enumerate(reference_children) if item["kind"] == "REFERENCE_CONFIGURATION")
|
|
reference_sfera_index = next(i for i, item in enumerate(reference_children) if item["label"] == "SFERA")
|
|
assert reference_index < reference_sfera_index
|
|
reference_lazy_tree = client.get(f"/projects/{reference_project}/metadata/tree", params={"object_limit_per_branch": 0})
|
|
assert reference_lazy_tree.status_code == 200
|
|
reference_lazy_children = reference_lazy_tree.json()["root"]["children"]
|
|
reference_lazy_index = next(i for i, item in enumerate(reference_lazy_children) if item["kind"] == "REFERENCE_CONFIGURATION")
|
|
reference_lazy_sfera_index = next(i for i, item in enumerate(reference_lazy_children) if item["label"] == "SFERA")
|
|
assert reference_lazy_index < reference_lazy_sfera_index
|
|
|
|
|
|
def test_normalized_object_modules_returns_linked_bsl_source(tmp_path: Path):
|
|
catalog_dir = tmp_path / "Catalogs" / "Контрагенты"
|
|
catalog_dir.mkdir(parents=True)
|
|
(catalog_dir / "Контрагенты.mdo").write_text(
|
|
"""
|
|
<mdclass:Catalog xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>Контрагенты</name>
|
|
</mdclass:Catalog>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(catalog_dir / "ObjectModule.bsl").write_text(
|
|
"""
|
|
Процедура ПередЗаписью(Отказ)
|
|
ПроверитьКонтрагента();
|
|
Отказ = Ложь;
|
|
КонецПроцедуры
|
|
|
|
Процедура ПроверитьКонтрагента()
|
|
Запрос = Новый Запрос("ВЫБРАТЬ Контрагенты.Ссылка ИЗ Справочник.Контрагенты КАК Контрагенты");
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"object-modules-{uuid4()}"
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
|
|
modules = client.get(
|
|
f"/projects/{project_id}/normalized/object/modules",
|
|
params={"qualified_name": "Справочник.Контрагенты"},
|
|
)
|
|
|
|
assert modules.status_code == 200
|
|
payload = modules.json()
|
|
assert payload[0]["module_role"] == "OBJECT_MODULE"
|
|
assert "ПередЗаписью" in payload[0]["source_text"]
|
|
assert payload[0]["routines_count"] == 2
|
|
assert payload[0]["routines"][0]["name"] == "ПередЗаписью"
|
|
assert payload[0]["routines"][0]["kind"] == "PROCEDURE"
|
|
assert payload[0]["routines"][0]["calls"] == ["ObjectModule.ПроверитьКонтрагента"]
|
|
assert payload[0]["routines"][1]["queries_count"] == 1
|
|
assert "Справочник.Контрагенты" in payload[0]["routines"][1]["queries"][0]
|
|
assert payload[0]["routines"][1]["impact_level"] == "MEDIUM"
|
|
assert "reads query tables" in payload[0]["routines"][1]["impact_reasons"]
|
|
|
|
tree = client.get(f"/projects/{project_id}/metadata/tree")
|
|
assert tree.status_code == 200
|
|
main_configuration = next(item for item in tree.json()["root"]["children"] if item["label"] == "Основная конфигурация")
|
|
catalogs = next(item for item in main_configuration["children"] if item["label"] == "Справочники")
|
|
catalog = catalogs["children"][0]
|
|
object_module_group = next(item for item in catalog["children"] if item["label"] == "Модуль объекта")
|
|
module_children = client.get(
|
|
f"/projects/{project_id}/metadata/tree/children",
|
|
params={"node_id": object_module_group["id"], "offset": 0, "limit": 80},
|
|
)
|
|
assert module_children.status_code == 200
|
|
assert module_children.json()["children"][0]["kind"] == "MODULE"
|
|
|
|
module_qname = module_children.json()["children"][0]["qualified_name"]
|
|
selected_module = client.get(
|
|
f"/projects/{project_id}/normalized/object/modules",
|
|
params={"qualified_name": module_qname},
|
|
)
|
|
assert selected_module.status_code == 200
|
|
assert selected_module.json()[0]["source_text"] == payload[0]["source_text"]
|
|
|
|
selected_normalized_module = client.get(
|
|
f"/projects/{project_id}/normalized/object/modules",
|
|
params={"qualified_name": "Справочник.Контрагенты.МодульОбъекта"},
|
|
)
|
|
assert selected_normalized_module.status_code == 200
|
|
assert selected_normalized_module.json()[0]["source_text"] == payload[0]["source_text"]
|
|
|
|
selected_routine = client.get(
|
|
f"/projects/{project_id}/normalized/object/modules",
|
|
params={"qualified_name": "ПередЗаписью"},
|
|
)
|
|
assert selected_routine.status_code == 200
|
|
assert selected_routine.json()[0]["source_text"] == payload[0]["source_text"]
|
|
|
|
|
|
def test_normalized_object_modules_resolves_common_module_code(tmp_path: Path):
|
|
common_module_dir = tmp_path / "CommonModules" / "CRMСервер"
|
|
common_module_dir.mkdir(parents=True)
|
|
(common_module_dir / "CRMСервер.mdo").write_text(
|
|
"""
|
|
<mdclass:CommonModule xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>CRMСервер</name>
|
|
</mdclass:CommonModule>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(common_module_dir / "Module.bsl").write_text(
|
|
"""
|
|
Процедура ЗагрузитьКонтрагентов() Экспорт
|
|
Сообщить("Готово");
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"common-module-code-{uuid4()}"
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
|
|
common_module = client.get(
|
|
f"/projects/{project_id}/normalized/object/modules",
|
|
params={"qualified_name": "ОбщийМодуль.CRMСервер"},
|
|
)
|
|
assert common_module.status_code == 200
|
|
payload = common_module.json()
|
|
assert payload[0]["module_role"] == "MODULE"
|
|
assert "ЗагрузитьКонтрагентов" in payload[0]["source_text"]
|
|
|
|
normalized_module_node = client.get(
|
|
f"/projects/{project_id}/normalized/object/modules",
|
|
params={"qualified_name": "ОбщийМодуль.CRMСервер.Модуль"},
|
|
)
|
|
assert normalized_module_node.status_code == 200
|
|
assert normalized_module_node.json()[0]["source_text"] == payload[0]["source_text"]
|
|
|
|
|
|
def test_normalized_object_modules_reads_canonical_project_before_snapshot():
|
|
client = TestClient(app)
|
|
project_id = f"normalized-source-{uuid4()}"
|
|
source_text = "Процедура Выполнить() Экспорт\n Сообщить(\"ok\");\nКонецПроцедуры\n"
|
|
normalized = NormalizedProject(
|
|
project_id=project_id,
|
|
configuration=ConfigurationRoot(
|
|
groups=[
|
|
MetadataGroup(
|
|
name="Общие модули",
|
|
object_kinds=["COMMON_MODULE"],
|
|
objects=[
|
|
MetadataObject(
|
|
name="CRMСервер",
|
|
qualified_name="ОбщийМодуль.CRMСервер",
|
|
object_kind="COMMON_MODULE",
|
|
modules=[
|
|
Module(
|
|
name="Module",
|
|
qualified_name="ОбщийМодуль.CRMСервер.Модуль",
|
|
source_path="/edt/CommonModules/CRMСервер/Module.bsl",
|
|
module_kind="MODULE",
|
|
attributes={"source_text": source_text, "module_role": "MODULE"},
|
|
)
|
|
],
|
|
)
|
|
],
|
|
)
|
|
]
|
|
),
|
|
source_path="/edt",
|
|
)
|
|
main._save_normalized_project(project_id, normalized)
|
|
|
|
by_object = client.get(
|
|
f"/projects/{project_id}/normalized/object/modules",
|
|
params={"qualified_name": "ОбщийМодуль.CRMСервер"},
|
|
)
|
|
assert by_object.status_code == 200
|
|
assert by_object.json()[0]["source_text"] == source_text
|
|
assert by_object.json()[0]["routines"][0]["name"] == "Выполнить"
|
|
assert by_object.json()[0]["routines"][0]["export"] is True
|
|
|
|
by_module = client.get(
|
|
f"/projects/{project_id}/normalized/object/modules",
|
|
params={"qualified_name": "ОбщийМодуль.CRMСервер.Модуль"},
|
|
)
|
|
assert by_module.status_code == 200
|
|
assert by_module.json()[0]["source_text"] == source_text
|
|
|
|
|
|
def test_bsl_completions_returns_only_exported_common_module_routines():
|
|
client = TestClient(app)
|
|
project_id = f"bsl-completion-{uuid4()}"
|
|
source_text = "\n".join([
|
|
"Процедура Выполнить() Экспорт",
|
|
"КонецПроцедуры",
|
|
"",
|
|
"Функция ВнутреннийРасчет()",
|
|
" Возврат 1;",
|
|
"КонецФункции",
|
|
"",
|
|
])
|
|
normalized = NormalizedProject(
|
|
project_id=project_id,
|
|
configuration=ConfigurationRoot(
|
|
groups=[
|
|
MetadataGroup(
|
|
name="Общие модули",
|
|
object_kinds=["COMMON_MODULE"],
|
|
objects=[
|
|
MetadataObject(
|
|
name="CRMСервер",
|
|
qualified_name="ОбщийМодуль.CRMСервер",
|
|
object_kind="COMMON_MODULE",
|
|
modules=[
|
|
Module(
|
|
name="Module",
|
|
qualified_name="ОбщийМодуль.CRMСервер.Модуль",
|
|
source_path="/edt/CommonModules/CRMСервер/Module.bsl",
|
|
module_kind="MODULE",
|
|
attributes={"source_text": source_text},
|
|
)
|
|
],
|
|
)
|
|
],
|
|
)
|
|
]
|
|
),
|
|
)
|
|
main._save_normalized_project(project_id, normalized)
|
|
|
|
response = client.get(
|
|
f"/projects/{project_id}/bsl/completions",
|
|
params={"receiver": "CRMСервер"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
labels = {item["label"] for item in response.json()}
|
|
assert "Выполнить" in labels
|
|
assert "ВнутреннийРасчет" not in labels
|
|
|
|
|
|
def test_project_flowchart_returns_overview_and_focus(tmp_path: Path):
|
|
catalog_dir = tmp_path / "Catalogs" / "Clients"
|
|
catalog_dir.mkdir(parents=True)
|
|
(catalog_dir / "Clients.mdo").write_text(
|
|
"""
|
|
<mdclass:Catalog xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>Clients</name>
|
|
<attributes name="Code" />
|
|
</mdclass:Catalog>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(catalog_dir / "ObjectModule.bsl").write_text(
|
|
"Процедура ПередЗаписью(Отказ)\n Отказ = Ложь;\nКонецПроцедуры\n",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"flowchart-{uuid4()}"
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
|
|
overview = client.get(f"/projects/{project_id}/flowchart")
|
|
assert overview.status_code == 200
|
|
overview_payload = overview.json()
|
|
assert overview_payload["mode"] == "overview"
|
|
assert any(node["kind"] == "CATALOG" and node["count"] == 1 for node in overview_payload["nodes"])
|
|
assert all(node["kind"] not in {"FORM", "ATTRIBUTE", "TABULAR_SECTION"} for node in overview_payload["nodes"])
|
|
|
|
focus = client.get(
|
|
f"/projects/{project_id}/flowchart",
|
|
params={"focus": "Справочник.Clients", "depth": 2, "limit": 50},
|
|
)
|
|
assert focus.status_code == 200
|
|
focus_payload = focus.json()
|
|
assert focus_payload["mode"] == "focus"
|
|
assert any(node["qualified_name"] == "Справочник.Clients" for node in focus_payload["nodes"])
|
|
assert all(node["kind"] not in {"FORM", "ATTRIBUTE", "TABULAR_SECTION"} for node in focus_payload["nodes"])
|
|
assert focus_payload["edges"] == []
|
|
|
|
|
|
def test_project_flowchart_collapses_module_logic_to_object_links(tmp_path: Path):
|
|
catalog_dir = tmp_path / "Catalogs" / "Clients"
|
|
document_dir = tmp_path / "Documents" / "Order"
|
|
catalog_dir.mkdir(parents=True)
|
|
document_dir.mkdir(parents=True)
|
|
(catalog_dir / "Clients.mdo").write_text(
|
|
"""
|
|
<mdclass:Catalog xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>Clients</name>
|
|
</mdclass:Catalog>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(document_dir / "Order.mdo").write_text(
|
|
"""
|
|
<mdclass:Document xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
|
<name>Order</name>
|
|
</mdclass:Document>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(document_dir / "ObjectModule.bsl").write_text(
|
|
"""
|
|
Процедура Проведение(Отказ)
|
|
Запрос = Новый Запрос("ВЫБРАТЬ Clients.Ссылка ИЗ Справочник.Clients КАК Clients");
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"flowchart-logic-{uuid4()}"
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
|
|
focus = client.get(
|
|
f"/projects/{project_id}/flowchart",
|
|
params={"focus": "Документ.Order", "depth": 2, "limit": 50},
|
|
)
|
|
assert focus.status_code == 200
|
|
payload = focus.json()
|
|
assert any(node["qualified_name"] == "Документ.Order" for node in payload["nodes"])
|
|
assert any(node["qualified_name"] == "Справочник.Clients" for node in payload["nodes"])
|
|
assert any(edge["kind"] == "READS_TABLE" for edge in payload["edges"])
|
|
|
|
|
|
def test_index_project_and_query_impact(tmp_path: Path):
|
|
module = tmp_path / "demo_module.bsl"
|
|
module.write_text(
|
|
"""
|
|
Процедура Проведение()
|
|
ПроверитьОстатки();
|
|
Движения.ОстаткиТоваров.Записать();
|
|
КонецПроцедуры
|
|
|
|
Процедура ПроверитьОстатки()
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-api"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
assert indexed.json()["snapshot"]["project_id"] == "demo-api"
|
|
assert client.get("/storage").json()["status"] == "configured"
|
|
assert any(item["project_id"] == "demo-api" for item in client.get("/storage/snapshots").json())
|
|
|
|
loaded = client.post("/projects/demo-api/load")
|
|
assert loaded.status_code == 200
|
|
assert loaded.json()["snapshot"]["project_id"] == "demo-api"
|
|
|
|
snapshot = client.get("/projects/demo-api/snapshot")
|
|
assert snapshot.status_code == 200
|
|
assert snapshot.json()["node_count"] >= 3
|
|
exported = client.get("/projects/demo-api/snapshot/export")
|
|
assert exported.status_code == 200
|
|
assert exported.json()["project_id"] == "demo-api"
|
|
|
|
impact = client.get("/projects/demo-api/impact/Проведение")
|
|
assert impact.status_code == 200
|
|
payload = impact.json()
|
|
assert [node["name"] for node in payload["callees"]] == ["ПроверитьОстатки"]
|
|
assert [node["name"] for node in payload["writes"]] == ["ОстаткиТоваров"]
|
|
|
|
search = client.get("/projects/demo-api/search", params={"q": "Пров", "kind": "PROCEDURE"})
|
|
assert search.status_code == 200
|
|
assert search.json()["results"][0]["name"] == "Проведение"
|
|
routine_lineage = search.json()["results"][0]["lineage_id"]
|
|
versions = client.get("/projects/demo-api/versions")
|
|
assert versions.status_code == 200
|
|
assert any(item["lineage_id"] == routine_lineage for item in versions.json())
|
|
lineage_history = client.get(f"/versions/{routine_lineage}")
|
|
assert lineage_history.status_code == 200
|
|
assert lineage_history.json()[0]["lineage_id"] == routine_lineage
|
|
|
|
usage = client.get("/projects/demo-api/tables/usage", params={"table": "ОстаткиТоваров"})
|
|
assert usage.status_code == 200
|
|
assert usage.json()[0]["writers"][0]["name"] == "Проведение"
|
|
|
|
writes = client.get(
|
|
"/projects/demo-api/transactions/writes",
|
|
params={"target": "ОстаткиТоваров"},
|
|
)
|
|
assert writes.status_code == 200
|
|
assert writes.json()[0]["routine"]["name"] == "Проведение"
|
|
|
|
module.write_text(
|
|
"""
|
|
Процедура Проведение()
|
|
ПроверитьОстатки();
|
|
КонецПроцедуры
|
|
|
|
Процедура ПроверитьОстатки()
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
incremental = client.post(
|
|
"/projects/demo-api/incremental/file",
|
|
json={"path": str(module)},
|
|
)
|
|
assert incremental.status_code == 200
|
|
assert incremental.json()["removed_edges"] >= 1
|
|
assert client.get(
|
|
"/projects/demo-api/transactions/writes",
|
|
params={"target": "ОстаткиТоваров"},
|
|
).json() == []
|
|
|
|
signal = client.post(
|
|
"/projects/demo-api/runtime/signals",
|
|
json={
|
|
"signal": {
|
|
"signal_id": "signal.1",
|
|
"lineage_id": routine_lineage,
|
|
"kind": "ERROR",
|
|
"duration_ms": 50.0,
|
|
}
|
|
},
|
|
)
|
|
assert signal.status_code == 200
|
|
|
|
runtime = client.get("/projects/demo-api/runtime/summary")
|
|
assert runtime.status_code == 200
|
|
assert runtime.json()[0]["node"]["name"] == "Проведение"
|
|
assert runtime.json()[0]["error_count"] == 1
|
|
|
|
knowledge = client.post(
|
|
"/knowledge",
|
|
json={
|
|
"record_id": "knowledge.demo",
|
|
"scope": "PROJECT",
|
|
"title": "Проведение документов",
|
|
"body": "Правила проведения заказа.",
|
|
"related_lineages": [routine_lineage],
|
|
},
|
|
)
|
|
assert knowledge.status_code == 200
|
|
search_knowledge = client.get("/knowledge/search", params={"q": "заказа"})
|
|
assert search_knowledge.status_code == 200
|
|
assert any(item["record_id"] == "knowledge.demo" for item in search_knowledge.json()["results"])
|
|
|
|
coverage = client.get("/projects/demo-api/knowledge/coverage")
|
|
assert coverage.status_code == 200
|
|
assert any(item["record_count"] == 1 for item in coverage.json())
|
|
|
|
|
|
def test_project_symbol_navigation_endpoints(tmp_path: Path):
|
|
module = tmp_path / "demo_module.bsl"
|
|
module.write_text(
|
|
"""
|
|
Процедура Проведение()
|
|
ПроверитьОстатки();
|
|
КонецПроцедуры
|
|
|
|
Процедура ПроверитьОстатки()
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"symbols-api-{uuid4()}"
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
|
|
symbols = client.get(f"/projects/{project_id}/symbols", params={"q": "Проверить", "kind": "PROCEDURE"})
|
|
assert symbols.status_code == 200
|
|
symbol_payload = symbols.json()
|
|
assert symbol_payload[0]["node"]["name"] == "ПроверитьОстатки"
|
|
assert symbol_payload[0]["source"]["source_path"].endswith("demo_module.bsl")
|
|
lineage_id = symbol_payload[0]["node"]["lineage_id"]
|
|
|
|
definition = client.get(f"/projects/{project_id}/symbols/definition", params={"lineage_id": lineage_id})
|
|
assert definition.status_code == 200
|
|
assert definition.json()["node"]["qualified_name"] == "demo_module.ПроверитьОстатки"
|
|
assert definition.json()["source"]["line_start"] is not None
|
|
|
|
references = client.get(f"/projects/{project_id}/symbols/references", params={"lineage_id": lineage_id})
|
|
assert references.status_code == 200
|
|
reference_payload = references.json()
|
|
assert reference_payload["symbol"]["node"]["name"] == "ПроверитьОстатки"
|
|
assert any(
|
|
reference["kind"] == "CALLS"
|
|
and reference["source"]["name"] == "Проведение"
|
|
and reference["direction"] == "incoming"
|
|
for reference in reference_payload["references"]
|
|
)
|
|
|
|
|
|
def test_persisted_project_endpoints_load_snapshot_after_memory_clear(tmp_path: Path):
|
|
project_id = f"persisted-api-{uuid4()}"
|
|
module = tmp_path / "persisted_module.bsl"
|
|
module.write_text(
|
|
"""
|
|
Процедура Проведение()
|
|
ПроверитьОстатки();
|
|
Движения.ОстаткиТоваров.Записать();
|
|
КонецПроцедуры
|
|
|
|
Процедура ПроверитьОстатки()
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
main._snapshots.pop(project_id, None)
|
|
main._graphs.pop(project_id, None)
|
|
|
|
assert client.get(f"/projects/{project_id}/snapshot").status_code == 200
|
|
search = client.get(f"/projects/{project_id}/search", params={"q": "Пров", "kind": "PROCEDURE"})
|
|
assert search.status_code == 200
|
|
assert search.json()["results"][0]["name"] == "Проведение"
|
|
assert client.get(f"/projects/{project_id}/review").status_code == 200
|
|
report = client.get(f"/projects/{project_id}/report")
|
|
assert report.status_code == 200
|
|
assert report.json()["project_id"] == project_id
|
|
|
|
|
|
def test_xml_ui_forms_endpoint(tmp_path: Path):
|
|
(tmp_path / "form.xml").write_text(
|
|
"""
|
|
<Form name="ФормаДокумента" qualifiedName="Документ.Заказ.ФормаДокумента">
|
|
<Command name="Провести" qualifiedName="Документ.Заказ.ФормаДокумента.Провести" action="ПровестиКоманда" />
|
|
</Form>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(tmp_path / "form_module.bsl").write_text(
|
|
"Процедура ПровестиКоманда()\nКонецПроцедуры\n",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-api-ui"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
forms = client.get("/projects/demo-api-ui/ui/forms")
|
|
assert forms.status_code == 200
|
|
assert forms.json()[0]["commands"][0]["name"] == "Провести"
|
|
handlers = forms.json()[0]["command_handlers"]
|
|
assert next(iter(handlers.values()))["name"] == "ПровестиКоманда"
|
|
|
|
|
|
def test_object_ui_endpoint_filters_forms_by_1c_object(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<Attribute name="Контрагент" qualifiedName="Документ.ЗаказПокупателя.Контрагент" />
|
|
<TabularSection name="Товары" qualifiedName="Документ.ЗаказПокупателя.Товары">
|
|
<Attribute name="Номенклатура" qualifiedName="Документ.ЗаказПокупателя.Товары.Номенклатура" />
|
|
</TabularSection>
|
|
<Form name="ФормаДокумента" qualifiedName="Документ.ЗаказПокупателя.ФормаДокумента">
|
|
<Command name="Провести" qualifiedName="Документ.ЗаказПокупателя.ФормаДокумента.Провести" action="ПровестиКоманда" />
|
|
</Form>
|
|
</Document>
|
|
<Document name="СчетПокупателю" qualifiedName="Документ.СчетПокупателю">
|
|
<Form name="ФормаСчета" qualifiedName="Документ.СчетПокупателю.ФормаСчета" />
|
|
</Document>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
module = tmp_path / "Documents" / "ЗаказПокупателя" / "Ext" / "ObjectModule.bsl"
|
|
module.parent.mkdir(parents=True)
|
|
module.write_text("Процедура ПровестиКоманда()\nКонецПроцедуры\n", encoding="utf-8")
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-api-object-ui"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get("/projects/demo-api-object-ui/objects/ui/Документ.ЗаказПокупателя")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["object"]["name"] == "ЗаказПокупателя"
|
|
assert [form["form"]["name"] for form in payload["forms"]] == ["ФормаДокумента"]
|
|
assert payload["forms"][0]["commands"][0]["name"] == "Провести"
|
|
assert next(iter(payload["forms"][0]["command_handlers"].values()))["name"] == "ПровестиКоманда"
|
|
|
|
|
|
def test_object_impact_endpoint(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<Attribute name="Контрагент" qualifiedName="Документ.ЗаказПокупателя.Контрагент" />
|
|
<TabularSection name="Товары" qualifiedName="Документ.ЗаказПокупателя.Товары">
|
|
<Attribute name="Номенклатура" qualifiedName="Документ.ЗаказПокупателя.Товары.Номенклатура" />
|
|
</TabularSection>
|
|
<Form name="ФормаДокумента" qualifiedName="Документ.ЗаказПокупателя.ФормаДокумента">
|
|
<Command name="Провести" qualifiedName="Документ.ЗаказПокупателя.ФормаДокумента.Провести" action="ПровестиКоманда" />
|
|
</Form>
|
|
</Document>
|
|
<Role name="Менеджер" qualifiedName="Роль.Менеджер">
|
|
<Right object="Документ.ЗаказПокупателя" read="true" write="true" post="true" />
|
|
</Role>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
module = tmp_path / "Documents" / "ЗаказПокупателя" / "Ext" / "ObjectModule.bsl"
|
|
module.parent.mkdir(parents=True)
|
|
module.write_text(
|
|
"""
|
|
Процедура Проведение()
|
|
Движения.ОстаткиТоваров.Записать();
|
|
КонецПроцедуры
|
|
|
|
Процедура ПровестиКоманда()
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-object-impact"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get(
|
|
"/projects/demo-object-impact/objects/impact/Документ.ЗаказПокупателя"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["object"]["name"] == "ЗаказПокупателя"
|
|
assert payload["modules"][0]["name"] == "ObjectModule"
|
|
assert [routine["name"] for routine in payload["routines"]] == ["ПровестиКоманда", "Проведение"]
|
|
assert payload["forms"][0]["name"] == "ФормаДокумента"
|
|
assert payload["commands"][0]["name"] == "Провести"
|
|
assert payload["attributes"][0]["name"] == "Контрагент"
|
|
assert payload["tabular_sections"][0]["name"] == "Товары"
|
|
assert next(iter(payload["tabular_section_columns"].values()))[0]["name"] == "Номенклатура"
|
|
assert payload["roles"][0]["name"] == "Менеджер"
|
|
assert payload["role_access"][0]["permissions"]["post"] == "true"
|
|
assert payload["writes"][0]["name"] == "ОстаткиТоваров"
|
|
|
|
object_access = client.get(
|
|
"/projects/demo-object-impact/access/objects/Документ.ЗаказПокупателя/roles"
|
|
)
|
|
assert object_access.status_code == 200
|
|
assert object_access.json()["grants"][0]["role"]["name"] == "Менеджер"
|
|
assert object_access.json()["grants"][0]["permissions"]["write"] == "true"
|
|
|
|
role_access = client.get("/projects/demo-object-impact/access/roles/Роль.Менеджер/objects")
|
|
assert role_access.status_code == 200
|
|
assert role_access.json()["objects"][0]["qualified_name"] == "Документ.ЗаказПокупателя"
|
|
assert role_access.json()["grants"][0]["permissions"]["post"] == "true"
|
|
|
|
|
|
def test_object_attributes_endpoint(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<Attribute name="Контрагент" qualifiedName="Документ.ЗаказПокупателя.Контрагент" />
|
|
<Attribute name="СуммаДокумента" qualifiedName="Документ.ЗаказПокупателя.СуммаДокумента" />
|
|
</Document>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-object-attributes"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get("/projects/demo-object-attributes/objects/attributes/Документ.ЗаказПокупателя")
|
|
|
|
assert response.status_code == 200
|
|
assert [row["name"] for row in response.json()["results"]] == ["Контрагент", "СуммаДокумента"]
|
|
|
|
|
|
def test_authoring_context_and_completion_preview(tmp_path: Path):
|
|
project_id = f"authoring-api-{uuid4()}"
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<Attribute name="Контрагент" qualifiedName="Документ.ЗаказПокупателя.Контрагент" />
|
|
<TabularSection name="Товары" qualifiedName="Документ.ЗаказПокупателя.Товары" />
|
|
</Document>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
module = tmp_path / "Documents" / "ЗаказПокупателя" / "Ext" / "ObjectModule.bsl"
|
|
module.parent.mkdir(parents=True)
|
|
source_text = """
|
|
Процедура Проведение(Отказ, РежимПроведения)
|
|
Сумма = 0;
|
|
Движения.ОстаткиТоваров.Записать();
|
|
КонецПроцедуры
|
|
"""
|
|
module.write_text(source_text, encoding="utf-8")
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
create_authoring_session(client, project_id, "task.authoring", "session.authoring")
|
|
create_authoring_session(client, project_id, "task.rollback", "session.rollback")
|
|
|
|
context = client.post(
|
|
f"/projects/{project_id}/authoring/context",
|
|
json={
|
|
"object_name": "Документ.ЗаказПокупателя",
|
|
"routine_name": "Проведение",
|
|
"cursor_line": 3,
|
|
"source_text": source_text,
|
|
},
|
|
)
|
|
|
|
assert context.status_code == 200
|
|
payload = context.json()
|
|
assert payload["object"]["qualified_name"] == "Документ.ЗаказПокупателя"
|
|
assert payload["routine"]["name"] == "Проведение"
|
|
assert "Сумма" in payload["local_variables"]
|
|
assert "Отказ" in payload["parameters"]
|
|
assert payload["object_attributes"][0]["name"] == "Контрагент"
|
|
assert payload["tabular_sections"][0]["name"] == "Товары"
|
|
assert "ЗначениеЗаполнено" in payload["available_methods"]
|
|
|
|
preview = client.post(
|
|
f"/projects/{project_id}/authoring/completion-preview",
|
|
json={
|
|
"object_name": "Документ.ЗаказПокупателя",
|
|
"routine_name": "Проведение",
|
|
"cursor_line": 3,
|
|
"source_text": source_text,
|
|
"intent": "fill-check",
|
|
},
|
|
)
|
|
|
|
assert preview.status_code == 200
|
|
preview_payload = preview.json()
|
|
assert preview_payload["allowed"] is False
|
|
assert "ЗначениеЗаполнено(Контрагент)" in preview_payload["insert_text"]
|
|
assert any(check["name"] == "apply" and check["status"] == "BLOCKED" for check in preview_payload["checks"])
|
|
assert preview_payload["semantic_diff"][0]["kind"] == "ADD"
|
|
|
|
html5_preview = client.post(
|
|
f"/html5/projects/{project_id}/authoring/completion-preview",
|
|
data={
|
|
"object_name": "Документ.ЗаказПокупателя",
|
|
"routine_name": "Проведение",
|
|
"cursor_line": "3",
|
|
"source_text": source_text,
|
|
"intent": "fill-check",
|
|
},
|
|
)
|
|
assert html5_preview.status_code == 200
|
|
assert "text/html" in html5_preview.headers["content-type"]
|
|
assert "data-html5-authoring-preview-result" in html5_preview.text
|
|
assert "data-html5-authoring-result-summary" in html5_preview.text
|
|
assert "diff lines" in html5_preview.text
|
|
assert "ЗначениеЗаполнено(Контрагент)" in html5_preview.text
|
|
assert "BLOCKED" in html5_preview.text
|
|
assert "ADD" in html5_preview.text
|
|
assert "<html" not in html5_preview.text
|
|
|
|
html5_diff_preview = client.post(
|
|
f"/html5/projects/{project_id}/authoring/semantic-diff-preview",
|
|
data={
|
|
"routine_name": "Проведение",
|
|
"source_path": str(module),
|
|
"original_text": source_text,
|
|
"proposed_text": source_text.replace(
|
|
" Сумма = 0;",
|
|
" Сумма = 0;\n Если Отказ Тогда\n Возврат;\n КонецЕсли;",
|
|
),
|
|
"task_id": "task.authoring",
|
|
"session_id": "session.authoring",
|
|
"user_id": "dev.ivan",
|
|
},
|
|
)
|
|
assert html5_diff_preview.status_code == 200
|
|
assert "text/html" in html5_diff_preview.headers["content-type"]
|
|
assert "data-html5-authoring-diff-result" in html5_diff_preview.text
|
|
assert "data-html5-authoring-result-summary" in html5_diff_preview.text
|
|
assert "changed" in html5_diff_preview.text
|
|
assert "data-html5-authoring-apply-form" in html5_diff_preview.text
|
|
assert f'hx-post="/html5/projects/{project_id}/authoring/apply-change-set"' in html5_diff_preview.text
|
|
assert "data-html5-authoring-apply-result" in html5_diff_preview.text
|
|
assert "Diff preview" in html5_diff_preview.text
|
|
assert "Если Отказ Тогда" in html5_diff_preview.text
|
|
assert "task-session" in html5_diff_preview.text
|
|
assert "BLOCKED" in html5_diff_preview.text
|
|
assert "<html" not in html5_diff_preview.text
|
|
|
|
html5_blocked_change_apply = client.post(
|
|
f"/html5/projects/{project_id}/authoring/apply-change-set",
|
|
data={
|
|
"routine_name": "Проведение",
|
|
"source_path": str(module),
|
|
"original_text": source_text,
|
|
"proposed_text": source_text.replace(
|
|
" Сумма = 0;",
|
|
" Сумма = 0;\n Если Отказ Тогда\n Возврат;\n КонецЕсли;",
|
|
),
|
|
"task_id": "task.authoring",
|
|
"session_id": "session.authoring",
|
|
"user_id": "dev.ivan",
|
|
"expected_next_version_id": "wrong-version",
|
|
},
|
|
)
|
|
assert html5_blocked_change_apply.status_code == 200
|
|
assert "text/html" in html5_blocked_change_apply.headers["content-type"]
|
|
assert "data-html5-authoring-apply-result" in html5_blocked_change_apply.text
|
|
assert "Expected version id does not match current preview" in html5_blocked_change_apply.text
|
|
assert "<html" not in html5_blocked_change_apply.text
|
|
|
|
diff_preview = client.post(
|
|
f"/projects/{project_id}/authoring/semantic-diff-preview",
|
|
json={
|
|
"routine_name": "Проведение",
|
|
"source_path": str(module),
|
|
"original_text": source_text,
|
|
"proposed_text": source_text.replace(
|
|
" Сумма = 0;",
|
|
" Сумма = 0;\n Если Отказ Тогда\n Возврат;\n КонецЕсли;",
|
|
),
|
|
"task_id": "task.authoring",
|
|
"session_id": "session.authoring",
|
|
"user_id": "dev.ivan",
|
|
},
|
|
)
|
|
|
|
assert diff_preview.status_code == 200
|
|
diff_payload = diff_preview.json()
|
|
assert diff_payload["changed"] is True
|
|
assert diff_payload["added_lines"] == 3
|
|
assert diff_payload["removed_lines"] == 0
|
|
assert diff_payload["target"]["name"] == "Проведение"
|
|
assert diff_payload["version_preview"]["task_id"] == "task.authoring"
|
|
assert diff_payload["version_preview"]["apply_available"] is False
|
|
assert any(row["name"] == "task-session" and row["status"] == "OK" for row in diff_payload["checks"])
|
|
assert any(row["name"] == "rbac" and row["status"] == "OK" for row in diff_payload["checks"])
|
|
assert any(row["name"] == "apply" and row["status"] == "BLOCKED" for row in diff_payload["checks"])
|
|
|
|
apply_response = client.post(
|
|
f"/projects/{project_id}/authoring/apply-change-set",
|
|
json={
|
|
"routine_name": "Проведение",
|
|
"source_path": str(module),
|
|
"original_text": source_text,
|
|
"proposed_text": source_text.replace(
|
|
" Сумма = 0;",
|
|
" Сумма = 0;\n Если Отказ Тогда\n Возврат;\n КонецЕсли;",
|
|
),
|
|
"task_id": "task.authoring",
|
|
"session_id": "session.authoring",
|
|
"user_id": "dev.ivan",
|
|
"expected_next_version_id": diff_payload["version_preview"]["next_version_id"],
|
|
"approved_by": "dev.ivan",
|
|
"approval_note": "preview checked",
|
|
},
|
|
)
|
|
|
|
assert apply_response.status_code == 200
|
|
apply_payload = apply_response.json()
|
|
assert apply_payload["status"] == "APPLIED_TO_WORKSPACE"
|
|
assert apply_payload["production_applied"] is False
|
|
assert apply_payload["version"]["version_id"] == diff_payload["version_preview"]["next_version_id"]
|
|
assert apply_payload["version"]["payload"]["kind"] == "AUTHORING_CHANGE_SET"
|
|
assert "Если Отказ Тогда" in apply_payload["version"]["payload"]["proposed_text"]
|
|
assert client.get(f"/versions/{apply_payload['version']['lineage_id']}").status_code == 200
|
|
|
|
changes = client.get(f"/projects/{project_id}/authoring/changes")
|
|
assert changes.status_code == 200
|
|
assert changes.json()[0]["change_id"] == apply_payload["change_id"]
|
|
assert changes.json()[0]["added_lines"] == 3
|
|
assert changes.json()[0]["production_applied"] is False
|
|
|
|
html5_changes = client.get(f"/html5/projects/{project_id}/authoring/changes")
|
|
assert html5_changes.status_code == 200
|
|
assert "text/html" in html5_changes.headers["content-type"]
|
|
assert "data-html5-authoring-changes" in html5_changes.text
|
|
assert "data-html5-authoring-summary" in html5_changes.text
|
|
assert "data-html5-authoring-recent-change" in html5_changes.text
|
|
assert "workspace" in html5_changes.text
|
|
assert "+3 / -0" in html5_changes.text
|
|
assert "data-html5-authoring-detail" in html5_changes.text
|
|
assert 'hx-target="[data-html5-authoring-detail]"' in html5_changes.text
|
|
assert apply_payload["change_id"] in html5_changes.text
|
|
assert apply_payload["version"]["version_id"] in html5_changes.text
|
|
assert "<html" not in html5_changes.text
|
|
|
|
rollback = client.get(
|
|
f"/projects/{project_id}/authoring/changes/{apply_payload['change_id']}/rollback-preview"
|
|
)
|
|
assert rollback.status_code == 200
|
|
rollback_payload = rollback.json()
|
|
assert rollback_payload["original_version_id"] == apply_payload["version"]["version_id"]
|
|
assert rollback_payload["apply_available"] is True
|
|
assert any(line["kind"] == "REMOVE" for line in rollback_payload["semantic_diff"])
|
|
assert any(check["name"] == "apply" and check["status"] == "READY" for check in rollback_payload["checks"])
|
|
|
|
html5_detail = client.get(f"/html5/projects/{project_id}/authoring/changes/{apply_payload['change_id']}")
|
|
assert html5_detail.status_code == 200
|
|
assert "text/html" in html5_detail.headers["content-type"]
|
|
assert "data-html5-authoring-detail" in html5_detail.text
|
|
assert "data-html5-authoring-detail-summary" in html5_detail.text
|
|
assert "rollback ready" in html5_detail.text
|
|
assert "diff lines" in html5_detail.text
|
|
assert "Rollback preview" in html5_detail.text
|
|
assert "data-html5-authoring-rollback-form" in html5_detail.text
|
|
assert f'hx-post="/html5/projects/{project_id}/authoring/changes/{apply_payload["change_id"]}/apply-rollback"' in html5_detail.text
|
|
assert "data-html5-authoring-result" in html5_detail.text
|
|
assert "READY" in html5_detail.text
|
|
assert "REMOVE" in html5_detail.text
|
|
assert apply_payload["change_id"] in html5_detail.text
|
|
assert "<html" not in html5_detail.text
|
|
|
|
html5_blocked_apply = client.post(
|
|
f"/html5/projects/{project_id}/authoring/changes/{apply_payload['change_id']}/apply-rollback",
|
|
data={
|
|
"expected_rollback_version_id": rollback_payload["rollback_version_id"],
|
|
"approved_by": "dev.ivan",
|
|
},
|
|
)
|
|
assert html5_blocked_apply.status_code == 200
|
|
assert "text/html" in html5_blocked_apply.headers["content-type"]
|
|
assert "data-html5-authoring-result" in html5_blocked_apply.text
|
|
assert "Task id is required" in html5_blocked_apply.text
|
|
assert "<html" not in html5_blocked_apply.text
|
|
|
|
rollback_apply = client.post(
|
|
f"/projects/{project_id}/authoring/changes/{apply_payload['change_id']}/apply-rollback",
|
|
json={
|
|
"expected_rollback_version_id": rollback_payload["rollback_version_id"],
|
|
"approved_by": "dev.ivan",
|
|
"approval_note": "rollback preview checked",
|
|
"task_id": "task.rollback",
|
|
"session_id": "session.rollback",
|
|
},
|
|
)
|
|
assert rollback_apply.status_code == 200
|
|
rollback_apply_payload = rollback_apply.json()
|
|
assert rollback_apply_payload["status"] == "ROLLED_BACK_TO_WORKSPACE"
|
|
assert rollback_apply_payload["production_applied"] is False
|
|
assert rollback_apply_payload["version"]["version_id"] == rollback_payload["rollback_version_id"]
|
|
assert rollback_apply_payload["version"]["payload"]["kind"] == "AUTHORING_ROLLBACK"
|
|
assert rollback_apply_payload["rollback_change_id"].startswith("rollback.")
|
|
|
|
html5_rollback_apply = client.post(
|
|
f"/html5/projects/{project_id}/authoring/changes/{apply_payload['change_id']}/apply-rollback",
|
|
data={
|
|
"expected_rollback_version_id": rollback_payload["rollback_version_id"],
|
|
"approved_by": "dev.ivan",
|
|
"approval_note": "rollback html5 checked",
|
|
"task_id": "task.rollback",
|
|
"session_id": "session.rollback",
|
|
},
|
|
)
|
|
assert html5_rollback_apply.status_code == 200
|
|
assert "text/html" in html5_rollback_apply.headers["content-type"]
|
|
assert "data-html5-authoring-result" in html5_rollback_apply.text
|
|
assert "data-html5-authoring-apply-summary" in html5_rollback_apply.text
|
|
assert 'data-html5-authoring-apply-kind="rollback"' in html5_rollback_apply.text
|
|
assert "ROLLED_BACK_TO_WORKSPACE" in html5_rollback_apply.text
|
|
assert rollback_apply_payload["rollback_change_id"] in html5_rollback_apply.text
|
|
assert "<html" not in html5_rollback_apply.text
|
|
|
|
version_diff = client.get(
|
|
f"/versions/{apply_payload['version']['lineage_id']}/diff",
|
|
params={
|
|
"from_version_id": apply_payload["version"]["version_id"],
|
|
"to_version_id": rollback_apply_payload["version"]["version_id"],
|
|
},
|
|
)
|
|
assert version_diff.status_code == 200
|
|
version_diff_payload = version_diff.json()
|
|
assert version_diff_payload["changed"] is True
|
|
assert any(entry["path"] == "kind" for entry in version_diff_payload["entries"])
|
|
|
|
rollback_production_apply = client.post(
|
|
f"/projects/{project_id}/authoring/changes/{apply_payload['change_id']}/apply-rollback",
|
|
json={
|
|
"expected_rollback_version_id": rollback_payload["rollback_version_id"],
|
|
"approved_by": "dev.ivan",
|
|
"apply_to_production": True,
|
|
},
|
|
)
|
|
assert rollback_production_apply.status_code == 403
|
|
|
|
production_apply = client.post(
|
|
f"/projects/{project_id}/authoring/apply-change-set",
|
|
json={
|
|
"routine_name": "Проведение",
|
|
"source_path": str(module),
|
|
"original_text": source_text,
|
|
"proposed_text": source_text + "\n",
|
|
"expected_next_version_id": diff_payload["version_preview"]["next_version_id"],
|
|
"approved_by": "dev.ivan",
|
|
"apply_to_production": True,
|
|
},
|
|
)
|
|
assert production_apply.status_code == 403
|
|
|
|
|
|
def test_authoring_apply_requires_active_task_session(tmp_path: Path):
|
|
project_id = f"authoring-guard-{uuid4()}"
|
|
module = tmp_path / "guard_module.bsl"
|
|
source_text = "Процедура Проверить()\nКонецПроцедуры\n"
|
|
module.write_text(source_text, encoding="utf-8")
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
|
|
preview = client.post(
|
|
f"/projects/{project_id}/authoring/semantic-diff-preview",
|
|
json={
|
|
"routine_name": "Проверить",
|
|
"source_path": str(module),
|
|
"original_text": source_text,
|
|
"proposed_text": source_text.replace("КонецПроцедуры", " Возврат;\nКонецПроцедуры"),
|
|
},
|
|
)
|
|
assert preview.status_code == 200
|
|
preview_payload = preview.json()
|
|
assert any(check["name"] == "task-session" and check["status"] == "BLOCKED" for check in preview_payload["checks"])
|
|
|
|
apply_response = client.post(
|
|
f"/projects/{project_id}/authoring/apply-change-set",
|
|
json={
|
|
"routine_name": "Проверить",
|
|
"source_path": str(module),
|
|
"original_text": source_text,
|
|
"proposed_text": source_text.replace("КонецПроцедуры", " Возврат;\nКонецПроцедуры"),
|
|
"expected_next_version_id": preview_payload["version_preview"]["next_version_id"],
|
|
"approved_by": "dev.ivan",
|
|
},
|
|
)
|
|
assert apply_response.status_code == 409
|
|
blocked = apply_response.json()["detail"]["blocked_checks"]
|
|
assert blocked[0]["name"] == "task-session"
|
|
|
|
|
|
def test_authoring_apply_requires_rbac_permission(tmp_path: Path):
|
|
project_id = f"authoring-rbac-{uuid4()}"
|
|
module = tmp_path / "rbac_module.bsl"
|
|
source_text = "Процедура Проверить()\nКонецПроцедуры\n"
|
|
module.write_text(source_text, encoding="utf-8")
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
user = client.post("/collaboration/users", json={"user_id": "viewer.ivan", "display_name": "Viewer"})
|
|
assert user.status_code == 200
|
|
grant = client.post("/security/users/viewer.ivan/roles/viewer")
|
|
assert grant.status_code == 200
|
|
task = client.post(
|
|
"/collaboration/tasks",
|
|
json={"task_id": "task.rbac", "project_id": project_id, "title": "RBAC authoring", "assignee_user_id": "viewer.ivan"},
|
|
)
|
|
assert task.status_code == 200
|
|
session = client.post(
|
|
"/collaboration/sessions",
|
|
json={"session": {"session_id": "session.rbac", "task_id": "task.rbac", "user_id": "viewer.ivan"}},
|
|
)
|
|
assert session.status_code == 200
|
|
|
|
preview = client.post(
|
|
f"/projects/{project_id}/authoring/semantic-diff-preview",
|
|
json={
|
|
"routine_name": "Проверить",
|
|
"source_path": str(module),
|
|
"original_text": source_text,
|
|
"proposed_text": source_text.replace("КонецПроцедуры", " Возврат;\nКонецПроцедуры"),
|
|
"task_id": "task.rbac",
|
|
"session_id": "session.rbac",
|
|
"user_id": "viewer.ivan",
|
|
},
|
|
)
|
|
assert preview.status_code == 200
|
|
preview_payload = preview.json()
|
|
assert any(check["name"] == "rbac" and check["status"] == "BLOCKED" for check in preview_payload["checks"])
|
|
|
|
apply_response = client.post(
|
|
f"/projects/{project_id}/authoring/apply-change-set",
|
|
json={
|
|
"routine_name": "Проверить",
|
|
"source_path": str(module),
|
|
"original_text": source_text,
|
|
"proposed_text": source_text.replace("КонецПроцедуры", " Возврат;\nКонецПроцедуры"),
|
|
"task_id": "task.rbac",
|
|
"session_id": "session.rbac",
|
|
"user_id": "viewer.ivan",
|
|
"expected_next_version_id": preview_payload["version_preview"]["next_version_id"],
|
|
"approved_by": "viewer.ivan",
|
|
},
|
|
)
|
|
assert apply_response.status_code == 409
|
|
assert any(check["name"] == "rbac" for check in apply_response.json()["detail"]["blocked_checks"])
|
|
|
|
|
|
def test_authoring_apply_blocks_sensitive_privacy_context(tmp_path: Path):
|
|
project_id = f"authoring-privacy-{uuid4()}"
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<Attribute name="Телефон" qualifiedName="Документ.ЗаказПокупателя.Телефон" />
|
|
</Document>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
module = tmp_path / "Documents" / "ЗаказПокупателя" / "Ext" / "ObjectModule.bsl"
|
|
module.parent.mkdir(parents=True)
|
|
source_text = "Процедура Проверить()\n Сообщить(Телефон);\nКонецПроцедуры\n"
|
|
module.write_text(source_text, encoding="utf-8")
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
create_authoring_session(client, project_id, "task.privacy", "session.privacy")
|
|
schema = client.get(f"/projects/{project_id}/objects/schema/Документ.ЗаказПокупателя")
|
|
phone_lineage = schema.json()["attributes"][0]["lineage_id"]
|
|
marker = client.post(
|
|
f"/projects/{project_id}/privacy/markers",
|
|
json={"target_id": phone_lineage, "classification": "PERSONAL_DATA", "reason": "phone number"},
|
|
)
|
|
assert marker.status_code == 200
|
|
|
|
preview = client.post(
|
|
f"/projects/{project_id}/authoring/semantic-diff-preview",
|
|
json={
|
|
"object_name": "Документ.ЗаказПокупателя",
|
|
"routine_name": "Проверить",
|
|
"source_path": str(module),
|
|
"original_text": source_text,
|
|
"proposed_text": source_text.replace("Сообщить(Телефон);", "Сообщить(Телефон);\n Возврат;"),
|
|
"task_id": "task.privacy",
|
|
"session_id": "session.privacy",
|
|
"user_id": "dev.ivan",
|
|
},
|
|
)
|
|
assert preview.status_code == 200
|
|
preview_payload = preview.json()
|
|
assert any(check["name"] == "privacy" and check["status"] == "BLOCKED" for check in preview_payload["checks"])
|
|
|
|
apply_response = client.post(
|
|
f"/projects/{project_id}/authoring/apply-change-set",
|
|
json={
|
|
"object_name": "Документ.ЗаказПокупателя",
|
|
"routine_name": "Проверить",
|
|
"source_path": str(module),
|
|
"original_text": source_text,
|
|
"proposed_text": source_text.replace("Сообщить(Телефон);", "Сообщить(Телефон);\n Возврат;"),
|
|
"task_id": "task.privacy",
|
|
"session_id": "session.privacy",
|
|
"user_id": "dev.ivan",
|
|
"expected_next_version_id": preview_payload["version_preview"]["next_version_id"],
|
|
"approved_by": "dev.ivan",
|
|
},
|
|
)
|
|
assert apply_response.status_code == 409
|
|
assert any(check["name"] == "privacy" for check in apply_response.json()["detail"]["blocked_checks"])
|
|
|
|
|
|
def test_authoring_metadata_object_preview_and_apply(tmp_path: Path):
|
|
project_id = f"metadata-authoring-api-{uuid4()}"
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Catalog name="Контрагенты" qualifiedName="Справочник.Контрагенты" />
|
|
<Catalog name="Номенклатура" qualifiedName="Справочник.Номенклатура" />
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
create_authoring_session(client, project_id, "task.metadata", "session.metadata")
|
|
create_authoring_session(client, project_id, "task.metadata.rollback", "session.metadata.rollback")
|
|
|
|
draft = {
|
|
"object_kind": "DOCUMENT",
|
|
"name": "ЗаявкаНаЗакупку",
|
|
"synonym": "Заявка на закупку",
|
|
"attributes": [
|
|
{"name": "Контрагент", "type": "СправочникСсылка.Контрагенты", "required": True}
|
|
],
|
|
"tabular_sections": [
|
|
{
|
|
"name": "Товары",
|
|
"attributes": [
|
|
{"name": "Номенклатура", "type": "СправочникСсылка.Номенклатура"},
|
|
{"name": "Количество", "type": "Число"},
|
|
],
|
|
}
|
|
],
|
|
"forms": ["ФормаДокумента"],
|
|
"commands": [{"name": "Заполнить", "handler": "ЗаполнитьКоманда"}],
|
|
"task_id": "task.metadata",
|
|
"session_id": "session.metadata",
|
|
"user_id": "dev.ivan",
|
|
}
|
|
|
|
preview = client.post(f"/projects/{project_id}/authoring/metadata-object-preview", json=draft)
|
|
assert preview.status_code == 200
|
|
preview_payload = preview.json()
|
|
assert preview_payload["target"]["qualified_name"] == "Документ.ЗаявкаНаЗакупку"
|
|
assert preview_payload["changed"] is True
|
|
assert preview_payload["version_preview"]["apply_available"] is True
|
|
assert any(check["name"] == "task-session" and check["status"] == "OK" for check in preview_payload["checks"])
|
|
assert any(check["name"] == "rbac" and check["status"] == "OK" for check in preview_payload["checks"])
|
|
assert any("Реквизит.Контрагент" in row["text"] for row in preview_payload["semantic_diff"])
|
|
assert any("ТабличнаяЧасть.Товары" in row["text"] for row in preview_payload["semantic_diff"])
|
|
assert any("Команда.Заполнить" in row["text"] for row in preview_payload["semantic_diff"])
|
|
|
|
html5_preview = client.post(
|
|
f"/html5/projects/{project_id}/authoring/metadata-object-preview",
|
|
data={
|
|
"object_kind": "DOCUMENT",
|
|
"name": "ЗаявкаНаЗакупкуHtml5",
|
|
"synonym": "Заявка на закупку HTML5",
|
|
"attributes": "Контрагент:СправочникСсылка.Контрагенты",
|
|
"tabular_sections": "Товары[Номенклатура:СправочникСсылка.Номенклатура;Количество:Число]",
|
|
"forms": "ФормаДокумента",
|
|
"commands": "Заполнить:ЗаполнитьКоманда",
|
|
"task_id": "task.metadata",
|
|
"session_id": "session.metadata",
|
|
"user_id": "dev.ivan",
|
|
},
|
|
)
|
|
assert html5_preview.status_code == 200
|
|
assert "text/html" in html5_preview.headers["content-type"]
|
|
assert "data-html5-metadata-preview-result" in html5_preview.text
|
|
assert "data-html5-authoring-result-summary" in html5_preview.text
|
|
assert "changed" in html5_preview.text
|
|
assert "diff lines" in html5_preview.text
|
|
assert "data-html5-metadata-apply-form" in html5_preview.text
|
|
assert f'hx-post="/html5/projects/{project_id}/authoring/apply-metadata-object"' in html5_preview.text
|
|
assert "Документ.ЗаявкаНаЗакупкуHtml5" in html5_preview.text
|
|
assert "Реквизит.Контрагент" in html5_preview.text
|
|
assert "<html" not in html5_preview.text
|
|
|
|
html5_blocked_apply = client.post(
|
|
f"/html5/projects/{project_id}/authoring/apply-metadata-object",
|
|
data={
|
|
"object_kind": "DOCUMENT",
|
|
"name": "ЗаявкаНаЗакупкуHtml5",
|
|
"synonym": "Заявка на закупку HTML5",
|
|
"attributes": "Контрагент:СправочникСсылка.Контрагенты",
|
|
"tabular_sections": "Товары[Номенклатура:СправочникСсылка.Номенклатура;Количество:Число]",
|
|
"forms": "ФормаДокумента",
|
|
"commands": "Заполнить:ЗаполнитьКоманда",
|
|
"task_id": "task.metadata",
|
|
"session_id": "session.metadata",
|
|
"user_id": "dev.ivan",
|
|
"expected_next_version_id": "wrong-version",
|
|
"approved_by": "dev.ivan",
|
|
},
|
|
)
|
|
assert html5_blocked_apply.status_code == 200
|
|
assert "text/html" in html5_blocked_apply.headers["content-type"]
|
|
assert "data-html5-metadata-apply-result" in html5_blocked_apply.text
|
|
assert "Expected version id does not match current metadata preview" in html5_blocked_apply.text
|
|
assert "<html" not in html5_blocked_apply.text
|
|
|
|
html5_expected_version = re.search(r'name="expected_next_version_id" value="([^"]+)"', html5_preview.text)
|
|
assert html5_expected_version is not None
|
|
html5_apply = client.post(
|
|
f"/html5/projects/{project_id}/authoring/apply-metadata-object",
|
|
data={
|
|
"object_kind": "DOCUMENT",
|
|
"name": "ЗаявкаНаЗакупкуHtml5",
|
|
"synonym": "Заявка на закупку HTML5",
|
|
"attributes": "Контрагент:СправочникСсылка.Контрагенты",
|
|
"tabular_sections": "Товары[Номенклатура:СправочникСсылка.Номенклатура;Количество:Число]",
|
|
"forms": "ФормаДокумента",
|
|
"commands": "Заполнить:ЗаполнитьКоманда",
|
|
"task_id": "task.metadata",
|
|
"session_id": "session.metadata",
|
|
"user_id": "dev.ivan",
|
|
"expected_next_version_id": html5_expected_version.group(1),
|
|
"approved_by": "dev.ivan",
|
|
"approval_note": "html5 metadata draft checked",
|
|
},
|
|
)
|
|
assert html5_apply.status_code == 200
|
|
assert "text/html" in html5_apply.headers["content-type"]
|
|
assert "data-html5-metadata-apply-result" in html5_apply.text
|
|
assert "data-html5-authoring-apply-summary" in html5_apply.text
|
|
assert 'data-html5-authoring-apply-kind="metadata"' in html5_apply.text
|
|
assert "METADATA_DRAFT_APPLIED_TO_WORKSPACE" in html5_apply.text
|
|
assert "ЗаявкаНаЗакупкуHtml5" in html5_apply.text
|
|
assert "<html" not in html5_apply.text
|
|
|
|
apply_response = client.post(
|
|
f"/projects/{project_id}/authoring/apply-metadata-object",
|
|
json={
|
|
**draft,
|
|
"expected_next_version_id": preview_payload["version_preview"]["next_version_id"],
|
|
"approved_by": "dev.ivan",
|
|
"approval_note": "metadata draft checked",
|
|
},
|
|
)
|
|
assert apply_response.status_code == 200
|
|
apply_payload = apply_response.json()
|
|
assert apply_payload["status"] == "METADATA_DRAFT_APPLIED_TO_WORKSPACE"
|
|
assert apply_payload["version"]["payload"]["kind"] == "METADATA_OBJECT_DRAFT"
|
|
assert apply_payload["version"]["payload"]["draft"]["name"] == "ЗаявкаНаЗакупку"
|
|
|
|
changes = client.get(f"/projects/{project_id}/authoring/changes")
|
|
assert changes.status_code == 200
|
|
assert changes.json()[0]["change_id"] == apply_payload["change_id"]
|
|
assert changes.json()[0]["target"]["qualified_name"] == "Документ.ЗаявкаНаЗакупку"
|
|
|
|
lineage = apply_payload["version"]["lineage_id"]
|
|
versions = client.get(f"/versions/{lineage}")
|
|
assert versions.status_code == 200
|
|
assert any(version["version_id"] == apply_payload["version"]["version_id"] for version in versions.json())
|
|
|
|
rollback = client.get(
|
|
f"/projects/{project_id}/authoring/changes/{apply_payload['change_id']}/rollback-preview"
|
|
)
|
|
assert rollback.status_code == 200
|
|
rollback_payload = rollback.json()
|
|
assert rollback_payload["apply_available"] is True
|
|
assert rollback_payload["target"]["qualified_name"] == "Документ.ЗаявкаНаЗакупку"
|
|
assert any(line["kind"] == "REMOVE" and "Реквизит.Контрагент" in line["text"] for line in rollback_payload["semantic_diff"])
|
|
|
|
rollback_apply = client.post(
|
|
f"/projects/{project_id}/authoring/changes/{apply_payload['change_id']}/apply-rollback",
|
|
json={
|
|
"expected_rollback_version_id": rollback_payload["rollback_version_id"],
|
|
"approved_by": "dev.ivan",
|
|
"approval_note": "metadata rollback checked",
|
|
"task_id": "task.metadata.rollback",
|
|
"session_id": "session.metadata.rollback",
|
|
},
|
|
)
|
|
assert rollback_apply.status_code == 200
|
|
rollback_apply_payload = rollback_apply.json()
|
|
assert rollback_apply_payload["status"] == "ROLLED_BACK_TO_WORKSPACE"
|
|
assert rollback_apply_payload["version"]["payload"]["kind"] == "METADATA_OBJECT_DRAFT_ROLLBACK"
|
|
assert rollback_apply_payload["version"]["payload"]["draft"]["name"] == "ЗаявкаНаЗакупку"
|
|
|
|
production_apply = client.post(
|
|
f"/projects/{project_id}/authoring/apply-metadata-object",
|
|
json={
|
|
**draft,
|
|
"expected_next_version_id": preview_payload["version_preview"]["next_version_id"],
|
|
"approved_by": "dev.ivan",
|
|
"apply_to_production": True,
|
|
},
|
|
)
|
|
assert production_apply.status_code == 403
|
|
|
|
invalid_preview = client.post(
|
|
f"/projects/{project_id}/authoring/metadata-object-preview",
|
|
json={
|
|
**draft,
|
|
"name": "123Недопустимо",
|
|
},
|
|
)
|
|
assert invalid_preview.status_code == 422
|
|
|
|
duplicate_preview = client.post(
|
|
f"/projects/{project_id}/authoring/metadata-object-preview",
|
|
json={
|
|
**draft,
|
|
"attributes": [
|
|
{"name": "Контрагент", "type": "СправочникСсылка.Контрагенты"},
|
|
{"name": "контрагент", "type": "СправочникСсылка.Контрагенты"},
|
|
],
|
|
},
|
|
)
|
|
assert duplicate_preview.status_code == 422
|
|
|
|
duplicate_form_preview = client.post(
|
|
f"/projects/{project_id}/authoring/metadata-object-preview",
|
|
json={
|
|
**draft,
|
|
"forms": ["ФормаДокумента", "формадокумента"],
|
|
},
|
|
)
|
|
assert duplicate_form_preview.status_code == 422
|
|
|
|
duplicate_command_preview = client.post(
|
|
f"/projects/{project_id}/authoring/metadata-object-preview",
|
|
json={
|
|
**draft,
|
|
"commands": [
|
|
{"name": "Заполнить", "handler": "ЗаполнитьКоманда"},
|
|
{"name": "заполнить", "handler": "ЗаполнитьКоманда"},
|
|
],
|
|
},
|
|
)
|
|
assert duplicate_command_preview.status_code == 422
|
|
|
|
|
|
def test_authoring_metadata_object_preview_supports_catalog_types(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text("<Configuration />", encoding="utf-8")
|
|
client = TestClient(app)
|
|
project_id = f"metadata-kind-catalog-{uuid4()}"
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
|
|
expected_targets = {
|
|
"COMMON_MODULE": "ОбщийМодуль.ИнтеграцияСервер",
|
|
"REPORT": "Отчет.АнализПродаж",
|
|
"DATA_PROCESSOR": "Обработка.ЗагрузкаДанных",
|
|
"INFORMATION_REGISTER": "РегистрСведений.НастройкиОбмена",
|
|
"ACCUMULATION_REGISTER": "РегистрНакопления.ОстаткиТоваров",
|
|
"ACCOUNTING_REGISTER": "РегистрБухгалтерии.Хозрасчетный",
|
|
"CALCULATION_REGISTER": "РегистрРасчета.Начисления",
|
|
"BUSINESS_PROCESS": "БизнесПроцесс.СогласованиеЗаявки",
|
|
"TASK": "Задача.ЗадачаИсполнителя",
|
|
"CHART_OF_ACCOUNTS": "ПланСчетов.Управленческий",
|
|
}
|
|
|
|
for object_kind, qualified_name in expected_targets.items():
|
|
response = client.post(
|
|
f"/projects/{project_id}/authoring/metadata-object-preview",
|
|
json={
|
|
"object_kind": object_kind,
|
|
"name": qualified_name.split(".", 1)[1],
|
|
"synonym": qualified_name.split(".", 1)[1],
|
|
"attributes": [{"name": "Комментарий", "type": "Строка"}],
|
|
"forms": ["ФормаОбъекта"],
|
|
"commands": [{"name": "Обновить", "handler": "ОбновитьКоманда"}],
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["target"]["qualified_name"] == qualified_name
|
|
assert payload["changed"] is True
|
|
|
|
|
|
def test_authoring_context_includes_query_tables(tmp_path: Path):
|
|
project_id = f"authoring-query-context-{uuid4()}"
|
|
module = tmp_path / "demo_module.bsl"
|
|
source_text = """
|
|
Процедура ПроверитьОстатки()
|
|
Запрос = Новый Запрос;
|
|
Запрос.Текст =
|
|
"ВЫБРАТЬ
|
|
Остатки.Номенклатура
|
|
ИЗ
|
|
РегистрНакопления.ОстаткиТоваров КАК Остатки";
|
|
КонецПроцедуры
|
|
"""
|
|
module.write_text(source_text, encoding="utf-8")
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
|
|
context = client.post(
|
|
f"/projects/{project_id}/authoring/context",
|
|
json={
|
|
"routine_name": "ПроверитьОстатки",
|
|
"cursor_line": 3,
|
|
"source_text": source_text,
|
|
},
|
|
)
|
|
|
|
assert context.status_code == 200
|
|
assert [row["qualified_name"] for row in context.json()["query_tables"]] == [
|
|
"РегистрНакопления.ОстаткиТоваров"
|
|
]
|
|
|
|
|
|
def test_object_schema_endpoint(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<Attribute name="Контрагент" qualifiedName="Документ.ЗаказПокупателя.Контрагент" />
|
|
<TabularSection name="Товары" qualifiedName="Документ.ЗаказПокупателя.Товары">
|
|
<Attribute name="Номенклатура" qualifiedName="Документ.ЗаказПокупателя.Товары.Номенклатура" />
|
|
</TabularSection>
|
|
</Document>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-object-schema"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get("/projects/demo-object-schema/objects/schema/Документ.ЗаказПокупателя")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["object"]["name"] == "ЗаказПокупателя"
|
|
assert payload["attributes"][0]["name"] == "Контрагент"
|
|
assert payload["tabular_sections"][0]["tabular_section"]["name"] == "Товары"
|
|
assert payload["tabular_sections"][0]["columns"][0]["name"] == "Номенклатура"
|
|
|
|
|
|
def test_knowledge_schema_coverage_endpoint(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<Attribute name="Контрагент" qualifiedName="Документ.ЗаказПокупателя.Контрагент" />
|
|
<TabularSection name="Товары" qualifiedName="Документ.ЗаказПокупателя.Товары">
|
|
<Attribute name="Номенклатура" qualifiedName="Документ.ЗаказПокупателя.Товары.Номенклатура" />
|
|
</TabularSection>
|
|
</Document>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-api-schema-coverage"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
snapshot = client.get("/projects/demo-api-schema-coverage/snapshot/export").json()
|
|
document_lineage = next(
|
|
node["lineage_id"]
|
|
for node in snapshot["nodes"]
|
|
if node["kind"] == "DOCUMENT"
|
|
)
|
|
knowledge = client.post(
|
|
"/knowledge",
|
|
json={
|
|
"record_id": "knowledge.schema.document",
|
|
"scope": "PROJECT",
|
|
"title": "Заказ покупателя",
|
|
"body": "Описание структуры документа.",
|
|
"related_lineages": [document_lineage],
|
|
},
|
|
)
|
|
assert knowledge.status_code == 200
|
|
|
|
response = client.get("/projects/demo-api-schema-coverage/knowledge/schema-coverage")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["covered_count"] == 1
|
|
assert payload["uncovered_count"] == 3
|
|
assert any(item["node"]["name"] == "ЗаказПокупателя" and item["record_count"] == 1 for item in payload["items"])
|
|
assert {node["name"] for node in payload["uncovered"]} == {"Контрагент", "Номенклатура", "Товары"}
|
|
|
|
|
|
def test_knowledge_pack_import_endpoint():
|
|
client = TestClient(app)
|
|
pack_id = f"bsp.core.{uuid4()}"
|
|
record_id = f"knowledge.bsp.{uuid4()}"
|
|
|
|
response = client.post(
|
|
"/knowledge/packs",
|
|
json={
|
|
"pack_id": pack_id,
|
|
"name": "БСП базовые правила",
|
|
"vendor": "1C",
|
|
"version": "3.1",
|
|
"records": [
|
|
{
|
|
"record_id": record_id,
|
|
"scope": "GLOBAL",
|
|
"title": "БСП роли",
|
|
"body": "Рекомендации по настройке ролей БСП.",
|
|
}
|
|
],
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
packs = client.get("/knowledge/packs")
|
|
assert packs.status_code == 200
|
|
assert any(pack["pack_id"] == pack_id for pack in packs.json())
|
|
|
|
search = client.get("/knowledge/search", params={"q": "БСП"})
|
|
assert search.status_code == 200
|
|
record = next(item for item in search.json()["results"] if item["record_id"] == record_id)
|
|
assert f"pack:{pack_id}" in record["tags"]
|
|
assert "vendor:1C" in record["tags"]
|
|
assert record["attributes"]["pack_version"] == "3.1"
|
|
|
|
summary = client.get("/admin/summary")
|
|
assert summary.status_code == 200
|
|
assert summary.json()["knowledge_packs"] >= 1
|
|
|
|
|
|
def test_object_ownership_endpoint(tmp_path: Path):
|
|
project_id = f"demo-api-ownership-{uuid4()}"
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя" />
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": project_id},
|
|
)
|
|
assert indexed.status_code == 200
|
|
snapshot = client.get(f"/projects/{project_id}/snapshot/export").json()
|
|
document_lineage = next(
|
|
node["lineage_id"]
|
|
for node in snapshot["nodes"]
|
|
if node["kind"] == "DOCUMENT"
|
|
)
|
|
|
|
user_id = f"user.{uuid4()}"
|
|
user = client.post(
|
|
"/collaboration/users",
|
|
json={"user_id": user_id, "display_name": "Owner"},
|
|
)
|
|
assert user.status_code == 200
|
|
ownership = client.post(
|
|
f"/projects/{project_id}/ownership",
|
|
json={
|
|
"target_id": document_lineage,
|
|
"owner_user_id": user_id,
|
|
"role": "RESPONSIBLE",
|
|
"assigned_by": user_id,
|
|
},
|
|
)
|
|
assert ownership.status_code == 200
|
|
|
|
project_ownership = client.get(f"/projects/{project_id}/ownership")
|
|
assert project_ownership.status_code == 200
|
|
assert project_ownership.json()[0]["target_id"] == document_lineage
|
|
|
|
response = client.get(f"/projects/{project_id}/objects/ownership/Документ.ЗаказПокупателя")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["object"]["name"] == "ЗаказПокупателя"
|
|
assert payload["owners"][0]["owner_user_id"] == user_id
|
|
assert payload["owners"][0]["role"] == "RESPONSIBLE"
|
|
|
|
report = client.get(f"/projects/{project_id}/report")
|
|
assert report.status_code == 200
|
|
assert report.json()["ownership_count"] == 1
|
|
assert report.json()["unowned_object_count"] == 0
|
|
|
|
review = client.get(f"/projects/{project_id}/review")
|
|
assert review.status_code == 200
|
|
assert not any(finding["title"] == "Missing 1C object owner" for finding in review.json())
|
|
|
|
|
|
def test_project_comments_endpoint(tmp_path: Path):
|
|
project_id = f"demo-api-comments-{uuid4()}"
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя" />
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": project_id},
|
|
)
|
|
assert indexed.status_code == 200
|
|
snapshot = client.get(f"/projects/{project_id}/snapshot/export").json()
|
|
document_lineage = next(
|
|
node["lineage_id"]
|
|
for node in snapshot["nodes"]
|
|
if node["kind"] == "DOCUMENT"
|
|
)
|
|
|
|
user_id = f"user.{uuid4()}"
|
|
user = client.post(
|
|
"/collaboration/users",
|
|
json={"user_id": user_id, "display_name": "Reviewer"},
|
|
)
|
|
assert user.status_code == 200
|
|
comment = client.post(
|
|
f"/projects/{project_id}/comments",
|
|
json={
|
|
"comment_id": f"comment.{uuid4()}",
|
|
"target_id": document_lineage,
|
|
"user_id": user_id,
|
|
"body": "Проверить правила проведения.",
|
|
},
|
|
)
|
|
assert comment.status_code == 200
|
|
|
|
comments = client.get(f"/projects/{project_id}/comments")
|
|
assert comments.status_code == 200
|
|
assert comments.json()[0]["target_id"] == document_lineage
|
|
|
|
target_comments = client.get(f"/projects/{project_id}/comments/{document_lineage}")
|
|
assert target_comments.status_code == 200
|
|
assert target_comments.json()[0]["body"] == "Проверить правила проведения."
|
|
|
|
activity = client.get(f"/projects/{project_id}/activity")
|
|
assert activity.status_code == 200
|
|
assert activity.json()[0]["verb"] == "ADD_COMMENT"
|
|
|
|
|
|
def test_review_endpoint_reports_missing_1c_object_owner(tmp_path: Path):
|
|
project_id = f"demo-api-missing-owner-{uuid4()}"
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя" />
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": project_id},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get(f"/projects/{project_id}/review")
|
|
|
|
assert response.status_code == 200
|
|
assert any(
|
|
finding["title"] == "Missing 1C object owner"
|
|
and "Документ.ЗаказПокупателя" in finding["message"]
|
|
for finding in response.json()
|
|
)
|
|
|
|
|
|
def test_review_endpoint_reports_missing_1c_schema_knowledge(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<Attribute name="Контрагент" qualifiedName="Документ.ЗаказПокупателя.Контрагент" />
|
|
</Document>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-api-schema-knowledge-review"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
snapshot = client.get("/projects/demo-api-schema-knowledge-review/snapshot/export").json()
|
|
document_lineage = next(
|
|
node["lineage_id"]
|
|
for node in snapshot["nodes"]
|
|
if node["kind"] == "DOCUMENT"
|
|
)
|
|
knowledge = client.post(
|
|
"/knowledge",
|
|
json={
|
|
"record_id": "knowledge.schema.review.document",
|
|
"scope": "PROJECT",
|
|
"title": "Документ заказа",
|
|
"body": "Описание документа.",
|
|
"related_lineages": [document_lineage],
|
|
},
|
|
)
|
|
assert knowledge.status_code == 200
|
|
|
|
response = client.get("/projects/demo-api-schema-knowledge-review/review")
|
|
|
|
assert response.status_code == 200
|
|
knowledge_findings = [
|
|
finding
|
|
for finding in response.json()
|
|
if finding["title"] == "Missing 1C schema knowledge"
|
|
]
|
|
assert any("Документ.ЗаказПокупателя.Контрагент" in finding["message"] for finding in knowledge_findings)
|
|
assert not any("Schema node Документ.ЗаказПокупателя has no" in finding["message"] for finding in knowledge_findings)
|
|
|
|
|
|
def test_privacy_marker_endpoint_classifies_1c_attribute(tmp_path: Path):
|
|
project_id = f"demo-api-privacy-{uuid4()}"
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<Attribute name="Телефон" qualifiedName="Документ.ЗаказПокупателя.Телефон" />
|
|
</Document>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": project_id},
|
|
)
|
|
assert indexed.status_code == 200
|
|
snapshot = client.get(f"/projects/{project_id}/snapshot/export").json()
|
|
phone_lineage = next(
|
|
node["lineage_id"]
|
|
for node in snapshot["nodes"]
|
|
if node["kind"] == "ATTRIBUTE" and node["name"] == "Телефон"
|
|
)
|
|
|
|
marker = client.post(
|
|
f"/projects/{project_id}/privacy/markers",
|
|
json={
|
|
"target_id": phone_lineage,
|
|
"classification": "PERSONAL_DATA",
|
|
"reason": "Контактный телефон клиента",
|
|
},
|
|
)
|
|
assert marker.status_code == 200
|
|
|
|
markers = client.get(f"/projects/{project_id}/privacy/markers")
|
|
assert markers.status_code == 200
|
|
assert markers.json()[0]["classification"] == "PERSONAL_DATA"
|
|
|
|
object_privacy = client.get(f"/projects/{project_id}/objects/privacy/Документ.ЗаказПокупателя")
|
|
assert object_privacy.status_code == 200
|
|
assert object_privacy.json()["markers"][0]["target_id"] == phone_lineage
|
|
|
|
report = client.get(f"/projects/{project_id}/report")
|
|
assert report.status_code == 200
|
|
assert report.json()["privacy_marker_count"] == 1
|
|
assert report.json()["sensitive_candidate_count"] == 1
|
|
assert report.json()["unclassified_sensitive_count"] == 0
|
|
|
|
review = client.get(f"/projects/{project_id}/review")
|
|
assert review.status_code == 200
|
|
assert not any(finding["title"] == "Unclassified sensitive 1C field" for finding in review.json())
|
|
|
|
|
|
def test_review_endpoint_reports_unclassified_sensitive_1c_field(tmp_path: Path):
|
|
project_id = f"demo-api-privacy-review-{uuid4()}"
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Catalog name="Контрагенты" qualifiedName="Справочник.Контрагенты">
|
|
<Attribute name="ИНН" qualifiedName="Справочник.Контрагенты.ИНН" />
|
|
</Catalog>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": project_id},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get(f"/projects/{project_id}/review")
|
|
|
|
assert response.status_code == 200
|
|
assert any(
|
|
finding["title"] == "Unclassified sensitive 1C field"
|
|
and "Справочник.Контрагенты.ИНН" in finding["message"]
|
|
for finding in response.json()
|
|
)
|
|
|
|
|
|
def test_project_report_includes_1c_schema_summary(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<Attribute name="Контрагент" qualifiedName="Документ.ЗаказПокупателя.Контрагент" />
|
|
<TabularSection name="Товары" qualifiedName="Документ.ЗаказПокупателя.Товары">
|
|
<Attribute name="Номенклатура" qualifiedName="Документ.ЗаказПокупателя.Товары.Номенклатура" />
|
|
</TabularSection>
|
|
<TabularSection name="Услуги" qualifiedName="Документ.ЗаказПокупателя.Услуги" />
|
|
</Document>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-api-schema-report"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get("/projects/demo-api-schema-report/report")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["attribute_count"] == 2
|
|
assert payload["object_attribute_count"] == 1
|
|
assert payload["tabular_section_count"] == 2
|
|
assert payload["tabular_section_column_count"] == 1
|
|
assert payload["empty_tabular_sections"] == ["Документ.ЗаказПокупателя.Услуги"]
|
|
|
|
|
|
def test_review_endpoint_reports_empty_1c_tabular_section(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<TabularSection name="Услуги" qualifiedName="Документ.ЗаказПокупателя.Услуги" />
|
|
</Document>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-api-empty-tabular-section"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get("/projects/demo-api-empty-tabular-section/review")
|
|
|
|
assert response.status_code == 200
|
|
assert any(
|
|
finding["title"] == "Empty 1C tabular section"
|
|
and "Документ.ЗаказПокупателя.Услуги" in finding["message"]
|
|
for finding in response.json()
|
|
)
|
|
|
|
|
|
def test_review_endpoint_reports_tabular_section_without_subject_column(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<TabularSection name="Товары" qualifiedName="Документ.ЗаказПокупателя.Товары">
|
|
<Attribute name="Количество" qualifiedName="Документ.ЗаказПокупателя.Товары.Количество" />
|
|
</TabularSection>
|
|
</Document>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-api-no-subject-column"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get("/projects/demo-api-no-subject-column/review")
|
|
|
|
assert response.status_code == 200
|
|
assert any(
|
|
finding["title"] == "No subject column in 1C tabular section"
|
|
and finding["severity"] == "INFO"
|
|
for finding in response.json()
|
|
)
|
|
|
|
|
|
def test_object_tabular_sections_endpoint(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<TabularSection name="Товары" qualifiedName="Документ.ЗаказПокупателя.Товары">
|
|
<Attribute name="Номенклатура" qualifiedName="Документ.ЗаказПокупателя.Товары.Номенклатура" />
|
|
</TabularSection>
|
|
</Document>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-object-tabular-sections"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get(
|
|
"/projects/demo-object-tabular-sections/objects/tabular-sections/Документ.ЗаказПокупателя"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["results"][0]["name"] == "Товары"
|
|
|
|
columns = client.get(
|
|
"/projects/demo-object-tabular-sections/objects/tabular-sections/Документ.ЗаказПокупателя/columns"
|
|
)
|
|
assert columns.status_code == 200
|
|
assert columns.json()[0]["tabular_section"]["name"] == "Товары"
|
|
assert columns.json()[0]["columns"][0]["name"] == "Номенклатура"
|
|
|
|
|
|
def test_incremental_xml_updates_metadata_semantics(tmp_path: Path):
|
|
xml = tmp_path / "metadata.xml"
|
|
xml.write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя" />
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-api-xml-incremental"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
xml.write_text(
|
|
"""
|
|
<Configuration>
|
|
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
|
<Form name="ФормаДокумента" qualifiedName="Документ.ЗаказПокупателя.ФормаДокумента">
|
|
<Command name="Провести" qualifiedName="Документ.ЗаказПокупателя.ФормаДокумента.Провести" />
|
|
</Form>
|
|
</Document>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
incremental = client.post(
|
|
"/projects/demo-api-xml-incremental/incremental/file",
|
|
json={"path": str(xml)},
|
|
)
|
|
assert incremental.status_code == 200
|
|
assert incremental.json()["added_nodes"] >= 2
|
|
|
|
forms = client.get("/projects/demo-api-xml-incremental/ui/forms")
|
|
assert forms.status_code == 200
|
|
assert forms.json()[0]["commands"][0]["name"] == "Провести"
|
|
|
|
|
|
def test_scheduled_jobs_endpoint(tmp_path: Path):
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<ScheduledJob name="ОбновлениеЦен" qualifiedName="РегламентноеЗадание.ОбновлениеЦен" method="ОбновитьЦены" schedule="КаждыйДень" />
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
module = tmp_path / "CommonModules" / "РегламентныеОперации" / "Module.bsl"
|
|
module.parent.mkdir(parents=True)
|
|
module.write_text("Процедура ОбновитьЦены()\nКонецПроцедуры\n", encoding="utf-8")
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-api-scheduled-jobs"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get("/projects/demo-api-scheduled-jobs/jobs/scheduled")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload[0]["name"] == "ОбновлениеЦен"
|
|
assert payload[0]["routine"]["name"] == "ОбновитьЦены"
|
|
assert payload[0]["schedule"] == "КаждыйДень"
|
|
|
|
impact = client.get(
|
|
"/projects/demo-api-scheduled-jobs/objects/impact/РегламентноеЗадание.ОбновлениеЦен"
|
|
)
|
|
assert impact.status_code == 200
|
|
assert impact.json()["jobs"][0]["name"] == "ОбновлениеЦен"
|
|
assert impact.json()["routines"][0]["name"] == "ОбновитьЦены"
|
|
|
|
|
|
def test_integrations_endpoint(tmp_path: Path):
|
|
(tmp_path / "integration.bsl").write_text(
|
|
"""
|
|
Процедура Отправить()
|
|
Соединение = Новый HTTPСоединение("api.example.local");
|
|
Адрес = "https://api.example.local/orders";
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<ExchangePlan name="ОбменСКассой" qualifiedName="ПланОбмена.ОбменСКассой" />
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": "demo-api-integrations"},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get("/projects/demo-api-integrations/integrations")
|
|
filtered = client.get(
|
|
"/projects/demo-api-integrations/integrations",
|
|
params={"kind": "EXCHANGE_PLAN"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert any(item["kind"] == "HTTP_SERVICE" for item in response.json())
|
|
assert filtered.status_code == 200
|
|
assert filtered.json()[0]["name"] == "ОбменСКассой"
|
|
|
|
|
|
def test_patterns_endpoint_finds_repeated_writes(tmp_path: Path):
|
|
(tmp_path / "module.bsl").write_text(
|
|
"""
|
|
Процедура ПровестиЗаказ()
|
|
Движения.ОстаткиТоваров.Записать();
|
|
КонецПроцедуры
|
|
|
|
Процедура ОтменитьЗаказ()
|
|
Движения.ОстаткиТоваров.Записать();
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
project_id = f"demo-api-patterns-{uuid4()}"
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
|
|
response = client.get(f"/projects/{project_id}/patterns")
|
|
|
|
assert response.status_code == 200
|
|
assert any(
|
|
pattern["kind"] == "REPEATED_TABLE_WRITE"
|
|
and pattern["support"] == 2
|
|
and pattern["targets"][0]["name"] == "ОстаткиТоваров"
|
|
for pattern in response.json()
|
|
)
|
|
|
|
|
|
def test_incremental_reprojects_neo4j_when_project_was_projected(monkeypatch, tmp_path: Path):
|
|
module = tmp_path / "demo_module.bsl"
|
|
module.write_text("Процедура Проведение()\nКонецПроцедуры\n", encoding="utf-8")
|
|
client = TestClient(app)
|
|
project_id = f"demo-api-neo4j-incremental-{uuid4().hex}"
|
|
projected: list[tuple[str, int]] = []
|
|
applied_deltas: list[tuple[str, int, int]] = []
|
|
|
|
async def fake_project_snapshot(project_id: str, snapshot):
|
|
projected.append((project_id, len(snapshot.nodes)))
|
|
return {"nodes": len(snapshot.nodes), "edges": len(snapshot.edges)}
|
|
|
|
async def fake_apply_delta(project_id: str, delta):
|
|
applied_deltas.append((project_id, len(delta.added_nodes), len(delta.added_edges)))
|
|
return {"nodes": len(delta.added_nodes), "edges": len(delta.added_edges)}
|
|
|
|
monkeypatch.setattr(main, "_project_snapshot_to_neo4j", fake_project_snapshot)
|
|
monkeypatch.setattr(main, "_apply_delta_to_neo4j", fake_apply_delta)
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
|
|
projection = client.post(f"/projects/{project_id}/graph/neo4j/project")
|
|
assert projection.status_code == 200
|
|
assert projected[-1][0] == project_id
|
|
|
|
module.write_text(
|
|
"""
|
|
Процедура Проведение()
|
|
Движения.ОстаткиТоваров.Записать();
|
|
КонецПроцедуры
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
incremental = client.post(
|
|
f"/projects/{project_id}/incremental/file",
|
|
json={"path": str(module)},
|
|
)
|
|
|
|
assert incremental.status_code == 200
|
|
assert incremental.json()["neo4j_projected"] is True
|
|
assert incremental.json()["neo4j_error"] is None
|
|
assert len(projected) == 1
|
|
assert applied_deltas == [(project_id, incremental.json()["added_nodes"], incremental.json()["added_edges"])]
|
|
|
|
|
|
def test_index_missing_path_returns_404():
|
|
client = TestClient(app)
|
|
response = client.post("/projects/index", json={"path": "Z:/definitely/missing"})
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_index_demo_project_endpoint():
|
|
client = TestClient(app)
|
|
response = client.post("/projects/demo/index")
|
|
assert response.status_code == 200
|
|
assert response.json()["snapshot"]["project_id"] == "demo"
|
|
|
|
|
|
def test_collaboration_endpoints():
|
|
client = TestClient(app)
|
|
suffix = uuid4().hex
|
|
user_id = f"user.api.collaboration.{suffix}"
|
|
task_id = f"task.api.collaboration.{suffix}"
|
|
session_id = f"session.api.collaboration.{suffix}"
|
|
|
|
user = client.post(
|
|
"/collaboration/users",
|
|
json={"user_id": user_id, "display_name": "API Tester"},
|
|
)
|
|
assert user.status_code == 200
|
|
|
|
task = client.post(
|
|
"/collaboration/tasks",
|
|
json={
|
|
"task_id": task_id,
|
|
"project_id": "demo-api",
|
|
"title": "Проверить индекс",
|
|
"assignee_user_id": user_id,
|
|
},
|
|
)
|
|
assert task.status_code == 200
|
|
|
|
session = client.post(
|
|
"/collaboration/sessions",
|
|
json={
|
|
"session": {
|
|
"session_id": session_id,
|
|
"task_id": task_id,
|
|
"user_id": user_id,
|
|
}
|
|
},
|
|
)
|
|
assert session.status_code == 200
|
|
|
|
finished = client.post(f"/collaboration/sessions/{session_id}/finish")
|
|
assert finished.status_code == 200
|
|
assert finished.json()["finished_at"] is not None
|
|
|
|
feed = client.get("/projects/demo-api/activity")
|
|
assert feed.status_code == 200
|
|
assert feed.json()[0]["verb"] == "FINISH_SESSION"
|
|
assert any(event["verb"] == "UPSERT_TASK" for event in feed.json())
|
|
|
|
permission = client.get(f"/security/users/{user_id}/permissions/READ_GRAPH")
|
|
assert permission.status_code == 200
|
|
assert permission.json()["allowed"] is False
|
|
|
|
grant = client.post(f"/security/users/{user_id}/roles/viewer")
|
|
assert grant.status_code == 200
|
|
permission = client.get(f"/security/users/{user_id}/permissions/READ_GRAPH")
|
|
assert permission.status_code == 200
|
|
assert permission.json()["allowed"] is True
|
|
permissions = client.get(f"/security/users/{user_id}/permissions")
|
|
assert permissions.status_code == 200
|
|
assert permissions.json()["permissions"] == ["READ_GRAPH"]
|
|
|
|
|
|
def test_operations_endpoints():
|
|
client = TestClient(app)
|
|
|
|
job = client.post("/operations/jobs", json={"job_id": "job.api", "kind": "INDEX_PROJECT"})
|
|
assert job.status_code == 200
|
|
|
|
update = client.patch(
|
|
"/operations/jobs/job.api",
|
|
json={"status": "SUCCEEDED", "result": {"indexed": True}},
|
|
)
|
|
assert update.status_code == 200
|
|
assert update.json()["status"] == "SUCCEEDED"
|
|
|
|
metric = client.post(
|
|
"/operations/metrics",
|
|
json={"metric_id": "metric.api", "name": "api.requests", "value": 1.0},
|
|
)
|
|
assert metric.status_code == 200
|
|
|
|
packages = client.post(
|
|
"/marketplace/packages",
|
|
json={"package_id": "pack.api", "name": "BSP Knowledge", "version": "1.0.0"},
|
|
)
|
|
assert packages.status_code == 200
|
|
|
|
license_response = client.get("/license")
|
|
assert license_response.status_code == 200
|
|
assert license_response.json()["valid"] is True
|
|
|
|
summary = client.get("/admin/summary")
|
|
assert summary.status_code == 200
|
|
assert "stored_snapshots" in summary.json()
|
|
|
|
|
|
def test_ai_usage_endpoints(tmp_path: Path):
|
|
project_id = f"demo-api-ai-usage-{uuid4()}"
|
|
module = tmp_path / "module.bsl"
|
|
module.write_text("Процедура Проверить()\nКонецПроцедуры\n", encoding="utf-8")
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post(
|
|
"/projects/index",
|
|
json={"path": str(tmp_path), "project_id": project_id},
|
|
)
|
|
assert indexed.status_code == 200
|
|
|
|
usage_id = f"usage.{uuid4()}"
|
|
usage = client.post(
|
|
"/ai/usage",
|
|
json={
|
|
"usage_id": usage_id,
|
|
"project_id": project_id,
|
|
"user_id": "user.ai",
|
|
"model": "gpt-test",
|
|
"operation": "review",
|
|
"prompt_tokens": 120,
|
|
"completion_tokens": 30,
|
|
"cost": 0.02,
|
|
},
|
|
)
|
|
assert usage.status_code == 200
|
|
assert usage.json()["total_tokens"] == 150
|
|
|
|
records = client.get("/ai/usage", params={"project_id": project_id})
|
|
assert records.status_code == 200
|
|
assert records.json()[0]["usage_id"] == usage_id
|
|
|
|
summary = client.get("/ai/usage/summary", params={"project_id": project_id})
|
|
assert summary.status_code == 200
|
|
assert summary.json()["request_count"] == 1
|
|
assert summary.json()["total_tokens"] == 150
|
|
|
|
policy = client.get("/ai/policy", params={"user_id": "user.ai"})
|
|
assert policy.status_code == 200
|
|
assert policy.json()["used_tokens"] >= 150
|
|
assert policy.json()["remaining_tokens"] is not None
|
|
|
|
report = client.get(f"/projects/{project_id}/report")
|
|
assert report.status_code == 200
|
|
assert report.json()["ai_usage_request_count"] == 1
|
|
assert report.json()["ai_usage_total_tokens"] == 150
|
|
|
|
|
|
def test_ai_answer_policy_allows_with_knowledge_context(tmp_path: Path):
|
|
project_id = f"demo-api-ai-answer-policy-{uuid4()}"
|
|
(tmp_path / "module.bsl").write_text("Процедура Проверить()\nКонецПроцедуры\n", encoding="utf-8")
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
snapshot = client.get(f"/projects/{project_id}/snapshot/export").json()
|
|
routine_lineage = next(node["lineage_id"] for node in snapshot["nodes"] if node["kind"] == "PROCEDURE")
|
|
knowledge = client.post(
|
|
"/knowledge",
|
|
json={
|
|
"record_id": f"knowledge.answer.{uuid4()}",
|
|
"scope": "PROJECT",
|
|
"title": "Правила проверки",
|
|
"body": "Процедура Проверить выполняет контроль.",
|
|
"related_lineages": [routine_lineage],
|
|
},
|
|
)
|
|
assert knowledge.status_code == 200
|
|
|
|
response = client.post(
|
|
f"/projects/{project_id}/ai/answer-policy",
|
|
json={
|
|
"user_id": "user.answer",
|
|
"question": "Что делает процедура Проверить?",
|
|
"related_lineages": [routine_lineage],
|
|
"estimated_tokens": 100,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["allowed"] is True
|
|
assert payload["reasons"] == []
|
|
assert payload["knowledge_records"][0]["title"] == "Правила проверки"
|
|
|
|
|
|
def test_ai_answer_policy_blocks_sensitive_context(tmp_path: Path):
|
|
project_id = f"demo-api-ai-answer-policy-sensitive-{uuid4()}"
|
|
(tmp_path / "metadata.xml").write_text(
|
|
"""
|
|
<Configuration>
|
|
<Catalog name="Контрагенты" qualifiedName="Справочник.Контрагенты">
|
|
<Attribute name="ИНН" qualifiedName="Справочник.Контрагенты.ИНН" />
|
|
</Catalog>
|
|
</Configuration>
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
|
assert indexed.status_code == 200
|
|
snapshot = client.get(f"/projects/{project_id}/snapshot/export").json()
|
|
inn_lineage = next(
|
|
node["lineage_id"]
|
|
for node in snapshot["nodes"]
|
|
if node["kind"] == "ATTRIBUTE" and node["name"] == "ИНН"
|
|
)
|
|
marker = client.post(
|
|
f"/projects/{project_id}/privacy/markers",
|
|
json={
|
|
"target_id": inn_lineage,
|
|
"classification": "PERSONAL_DATA",
|
|
"reason": "Налоговый идентификатор контрагента",
|
|
},
|
|
)
|
|
assert marker.status_code == 200
|
|
|
|
response = client.post(
|
|
f"/projects/{project_id}/ai/answer-policy",
|
|
json={
|
|
"user_id": "user.answer",
|
|
"question": "Объясни реквизит ИНН",
|
|
"related_lineages": [inn_lineage],
|
|
"estimated_tokens": 100,
|
|
"require_knowledge": False,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["allowed"] is False
|
|
assert "Privacy policy blocks sensitive 1C context" in payload["reasons"]
|
|
assert payload["privacy_markers"][0]["classification"] == "PERSONAL_DATA"
|
|
|
|
|
|
def test_operation_job_runner_indexes_project(tmp_path: Path):
|
|
module = tmp_path / "job_module.bsl"
|
|
module.write_text("Процедура JobRun()\nКонецПроцедуры\n", encoding="utf-8")
|
|
client = TestClient(app)
|
|
job_id = f"job.index.{uuid4().hex}"
|
|
|
|
job = client.post(
|
|
"/operations/jobs",
|
|
json={
|
|
"job_id": job_id,
|
|
"kind": "INDEX_PROJECT",
|
|
"payload": {"path": str(tmp_path), "project_id": "demo-job"},
|
|
},
|
|
)
|
|
assert job.status_code == 200
|
|
|
|
run = client.post(f"/operations/jobs/{job_id}/run")
|
|
assert run.status_code == 200
|
|
payload = run.json()
|
|
assert payload["status"] == "SUCCEEDED"
|
|
assert payload["result"]["snapshot"]["project_id"] == "demo-job"
|
|
|
|
|
|
def test_operation_job_runner_records_failure():
|
|
client = TestClient(app)
|
|
job_id = f"job.fail.{uuid4().hex}"
|
|
client.post(
|
|
"/operations/jobs",
|
|
json={"job_id": job_id, "kind": "INDEX_PROJECT", "payload": {}},
|
|
)
|
|
|
|
run = client.post(f"/operations/jobs/{job_id}/run")
|
|
|
|
assert run.status_code == 200
|
|
assert run.json()["status"] == "FAILED"
|
|
assert "payload.path" in run.json()["error"]
|
|
|
|
|
|
def test_neo4j_endpoints_contract_for_unindexed_project():
|
|
client = TestClient(app)
|
|
|
|
status = client.get("/graph/neo4j/status")
|
|
assert status.status_code == 200
|
|
assert status.json()["status"] in {"ok", "unavailable"}
|
|
|
|
projection = client.post("/projects/not-indexed/graph/neo4j/project")
|
|
assert projection.status_code == 404
|
|
|
|
|
|
def test_neo4j_callees_endpoint_contract(monkeypatch):
|
|
async def fake_query(project_id: str, routine_name: str, *, outgoing: bool):
|
|
assert project_id == "demo"
|
|
assert routine_name == "Проведение"
|
|
assert outgoing is True
|
|
return [
|
|
{
|
|
"lineage_id": "lineage.procedure.check",
|
|
"kind": "PROCEDURE",
|
|
"name": "ПроверитьОстатки",
|
|
"qualified_name": "Module.ПроверитьОстатки",
|
|
}
|
|
]
|
|
|
|
monkeypatch.setattr(main, "_neo4j_routine_query", fake_query)
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/projects/demo/graph/neo4j/callees/Проведение")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["results"][0]["name"] == "ПроверитьОстатки"
|
|
|
|
|
|
def test_neo4j_writes_endpoint_contract(monkeypatch):
|
|
async def fake_query(project_id: str, routine_name: str, *, relation: str):
|
|
assert project_id == "demo"
|
|
assert routine_name == "Проведение"
|
|
assert relation == "WRITES"
|
|
return [
|
|
{
|
|
"lineage_id": "lineage.register.stock",
|
|
"kind": "REGISTER",
|
|
"name": "ОстаткиТоваров",
|
|
"qualified_name": "РегистрНакопления.ОстаткиТоваров",
|
|
}
|
|
]
|
|
|
|
monkeypatch.setattr(main, "_neo4j_relation_query", fake_query)
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/projects/demo/graph/neo4j/writes/Проведение")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["results"][0]["name"] == "ОстаткиТоваров"
|
|
|
|
|
|
def test_neo4j_integrations_endpoints_contract(monkeypatch):
|
|
async def fake_integrations(project_id: str, *, kind: str | None = None):
|
|
assert project_id == "demo"
|
|
assert kind == "HTTP_SERVICE"
|
|
return [
|
|
{
|
|
"endpoint_id": "lineage.integration.http",
|
|
"name": "https://api.example.local/orders",
|
|
"kind": "HTTP_SERVICE",
|
|
"direction": "OUTBOUND",
|
|
"owner": "IntegrationModule",
|
|
"attributes": {"url": "https://api.example.local/orders"},
|
|
}
|
|
]
|
|
|
|
async def fake_modules(project_id: str, integration_name: str):
|
|
assert project_id == "demo"
|
|
assert integration_name == "https://api.example.local/orders"
|
|
return [
|
|
{
|
|
"lineage_id": "lineage.module.integration",
|
|
"kind": "MODULE",
|
|
"name": "IntegrationModule",
|
|
"qualified_name": "IntegrationModule",
|
|
}
|
|
]
|
|
|
|
monkeypatch.setattr(main, "_neo4j_integrations_query", fake_integrations)
|
|
monkeypatch.setattr(main, "_neo4j_integration_modules_query", fake_modules)
|
|
client = TestClient(app)
|
|
|
|
integrations = client.get(
|
|
"/projects/demo/graph/neo4j/integrations",
|
|
params={"kind": "HTTP_SERVICE"},
|
|
)
|
|
modules = client.get(
|
|
"/projects/demo/graph/neo4j/integrations/https://api.example.local/orders/modules"
|
|
)
|
|
|
|
assert integrations.status_code == 200
|
|
assert integrations.json()[0]["kind"] == "HTTP_SERVICE"
|
|
assert modules.status_code == 200
|
|
assert modules.json()["results"][0]["name"] == "IntegrationModule"
|
|
|
|
|
|
def test_neo4j_object_impact_endpoint_contract(monkeypatch):
|
|
async def fake_query(project_id: str, object_name: str):
|
|
assert project_id == "demo"
|
|
assert object_name == "Документ.ЗаказПокупателя"
|
|
return {
|
|
"object": {
|
|
"lineage_id": "lineage.document.order",
|
|
"kind": "DOCUMENT",
|
|
"name": "ЗаказПокупателя",
|
|
"qualified_name": "Документ.ЗаказПокупателя",
|
|
},
|
|
"modules": [
|
|
{
|
|
"lineage_id": "lineage.module.object",
|
|
"kind": "MODULE",
|
|
"name": "ObjectModule",
|
|
"qualified_name": "ObjectModule",
|
|
}
|
|
],
|
|
"routines": [
|
|
{
|
|
"lineage_id": "lineage.procedure.posting",
|
|
"kind": "PROCEDURE",
|
|
"name": "Проведение",
|
|
"qualified_name": "ObjectModule.Проведение",
|
|
}
|
|
],
|
|
"forms": [
|
|
{
|
|
"lineage_id": "lineage.form.main",
|
|
"kind": "FORM",
|
|
"name": "ФормаДокумента",
|
|
"qualified_name": "Документ.ЗаказПокупателя.ФормаДокумента",
|
|
}
|
|
],
|
|
"commands": [
|
|
{
|
|
"lineage_id": "lineage.command.post",
|
|
"kind": "COMMAND",
|
|
"name": "Провести",
|
|
"qualified_name": "Документ.ЗаказПокупателя.ФормаДокумента.Провести",
|
|
}
|
|
],
|
|
"attributes": [
|
|
{
|
|
"lineage_id": "lineage.attribute.counterparty",
|
|
"kind": "ATTRIBUTE",
|
|
"name": "Контрагент",
|
|
"qualified_name": "Документ.ЗаказПокупателя.Контрагент",
|
|
}
|
|
],
|
|
"tabular_sections": [
|
|
{
|
|
"lineage_id": "lineage.tabular.goods",
|
|
"kind": "TABULAR_SECTION",
|
|
"name": "Товары",
|
|
"qualified_name": "Документ.ЗаказПокупателя.Товары",
|
|
}
|
|
],
|
|
"tabular_section_columns": {
|
|
"lineage.tabular.goods": [
|
|
{
|
|
"lineage_id": "lineage.attribute.item",
|
|
"kind": "ATTRIBUTE",
|
|
"name": "Номенклатура",
|
|
"qualified_name": "Документ.ЗаказПокупателя.Товары.Номенклатура",
|
|
}
|
|
]
|
|
},
|
|
"roles": [
|
|
{
|
|
"lineage_id": "lineage.role.manager",
|
|
"kind": "ROLE",
|
|
"name": "Менеджер",
|
|
"qualified_name": "Роль.Менеджер",
|
|
}
|
|
],
|
|
"role_access": [
|
|
{
|
|
"role": {
|
|
"lineage_id": "lineage.role.manager",
|
|
"kind": "ROLE",
|
|
"name": "Менеджер",
|
|
"qualified_name": "Роль.Менеджер",
|
|
},
|
|
"permissions": {"read": "true", "write": "true", "post": "true"},
|
|
}
|
|
],
|
|
"callees": [],
|
|
"query_tables": [],
|
|
"writes": [
|
|
{
|
|
"lineage_id": "lineage.register.stock",
|
|
"kind": "REGISTER",
|
|
"name": "ОстаткиТоваров",
|
|
"qualified_name": "РегистрНакопления.ОстаткиТоваров",
|
|
}
|
|
],
|
|
}
|
|
|
|
monkeypatch.setattr(main, "_neo4j_object_impact_query", fake_query)
|
|
client = TestClient(app)
|
|
|
|
response = client.get(
|
|
"/projects/demo/graph/neo4j/objects/impact/Документ.ЗаказПокупателя"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["object"]["name"] == "ЗаказПокупателя"
|
|
assert payload["modules"][0]["name"] == "ObjectModule"
|
|
assert payload["routines"][0]["name"] == "Проведение"
|
|
assert payload["attributes"][0]["name"] == "Контрагент"
|
|
assert payload["tabular_sections"][0]["name"] == "Товары"
|
|
assert payload["tabular_section_columns"]["lineage.tabular.goods"][0]["name"] == "Номенклатура"
|
|
assert payload["roles"][0]["name"] == "Менеджер"
|
|
assert payload["role_access"][0]["permissions"]["post"] == "true"
|
|
assert payload["writes"][0]["name"] == "ОстаткиТоваров"
|
|
|
|
|
|
def test_neo4j_object_impact_endpoint_returns_404(monkeypatch):
|
|
async def fake_query(project_id: str, object_name: str):
|
|
return None
|
|
|
|
monkeypatch.setattr(main, "_neo4j_object_impact_query", fake_query)
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/projects/demo/graph/neo4j/objects/impact/НеСуществует")
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_neo4j_object_ui_endpoint_contract(monkeypatch):
|
|
async def fake_query(project_id: str, object_name: str):
|
|
assert project_id == "demo"
|
|
assert object_name == "Документ.ЗаказПокупателя"
|
|
return {
|
|
"object": {
|
|
"lineage_id": "lineage.document.order",
|
|
"kind": "DOCUMENT",
|
|
"name": "ЗаказПокупателя",
|
|
"qualified_name": "Документ.ЗаказПокупателя",
|
|
},
|
|
"forms": [
|
|
{
|
|
"form": {
|
|
"lineage_id": "lineage.form.order",
|
|
"kind": "FORM",
|
|
"name": "ФормаДокумента",
|
|
"qualified_name": "Документ.ЗаказПокупателя.ФормаДокумента",
|
|
},
|
|
"commands": [
|
|
{
|
|
"lineage_id": "lineage.command.post",
|
|
"kind": "COMMAND",
|
|
"name": "Провести",
|
|
"qualified_name": "Документ.ЗаказПокупателя.ФормаДокумента.Провести",
|
|
}
|
|
],
|
|
"elements": [],
|
|
"command_handlers": {
|
|
"lineage.command.post": {
|
|
"lineage_id": "lineage.procedure.post",
|
|
"kind": "PROCEDURE",
|
|
"name": "ПровестиКоманда",
|
|
"qualified_name": "ObjectModule.ПровестиКоманда",
|
|
}
|
|
},
|
|
}
|
|
],
|
|
}
|
|
|
|
monkeypatch.setattr(main, "_neo4j_object_ui_query", fake_query)
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/projects/demo/graph/neo4j/objects/ui/Документ.ЗаказПокупателя")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["object"]["name"] == "ЗаказПокупателя"
|
|
assert payload["forms"][0]["commands"][0]["name"] == "Провести"
|
|
assert next(iter(payload["forms"][0]["command_handlers"].values()))["name"] == "ПровестиКоманда"
|
|
|
|
|
|
def test_neo4j_object_attributes_endpoint_contract(monkeypatch):
|
|
async def fake_query(project_id: str, object_name: str):
|
|
assert project_id == "demo"
|
|
assert object_name == "Документ.ЗаказПокупателя"
|
|
return [
|
|
{
|
|
"lineage_id": "lineage.attribute.counterparty",
|
|
"kind": "ATTRIBUTE",
|
|
"name": "Контрагент",
|
|
"qualified_name": "Документ.ЗаказПокупателя.Контрагент",
|
|
}
|
|
]
|
|
|
|
monkeypatch.setattr(main, "_neo4j_object_attributes_query", fake_query)
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/projects/demo/graph/neo4j/objects/attributes/Документ.ЗаказПокупателя")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["results"][0]["name"] == "Контрагент"
|
|
|
|
|
|
def test_neo4j_object_schema_endpoint_contract(monkeypatch):
|
|
async def fake_query(project_id: str, object_name: str):
|
|
assert project_id == "demo"
|
|
assert object_name == "Документ.ЗаказПокупателя"
|
|
return {
|
|
"object": {
|
|
"lineage_id": "lineage.document.order",
|
|
"kind": "DOCUMENT",
|
|
"name": "ЗаказПокупателя",
|
|
"qualified_name": "Документ.ЗаказПокупателя",
|
|
},
|
|
"attributes": [
|
|
{
|
|
"lineage_id": "lineage.attribute.counterparty",
|
|
"kind": "ATTRIBUTE",
|
|
"name": "Контрагент",
|
|
"qualified_name": "Документ.ЗаказПокупателя.Контрагент",
|
|
}
|
|
],
|
|
"tabular_sections": [
|
|
{
|
|
"tabular_section": {
|
|
"lineage_id": "lineage.tabular.goods",
|
|
"kind": "TABULAR_SECTION",
|
|
"name": "Товары",
|
|
"qualified_name": "Документ.ЗаказПокупателя.Товары",
|
|
},
|
|
"columns": [
|
|
{
|
|
"lineage_id": "lineage.attribute.item",
|
|
"kind": "ATTRIBUTE",
|
|
"name": "Номенклатура",
|
|
"qualified_name": "Документ.ЗаказПокупателя.Товары.Номенклатура",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
|
|
monkeypatch.setattr(main, "_neo4j_object_schema_query", fake_query)
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/projects/demo/graph/neo4j/objects/schema/Документ.ЗаказПокупателя")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["object"]["name"] == "ЗаказПокупателя"
|
|
assert payload["attributes"][0]["name"] == "Контрагент"
|
|
assert payload["tabular_sections"][0]["columns"][0]["name"] == "Номенклатура"
|
|
|
|
|
|
def test_neo4j_object_tabular_sections_endpoint_contract(monkeypatch):
|
|
async def fake_query(project_id: str, object_name: str):
|
|
assert project_id == "demo"
|
|
assert object_name == "Документ.ЗаказПокупателя"
|
|
return [
|
|
{
|
|
"lineage_id": "lineage.tabular.goods",
|
|
"kind": "TABULAR_SECTION",
|
|
"name": "Товары",
|
|
"qualified_name": "Документ.ЗаказПокупателя.Товары",
|
|
}
|
|
]
|
|
|
|
monkeypatch.setattr(main, "_neo4j_object_tabular_sections_query", fake_query)
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/projects/demo/graph/neo4j/objects/tabular-sections/Документ.ЗаказПокупателя")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["results"][0]["name"] == "Товары"
|
|
|
|
|
|
def test_neo4j_object_tabular_section_columns_endpoint_contract(monkeypatch):
|
|
async def fake_query(project_id: str, object_name: str):
|
|
assert project_id == "demo"
|
|
assert object_name == "Документ.ЗаказПокупателя"
|
|
return [
|
|
{
|
|
"tabular_section": {
|
|
"lineage_id": "lineage.tabular.goods",
|
|
"kind": "TABULAR_SECTION",
|
|
"name": "Товары",
|
|
"qualified_name": "Документ.ЗаказПокупателя.Товары",
|
|
},
|
|
"columns": [
|
|
{
|
|
"lineage_id": "lineage.attribute.item",
|
|
"kind": "ATTRIBUTE",
|
|
"name": "Номенклатура",
|
|
"qualified_name": "Документ.ЗаказПокупателя.Товары.Номенклатура",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
|
|
monkeypatch.setattr(main, "_neo4j_object_tabular_section_columns_query", fake_query)
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/projects/demo/graph/neo4j/objects/tabular-sections/Документ.ЗаказПокупателя/columns")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()[0]["tabular_section"]["name"] == "Товары"
|
|
assert response.json()[0]["columns"][0]["name"] == "Номенклатура"
|
|
|
|
|
|
def test_neo4j_access_endpoints_contract(monkeypatch):
|
|
async def fake_object_access(project_id: str, object_name: str):
|
|
assert project_id == "demo"
|
|
assert object_name == "Документ.ЗаказПокупателя"
|
|
return {
|
|
"object": {
|
|
"lineage_id": "lineage.document.order",
|
|
"kind": "DOCUMENT",
|
|
"name": "ЗаказПокупателя",
|
|
"qualified_name": "Документ.ЗаказПокупателя",
|
|
},
|
|
"grants": [
|
|
{
|
|
"role": {
|
|
"lineage_id": "lineage.role.manager",
|
|
"kind": "ROLE",
|
|
"name": "Менеджер",
|
|
"qualified_name": "Роль.Менеджер",
|
|
},
|
|
"permissions": {"read": "true", "write": "true"},
|
|
}
|
|
],
|
|
}
|
|
|
|
async def fake_role_access(project_id: str, role_name: str):
|
|
assert project_id == "demo"
|
|
assert role_name == "Роль.Менеджер"
|
|
return {
|
|
"role": {
|
|
"lineage_id": "lineage.role.manager",
|
|
"kind": "ROLE",
|
|
"name": "Менеджер",
|
|
"qualified_name": "Роль.Менеджер",
|
|
},
|
|
"objects": [
|
|
{
|
|
"lineage_id": "lineage.document.order",
|
|
"kind": "DOCUMENT",
|
|
"name": "ЗаказПокупателя",
|
|
"qualified_name": "Документ.ЗаказПокупателя",
|
|
}
|
|
],
|
|
"grants": [
|
|
{
|
|
"object": {
|
|
"lineage_id": "lineage.document.order",
|
|
"kind": "DOCUMENT",
|
|
"name": "ЗаказПокупателя",
|
|
"qualified_name": "Документ.ЗаказПокупателя",
|
|
},
|
|
"permissions": {"post": "true"},
|
|
}
|
|
],
|
|
}
|
|
|
|
monkeypatch.setattr(main, "_neo4j_object_access_query", fake_object_access)
|
|
monkeypatch.setattr(main, "_neo4j_role_access_query", fake_role_access)
|
|
client = TestClient(app)
|
|
|
|
object_response = client.get(
|
|
"/projects/demo/graph/neo4j/access/objects/Документ.ЗаказПокупателя/roles"
|
|
)
|
|
role_response = client.get("/projects/demo/graph/neo4j/access/roles/Роль.Менеджер/objects")
|
|
|
|
assert object_response.status_code == 200
|
|
assert object_response.json()["grants"][0]["role"]["name"] == "Менеджер"
|
|
assert role_response.status_code == 200
|
|
assert role_response.json()["objects"][0]["name"] == "ЗаказПокупателя"
|