858 lines
47 KiB
Python
858 lines
47 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "plugins" / "1c" / "connector"))
|
|
|
|
import repository_control # noqa: E402
|
|
import adapter_1c_server # noqa: E402
|
|
|
|
|
|
def configured_base(monkeypatch, tmp_path: Path, backend: str = "karman_bridge") -> None:
|
|
config = {
|
|
"base": {
|
|
"server": "sql",
|
|
"database": "db",
|
|
"repository": {
|
|
"backend": backend,
|
|
"designer_path": "designer.exe",
|
|
"endpoint": "tcp://configured.example:15420/configured-repository",
|
|
"bridge_id": "configured-bridge",
|
|
"infobase": {"server": "onec/ib"},
|
|
"repository_user": "repo-user",
|
|
"repository_password_env": "TEST_REPOSITORY_PASSWORD",
|
|
},
|
|
}
|
|
}
|
|
monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(config))
|
|
monkeypatch.setenv("TEST_REPOSITORY_PASSWORD", "secret-value")
|
|
monkeypatch.setenv("ONEC_REPOSITORY_STATE_FILE", str(tmp_path / "locks.json"))
|
|
|
|
|
|
def configured_layers(monkeypatch, tmp_path: Path) -> None:
|
|
config = {
|
|
"base": {
|
|
"server": "sql", "database": "db", "user": "reader",
|
|
"development_layers": {
|
|
"base": {"repository": {"mode": "manual"}, "support": {"mode": "rules", "rules": {"Документ.Заказ": "vendor_not_editable", "Документ.Заказ.Форма.ФормаДокумента": "vendor_editable_support_preserved"}}},
|
|
"extension:11111111-2222-3333-4444-555555555555": {"repository": {"mode": "none"}, "support": {"mode": "none"}},
|
|
"extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee": {"repository": {"mode": "manual"}, "support": {"mode": "editable"}},
|
|
},
|
|
}
|
|
}
|
|
monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(config))
|
|
monkeypatch.setenv("ONEC_REPOSITORY_STATE_FILE", str(tmp_path / "locks.json"))
|
|
|
|
|
|
def test_repositoryless_base_inherits_test_write_profile_to_discovered_extension(monkeypatch, tmp_path: Path) -> None:
|
|
config = {
|
|
"upo_test": {
|
|
"server": "sql", "database": "upo_test", "user": "reader",
|
|
"development_layers": {
|
|
"base": {
|
|
"repository": {"mode": "none", "connection_state": "not_configured"},
|
|
"support": {"mode": "unknown"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
extension_guid = "11111111-2222-3333-4444-555555555555"
|
|
monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(config))
|
|
monkeypatch.setenv("ONEC_REPOSITORY_STATE_FILE", str(tmp_path / "locks.json"))
|
|
|
|
payload = {"base_id": "upo_test", "extension_guid": extension_guid}
|
|
repository, repository_error = repository_control.repository_config("upo_test", f"extension:{extension_guid}")
|
|
|
|
assert repository_error is None
|
|
assert repository["mode"] == "none"
|
|
assert repository["inherited_from_layer"] == "base"
|
|
assert repository_control.write_gate(payload)["allowed"] is True
|
|
support = repository_control.support_gate(payload)
|
|
assert support["allowed"] is True
|
|
assert support["status"] == "not_on_support_inherited_no_repository"
|
|
|
|
|
|
def test_repository_policy_is_not_inherited_from_real_base_to_extension(monkeypatch, tmp_path: Path) -> None:
|
|
configured_layers(monkeypatch, tmp_path)
|
|
|
|
repository, error = repository_control.repository_config(
|
|
"base", "extension:99999999-2222-3333-4444-555555555555"
|
|
)
|
|
|
|
assert repository is None
|
|
assert error["status"] == "layer_not_configured"
|
|
|
|
|
|
def test_repository_state_migrates_once_to_local_sqlite(monkeypatch, tmp_path: Path) -> None:
|
|
legacy_path = tmp_path / "locks.json"
|
|
state_path = tmp_path / "adapter-state.sqlite"
|
|
legacy = {
|
|
"requests": {
|
|
"rreq-legacy": {
|
|
"base_id": "base",
|
|
"layer_id": "base",
|
|
"status": "cancelled",
|
|
"created_at": 1.0,
|
|
}
|
|
},
|
|
"sessions": {},
|
|
"audit": [{"event": "legacy_event", "time": 1.0, "base_id": "base"}],
|
|
}
|
|
legacy_path.write_text(json.dumps(legacy), encoding="utf-8")
|
|
original = legacy_path.read_bytes()
|
|
monkeypatch.setenv("ONEC_REPOSITORY_STATE_FILE", str(legacy_path))
|
|
monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(state_path))
|
|
|
|
state = repository_control._read_state()
|
|
|
|
assert state["requests"]["rreq-legacy"]["status"] == "cancelled"
|
|
assert legacy_path.read_bytes() == original
|
|
with sqlite3.connect(state_path) as conn:
|
|
assert conn.execute(
|
|
"SELECT value FROM adapter_state_meta WHERE key='legacy_repository_state_imported'"
|
|
).fetchone()[0] == "1"
|
|
assert int(conn.execute(
|
|
"SELECT value FROM adapter_state_meta WHERE key='adapter_state_schema_version'"
|
|
).fetchone()[0]) >= 2
|
|
assert conn.execute("SELECT COUNT(*) FROM repository_lock_requests").fetchone()[0] == 1
|
|
assert conn.execute("SELECT COUNT(*) FROM repository_lock_events").fetchone()[0] == 1
|
|
|
|
|
|
def test_repository_backend_and_endpoint_come_only_from_base_settings(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
config, error = repository_control.repository_config("base")
|
|
assert error is None
|
|
assert config["backend"] == "karman_bridge"
|
|
assert config["endpoint"] == "tcp://configured.example:15420/configured-repository"
|
|
public = repository_control.status({"base_id": "base"})
|
|
assert public["repository"]["bridge_id"] == "configured-bridge"
|
|
assert "secret-value" not in json.dumps(public)
|
|
|
|
|
|
def test_http_runner_profile_does_not_require_local_designer_or_credentials(monkeypatch, tmp_path: Path) -> None:
|
|
config = {"base": {"repository": {"backend": "karman_bridge", "runner": {"kind": "http", "url": "http://runner:8121", "token_env": "TOKEN"}}}}
|
|
monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(config))
|
|
configured, error = repository_control.repository_config("base")
|
|
assert error is None
|
|
assert configured["runner"]["kind"] == "http"
|
|
assert "designer_path" not in configured
|
|
|
|
|
|
def test_activation_debug_probe_calls_dedicated_http_endpoint_without_execution(
|
|
monkeypatch,
|
|
) -> None:
|
|
captured = {}
|
|
|
|
class Response:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
def read(self):
|
|
return json.dumps(
|
|
{
|
|
"status": "ready",
|
|
"runner": {
|
|
"kind": "local",
|
|
"reachable": True,
|
|
"designer_available": True,
|
|
"infobase_selector_configured": True,
|
|
},
|
|
"operation": {
|
|
"kind": "/UpdateDBCfg",
|
|
"execution_supported": False,
|
|
},
|
|
"execution": {
|
|
"mode": "debug",
|
|
"performed": False,
|
|
"designer_started": False,
|
|
},
|
|
"debug_acceptance": {
|
|
"accepted": True,
|
|
"request_id": "actreq-" + "1" * 32,
|
|
"fingerprint": "a" * 64,
|
|
"receipt": "b" * 64,
|
|
},
|
|
}
|
|
).encode("utf-8")
|
|
|
|
def fake_urlopen(request, timeout):
|
|
captured["url"] = request.full_url
|
|
captured["body"] = json.loads(request.data.decode("utf-8"))
|
|
captured["authorization"] = request.headers.get("Authorization")
|
|
captured["timeout"] = timeout
|
|
return Response()
|
|
|
|
monkeypatch.setenv("ACTIVATION_RUNNER_TOKEN", "runner-secret")
|
|
monkeypatch.setattr(repository_control.urllib.request, "urlopen", fake_urlopen)
|
|
|
|
result = repository_control.activation_debug_probe(
|
|
"upo_test",
|
|
{
|
|
"runner": {
|
|
"kind": "http",
|
|
"url": "http://runner:8121",
|
|
"token_env": "ACTIVATION_RUNNER_TOKEN",
|
|
}
|
|
},
|
|
layer="base_saved_state",
|
|
timeout_seconds=7,
|
|
request_id="actreq-" + "1" * 32,
|
|
fingerprint="a" * 64,
|
|
)
|
|
|
|
assert result["status"] == "ready"
|
|
assert captured == {
|
|
"url": "http://runner:8121/configuration/activation/debug",
|
|
"body": {
|
|
"base_id": "upo_test",
|
|
"layer": "base_saved_state",
|
|
"mode": "debug",
|
|
"request_id": "actreq-" + "1" * 32,
|
|
"fingerprint": "a" * 64,
|
|
},
|
|
"authorization": "Bearer runner-secret",
|
|
"timeout": 7,
|
|
}
|
|
assert result["execution"]["performed"] is False
|
|
assert result["execution"]["designer_started"] is False
|
|
assert result["debug_acceptance"]["receipt"] == "b" * 64
|
|
|
|
|
|
def test_lock_plan_maps_child_metadata_to_development_owner() -> None:
|
|
result = repository_control.lock_plan({"object": "РегистрСведений.Настройки.Реквизит.Код"})
|
|
assert result["status"] == "ready"
|
|
assert result["scope_status"] == "resolved"
|
|
assert result["lock_objects"] == ["РегистрСведений.Настройки"]
|
|
assert result["native_lock_state"] == "unknown"
|
|
assert result["native_lock_verification"] == "not_available_via_infobase_sql"
|
|
assert result["capture_required"] is None
|
|
|
|
|
|
def test_manual_lock_confirmation_is_scoped_and_not_reported_as_verified(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
values = json.loads(__import__("os").environ["ONEC_SQL_BASES_JSON"])
|
|
values["base"]["repository"]["lock_mode"] = "manual"
|
|
monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(values))
|
|
|
|
planned = repository_control.lock_plan({"base_id": "base", "object": "Справочник.Товары.МодульОбъекта"})
|
|
assert planned["workflow"] == "manual"
|
|
assert planned["next_method"] == "repository.lock.request"
|
|
assert planned["user_action"]["action"] == "confirm_lock_in_configurator"
|
|
assert "уже захвачены" in planned["user_action"]["message"]
|
|
assert planned["user_action"]["objects"] == ["Справочник.Товары"]
|
|
requested = repository_control.create_lock_request({"base_id": "base", "object": "Справочник.Товары"})
|
|
confirmed = repository_control.confirm_manual_lock({"base_id": "base", "request_id": requested["request_id"], "user_confirmed_locked": True, "confirmed_repository_user": "repo-user"})
|
|
assert confirmed["status"] == "manual_confirmed"
|
|
assert confirmed["capture_state"] == "manually_confirmed"
|
|
assert confirmed["native_lock_state"] == "unknown"
|
|
assert confirmed["native_lock_verification"] == "not_available_via_infobase_sql"
|
|
assert confirmed["automatically_verified"] is False
|
|
verified = repository_control.verify({"lock_session_id": confirmed["lock_session_id"]})
|
|
assert verified["status"] == "manual_confirmation_unverified"
|
|
assert repository_control.write_gate({"base_id": "base", "lock_session_id": confirmed["lock_session_id"], "repository_object": "Справочник.Товары"})["allowed"] is True
|
|
assert repository_control.write_gate({"base_id": "base", "lock_session_id": confirmed["lock_session_id"], "repository_object": "Справочник.Другой"})["allowed"] is False
|
|
|
|
|
|
def test_manual_lock_request_stays_pending_until_user_confirms_exact_saved_scope(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
values = json.loads(__import__("os").environ["ONEC_SQL_BASES_JSON"])
|
|
values["base"]["repository"]["lock_mode"] = "manual"
|
|
monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(values))
|
|
|
|
requested = repository_control.create_lock_request({"base_id": "base", "object": "РегистрСведений.Настройки.Реквизит.Код"})
|
|
assert requested["status"] == "pending_user_lock"
|
|
assert requested["objects"] == ["РегистрСведений.Настройки"]
|
|
assert requested["automatically_locked"] is False
|
|
assert requested["next_call"] == {
|
|
"method": "repository.lock.confirm",
|
|
"params": {"base_id": "base", "request_id": requested["request_id"], "user_confirmed_locked": True},
|
|
}
|
|
pending = repository_control.lock_request_status({"request_id": requested["request_id"]})
|
|
assert pending["status"] == "pending_user_lock"
|
|
assert pending["base_id"] == "base"
|
|
assert pending["objects"] == ["РегистрСведений.Настройки"]
|
|
assert pending["native_lock_state"] == "unknown"
|
|
assert pending["next_call"] == requested["next_call"]
|
|
|
|
incomplete = repository_control.confirm_manual_lock({"base_id": "base", "request_id": requested["request_id"]})
|
|
assert incomplete["status"] == "confirmation_required"
|
|
assert incomplete["error"] == "user_confirmed_locked_required"
|
|
assert incomplete["required_fields"] == ["base_id", "request_id", "user_confirmed_locked"]
|
|
assert incomplete["next_call"] == requested["next_call"]
|
|
assert incomplete["sql_resolution"] == []
|
|
|
|
confirmed = repository_control.confirm_manual_lock({
|
|
"base_id": "base",
|
|
"request_id": requested["request_id"],
|
|
"objects": ["Справочник.Подмена"],
|
|
"user_confirmed_locked": True,
|
|
"confirmed_repository_user": "repo-user",
|
|
})
|
|
assert confirmed["objects"] == ["РегистрСведений.Настройки"]
|
|
assert confirmed["automatically_verified"] is False
|
|
assert confirmed["write_context"]["lock_session_id"] == confirmed["lock_session_id"]
|
|
assert confirmed["write_context"]["repository_object"] == "РегистрСведений.Настройки"
|
|
completed = repository_control.lock_request_status({"request_id": requested["request_id"]})
|
|
assert completed["status"] == "confirmed_by_user"
|
|
|
|
closed = repository_control.close_manual_lock({"lock_session_id": confirmed["lock_session_id"], "user_confirmed_released": True})
|
|
assert closed["status"] == "closed"
|
|
assert closed["request_id"] == requested["request_id"]
|
|
completed = repository_control.lock_request_status({"request_id": requested["request_id"]})
|
|
assert completed["status"] == "closed"
|
|
assert completed["request"]["closed_at"] > completed["request"]["confirmed_at"]
|
|
closed_again = repository_control.close_manual_lock({"lock_session_id": confirmed["lock_session_id"], "user_confirmed_released": True})
|
|
assert closed_again["status"] == "closed"
|
|
assert closed_again["request_id"] == requested["request_id"]
|
|
assert repository_control.write_gate({"base_id": "base", "lock_session_id": confirmed["lock_session_id"], "repository_object": "РегистрСведений.Настройки"})["allowed"] is False
|
|
|
|
|
|
def test_pending_request_can_be_cancelled_and_old_state_expires(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
values = json.loads(__import__("os").environ["ONEC_SQL_BASES_JSON"])
|
|
values["base"]["repository"]["lock_mode"] = "manual"
|
|
monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(values))
|
|
request = repository_control.create_lock_request({"base_id": "base", "object": "Справочник.Товары"})
|
|
cancelled = repository_control.cancel_lock_request({"request_id": request["request_id"], "confirm_cancel": True})
|
|
assert cancelled["status"] == "cancelled"
|
|
|
|
old = repository_control.create_lock_request({"base_id": "base", "object": "Справочник.Склады"})
|
|
state = repository_control._read_state()
|
|
state["requests"][old["request_id"]]["created_at"] = 0
|
|
repository_control._write_state(state)
|
|
monkeypatch.setenv("ONEC_REPOSITORY_REQUEST_TTL_SECONDS", "60")
|
|
assert repository_control.lock_request_status({"request_id": old["request_id"]})["status"] == "expired"
|
|
|
|
|
|
def test_adapter_resolves_lock_request_object_against_live_sql(monkeypatch) -> None:
|
|
calls = []
|
|
|
|
def fake_get_object(kind, name, **kwargs):
|
|
calls.append((kind, name, kwargs["base_id"]))
|
|
return {"status": "ok", "object": {"kind_ru": "РегистрСведений", "name": name, "guid": "guid"}}
|
|
|
|
monkeypatch.setattr(adapter_1c_server, "get_object", fake_get_object)
|
|
normalized, error = adapter_1c_server.validate_repository_request_objects_sql({
|
|
"base_id": "upo_test",
|
|
"object": "РегистрСведений.Настройки.Реквизит.Код",
|
|
})
|
|
assert error is None
|
|
assert calls == [("InformationRegister", "Настройки", "upo_test")]
|
|
assert normalized["objects"] == ["РегистрСведений.Настройки"]
|
|
|
|
|
|
def test_form_lock_scope_is_resolved_before_plan_request_and_confirmation(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638"
|
|
|
|
def fake_get_object(kind, name, **kwargs):
|
|
assert kind == "Document"
|
|
assert name == "тл_Планировщик"
|
|
return {"status": "ok", "object": {"kind": "Document", "name": "тл_Планировщик", "guid": "owner-guid"}}
|
|
|
|
monkeypatch.setattr(adapter_1c_server, "get_object", fake_get_object)
|
|
def fake_metadata_object_forms(payload):
|
|
assert "extension_guid" not in payload
|
|
return {"status": "ok", "forms": [{"guid": form_guid, "name": "ФормаДокументаНовая"}]}
|
|
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_object_forms", fake_metadata_object_forms)
|
|
reference = "Document.тл_Планировщик.Form.ФормаДокументаНовая"
|
|
planned = adapter_1c_server.call_method("repository.lock.plan", {"base_id": "base", "objects": [reference]})
|
|
assert planned["status"] == "ready"
|
|
assert planned["lock_objects"] == [reference]
|
|
resolution = planned["sql_resolution"][0]
|
|
assert resolution["requested_object"] == reference
|
|
assert resolution["resolved_object"] == reference
|
|
assert resolution["form_guid"] == form_guid
|
|
assert resolution["owner"] == {"kind": "Document", "name": "тл_Планировщик", "guid": "owner-guid"}
|
|
assert resolution["table"] == "Config"
|
|
assert resolution["source"] == "live_sql_form_owner"
|
|
assert resolution["attempts"][-1] == {"stage": "form", "table": "Config", "status": "ok"}
|
|
request = adapter_1c_server.call_method("repository.lock.request", {"base_id": "base", "objects": [reference]})
|
|
assert request["status"] == "pending_user_lock"
|
|
assert request["sql_resolution"] == planned["sql_resolution"]
|
|
confirmed = repository_control.confirm_manual_lock({
|
|
"base_id": "base", "request_id": request["request_id"], "user_confirmed_locked": True,
|
|
"confirmed_repository_user": "repo-user",
|
|
})
|
|
assert confirmed["status"] == "manual_confirmed"
|
|
assert repository_control.write_gate({"base_id": "base", "lock_session_id": confirmed["lock_session_id"], "repository_object": reference})["allowed"] is True
|
|
mismatch = repository_control.write_gate({"base_id": "base", "lock_session_id": confirmed["lock_session_id"], "repository_object": "Document.тл_Планировщик.Form.Другая"})
|
|
assert mismatch["status"] == "blocked_repository_scope_mismatch"
|
|
assert mismatch["error"] == "lock_object_mismatch"
|
|
|
|
|
|
@pytest.mark.parametrize("kind", ["Catalog", "DataProcessor", "Report"])
|
|
def test_owned_form_lock_resolver_supports_other_owner_kinds(monkeypatch, tmp_path: Path, kind: str) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
monkeypatch.setattr(adapter_1c_server, "get_object", lambda actual_kind, name, **kwargs: {"status": "ok", "object": {"kind": actual_kind, "name": name, "guid": "owner-guid"}})
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_object_forms", lambda payload: {"status": "ok", "forms": [{"guid": "11111111-2222-3333-4444-555555555555", "name": "Основная"}]})
|
|
result = adapter_1c_server.call_method("repository.lock.plan", {"base_id": "base", "object": f"{kind}.Объект.Form.Основная"})
|
|
assert result["status"] == "ready"
|
|
assert result["lock_objects"] == [f"{kind}.Объект.Form.Основная"]
|
|
|
|
|
|
def test_owned_extension_form_lock_discovers_its_sql_layer(monkeypatch, tmp_path: Path) -> None:
|
|
configured_layers(monkeypatch, tmp_path)
|
|
extension_guid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
reference = "Document.Заказ.Form.Основная"
|
|
calls: list[str | None] = []
|
|
|
|
def fake_resolve(**kwargs):
|
|
guid = kwargs.get("extension_guid")
|
|
calls.append(guid)
|
|
if guid != extension_guid:
|
|
return None, {"status": "not_found", "error": "repository_form_not_found_in_sql"}
|
|
return {
|
|
"requested_object": reference,
|
|
"resolved_object": reference,
|
|
"form_guid": "11111111-2222-3333-4444-555555555555",
|
|
"owner": {"kind": "Document", "name": "Заказ", "guid": "owner-guid"},
|
|
"table": "ConfigCAS",
|
|
"source": "live_sql_form_owner",
|
|
"extension_guid": extension_guid,
|
|
}, None
|
|
|
|
monkeypatch.setattr(adapter_1c_server, "repository_resolve_owned_form_sql", fake_resolve)
|
|
monkeypatch.setattr(adapter_1c_server, "list_extensions", lambda payload: {
|
|
"status": "ok", "extensions": [{"guid": extension_guid}],
|
|
})
|
|
result = adapter_1c_server.call_method("repository.lock.plan", {"base_id": "base", "object": reference})
|
|
assert result["status"] == "ready"
|
|
assert result["layer_id"] == f"extension:{extension_guid}"
|
|
assert result["sql_resolution"][0]["source"] == "live_sql_extension_form_owner"
|
|
assert calls == [None, extension_guid]
|
|
|
|
|
|
def test_form_guid_lock_scope_uses_cached_owner_only_after_live_validation(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638"
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_form_owner_cache_lookup", lambda *args, **kwargs: {"owner": {"kind": "Document", "name": "тл_Планировщик"}, "form": {"name": "ФормаДокументаНовая"}})
|
|
monkeypatch.setattr(adapter_1c_server, "get_object", lambda kind, name, **kwargs: {"status": "ok", "object": {"kind": kind, "name": name, "guid": "owner-guid"}})
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_object_forms", lambda payload: {"status": "ok", "forms": [{"guid": guid, "name": "ФормаДокументаНовая"}]})
|
|
result = adapter_1c_server.call_method("repository.lock.plan", {"base_id": "base", "object": f"Form.{guid}"})
|
|
assert result["status"] == "ready"
|
|
assert result["lock_objects"] == ["Document.тл_Планировщик.Form.ФормаДокументаНовая"]
|
|
|
|
|
|
def test_form_guid_lock_scope_falls_back_to_live_sql_owner_scan(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638"
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_form_owner_cache_lookup", lambda *args, **kwargs: None)
|
|
monkeypatch.setattr(adapter_1c_server, "list_objects", lambda kind, **kwargs: {"status": "ok", "objects": [{"kind": "Document", "name": "тл_Планировщик", "guid": "owner-guid"}] if kind == "Document" else []})
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_object_related", lambda payload: {"status": "ok", "related": [{"category": "Form", "identity": {"guid": guid, "name": "ФормаДокументаНовая"}}]})
|
|
monkeypatch.setattr(adapter_1c_server, "get_object", lambda kind, name, **kwargs: {"status": "ok", "object": {"kind": kind, "name": name, "guid": "owner-guid"}})
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_object_forms", lambda payload: {"status": "ok", "forms": [{"guid": guid, "name": "ФормаДокументаНовая"}]})
|
|
result = adapter_1c_server.call_method("repository.lock.plan", {"base_id": "base", "object": f"Form.{guid}"})
|
|
assert result["status"] == "ready"
|
|
assert result["lock_objects"] == ["Document.тл_Планировщик.Form.ФормаДокументаНовая"]
|
|
|
|
|
|
def test_missing_owned_form_is_rejected_by_lock_plan(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
monkeypatch.setattr(adapter_1c_server, "get_object", lambda kind, name, **kwargs: {"status": "ok", "object": {"kind": kind, "name": name, "guid": "owner-guid"}})
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_object_forms", lambda payload: {"status": "ok", "forms": []})
|
|
result = adapter_1c_server.call_method("repository.lock.plan", {"base_id": "base", "object": "Document.тл_Планировщик.Form.НетТакой"})
|
|
assert result["status"] == "not_found"
|
|
assert result["error"] == "repository_form_not_found_in_sql"
|
|
|
|
|
|
def test_structural_lock_plan_requires_confirmation() -> None:
|
|
result = repository_control.lock_plan({"operation": "delete", "object": "Справочник.Склады"})
|
|
assert result["status"] == "needs_confirmation"
|
|
|
|
|
|
def test_write_gate_requires_adapter_lock_for_configured_base(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
assert repository_control.write_gate({"base_id": "base"})["status"] == "needs_repository_lock"
|
|
|
|
|
|
def test_adapter_dispatch_exposes_repository_status(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path, backend="direct")
|
|
monkeypatch.setenv("ONEC_ADAPTER_ENABLE_EXTERNAL_1C", "true")
|
|
result = adapter_1c_server.call_method("repository.status", {"base_id": "base"})
|
|
assert result["status"] == "configured"
|
|
assert result["repository"]["backend"] == "direct"
|
|
|
|
|
|
def test_repository_confirmation_help_exposes_machine_readable_contract() -> None:
|
|
result = adapter_1c_server.call_method("help.methods", {"method": "repository.lock.confirm"})
|
|
schema = result["methods"][0]["input_schema"]
|
|
assert schema["required"] == ["base_id", "request_id", "user_confirmed_locked"]
|
|
assert schema["example"]["user_confirmed_locked"] is True
|
|
assert "lock_session_id" in schema["notes"][-1]
|
|
|
|
|
|
def test_sql_only_repository_status_does_not_claim_repository_connection(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
monkeypatch.delenv("ONEC_ADAPTER_ENABLE_EXTERNAL_1C", raising=False)
|
|
result = repository_control.status({"base_id": "base", "probe": True})
|
|
assert result["status"] == "manual_confirmation_required"
|
|
assert result["connected"] is False
|
|
assert result["available"] is None
|
|
assert result["sql_lock_evidence"]["native_lock_state"] == "unknown"
|
|
assert result["sql_lock_evidence"]["excluded_sources"]["_ConfigChngR"] == "exchange_plan_change_registration"
|
|
|
|
|
|
def test_repository_policy_is_selected_per_base_or_extension_guid(monkeypatch, tmp_path: Path) -> None:
|
|
configured_layers(monkeypatch, tmp_path)
|
|
base = repository_control.write_gate({"base_id": "base"})
|
|
no_repository = repository_control.write_gate({"base_id": "base", "extension_guid": "11111111-2222-3333-4444-555555555555"})
|
|
extension = repository_control.write_gate({"base_id": "base", "extension_guid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"})
|
|
assert base["status"] == "needs_repository_lock"
|
|
assert base["layer_id"] == "base"
|
|
assert no_repository == {"required": False, "allowed": True, "status": "repository_not_connected", "layer_id": "extension:11111111-2222-3333-4444-555555555555"}
|
|
assert extension["status"] == "needs_repository_lock"
|
|
assert extension["layer_id"] == "extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
missing = repository_control.write_gate({"base_id": "base", "extension_guid": "99999999-2222-3333-4444-555555555555"})
|
|
assert missing["status"] == "blocked_repository_configuration"
|
|
assert missing["problem"]["status"] == "layer_not_configured"
|
|
|
|
|
|
def test_repository_connection_state_is_per_resolved_configuration_layer(monkeypatch, tmp_path: Path) -> None:
|
|
configured_layers(monkeypatch, tmp_path)
|
|
values = json.loads(__import__("os").environ["ONEC_SQL_BASES_JSON"])
|
|
layers = values["base"]["development_layers"]
|
|
layers["base"]["repository"].update({"mode": "automatic", "connection_state": "unavailable", "repository_user": "repo-user", "runner": {"kind": "http", "url": "http://runner.invalid"}})
|
|
layers["extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"]["repository"]["connection_state"] = "unknown"
|
|
monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(values))
|
|
|
|
base = repository_control.lock_plan({"base_id": "base", "object": "ОбщийМодуль.Тест"})
|
|
assert base["workflow"] == "manual"
|
|
assert base["status"] == "ready"
|
|
assert repository_control.status({"base_id": "base"})["status"] == "manual_confirmation_required"
|
|
extension = repository_control.write_gate({
|
|
"base_id": "base",
|
|
"target": {"origin": {"extension": {"guid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}}},
|
|
})
|
|
assert extension["layer_id"] == "extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
assert extension["status"] == "blocked_repository_connection_unknown"
|
|
|
|
|
|
def test_manual_confirmation_uses_repository_user_saved_for_the_layer(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
values = json.loads(__import__("os").environ["ONEC_SQL_BASES_JSON"])
|
|
values["base"]["repository"].update({"lock_mode": "manual", "connection_state": "unavailable"})
|
|
monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(values))
|
|
request = repository_control.create_lock_request({"base_id": "base", "object": "Справочник.Товары"})
|
|
assert request["repository_user"] == "repo-user"
|
|
assert request["repository_user_source"] == "layer_connection_setting"
|
|
confirmed_without_repeating_user = repository_control.confirm_manual_lock({
|
|
"base_id": "base", "request_id": request["request_id"], "user_confirmed_locked": True,
|
|
})
|
|
assert confirmed_without_repeating_user["status"] == "manual_confirmed"
|
|
assert confirmed_without_repeating_user["repository_user"] == "repo-user"
|
|
|
|
request = repository_control.create_lock_request({"base_id": "base", "object": "Справочник.Товары"})
|
|
mismatch = repository_control.confirm_manual_lock({"base_id": "base", "request_id": request["request_id"], "user_confirmed_locked": True, "confirmed_repository_user": "other"})
|
|
assert mismatch["error"] == "repository_user_mismatch"
|
|
confirmed = repository_control.confirm_manual_lock({"base_id": "base", "request_id": request["request_id"], "user_confirmed_locked": True, "confirmed_repository_user": "repo-user"})
|
|
assert confirmed["repository_user"] == "repo-user"
|
|
|
|
|
|
def test_repository_confirmation_cannot_cross_extension_layers(monkeypatch, tmp_path: Path) -> None:
|
|
configured_layers(monkeypatch, tmp_path)
|
|
guid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
request = repository_control.create_lock_request({"base_id": "base", "extension_guid": guid, "object": "ОбщийМодуль.МодульРасширения"})
|
|
confirmed = repository_control.confirm_manual_lock({"base_id": "base", "extension_guid": guid, "request_id": request["request_id"], "user_confirmed_locked": True, "confirmed_repository_user": "operator"})
|
|
assert repository_control.write_gate({"base_id": "base", "extension_guid": guid, "lock_session_id": confirmed["lock_session_id"], "repository_object": "ОбщийМодуль.МодульРасширения"})["allowed"] is True
|
|
mismatch = repository_control.write_gate({"base_id": "base", "lock_session_id": confirmed["lock_session_id"], "repository_object": "ОбщийМодуль.МодульРасширения"})
|
|
assert mismatch["status"] == "blocked_repository_lock_session"
|
|
|
|
|
|
def test_support_gate_uses_exact_target_scope_independently_from_repository_scope(monkeypatch, tmp_path: Path) -> None:
|
|
configured_layers(monkeypatch, tmp_path)
|
|
parent = repository_control.support_gate({"base_id": "base", "canonical_path": "Документ.Заказ"})
|
|
form = repository_control.support_gate({"base_id": "base", "canonical_path": "Документ.Заказ.Форма.ФормаДокумента"})
|
|
attribute = repository_control.support_gate({"base_id": "base", "canonical_path": "Документ.Заказ.Реквизит.Комментарий"})
|
|
extension = repository_control.support_gate({"base_id": "base", "extension_guid": "11111111-2222-3333-4444-555555555555", "canonical_path": "ОбщийМодуль.Тест"})
|
|
assert parent["allowed"] is False
|
|
assert form["allowed"] is True
|
|
assert form["matched_scope"] == "Документ.Заказ.Форма.ФормаДокумента"
|
|
assert attribute["allowed"] is False
|
|
assert extension["status"] == "not_on_support"
|
|
|
|
|
|
def test_write_preflight_reports_repository_lock_requirement(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
monkeypatch.setattr(
|
|
adapter_1c_server,
|
|
"metadata_write_plan",
|
|
lambda payload: {
|
|
"allowed": True,
|
|
"status": "ready",
|
|
"path_resolution": {},
|
|
"route": {"apply_method": "metadata.module.write_apply", "write_surface": "saved_state"},
|
|
"required_guards": [],
|
|
"problems": [],
|
|
},
|
|
)
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_write_preflight_saved_target", lambda payload, plan: {})
|
|
result = adapter_1c_server.metadata_write_preflight({"base_id": "base"})
|
|
assert result["allowed"] is False
|
|
assert result["status"] == "needs_repository_lock"
|
|
assert result["repository"]["backend"] == "karman_bridge"
|
|
|
|
|
|
def test_write_preflight_uses_resolved_origin_layer_not_a_client_selector(monkeypatch, tmp_path: Path) -> None:
|
|
configured_layers(monkeypatch, tmp_path)
|
|
values = json.loads(__import__("os").environ["ONEC_SQL_BASES_JSON"])
|
|
values["base"]["development_layers"]["extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"]["repository"]["connection_state"] = "unknown"
|
|
monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(values))
|
|
monkeypatch.setattr(
|
|
adapter_1c_server,
|
|
"metadata_write_plan",
|
|
lambda payload: {
|
|
"allowed": True, "status": "ready", "path_resolution": {},
|
|
"target": {"origin": {"extension": {"guid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}}},
|
|
"route": {}, "required_guards": [], "problems": [],
|
|
},
|
|
)
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_write_preflight_saved_target", lambda payload, plan: {})
|
|
monkeypatch.setattr(adapter_1c_server, "live_sql_support_gate", lambda payload: {"allowed": True, "status": "ok"})
|
|
result = adapter_1c_server.metadata_write_preflight({"base_id": "base"})
|
|
assert result["repository"]["layer_id"] == "extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
assert result["status"] == "blocked_repository_connection_unknown"
|
|
|
|
|
|
def test_form_module_write_gate_context_is_resolved_from_live_sql(monkeypatch) -> None:
|
|
form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638"
|
|
reference = "Document.тл_Планировщик.Form.ФормаДокументаНовая"
|
|
monkeypatch.setattr(adapter_1c_server, "repository_resolve_form_guid_sql", lambda **kwargs: ({
|
|
"resolved_object": reference,
|
|
"form_guid": form_guid,
|
|
"owner": {"kind": "Document", "name": "тл_Планировщик", "guid": "owner-guid"},
|
|
}, None))
|
|
context = adapter_1c_server.resolve_write_gate_context({
|
|
"base_id": "neft", "module_id": f"Config:{form_guid}.0",
|
|
"repository_object": "Document.Другая.Форма.Другая",
|
|
})
|
|
assert context["repository_object"] == reference
|
|
assert context["support_object_guid"] == form_guid
|
|
assert context["owner_resolution"]["layer_id"] == "base"
|
|
|
|
|
|
def test_confirm_write_context_can_be_forwarded_under_repository_lock() -> None:
|
|
context, error = adapter_1c_server.normalize_repository_write_context({
|
|
"base_id": "neft",
|
|
"repository_lock": {
|
|
"lock_session_id": "rlock-test",
|
|
"repository_object": "Document.тл_Планировщик.Form.ФормаДокументаНовая",
|
|
"layer_id": "base",
|
|
},
|
|
}, "metadata.write.preflight")
|
|
assert error is None
|
|
assert context["lock_session_id"] == "rlock-test"
|
|
assert context["repository_object"] == "Document.тл_Планировщик.Form.ФормаДокументаНовая"
|
|
assert context["layer_id"] == "base"
|
|
forwarded = adapter_1c_server.repository_write_context(context)
|
|
assert forwarded["layer_id"] == "base"
|
|
assert forwarded["lock_session_id"] == "rlock-test"
|
|
|
|
_context, conflict = adapter_1c_server.normalize_repository_write_context({
|
|
"lock_session_id": "rlock-one",
|
|
"write_context": {"lock_session_id": "rlock-two"},
|
|
}, "code.write")
|
|
assert conflict["error"] == "repository_lock_context_conflict"
|
|
|
|
|
|
def test_preflight_uses_form_module_context_for_both_gates(monkeypatch) -> None:
|
|
form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638"
|
|
reference = "Document.тл_Планировщик.Form.ФормаДокументаНовая"
|
|
monkeypatch.setattr(adapter_1c_server, "repository_resolve_form_guid_sql", lambda **kwargs: ({
|
|
"resolved_object": reference, "form_guid": form_guid,
|
|
"owner": {"kind": "Document", "name": "тл_Планировщик"},
|
|
}, None))
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_write_plan", lambda payload: {
|
|
"allowed": True, "status": "ready", "path_resolution": {}, "target": {},
|
|
"route": {}, "required_guards": [], "problems": [],
|
|
})
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_write_preflight_saved_target", lambda payload, plan: {})
|
|
captured: dict[str, dict] = {}
|
|
def fake_repository_gate(payload):
|
|
captured["repository"] = dict(payload)
|
|
return {"allowed": True, "status": "ready"}
|
|
|
|
def fake_support_gate(payload):
|
|
captured["support"] = dict(payload)
|
|
return {"allowed": True, "status": "ready"}
|
|
|
|
monkeypatch.setattr(repository_control, "write_gate", fake_repository_gate)
|
|
monkeypatch.setattr(adapter_1c_server, "live_sql_support_gate", fake_support_gate)
|
|
result = adapter_1c_server.metadata_write_preflight({
|
|
"base_id": "neft", "module_id": f"Config:{form_guid}.0",
|
|
"repository_lock": {"lock_session_id": "rlock-test", "repository_object": reference, "layer_id": "base"},
|
|
})
|
|
assert result["owner_resolution"]["repository_object"] == reference
|
|
assert captured["repository"]["repository_object"] == reference
|
|
assert captured["repository"]["lock_session_id"] == "rlock-test"
|
|
assert captured["support"]["support_object_guid"] == form_guid
|
|
|
|
|
|
def test_form_module_preflight_returns_concrete_saved_state_prepare_plan(monkeypatch) -> None:
|
|
form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638"
|
|
monkeypatch.setattr(adapter_1c_server, "repository_resolve_form_guid_sql", lambda **kwargs: ({
|
|
"resolved_object": "Document.тл_Планировщик.Form.ФормаДокументаНовая",
|
|
"form_guid": form_guid, "owner": {"kind": "Document", "name": "тл_Планировщик"},
|
|
}, None))
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_write_plan", lambda payload: {
|
|
"allowed": False, "status": "needs_origin", "path_resolution": {}, "target": {},
|
|
"route": {}, "required_guards": [], "problems": [{"code": "origin_lookup_required"}],
|
|
})
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_write_preflight_saved_target", lambda payload, plan: {})
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_write_resolve_module_target", lambda payload, target, mode: ({
|
|
"error": "saved_state_prepare_required",
|
|
"prepared_module_ref": f"ConfigSave:{form_guid}.0",
|
|
"next_resolution": {
|
|
"method": "metadata.saved_state.prepare",
|
|
"params": {
|
|
"base_id": "neft",
|
|
"layer": "base_saved_state",
|
|
"ref": "Document.тл_Планировщик",
|
|
"mode": "plan",
|
|
},
|
|
},
|
|
"prepare_plan": {"status": "plan_ready"},
|
|
}, None))
|
|
monkeypatch.setattr(repository_control, "write_gate", lambda payload: {"allowed": True, "status": "ready"})
|
|
monkeypatch.setattr(adapter_1c_server, "live_sql_support_gate", lambda payload: {"allowed": True, "status": "ready"})
|
|
result = adapter_1c_server.metadata_write_preflight({"base_id": "neft", "module_id": f"Config:{form_guid}.0"})
|
|
assert result["status"] == "needs_prepare"
|
|
assert result["saved_state"]["prepare_plan"]["status"] == "plan_ready"
|
|
assert result["route"]["auto_resolves_saved_state"] is True
|
|
assert result["route"]["next_resolution"]["method"] == "metadata.saved_state.prepare"
|
|
assert result["route"]["next_resolution"]["params"]["ref"] == "Document.тл_Планировщик"
|
|
assert "payload" not in result["route"]["next_resolution"]
|
|
|
|
|
|
def test_form_module_context_selects_resolved_extension_layer(monkeypatch) -> None:
|
|
form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638"
|
|
extension_guid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
monkeypatch.setattr(adapter_1c_server, "repository_resolve_form_guid_sql", lambda **kwargs: ({
|
|
"resolved_object": "Document.Заказ.Form.Основная", "form_guid": form_guid,
|
|
"owner": {"kind": "Document", "name": "Заказ"}, "extension_guid": extension_guid,
|
|
}, None))
|
|
context = adapter_1c_server.resolve_write_gate_context({"base_id": "base", "module_id": f"Config:{form_guid}.0"})
|
|
assert context["extension_guid"] == extension_guid
|
|
assert repository_control.development_layer_id(context) == f"extension:{extension_guid}"
|
|
assert context["target"]["origin"]["extension"]["guid"] == extension_guid
|
|
|
|
|
|
def test_code_write_carries_form_module_gate_context_into_metadata_write(monkeypatch) -> None:
|
|
form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638"
|
|
reference = "Document.тл_Планировщик.Form.ФормаДокументаНовая"
|
|
monkeypatch.setattr(adapter_1c_server, "repository_resolve_form_guid_sql", lambda **kwargs: ({
|
|
"resolved_object": reference, "form_guid": form_guid,
|
|
"owner": {"kind": "Document", "name": "тл_Планировщик"},
|
|
}, None))
|
|
seen: dict[str, object] = {}
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_write", lambda payload: (seen.update(payload) or {"status": "blocked"}))
|
|
result = adapter_1c_server.code_write({"base_id": "neft", "module_id": f"Config:{form_guid}.0", "routine_text": "Процедура Тест()\nКонецПроцедуры"})
|
|
assert result["owner_resolution"]["repository_object"] == reference
|
|
assert seen["repository_object"] == reference
|
|
assert seen["support_object_guid"] == form_guid
|
|
|
|
|
|
def test_saved_state_prepare_keeps_form_gate_context(monkeypatch) -> None:
|
|
form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638"
|
|
reference = "Document.тл_Планировщик.Form.ФормаДокументаНовая"
|
|
captured: dict[str, object] = {}
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_saved_state_prepare", lambda payload: (
|
|
captured.update(payload) or {"status": "verified"}
|
|
))
|
|
resolved, error = adapter_1c_server.metadata_write_resolve_module_target({
|
|
"base_id": "neft", "module_id": f"Config:{form_guid}.0",
|
|
"allow_sql_saved_state_apply": True,
|
|
"lock_session_id": "rlock-test",
|
|
"repository_object": reference,
|
|
"support_object_guid": form_guid,
|
|
}, {}, "apply")
|
|
assert error is not None
|
|
assert captured["lock_session_id"] == "rlock-test"
|
|
assert captured["repository_object"] == reference
|
|
assert captured["support_object_guid"] == form_guid
|
|
assert resolved["module_ref"] == f"ConfigSave:{form_guid}.0"
|
|
|
|
|
|
def test_non_form_module_id_does_not_receive_form_write_gate_context(monkeypatch) -> None:
|
|
called = False
|
|
|
|
def unexpected(**kwargs):
|
|
nonlocal called
|
|
called = True
|
|
return None, {"status": "not_found"}
|
|
|
|
monkeypatch.setattr(adapter_1c_server, "repository_resolve_form_guid_sql", unexpected)
|
|
context = adapter_1c_server.resolve_write_gate_context({"base_id": "neft", "module_id": "Config:not-a-guid.0"})
|
|
assert "owner_resolution" not in context
|
|
assert called is False
|
|
|
|
|
|
def test_repository_layers_audit_reports_all_discovered_layers(monkeypatch, tmp_path: Path) -> None:
|
|
configured_layers(monkeypatch, tmp_path)
|
|
monkeypatch.setattr(adapter_1c_server, "list_extensions", lambda payload: {"status": "ok", "extensions": [{"name": "Тест", "guid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}]})
|
|
monkeypatch.setattr(adapter_1c_server, "metadata_support_decode", lambda payload: {"status": "ok", "source": {"kind": "live_sql", "support_payload_present": False}, "suppliers": [{"general_mode": "editable"}], "diagnostics": {"source_discovery": "test"}, "object_support": {"status": "not_supported"}})
|
|
result = adapter_1c_server.call_method("repository.layers.audit", {"base_id": "base"})
|
|
assert result["status"] == "ok"
|
|
assert [layer["layer_id"] for layer in result["layers"]] == ["base", "extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"]
|
|
assert result["layers"][1]["safe_next_write_action"] == "request_and_confirm_repository_capture"
|
|
assert result["layers"][0]["support"]["general_modes"] == ["editable"]
|
|
|
|
|
|
def test_direct_apply_method_is_repository_gated(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
result = adapter_1c_server.metadata_module_write_apply({"base_id": "base", "execution_mode": "apply"})
|
|
assert result["status"] == "blocked"
|
|
assert result["error"] == "needs_repository_lock"
|
|
|
|
|
|
def test_lock_and_commit_use_configured_designer_endpoint(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
monkeypatch.setenv("ONEC_ADAPTER_ENABLE_EXTERNAL_1C", "true")
|
|
calls = []
|
|
|
|
def fake_run(config, operation, timeout_seconds):
|
|
calls.append((dict(config), list(operation), timeout_seconds))
|
|
return {"status": "ok", "exit_code": 0, "duration_ms": 1, "output": ""}
|
|
|
|
monkeypatch.setattr(repository_control, "_run_designer", fake_run)
|
|
locked = repository_control.lock({"base_id": "base", "object": "Справочник.Товары", "allow_repository_lock": True})
|
|
assert locked["status"] == "acquired"
|
|
assert calls[0][0]["endpoint"] == "tcp://configured.example:15420/configured-repository"
|
|
assert calls[0][1][0] == "/ConfigurationRepositoryLock"
|
|
assert repository_control.write_gate({"base_id": "base", "lock_session_id": locked["lock_session_id"], "repository_object": "Справочник.Товары"})["allowed"] is True
|
|
mismatch = repository_control.write_gate({"base_id": "base", "lock_session_id": locked["lock_session_id"], "repository_object": "Справочник.Другой"})
|
|
assert mismatch["status"] == "blocked_repository_scope_mismatch"
|
|
|
|
blocked = repository_control.commit({"lock_session_id": locked["lock_session_id"], "comment": "test"})
|
|
assert blocked["error"] == "explicit_repository_commit_required"
|
|
committed = repository_control.commit({"lock_session_id": locked["lock_session_id"], "comment": "test", "allow_repository_commit": True})
|
|
assert committed["status"] == "committed"
|
|
assert calls[1][1][0] == "/ConfigurationRepositoryCommit"
|
|
|
|
|
|
def test_sql_only_mode_never_calls_external_repository_runner(monkeypatch, tmp_path: Path) -> None:
|
|
configured_base(monkeypatch, tmp_path)
|
|
monkeypatch.delenv("ONEC_ADAPTER_ENABLE_EXTERNAL_1C", raising=False)
|
|
called = False
|
|
|
|
def forbidden(*args, **kwargs):
|
|
nonlocal called
|
|
called = True
|
|
return {"status": "ok"}
|
|
|
|
monkeypatch.setattr(repository_control, "_run_designer", forbidden)
|
|
result = repository_control.lock({"base_id": "base", "object": "Справочник.Товары", "allow_repository_lock": True})
|
|
assert result["status"] == "manual_action_required"
|
|
assert called is False
|