fix: searchable client combobox in Nova instalação

Replaces the native list with a combobox (name/code/CNPJ search, accent- and
case-insensitive, keyboard + mouse, custom dropdown, hidden client_id = the
Gerenciador's real CLIENTE.ID). The backend resolves client_id against the
active GTI_GERENCIADOR mirror and rejects free text/unknown ids. Adds
Tenant.external_cnpj (migration 0025) mirrored via the internal endpoint.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-24 08:12:52 -03:00
co-authored by Claude Sonnet 5
parent 3ccef36c50
commit 9f9e06e9e9
9 changed files with 156 additions and 40 deletions
+4 -4
View File
@@ -162,7 +162,7 @@ def test_full_installation_flow_creates_a_working_agent(hub):
release_id = publish_release(factory)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "tenant_id": tenant_id(factory),
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Servidor Principal", "expected_hostname": "SRV-UNIAO",
"release_id": release_id}, follow_redirects=False)
assert created.status_code == 303
@@ -221,7 +221,7 @@ def test_direct_authenticated_download_does_not_need_a_link(hub):
client, factory = hub
release_id = publish_release(factory)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "tenant_id": tenant_id(factory),
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Loja 2", "release_id": release_id}, follow_redirects=False)
installation_uuid = created.headers["location"].rsplit("/", 1)[-1]
downloaded = client.get(f"/agents/pending/{installation_uuid}/download")
@@ -303,7 +303,7 @@ def test_revoked_installation_link_rejected(hub):
client, factory = hub
release_id = publish_release(factory)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "tenant_id": tenant_id(factory),
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Teste", "release_id": release_id}, follow_redirects=False)
uid = created.headers["location"].rsplit("/", 1)[-1]
linked = client.post(f"/agents/pending/{uid}/link", data={"csrf_token": csrf(client)}, follow_redirects=False)
@@ -322,7 +322,7 @@ def test_release_revoked_blocks_download(hub):
client, factory = hub
release_id = publish_release(factory)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "tenant_id": tenant_id(factory),
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Teste", "release_id": release_id}, follow_redirects=False)
uid = created.headers["location"].rsplit("/", 1)[-1]
linked = client.post(f"/agents/pending/{uid}/link", data={"csrf_token": csrf(client)}, follow_redirects=False)
@@ -340,7 +340,7 @@ def test_panel_flow_code_generate_download_and_enroll_with_exe(hub, tmp_path):
client, factory = hub
release_id = publish_exe_release(factory, tmp_path)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "tenant_id": tenant_id(factory),
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Servidor EXE", "release_id": release_id}, follow_redirects=False)
assert created.status_code == 303
installation_uuid = created.headers["location"].rsplit("/", 1)[-1]
@@ -391,7 +391,7 @@ def test_rotation_generates_a_code_that_invalidates_the_previous(hub, tmp_path):
client, factory = hub
release_id = publish_exe_release(factory, tmp_path)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "tenant_id": tenant_id(factory),
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Rotação", "release_id": release_id}, follow_redirects=False)
installation_uuid = created.headers["location"].rsplit("/", 1)[-1]
first = client.post(f"/agents/pending/{installation_uuid}/code", data={"csrf_token": csrf(client)}, follow_redirects=True)
@@ -414,7 +414,7 @@ def test_revoke_clears_code_and_is_audited(hub, tmp_path):
client, factory = hub
release_id = publish_exe_release(factory, tmp_path)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "tenant_id": tenant_id(factory),
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Revoga", "release_id": release_id}, follow_redirects=False)
installation_uuid = created.headers["location"].rsplit("/", 1)[-1]
client.post(f"/agents/pending/{installation_uuid}/code", data={"csrf_token": csrf(client)})
+34 -10
View File
@@ -23,8 +23,8 @@ def hub(tmp_path, monkeypatch):
factory = setup_db()
seed(factory) # tenant "Acme" (LOCAL, no source) + admin "operator"/"a-secure-password"
with factory() as session:
session.add(Tenant(name="Mercado Ativo", source="GTI_GERENCIADOR", external_client_id=1, active=True))
session.add(Tenant(name="Loja Inativa", source="GTI_GERENCIADOR", external_client_id=2, active=False))
session.add(Tenant(name="Mercado Ativo", source="GTI_GERENCIADOR", external_client_id=1, external_code="000001", external_cnpj="11222333000181", active=True))
session.add(Tenant(name="Loja <Inativa>", source="GTI_GERENCIADOR", external_client_id=2, external_cnpj="99888777000166", active=False))
release, _ = publish(session, write_temp_zip(make_release_zip(VERSION), VERSION), version=VERSION)
release_id = release.id
session.commit()
@@ -46,7 +46,7 @@ def test_new_installation_form_lists_only_active_gerenciador_tenants(hub):
login(client)
body = client.get("/agents/new").text
assert "Mercado Ativo" in body
assert "Loja Inativa" not in body # inactive: excluded from the selector
assert "Loja <Inativa>" not in body # inactive: excluded from the selector
assert "Acme" not in body # LOCAL (not from GTI Gerenciador): excluded
@@ -54,8 +54,8 @@ def test_cannot_create_installation_for_an_inactive_tenant(hub):
client, factory, release_id = hub
login(client)
with factory() as session:
inactive_id = session.scalar(select(Tenant).where(Tenant.name == "Loja Inativa")).id
response = client.post("/agents/new", data={"csrf_token": csrf(client), "tenant_id": inactive_id, "display_name": "Teste", "release_id": release_id})
inactive_id = session.scalar(select(Tenant).where(Tenant.name == "Loja <Inativa>")).external_client_id
response = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": inactive_id, "display_name": "Teste", "release_id": release_id})
assert response.status_code == 400 and "ativo" in response.text.lower()
with factory() as session:
assert session.scalar(select(AgentEnrollment)) is None
@@ -65,8 +65,10 @@ def test_cannot_create_installation_for_a_local_tenant(hub):
client, factory, release_id = hub
login(client)
with factory() as session:
local_id = session.scalar(select(Tenant).where(Tenant.name == "Acme")).id
response = client.post("/agents/new", data={"csrf_token": csrf(client), "tenant_id": local_id, "display_name": "Teste", "release_id": release_id})
acme = session.scalar(select(Tenant).where(Tenant.name == "Acme"))
assert acme.source == "LOCAL" and acme.external_client_id is None
local_id = 987654 # a LOCAL tenant has no external id, so nothing can ever match it
response = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": local_id, "display_name": "Teste", "release_id": release_id})
assert response.status_code == 400
with factory() as session:
assert session.scalar(select(AgentEnrollment)) is None
@@ -76,9 +78,31 @@ def test_creates_installation_for_an_active_gerenciador_tenant(hub):
client, factory, release_id = hub
login(client)
with factory() as session:
active_id = session.scalar(select(Tenant).where(Tenant.name == "Mercado Ativo")).id
response = client.post("/agents/new", data={"csrf_token": csrf(client), "tenant_id": active_id, "display_name": "Teste", "release_id": release_id}, follow_redirects=False)
active = session.scalar(select(Tenant).where(Tenant.name == "Mercado Ativo")); active_id, tenant_pk = active.external_client_id, active.id
response = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": active_id, "display_name": "Teste", "release_id": release_id}, follow_redirects=False)
assert response.status_code == 303
with factory() as session:
enr = session.scalar(select(AgentEnrollment))
assert enr is not None and enr.tenant_id == active_id
assert enr is not None and enr.tenant_id == tenant_pk
def test_combobox_markup_and_client_data(hub):
import json, re
client, _factory, _release_id = hub
login(client)
body = client.get("/agents/new").text
assert 'role="combobox"' in body and 'name="client_id"' in body and "<select name=\"tenant_id\"" not in body
assert "Selecione um cliente da lista." in body
data = json.loads(re.search(r'id="clientes-json">(.*?)</script>', body, re.S).group(1))
assert data == [{"id": 1, "name": "Mercado Ativo", "code": "000001", "cnpj": "11222333000181"}] # active only, real external id, cnpj for search
assert "Loja" not in body
def test_free_text_or_unknown_client_id_is_rejected(hub):
client, factory, release_id = hub
login(client)
for bad in ("", "Mercado Ativo", "999", "1; DROP"):
response = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": bad, "display_name": "T", "release_id": release_id})
assert response.status_code == 400 and "Selecione um cliente da lista." in response.text
with factory() as session:
assert session.scalar(select(AgentEnrollment)) is None