from __future__ import annotations import json import sys from pathlib import Path 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 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_lock_plan_maps_child_metadata_to_development_owner() -> None: result = repository_control.lock_plan({"object": "РегистрСведений.Настройки.Реквизит.Код"}) assert result["status"] == "ready" assert result["lock_objects"] == ["РегистрСведений.Настройки"] 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["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}) assert confirmed["status"] == "manual_confirmed" 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 pending = repository_control.lock_request_status({"request_id": requested["request_id"]}) assert pending["status"] == "pending_user_lock" confirmed = repository_control.confirm_manual_lock({ "base_id": "base", "request_id": requested["request_id"], "objects": ["Справочник.Подмена"], "user_confirmed_locked": True, }) assert confirmed["objects"] == ["РегистрСведений.Настройки"] assert confirmed["automatically_verified"] is False 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_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") result = adapter_1c_server.call_method("repository.status", {"base_id": "base"}) assert result["status"] == "configured" assert result["repository"]["backend"] == "direct" 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_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"] == "blocked" assert result["execution"]["status"] == "external_1c_disabled" assert called is False