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:
@@ -0,0 +1,20 @@
|
||||
"""Tenant.external_cnpj (digits) mirrored from GTI Gerenciador for the client selector search.
|
||||
|
||||
Revision ID: 0025_tenant_external_cnpj
|
||||
Revises: 0024_tenant_gerenciador_mirror
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0025_tenant_external_cnpj"
|
||||
down_revision = "0024_tenant_gerenciador_mirror"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column("tenants", sa.Column("external_cnpj", sa.String(20), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column("tenants", "external_cnpj")
|
||||
@@ -74,7 +74,7 @@ def sync_from_gerenciador(session, clients: list[dict]) -> SyncResult:
|
||||
|
||||
if row is None:
|
||||
name = wanted_name if wanted_name not in taken_names else f"{wanted_name} ({code})"
|
||||
row = Tenant(name=name, source=SOURCE, external_client_id=external_id, external_code=code, active=active, synced_at=now)
|
||||
row = Tenant(name=name, source=SOURCE, external_client_id=external_id, external_code=code, external_cnpj=client.get("cnpj"), active=active, synced_at=now)
|
||||
session.add(row)
|
||||
taken_names.add(name)
|
||||
created += 1
|
||||
@@ -89,6 +89,9 @@ def sync_from_gerenciador(session, clients: list[dict]) -> SyncResult:
|
||||
if row.external_code != code:
|
||||
row.external_code = code
|
||||
changed = True
|
||||
if row.external_cnpj != client.get("cnpj"):
|
||||
row.external_cnpj = client.get("cnpj")
|
||||
changed = True
|
||||
if row.active != active:
|
||||
row.active = active
|
||||
changed = True
|
||||
|
||||
@@ -229,6 +229,7 @@ class Tenant(Base):
|
||||
source: Mapped[str] = mapped_column(String(16), default="LOCAL", server_default="LOCAL") # LOCAL | GTI_GERENCIADOR
|
||||
external_client_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
|
||||
external_code: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
external_cnpj: Mapped[str | None] = mapped_column(String(20), nullable=True) # digits only, for the client selector search
|
||||
active: Mapped[bool] = mapped_column(Boolean, default=True, server_default="true")
|
||||
synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
|
||||
+11
-6
@@ -208,6 +208,11 @@ def router(session_factory):
|
||||
if search: query = query.where(Tenant.name.ilike(f"%{search}%"))
|
||||
return session.scalars(query.order_by(Tenant.name)).all()
|
||||
|
||||
def _clients_json(tenants):
|
||||
import json
|
||||
# "<" escaped so the payload can never close the <script> tag it is embedded in.
|
||||
return json.dumps([{"id": t.external_client_id, "name": t.name, "code": t.external_code or "", "cnpj": t.external_cnpj or ""} for t in tenants], ensure_ascii=False).replace("<", "\u003c")
|
||||
|
||||
@routes.get("/agents/new", response_class=HTMLResponse)
|
||||
def new_installation_form(request: Request, search: str | None = None):
|
||||
session, auth_data, redirect = page_auth(request)
|
||||
@@ -215,7 +220,7 @@ def router(session_factory):
|
||||
try:
|
||||
tenants = _active_gerenciador_tenants(session, search)
|
||||
releases = session.scalars(select(AgentRelease).where(AgentRelease.status == "PUBLISHED").order_by(AgentRelease.created_at.desc())).all()
|
||||
return render(request, "agent_new_installation.html", {"tenants": tenants, "releases": releases, "auth": auth_data, "error": None, "search": search or ""})
|
||||
return render(request, "agent_new_installation.html", {"tenants": tenants, "clients_json": _clients_json(tenants), "releases": releases, "auth": auth_data, "error": None, "search": search or ""})
|
||||
finally: session.close()
|
||||
|
||||
@routes.post("/agents/new")
|
||||
@@ -225,24 +230,24 @@ def router(session_factory):
|
||||
try:
|
||||
values = await form_values(request); web_session, admin = auth_data
|
||||
require_csrf(web_session, values.get("csrf_token"))
|
||||
tenant_id = values.get("tenant_id")
|
||||
client_id = values.get("client_id") # Gerenciador's real CLIENTE.ID, never the text shown in the combobox
|
||||
display_name = (values.get("display_name") or "").strip()[:160]
|
||||
expected_hostname = (values.get("expected_hostname") or "").strip()[:255] or None
|
||||
release_id = values.get("release_id")
|
||||
errors = []
|
||||
tenant = session.get(Tenant, int(tenant_id)) if tenant_id and tenant_id.isdigit() else None
|
||||
tenant = session.scalar(select(Tenant).where(Tenant.source == client_sync.SOURCE, Tenant.external_client_id == int(client_id))) if client_id and client_id.isdigit() else None
|
||||
release = session.get(AgentRelease, int(release_id)) if release_id and release_id.isdigit() else None
|
||||
# Server-side, not just a hidden option in the form: a LOCAL or
|
||||
# inactive tenant can never receive a new installation, matching
|
||||
# GTI Gerenciador's own active/inactive rule.
|
||||
if tenant and (tenant.source != client_sync.SOURCE or not tenant.active): tenant = None
|
||||
if not tenant: errors.append("Selecione um cliente ativo válido (cadastrado no GTI Gerenciador).")
|
||||
if tenant and not tenant.active: tenant = None
|
||||
if not tenant: errors.append("Selecione um cliente da lista.")
|
||||
if not display_name: errors.append("Informe um nome para a instalação.")
|
||||
if not release or release.status != "PUBLISHED": errors.append("Selecione uma versão publicada do Agent.")
|
||||
if errors:
|
||||
tenants = _active_gerenciador_tenants(session)
|
||||
releases = session.scalars(select(AgentRelease).where(AgentRelease.status == "PUBLISHED").order_by(AgentRelease.created_at.desc())).all()
|
||||
return render(request, "agent_new_installation.html", {"tenants": tenants, "releases": releases, "auth": auth_data, "error": " ".join(errors), "search": ""}, 400)
|
||||
return render(request, "agent_new_installation.html", {"tenants": tenants, "clients_json": _clients_json(tenants), "releases": releases, "auth": auth_data, "error": " ".join(errors), "search": ""}, 400)
|
||||
enrollment_uuid = str(uuid.uuid4())
|
||||
enr = AgentEnrollment(enrollment_uuid=enrollment_uuid, tenant_id=tenant.id, display_name=display_name, expected_hostname=expected_hostname, release_id=release.id, status="PENDING", created_by=admin.id)
|
||||
session.add(enr)
|
||||
|
||||
@@ -11,12 +11,16 @@
|
||||
{% else %}
|
||||
<form method="post" action="/agents/new">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label>Cliente
|
||||
<input type="text" id="filtro-cliente" placeholder="Buscar cliente..." autocomplete="off" oninput="filtrarClientes(this.value)">
|
||||
<select name="tenant_id" id="select-cliente" size="8" required>
|
||||
{% for tenant in tenants %}<option value="{{ tenant.id }}">{{ tenant.name }}</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<div class="combo-field">
|
||||
<label for="cliente-busca">Cliente</label>
|
||||
<div class="combo" id="combo-cliente">
|
||||
<input type="text" id="cliente-busca" placeholder="Buscar por nome, código ou CNPJ..." autocomplete="off" role="combobox" aria-expanded="false" aria-controls="cliente-lista" aria-autocomplete="list">
|
||||
<input type="hidden" name="client_id" id="cliente-id" value="">
|
||||
<ul id="cliente-lista" class="combo-lista" role="listbox" hidden></ul>
|
||||
</div>
|
||||
<p class="error combo-erro" id="cliente-erro" role="alert" hidden>Selecione um cliente da lista.</p>
|
||||
</div>
|
||||
<script type="application/json" id="clientes-json">{{ clients_json|safe }}</script>
|
||||
<label>Nome da instalação
|
||||
<input name="display_name" placeholder="ex. Servidor Principal" maxlength="160" required>
|
||||
</label>
|
||||
@@ -30,13 +34,72 @@
|
||||
</label>
|
||||
<p><button type="submit">Criar instalação</button></p>
|
||||
</form>
|
||||
<style>
|
||||
.combo { position: relative; }
|
||||
.combo-lista { position: absolute; z-index: 20; left: 0; right: 0; top: 100%; margin: 4px 0 0; padding: 4px; list-style: none; max-height: 280px; overflow-y: auto; background: var(--cor-superficie); border: 1px solid var(--cor-borda); border-radius: 8px; box-shadow: var(--sombra-card); }
|
||||
.combo-lista li { padding: 8px 10px; border-radius: 6px; cursor: pointer; display: flex; flex-direction: column; gap: 2px; }
|
||||
.combo-lista li small { color: var(--cor-texto-secundario); }
|
||||
.combo-lista li:hover, .combo-lista li.ativo { background: var(--cor-superficie-alt); }
|
||||
.combo-lista li.selecionado { background: var(--cor-acento-suave); color: var(--cor-acento-texto); font-weight: 600; }
|
||||
.combo-lista li.vazio { cursor: default; color: var(--cor-texto-secundario); }
|
||||
.combo.invalido input[type=text] { border-color: var(--cor-perigo); }
|
||||
.combo-field { margin-bottom: 12px; }
|
||||
</style>
|
||||
<script>
|
||||
function filtrarClientes(termo) {
|
||||
var alvo = termo.trim().toLowerCase();
|
||||
document.querySelectorAll("#select-cliente option").forEach(function (opcao) {
|
||||
opcao.hidden = alvo.length > 0 && opcao.textContent.toLowerCase().indexOf(alvo) === -1;
|
||||
});
|
||||
(function () {
|
||||
var clientes = JSON.parse(document.getElementById("clientes-json").textContent);
|
||||
var box = document.getElementById("cliente-busca"), hid = document.getElementById("cliente-id");
|
||||
var lista = document.getElementById("cliente-lista"), erro = document.getElementById("cliente-erro");
|
||||
var combo = document.getElementById("combo-cliente"), form = box.form;
|
||||
var visiveis = [], ativo = -1;
|
||||
function norm(t) { return (t || "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase(); }
|
||||
function cnpjFmt(d) {
|
||||
if (!d) return "";
|
||||
if (d.length === 14) return d.replace(/^(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})$/, "$1.$2.$3/$4-$5");
|
||||
if (d.length === 11) return d.replace(/^(\d{3})(\d{3})(\d{3})(\d{2})$/, "$1.$2.$3-$4");
|
||||
return d;
|
||||
}
|
||||
function abrir(v) { lista.hidden = !v; box.setAttribute("aria-expanded", v ? "true" : "false"); }
|
||||
function render() {
|
||||
var q = norm(box.value), qd = q.replace(/\D/g, "");
|
||||
visiveis = clientes.filter(function (c) {
|
||||
if (!q) return true;
|
||||
return norm(c.name).indexOf(q) !== -1 || norm(c.code).indexOf(q) !== -1 || (qd && (c.cnpj || "").indexOf(qd) !== -1);
|
||||
}).slice(0, 100);
|
||||
lista.innerHTML = "";
|
||||
if (!visiveis.length) {
|
||||
var vazio = document.createElement("li"); vazio.className = "vazio"; vazio.textContent = "Nenhum cliente ativo encontrado."; lista.appendChild(vazio);
|
||||
}
|
||||
visiveis.forEach(function (c, i) {
|
||||
var li = document.createElement("li"); li.setAttribute("role", "option"); li.dataset.i = i;
|
||||
if (String(c.id) === hid.value) li.classList.add("selecionado");
|
||||
var nome = document.createElement("span"); nome.textContent = c.name; li.appendChild(nome);
|
||||
if (c.cnpj) { var sm = document.createElement("small"); sm.textContent = cnpjFmt(c.cnpj); li.appendChild(sm); }
|
||||
li.addEventListener("mousedown", function (e) { e.preventDefault(); escolher(c); });
|
||||
lista.appendChild(li);
|
||||
});
|
||||
ativo = -1; abrir(true);
|
||||
}
|
||||
function destacar(n) {
|
||||
var itens = lista.querySelectorAll("li[role=option]"); if (!itens.length) return;
|
||||
ativo = (n + itens.length) % itens.length;
|
||||
itens.forEach(function (li, i) { li.classList.toggle("ativo", i === ativo); });
|
||||
itens[ativo].scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
function escolher(c) { box.value = c.name; hid.value = c.id; erro.hidden = true; combo.classList.remove("invalido"); abrir(false); }
|
||||
box.addEventListener("input", function () { hid.value = ""; render(); });
|
||||
box.addEventListener("focus", render);
|
||||
box.addEventListener("blur", function () { setTimeout(function () { abrir(false); }, 120); });
|
||||
box.addEventListener("keydown", function (e) {
|
||||
if (e.key === "ArrowDown") { e.preventDefault(); if (lista.hidden) render(); destacar(ativo + 1); }
|
||||
else if (e.key === "ArrowUp") { e.preventDefault(); destacar(ativo - 1); }
|
||||
else if (e.key === "Enter") { if (!lista.hidden && ativo >= 0 && visiveis[ativo]) { e.preventDefault(); escolher(visiveis[ativo]); } else if (!hid.value) { e.preventDefault(); } }
|
||||
else if (e.key === "Escape") { abrir(false); }
|
||||
});
|
||||
form.addEventListener("submit", function (e) {
|
||||
if (!hid.value) { e.preventDefault(); erro.hidden = false; combo.classList.add("invalido"); box.focus(); }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
@@ -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)})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"artifact": "RELEASE_MANIFEST",
|
||||
"GTI_FISCAL_VERSION": "1.1.2",
|
||||
"generated_at": "2026-09-24T01:54:42+00:00",
|
||||
"generated_at": "2026-09-24T11:12:47+00:00",
|
||||
"hub": {
|
||||
"version": "1.1.2",
|
||||
"api_compat": "1.0",
|
||||
@@ -17,10 +17,10 @@
|
||||
"ICMS_CONTEXT_ENGINE_VERSION": "icms-context-v1"
|
||||
},
|
||||
"migrations": {
|
||||
"alembic_head_in_repo": "0024_tenant_gerenciador_mirror",
|
||||
"applied_revision": "0024_tenant_gerenciador_mirror",
|
||||
"applied_revision_source": "production Hub 10.0.10.117 (gti-fiscal-hub-fiscal-hub-1), verified 2026-09-24 (fresh DB == upgrade from 0023, pg_dump schema-identical)",
|
||||
"count": 24
|
||||
"alembic_head_in_repo": "0025_tenant_external_cnpj",
|
||||
"applied_revision": "0025_tenant_external_cnpj",
|
||||
"applied_revision_source": "production Hub 10.0.10.117 (gti-fiscal-hub-fiscal-hub-1), verified 2026-09-24",
|
||||
"count": 25
|
||||
},
|
||||
"datasets_active": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user