Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
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 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
|
||||
Reference in New Issue
Block a user