Files
GTI_GERENCIADOR_FISCAL/fiscal/tests/test_zero_touch_installer.py
gti d21a9d1140 Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'
git-subtree-dir: fiscal
git-subtree-mainline: d95165fc8f
git-subtree-split: 102ba84500
2026-09-29 14:43:23 -03:00

219 lines
12 KiB
Python

"""Zero-touch Setup: discovery -> enroll -> secrets in DPAPI stores -> service registered AND started
-> first heartbeat awaited; upgrade preserves identity and drops the legacy local toggle.
The Windows/SCM/Hub edges are faked; the orchestration is real."""
import importlib
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).parents[1]
for extra in (ROOT, ROOT / "installer", ROOT / "agent" / "src"):
sys.path.insert(0, str(extra))
setup_main = importlib.import_module("setup_main")
common = setup_main.common
fbd = setup_main.fbd
ENROLL = {"agent_id": 7, "agent_token": "tok-never-in-env", "hub_url": "https://gtifiscal.gtigrupo.com.br", "agent_version": "1.1.3",
"installation_id": "00000000-0000-4000-8000-0000000000bb"}
@pytest.fixture
def world(tmp_path, monkeypatch):
calls = []
monkeypatch.setenv("GTI_LOG_DIR", str(tmp_path / "logs")) # never write the real ProgramData installer.log from tests
monkeypatch.setenv("GTI_INSTALL_DIR", str(tmp_path / "inst"))
monkeypatch.setattr(setup_main, "MEIPASS", tmp_path / "payload")
payload = tmp_path / "payload"
(payload / "GTIFiscalAgentService").mkdir(parents=True); (payload / "GTIFiscalAgentService" / "GTIFiscalAgentService.exe").write_bytes(b"x")
(payload / "Uninstall.exe").write_bytes(b"x"); (payload / "scripts").mkdir(); (payload / "scripts" / "firebird_write_executor.py").write_text("# x")
(payload / "VERSION").write_text("1.1.3\n")
monkeypatch.setattr(common, "enroll", lambda code, hub=None: calls.append(("enroll", code)) or dict(ENROLL))
monkeypatch.setattr(common, "write_credential", lambda d, t: calls.append(("hub_cred", t)))
monkeypatch.setattr(common, "write_firebird_credential", lambda d, u, p: calls.append(("fb_cred", u)))
monkeypatch.setattr(common, "install_service", lambda d: calls.append(("install_service",)))
monkeypatch.setattr(common, "write_arp_uninstall", lambda d: None)
monkeypatch.setattr(common, "service_running", lambda: False)
monkeypatch.setattr(common, "service_installed", lambda: True)
monkeypatch.setattr(common, "start_and_verify", lambda *a, **k: calls.append(("start",)) or (True, "RUNNING"))
monkeypatch.setattr(common, "ensure_service_stopped", lambda timeout=45.0: calls.append(("stop",)))
monkeypatch.setattr(common, "wait_service_state", lambda wanted, timeout=45: True)
monkeypatch.setattr(common, "wait_for_heartbeat", lambda token, hub, timeout=120: calls.append(("heartbeat_wait",)) or True)
monkeypatch.setattr(common, "audit_event", lambda *a, **k: None)
return calls, tmp_path / "inst"
def one_db(monkeypatch):
cand = fbd.Candidate(r"D:\SGV\loja.fdb", "OK", "SYSDBA", "masterkey")
monkeypatch.setattr(fbd, "discover", lambda **k: fbd.Discovery("SELECTED", [cand]))
def test_zero_touch_install_ends_with_service_started_and_heartbeat_confirmed(world, monkeypatch):
calls, inst = world
one_db(monkeypatch)
result = setup_main.install_flow("AAAA-BBBB-CCCC-DDDD")
names = [c[0] for c in calls]
assert names == ["enroll", "hub_cred", "fb_cred", "install_service", "start", "heartbeat_wait"]
assert result["service_running"] is True and result["heartbeat"] is True and result["firebird_path"] == r"D:\SGV\loja.fdb"
env = (inst / "agent.env").read_text(encoding="utf-8")
assert r"FIREBIRD_DATABASE_PATH=D:\SGV\loja.fdb" in env and "FIREBIRD_WRITES=0" in env and "FISCAL_MODE=DRY_RUN" in env
assert "tok-never-in-env" not in env and "masterkey" not in env and "AGENT_JOBS_ENABLED" not in env
assert "Nenhuma outra ação" in setup_main.final_message(result) # the operator is told nothing else is needed
def test_ambiguous_databases_ask_the_operator_and_nothing_is_enrolled_if_cancelled(world, monkeypatch):
calls, _ = world
a, b = fbd.Candidate(r"D:\a.fdb", "OK", "SYSDBA", "masterkey"), fbd.Candidate(r"D:\b.fdb", "OK", "SYSDBA", "masterkey")
monkeypatch.setattr(fbd, "discover", lambda **k: fbd.Discovery("AMBIGUOUS", [a, b]))
with pytest.raises(RuntimeError, match="seleção do banco Firebird"):
setup_main.install_flow("AAAA-BBBB-CCCC-DDDD") # headless, no chooser: refuses to guess
assert calls == []
with pytest.raises(RuntimeError, match="cancelada"):
setup_main.install_flow("AAAA-BBBB-CCCC-DDDD", chooser=lambda found: None)
assert calls == [] # the install code was NOT consumed by an install that could not complete
def test_operator_choice_is_verified_before_use(world, monkeypatch):
calls, inst = world
a, b = fbd.Candidate(r"D:\a.fdb", "OK", "SYSDBA", "masterkey"), fbd.Candidate(r"D:\b.fdb", "OK", "SYSDBA", "masterkey")
monkeypatch.setattr(fbd, "discover", lambda **k: fbd.Discovery("AMBIGUOUS", [a, b]))
monkeypatch.setattr(fbd, "probe", lambda path, u, p, **k: fbd.Candidate(path, "OK", u, p))
result = setup_main.install_flow("AAAA-BBBB-CCCC-DDDD", chooser=lambda found: {"path": r"D:\b.fdb", "user": "SYSDBA", "password": "masterkey"})
assert result["firebird_path"] == r"D:\b.fdb"
monkeypatch.setattr(fbd, "probe", lambda path, u, p, **k: fbd.Candidate(path, "AUTH_FAILED"))
with pytest.raises(RuntimeError, match="não é utilizável"):
setup_main.install_flow("AAAA-BBBB-CCCC-DDDD", chooser=lambda found: {"path": r"D:\b.fdb", "user": "x", "password": "y"})
def test_upgrade_preserves_identity_drops_local_toggle_and_restarts(world):
calls, inst = world
inst.mkdir(parents=True)
(inst / "credentials.bin").write_bytes(b"blob")
(inst / "agent.env").write_text("GTI_HUB_URL=https://gtifiscal.gtigrupo.com.br\nAGENT_ID=2\nINSTALLATION_ID=bb50884a-5e78-4f26-8cac-804932dae8de\n"
"AGENT_JOBS_ENABLED=0\nAGENT_VERSION=1.1.1\nFIREBIRD_DATABASE_PATH=D:/SGV/x.fdb\nFISCAL_MODE=DRY_RUN\nFIREBIRD_WRITES=0\n", encoding="utf-8")
result = setup_main.upgrade_flow()
env = (inst / "agent.env").read_text(encoding="utf-8")
assert "AGENT_JOBS_ENABLED" not in env and "AGENT_VERSION=1.1.3" in env
assert "AGENT_ID=2" in env and "INSTALLATION_ID=bb50884a-5e78-4f26-8cac-804932dae8de" in env and "FIREBIRD_DATABASE_PATH=D:/SGV/x.fdb" in env
assert [c[0] for c in calls] == ["stop", "install_service", "start"]
assert "enroll" not in [c[0] for c in calls] # NEVER re-enrolls: no new Agent, no duplicate installation
assert result["mode"] == "UPGRADE" and result["installation_id"] == "bb50884a-5e78-4f26-8cac-804932dae8de" and result["service_running"]
def test_second_run_on_enrolled_host_is_an_upgrade_not_a_new_install(world):
calls, inst = world
inst.mkdir(parents=True); (inst / "credentials.bin").write_bytes(b"b"); (inst / "agent.env").write_text("AGENT_ID=2\nINSTALLATION_ID=x\n")
assert common.detect_existing(inst)["agent_id"] == "2"
assert common.detect_existing(inst / "nope") is None
def test_service_is_registered_delayed_auto_with_restart_recovery():
text = (ROOT / "installer" / "install_common.py").read_text(encoding="utf-8")
assert '"start=", "delayed-auto"' in text and 'SERVICE_FAILURE = "restart/60000/restart/60000/restart/60000"' in text
def test_no_write_flags_anywhere_in_zero_touch_installer():
for name in ("setup_main.py", "firebird_discovery.py"):
text = (ROOT / "installer" / name).read_text(encoding="utf-8")
assert "FIREBIRD_WRITES=1" not in text and "AUTO_APPLY" not in text and "UPDATE " not in text.upper().replace("UPDATE_", "")
def test_start_and_verify_retries_after_a_failed_first_start_and_logs_the_cause(monkeypatch, tmp_path):
"""First start after fresh files typically dies with 1053 (SCM 30 s limit); the retry must succeed and installer.log must say why."""
monkeypatch.setenv("GTI_LOG_DIR", str(tmp_path))
monkeypatch.setenv("GTI_INSTALL_DIR", str(tmp_path / "inst"))
state = {"n": 0, "starts": 0}
def fake_state():
return "RUNNING" if state["starts"] >= 2 else "STOPPED"
def fake_sc(args):
if args[0] == "start":
state["starts"] += 1
import subprocess
return subprocess.CompletedProcess(args, 0, "", "")
class Q:
stdout = "WIN32_EXIT_CODE : 1053"
monkeypatch.setattr(common, "service_state", fake_state)
monkeypatch.setattr(common, "_sc", fake_sc)
monkeypatch.setattr(common, "service_process_alive", lambda: True)
monkeypatch.setattr(common.subprocess, "run", lambda *a, **k: Q())
monkeypatch.setattr(common.time, "sleep", lambda s: None)
ok, detail = common.start_and_verify(attempts=3, per_attempt=0.01)
assert ok and detail == "RUNNING" and state["starts"] == 2
log = (tmp_path / "installer.log").read_text(encoding="utf-8")
assert "attempt 1" in log and "1053" in log and "host process alive" in log
def test_start_and_verify_reports_failure_after_all_attempts(monkeypatch, tmp_path):
monkeypatch.setenv("GTI_LOG_DIR", str(tmp_path))
monkeypatch.setenv("GTI_INSTALL_DIR", str(tmp_path / "inst"))
monkeypatch.setattr(common, "service_state", lambda: "STOPPED")
monkeypatch.setattr(common, "_sc", lambda a: None)
monkeypatch.setattr(common.subprocess, "run", lambda *a, **k: type("Q", (), {"stdout": "WIN32_EXIT_CODE : 1053"})())
monkeypatch.setattr(common.time, "sleep", lambda s: None)
ok, detail = common.start_and_verify(attempts=2, per_attempt=0.01)
assert not ok and "1053" in detail
assert "NÃO iniciou" in setup_main.final_message({"service_running": False, "service_detail": detail, "install_dir": "x"})
def test_install_service_tolerates_localized_already_exists(monkeypatch):
"""pt-BR Windows says 'já existe', not 'already exists': the return code (1073) must decide."""
import subprocess
seen = []
def fake_sc(args):
seen.append(args[0])
if args[0] == "create":
return subprocess.CompletedProcess(args, 1073, "[SC] CreateService FALHA 1073: O serviço especificado já existe.", "")
if args[0] == "qc":
return subprocess.CompletedProcess(args, 0, "START_TYPE : 2 AUTO_START (DELAYED)", "")
return subprocess.CompletedProcess(args, 0, "", "")
monkeypatch.setattr(common, "_sc", fake_sc)
common.install_service(Path("C:/x"))
assert seen[:3] == ["create", "config", "config"] and "failure" in seen
def test_failed_upgrade_step_restarts_the_existing_service(world, monkeypatch):
calls, inst = world
inst.mkdir(parents=True)
(inst / "credentials.bin").write_bytes(b"b")
(inst / "agent.env").write_text("AGENT_ID=2\nINSTALLATION_ID=x\nGTI_HUB_URL=https://h\n")
def boom(d):
raise RuntimeError("boom")
monkeypatch.setattr(common, "install_service", boom)
with pytest.raises(RuntimeError, match="boom"):
setup_main.upgrade_flow()
assert ("start",) in calls # the Agent was brought back up despite the failure
def test_fresh_install_archives_a_state_database_left_by_a_previous_installation(tmp_path, monkeypatch):
"""Uninstall keeps local data; a NEW enrollment must not inherit the old installation_id (the Agent refuses the mix and used to sit dead while 'Running')."""
monkeypatch.setenv("GTI_LOG_DIR", str(tmp_path / "logs"))
inst = tmp_path / "inst"
inst.mkdir()
(inst / "agent_state.sqlite").write_bytes(b"old identity")
(inst / "agent_state.sqlite-wal").write_bytes(b"wal")
moved = common.archive_stale_state(inst)
assert len(moved) == 2 and not (inst / "agent_state.sqlite").exists()
assert (inst / moved[0]).read_bytes() in (b"old identity", b"wal") # renamed aside, never deleted
assert common.archive_stale_state(inst) == [] # nothing left to archive
def test_install_flow_archives_stale_state_before_starting_the_service(world, monkeypatch):
calls, inst = world
one_db(monkeypatch)
inst.mkdir(parents=True)
(inst / "agent_state.sqlite").write_bytes(b"identity of a previous installation")
order = []
monkeypatch.setattr(common, "archive_stale_state", lambda d: order.append("archive") or [])
monkeypatch.setattr(common, "start_and_verify", lambda *a, **k: order.append("start") or (True, "RUNNING"))
setup_main.install_flow("AAAA-BBBB-CCCC-DDDD")
assert order == ["archive", "start"]