fix(hub): 'Sair' do /account-suspended encerra a sessão

Era um <Link href="/login">: a pessoa continuava logada, e o próximo /app
a devolvia ao hub — que agora é onde todo membro de empresa suspensa cai.
Vira <form action={signOut}> com botão de envio, como a irmã
/acesso-revogado.

Provas: page.test.tsx (o botão 'Sair' é type=submit dentro de um form, e
não há link 'Sair'); no e2e da suspensão a atendente clica em Sair, cai no
/login, e /app a manda de volta ao login.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
melgarafael
2026-09-30 03:40:55 -03:00
co-authored by Claude Opus 5.5
parent 5cae8c7fa6
commit b1e17024da
4 changed files with 24 additions and 6 deletions
+9
View File
@@ -49,6 +49,7 @@ vi.mock("@/lib/supabase/admin", () => ({
}),
}));
vi.mock("@/lib/branding/saida", () => ({ emailDeSuporte: async () => "suporte@revenda.test" }));
vi.mock("@/app/actions/auth/signOut", () => ({ signOut: vi.fn() }));
vi.mock("@/app/app/lgpd/requests/RequestsTable", () => ({
RequestsTable: ({ baseDoPedido }: { baseDoPedido?: string }) => <p data-testid="lgpd-lista">{baseDoPedido}</p>,
}));
@@ -123,6 +124,14 @@ describe("/account-suspended: o hub de quem está numa empresa suspensa", () =>
expect(screen.queryByTestId("lgpd-pedido")).toBeNull();
});
it("'Sair' encerra a sessão: é um botão de envio dentro de um form, não um link para /login", async () => {
await montar();
const sair = screen.getByRole("button", { name: "Sair" });
expect(sair).toHaveAttribute("type", "submit");
expect(sair.closest("form")).not.toBeNull();
expect(screen.queryByRole("link", { name: "Sair" })).toBeNull();
});
it("leitura do estado que falha LANÇA: nem hub nem redirect por palpite", async () => {
cena.falhaNaLeitura = true;
await expect(montar()).rejects.toThrow(/account_suspended_status_indisponivel/);
+8 -4
View File
@@ -1,7 +1,7 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { z } from "zod";
import { signOut } from "@/app/actions/auth/signOut";
import { LgpdRequestDetail } from "@/app/app/lgpd/requests/[id]/_client";
import { RequestsTable } from "@/app/app/lgpd/requests/RequestsTable";
import { OutrasOrganizacoes } from "@/app/onboarding/_components/OutrasOrganizacoes";
@@ -109,9 +109,13 @@ export default async function AccountSuspendedPage({
)}
<div className="flex flex-wrap items-center justify-center gap-2 pt-2">
<OutrasOrganizacoes outras={outras} />
<Button asChild variant="outline">
<Link href="/login">{t("Sair")}</Link>
</Button>
{/* Encerra a sessão, como a irmã /acesso-revogado: um link para /login
deixava a pessoa logada, e o próximo /app a trazia de volta aqui. */}
<form action={signOut}>
<Button type="submit" variant="outline">
{t("Sair")}
</Button>
</form>
</div>
</Card>
{administra && (
+1 -1
View File
@@ -1300,7 +1300,7 @@ painel suspendia e reativava empresas.
| A admin da empresa suspensa cai no hub: texto do suporte, pedido de LGPD abrindo no próprio hub, volta para a empresa que opera | idem | CI (PARTE_6) |
| `/app/inbox` volta para o hub; o token `dsk_` da empresa responde 403 `org_suspended` | idem | CI (PARTE_6) |
| A captação por `webhooks/in/[token]` é gravada durante a suspensão; nenhuma `llm_calls` nem mensagem de saída nasce | idem | CI (PARTE_6) |
| A atendente da empresa suspensa lê "Avise o administrador da sua empresa", sem LGPD | idem | CI (PARTE_6) |
| A atendente da empresa suspensa lê "Avise o administrador da sua empresa", sem LGPD; "Sair" encerra a sessão e `/app` manda ao login | idem | CI (PARTE_6) |
| O dono reativa: nada sai em rajada, e a Central mostra o aviso que leva ao Inbox | idem | CI (PARTE_6) |
| O aviso de reativação conta só conversa que não é de grupo, diz só o fato, e a orientação manda procurar nas abas Fila e Automático (numa empresa com IA a conversa sem dono está em Automático) | `tests/invariants/org-suspensa.test.ts`, `lib/ai/inbox-destino.ts` | test:db |
| Hub: empresa que opera volta para `/app`, pedido inválido cai na lista, leitura que falha lança | `app/account-suspended/page.test.tsx` | unit |
+6 -1
View File
@@ -13,7 +13,7 @@
* 4. `/app/inbox` volta para o hub, e o token `dsk_` de B responde 403;
* 5. a captação por `webhooks/in/[token]` é GRAVADA, e nada responde:
* nenhuma `llm_calls`, nenhuma mensagem de saída;
* 6. a atendente de B lê "Avise o administrador", sem LGPD;
* 6. a atendente de B lê "Avise o administrador", sem LGPD, e "Sair" encerra a sessão;
* 7. o dono reativa. Nada sai em rajada, e a Central mostra o item de revisão.
*
* Self-contida: orgs, pessoas, token, fonte e pedido de LGPD nascem pelo
@@ -257,6 +257,11 @@ test("suspender cala B pela tela; o hub atende quem ficou; reativar não solta r
await expect(pAtendente.getByText("Sua conta está suspensa. Avise o administrador da sua empresa.")).toBeVisible();
await expect(pAtendente.getByRole("heading", { name: "Solicitações LGPD" })).toHaveCount(0);
await pAtendente.screenshot({ path: `${EVIDENCIA}/hub-atendente.png`, fullPage: true });
// 'Sair' encerra a sessão: sem ela, /app não tem para onde mandar além do login.
await pAtendente.getByRole("button", { name: "Sair" }).click();
await pAtendente.waitForURL(/\/login/);
await pAtendente.goto("/app");
await expect(pAtendente).toHaveURL(/\/login/);
// ── 7. O dono reativa: nada em rajada, e a Central pede revisão ────────
await page.goto(`/admin/tenants/${orgB}`);