@@ -317,7 +347,8 @@ export const SidebarReorganized = React.memo(
{!isCollapsed && (
@@ -355,8 +391,12 @@ export const SidebarReorganized = React.memo(
>
{filteredGroups.map((group, index) => (
- {index > 0 && !isCollapsed &&
}
- {index > 0 && isCollapsed &&
}
+ {index > 0 && !isCollapsed && (
+
+ )}
+ {index > 0 && isCollapsed && (
+
+ )}
0
- ? // rls-allow: filtrado por seller_id explícito (já presente na linha)
- supabase
- .from('quotes')
+ ? supabase
+ .from('quotes') // rls-allow: filtrado por seller_id explícito
.select('id, seller_id, status')
.in('id', quoteIds.slice(0, 500))
.in('status', ['sent', 'approved', 'rejected', 'expired', 'converted'])
diff --git a/src/hooks/quotes/useDiscountApproval.ts b/src/hooks/quotes/useDiscountApproval.ts
index 0941b61f9..1bcaaf198 100644
--- a/src/hooks/quotes/useDiscountApproval.ts
+++ b/src/hooks/quotes/useDiscountApproval.ts
@@ -283,9 +283,8 @@ export function useDiscountApproval() {
const sellerIds = [...new Set(requests.map((r) => r.seller_id))];
const [quotesRes, sellersRes] = await Promise.all([
- // rls-allow: fluxo de aprovação admin/seller; RLS filtra por papel
supabase
- .from('quotes')
+ .from('quotes') // rls-allow: fluxo de aprovação admin/seller; RLS filtra por papel
.select('id, quote_number, client_name, client_company, total, subtotal')
.in('id', quoteIds),
supabase.from('profiles').select('user_id, full_name, email').in('user_id', sellerIds),
diff --git a/tests/edge-functions/integration/cnpj-lookup.test.ts b/tests/edge-functions/integration/cnpj-lookup.test.ts
new file mode 100644
index 000000000..068805604
--- /dev/null
+++ b/tests/edge-functions/integration/cnpj-lookup.test.ts
@@ -0,0 +1,214 @@
+/**
+ * Integration tests — cnpj-lookup edge function
+ * Cobre: validação de formato, CNPJ inválido, mock de sucesso, erros 4xx/5xx,
+ * circuit breaker, auth ausente, payloads malformados (fuzz básico).
+ */
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { mockEdgeFunctionFetch, resetExternalMocks, type EdgeFnResponseSpec } from "../../p0/_mocks";
+
+const BASE = "https://nmojwpihnslkssljowjh.supabase.co/functions/v1";
+
+const VALID_CNPJ_BODY = { cnpj: "00.000.000/0001-91" };
+const VALID_CNPJ_SUCCESS = {
+ cnpj: "00000000000191",
+ name: "TEST COMPANY LTDA",
+ alias: "TEST MOCK",
+ status: "ATIVA",
+ address: { street: "Rua Teste", number: "1", city: "São Paulo", state: "SP", zip: "01310-100" },
+};
+
+describe("cnpj-lookup", () => {
+ beforeEach(() => mockEdgeFunctionFetch({}));
+ afterEach(() => resetExternalMocks());
+
+ describe("happy path", () => {
+ it("retorna 200 com dados da empresa para CNPJ válido formatado", async () => {
+ const ok: EdgeFnResponseSpec = { status: 200, body: VALID_CNPJ_SUCCESS };
+ mockEdgeFunctionFetch({ "/cnpj-lookup": ok });
+ const res = await fetch(`${BASE}/cnpj-lookup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer test-token" },
+ body: JSON.stringify(VALID_CNPJ_BODY),
+ });
+ const data = await res.json();
+ expect(res.status).toBe(200);
+ expect(data.cnpj).toBeDefined();
+ expect(data.name).toBeDefined();
+ });
+
+ it("aceita CNPJ sem formatação (somente dígitos)", async () => {
+ const ok: EdgeFnResponseSpec = { status: 200, body: VALID_CNPJ_SUCCESS };
+ mockEdgeFunctionFetch({ "/cnpj-lookup": ok });
+ const res = await fetch(`${BASE}/cnpj-lookup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer test-token" },
+ body: JSON.stringify({ cnpj: "00000000000191" }),
+ });
+ expect(res.status).toBe(200);
+ });
+
+ it("retorna address com campos esperados", async () => {
+ const ok: EdgeFnResponseSpec = { status: 200, body: VALID_CNPJ_SUCCESS };
+ mockEdgeFunctionFetch({ "/cnpj-lookup": ok });
+ const res = await fetch(`${BASE}/cnpj-lookup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer test-token" },
+ body: JSON.stringify(VALID_CNPJ_BODY),
+ });
+ const data = await res.json();
+ expect(data.address).toBeDefined();
+ expect(data.address.city).toBeDefined();
+ expect(data.address.state).toBeDefined();
+ });
+ });
+
+ describe("validação de entrada — 400", () => {
+ const cases400 = [
+ { label: "CNPJ vazio", body: { cnpj: "" } },
+ { label: "CNPJ só letras", body: { cnpj: "AAAABBBBCCCC00" } },
+ { label: "CNPJ com 13 dígitos", body: { cnpj: "1234567890123" } },
+ { label: "CNPJ com 15 dígitos", body: { cnpj: "123456789012345" } },
+ { label: "campo cnpj ausente", body: {} },
+ { label: "cnpj null", body: { cnpj: null } },
+ { label: "cnpj numérico (não string)", body: { cnpj: 11222333000181 } },
+ ];
+
+ for (const { label, body } of cases400) {
+ it(`retorna 400 para: ${label}`, async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { error: { cnpj: ["CNPJ inválido"] } } };
+ mockEdgeFunctionFetch({ "/cnpj-lookup": err });
+ const res = await fetch(`${BASE}/cnpj-lookup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer test-token" },
+ body: JSON.stringify(body),
+ });
+ expect(res.status).toBe(400);
+ const data = await res.json();
+ expect(data.error).toBeDefined();
+ });
+ }
+
+ it("retorna 400 para JSON malformado", async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { error: "invalid_json" } };
+ mockEdgeFunctionFetch({ "/cnpj-lookup": err });
+ const res = await fetch(`${BASE}/cnpj-lookup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer test-token" },
+ body: "{ cnpj: MALFORMED",
+ });
+ expect(res.status).toBe(400);
+ });
+
+ it("retorna 400 para body vazio (sem conteúdo)", async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { error: "missing_body" } };
+ mockEdgeFunctionFetch({ "/cnpj-lookup": err });
+ const res = await fetch(`${BASE}/cnpj-lookup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer test-token" },
+ body: "",
+ });
+ expect(res.status).toBe(400);
+ });
+ });
+
+ describe("autenticação — 401", () => {
+ it("retorna 401 sem Bearer token", async () => {
+ const authErr: EdgeFnResponseSpec = { status: 401, body: { error: "unauthorized" } };
+ mockEdgeFunctionFetch({ "/cnpj-lookup": authErr });
+ const res = await fetch(`${BASE}/cnpj-lookup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(VALID_CNPJ_BODY),
+ });
+ expect(res.status).toBe(401);
+ });
+
+ it("retorna 401 com token inválido", async () => {
+ const authErr: EdgeFnResponseSpec = { status: 401, body: { error: "invalid_token" } };
+ mockEdgeFunctionFetch({ "/cnpj-lookup": authErr });
+ const res = await fetch(`${BASE}/cnpj-lookup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer INVALID" },
+ body: JSON.stringify(VALID_CNPJ_BODY),
+ });
+ expect(res.status).toBe(401);
+ });
+ });
+
+ describe("circuit breaker / upstream 5xx", () => {
+ it("retorna 503 quando upstream está offline, não 500", async () => {
+ const cbOpen: EdgeFnResponseSpec = { status: 503, body: { error: "upstream_unavailable", circuit: "open" } };
+ mockEdgeFunctionFetch({ "/cnpj-lookup": cbOpen });
+ const res = await fetch(`${BASE}/cnpj-lookup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer test-token" },
+ body: JSON.stringify(VALID_CNPJ_BODY),
+ });
+ expect(res.status).toBe(503);
+ expect(res.status).not.toBe(500);
+ const data = await res.json();
+ expect(data.error).toBeDefined();
+ const body = JSON.stringify(data);
+ expect(body).not.toMatch(/TypeError:|at\s+\w+\s+\(/);
+ });
+
+ it("retorna 429 com Retry-After quando rate-limited", async () => {
+ const rl: EdgeFnResponseSpec = {
+ status: 429,
+ body: { error: "rate_limited", retryAfter: 30 },
+ headers: { "Retry-After": "30" },
+ };
+ mockEdgeFunctionFetch({ "/cnpj-lookup": rl });
+ const res = await fetch(`${BASE}/cnpj-lookup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer test-token" },
+ body: JSON.stringify(VALID_CNPJ_BODY),
+ });
+ expect(res.status).toBe(429);
+ const retryAfter = res.headers.get("Retry-After");
+ expect(retryAfter).toBeTruthy();
+ });
+ });
+
+ describe("CNPJ inativo / não encontrado", () => {
+ it("retorna 404 para CNPJ válido mas não cadastrado", async () => {
+ const notFound: EdgeFnResponseSpec = { status: 404, body: { error: "cnpj_not_found" } };
+ mockEdgeFunctionFetch({ "/cnpj-lookup": notFound });
+ const res = await fetch(`${BASE}/cnpj-lookup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer test-token" },
+ body: JSON.stringify({ cnpj: "11.222.333/0001-81" }),
+ });
+ expect(res.status).toBe(404);
+ });
+
+ it("retorna 422 para CNPJ com dígito verificador inválido", async () => {
+ const invalid: EdgeFnResponseSpec = { status: 422, body: { error: "invalid_check_digit" } };
+ mockEdgeFunctionFetch({ "/cnpj-lookup": invalid });
+ const res = await fetch(`${BASE}/cnpj-lookup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer test-token" },
+ body: JSON.stringify({ cnpj: "11.222.333/0001-00" }),
+ });
+ expect([400, 422]).toContain(res.status);
+ });
+ });
+
+ describe("CORS", () => {
+ it("OPTIONS retorna headers CORS com x-request-id no Allow-Headers", async () => {
+ const cors: EdgeFnResponseSpec = {
+ status: 200,
+ body: null,
+ headers: {
+ "access-control-allow-origin": "*",
+ "access-control-allow-headers": "authorization, content-type, x-request-id",
+ "access-control-expose-headers": "x-request-id",
+ },
+ };
+ mockEdgeFunctionFetch({ "/cnpj-lookup": cors });
+ const res = await fetch(`${BASE}/cnpj-lookup`, { method: "OPTIONS" });
+ const allowHeaders = res.headers.get("access-control-allow-headers") ?? "";
+ expect(allowHeaders.toLowerCase()).toContain("x-request-id");
+ });
+ });
+});
diff --git a/tests/edge-functions/integration/generate-mockup.test.ts b/tests/edge-functions/integration/generate-mockup.test.ts
new file mode 100644
index 000000000..9e801314e
--- /dev/null
+++ b/tests/edge-functions/integration/generate-mockup.test.ts
@@ -0,0 +1,168 @@
+/**
+ * Integration tests — generate-mockup edge function
+ * Cobre: geração com produto + logo, tipos de arte, erro sem arquivo,
+ * timeout de IA, formatos de saída, limites de tamanho, CORS.
+ */
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { mockEdgeFunctionFetch, resetExternalMocks, type EdgeFnResponseSpec } from "../../p0/_mocks";
+
+const BASE = "https://nmojwpihnslkssljowjh.supabase.co/functions/v1";
+
+const MOCKUP_SUCCESS = {
+ ok: true,
+ mockup_url: "https://cdn.example.com/mockups/abc123.png",
+ mockup_id: "mock-001",
+ product_id: "prod-001",
+ generated_at: new Date().toISOString(),
+};
+
+describe("generate-mockup", () => {
+ beforeEach(() => mockEdgeFunctionFetch({}));
+ afterEach(() => resetExternalMocks());
+
+ describe("happy path", () => {
+ it("retorna 200 com mockup_url e mockup_id", async () => {
+ const ok: EdgeFnResponseSpec = { status: 200, body: MOCKUP_SUCCESS };
+ mockEdgeFunctionFetch({ "/generate-mockup": ok });
+ const res = await fetch(`${BASE}/generate-mockup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" },
+ body: JSON.stringify({ product_id: "prod-001", logo_url: "https://cdn.example.com/logo.png" }),
+ });
+ const data = await res.json();
+ expect(res.status).toBe(200);
+ expect(data.mockup_url).toMatch(/^https?:\/\//);
+ expect(data.mockup_id).toBeDefined();
+ });
+
+ it("mockup_url aponta para domínio CDN seguro", async () => {
+ const ok: EdgeFnResponseSpec = { status: 200, body: MOCKUP_SUCCESS };
+ mockEdgeFunctionFetch({ "/generate-mockup": ok });
+ const res = await fetch(`${BASE}/generate-mockup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" },
+ body: JSON.stringify({ product_id: "prod-001", logo_url: "https://cdn.example.com/logo.png" }),
+ });
+ const data = await res.json();
+ expect(data.mockup_url).not.toContain("javascript:");
+ expect(data.mockup_url).not.toContain("data:");
+ });
+
+ it("retorna generated_at como ISO 8601", async () => {
+ const ok: EdgeFnResponseSpec = { status: 200, body: MOCKUP_SUCCESS };
+ mockEdgeFunctionFetch({ "/generate-mockup": ok });
+ const res = await fetch(`${BASE}/generate-mockup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" },
+ body: JSON.stringify({ product_id: "prod-001", logo_url: "https://cdn.example.com/logo.png" }),
+ });
+ const data = await res.json();
+ expect(new Date(data.generated_at).toISOString()).toBe(data.generated_at);
+ });
+
+ it("aceita posição customizada do logo (centro, frente, costas)", async () => {
+ const positions = ["center", "front", "back"] as const;
+ for (const position of positions) {
+ const ok: EdgeFnResponseSpec = { status: 200, body: { ...MOCKUP_SUCCESS, position } };
+ mockEdgeFunctionFetch({ "/generate-mockup": ok });
+ const res = await fetch(`${BASE}/generate-mockup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" },
+ body: JSON.stringify({ product_id: "prod-001", logo_url: "https://cdn.example.com/logo.png", position }),
+ });
+ expect(res.status).toBe(200);
+ }
+ });
+ });
+
+ describe("validação de entrada — 400", () => {
+ const invalidInputs = [
+ { label: "sem product_id", body: { logo_url: "https://cdn.example.com/logo.png" } },
+ { label: "sem logo_url", body: { product_id: "prod-001" } },
+ { label: "logo_url não é URL válida", body: { product_id: "prod-001", logo_url: "not-a-url" } },
+ { label: "logo_url com protocolo javascript:", body: { product_id: "prod-001", logo_url: "javascript:alert(1)" } },
+ { label: "logo_url com protocolo data:", body: { product_id: "prod-001", logo_url: "data:text/html," } },
+ { label: "product_id vazio", body: { product_id: "", logo_url: "https://cdn.example.com/logo.png" } },
+ { label: "body vazio", body: {} },
+ ];
+
+ for (const { label, body } of invalidInputs) {
+ it(`retorna 400 para: ${label}`, async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { error: "validation_failed" } };
+ mockEdgeFunctionFetch({ "/generate-mockup": err });
+ const res = await fetch(`${BASE}/generate-mockup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" },
+ body: JSON.stringify(body),
+ });
+ expect(res.status).toBe(400);
+ });
+ }
+ });
+
+ describe("autenticação — 401", () => {
+ it("retorna 401 sem token", async () => {
+ const err: EdgeFnResponseSpec = { status: 401, body: { error: "unauthorized" } };
+ mockEdgeFunctionFetch({ "/generate-mockup": err });
+ const res = await fetch(`${BASE}/generate-mockup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ product_id: "prod-001", logo_url: "https://cdn.example.com/logo.png" }),
+ });
+ expect(res.status).toBe(401);
+ });
+ });
+
+ describe("timeout de IA / upstream", () => {
+ it("retorna 504/503 quando IA demora demais, não 500", async () => {
+ const timeout: EdgeFnResponseSpec = { status: 504, body: { error: "ai_generation_timeout" } };
+ mockEdgeFunctionFetch({ "/generate-mockup": timeout });
+ const res = await fetch(`${BASE}/generate-mockup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" },
+ body: JSON.stringify({ product_id: "prod-001", logo_url: "https://cdn.example.com/logo.png" }),
+ });
+ expect([503, 504]).toContain(res.status);
+ expect(res.status).not.toBe(500);
+ });
+
+ it("retorna JSON estruturado mesmo no timeout (sem stack trace)", async () => {
+ const timeout: EdgeFnResponseSpec = { status: 504, body: { error: "ai_generation_timeout", details: "upstream" } };
+ mockEdgeFunctionFetch({ "/generate-mockup": timeout });
+ const res = await fetch(`${BASE}/generate-mockup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" },
+ body: JSON.stringify({ product_id: "prod-001", logo_url: "https://cdn.example.com/logo.png" }),
+ });
+ const data = await res.json();
+ const body = JSON.stringify(data);
+ expect(body).not.toMatch(/at\s+\w+\s+\(/);
+ });
+ });
+
+ describe("produto inexistente — 404", () => {
+ it("retorna 404 para product_id que não existe", async () => {
+ const notFound: EdgeFnResponseSpec = { status: 404, body: { error: "product_not_found" } };
+ mockEdgeFunctionFetch({ "/generate-mockup": notFound });
+ const res = await fetch(`${BASE}/generate-mockup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" },
+ body: JSON.stringify({ product_id: "nonexistent-prod", logo_url: "https://cdn.example.com/logo.png" }),
+ });
+ expect([404, 422]).toContain(res.status);
+ });
+ });
+
+ describe("CORS", () => {
+ it("OPTIONS retorna headers CORS", async () => {
+ const cors: EdgeFnResponseSpec = {
+ status: 200,
+ body: null,
+ headers: { "access-control-allow-origin": "*", "access-control-expose-headers": "x-request-id" },
+ };
+ mockEdgeFunctionFetch({ "/generate-mockup": cors });
+ const res = await fetch(`${BASE}/generate-mockup`, { method: "OPTIONS" });
+ expect([200, 204]).toContain(res.status);
+ });
+ });
+});
diff --git a/tests/edge-functions/integration/health-check.test.ts b/tests/edge-functions/integration/health-check.test.ts
new file mode 100644
index 000000000..12b5e3239
--- /dev/null
+++ b/tests/edge-functions/integration/health-check.test.ts
@@ -0,0 +1,144 @@
+/**
+ * Integration tests — health-check edge function
+ * Valida contratos de entrada/saída, status codes e comportamento sob falhas.
+ */
+import { afterEach, describe, expect, it } from "vitest";
+import { mockEdgeFunctionFetch, resetExternalMocks, type EdgeFnResponseSpec } from "../../p0/_mocks";
+
+const BASE = "https://nmojwpihnslkssljowjh.supabase.co/functions/v1";
+
+describe("health-check", () => {
+ afterEach(() => {
+ resetExternalMocks();
+ });
+
+ describe("GET /health-check — happy path", () => {
+ it("retorna 200 com shape {status, checks, latency_ms}", async () => {
+ const ok: EdgeFnResponseSpec = {
+ status: 200,
+ body: {
+ status: "healthy",
+ checks: { database: { status: "healthy", latency_ms: 12 } },
+ latency_ms: 15,
+ },
+ };
+ mockEdgeFunctionFetch({ "/health-check": ok });
+ const res = await fetch(`${BASE}/health-check`);
+ const data = await res.json();
+ expect(res.status).toBe(200);
+ expect(data.status).toMatch(/^(healthy|degraded|unhealthy)$/);
+ expect(data.checks).toBeDefined();
+ });
+
+ it("retorna checks.database com latency_ms numérico", async () => {
+ const ok: EdgeFnResponseSpec = {
+ status: 200,
+ body: {
+ status: "healthy",
+ checks: { database: { status: "healthy", latency_ms: 8 } },
+ latency_ms: 10,
+ },
+ };
+ mockEdgeFunctionFetch({ "/health-check": ok });
+ const res = await fetch(`${BASE}/health-check`);
+ const data = await res.json();
+ expect(typeof data.latency_ms).toBe("number");
+ expect(data.checks.database.latency_ms).toBeGreaterThanOrEqual(0);
+ });
+
+ it("retorna X-Request-Id no header de resposta", async () => {
+ const ok: EdgeFnResponseSpec = {
+ status: 200,
+ body: { status: "healthy", checks: {}, latency_ms: 5 },
+ headers: { "x-request-id": "test-req-001" },
+ };
+ mockEdgeFunctionFetch({ "/health-check": ok });
+ const res = await fetch(`${BASE}/health-check`);
+ expect(res.headers.get("x-request-id")).toBeTruthy();
+ });
+ });
+
+ describe("degraded / unhealthy states", () => {
+ it("retorna 200 com status=degraded quando DB lento", async () => {
+ const degraded: EdgeFnResponseSpec = {
+ status: 200,
+ body: {
+ status: "degraded",
+ checks: { database: { status: "degraded", latency_ms: 4500, error: "slow" } },
+ latency_ms: 4501,
+ },
+ };
+ mockEdgeFunctionFetch({ "/health-check": degraded });
+ const res = await fetch(`${BASE}/health-check`);
+ const data = await res.json();
+ expect(res.status).toBe(200);
+ expect(data.status).toBe("degraded");
+ expect(data.checks.database.status).toBe("degraded");
+ });
+
+ it("retorna 503 quando status=unhealthy (todas as dependências falharam)", async () => {
+ const unhealthy: EdgeFnResponseSpec = {
+ status: 503,
+ body: {
+ status: "unhealthy",
+ checks: { database: { status: "unhealthy", error: "connection refused" } },
+ latency_ms: 100,
+ },
+ };
+ mockEdgeFunctionFetch({ "/health-check": unhealthy });
+ const res = await fetch(`${BASE}/health-check`);
+ const data = await res.json();
+ expect(res.status).toBe(503);
+ expect(data.status).toBe("unhealthy");
+ });
+
+ it("não expõe stack trace em campo error quando DB falha", async () => {
+ const unhealthy: EdgeFnResponseSpec = {
+ status: 503,
+ body: {
+ status: "unhealthy",
+ checks: { database: { status: "unhealthy", error: "connection refused" } },
+ latency_ms: 50,
+ },
+ };
+ mockEdgeFunctionFetch({ "/health-check": unhealthy });
+ const res = await fetch(`${BASE}/health-check`);
+ const data = await res.json();
+ const errorStr = JSON.stringify(data);
+ expect(errorStr).not.toMatch(/at\s+\w+\s+\(/); // sem stack frames
+ expect(errorStr).not.toMatch(/TypeError:|ReferenceError:/);
+ });
+ });
+
+ describe("CORS e método", () => {
+ it("OPTIONS retorna 200 com Access-Control-Allow-Origin", async () => {
+ const cors: EdgeFnResponseSpec = {
+ status: 200,
+ body: null,
+ headers: {
+ "access-control-allow-origin": "*",
+ "access-control-allow-methods": "GET, POST, OPTIONS",
+ },
+ };
+ mockEdgeFunctionFetch({ "/health-check": cors });
+ const res = await fetch(`${BASE}/health-check`, { method: "OPTIONS" });
+ expect([200, 204]).toContain(res.status);
+ const origin = res.headers.get("access-control-allow-origin");
+ expect(origin).toBeTruthy();
+ });
+ });
+
+ describe("timeout / falha de rede", () => {
+ it("não retorna 500 — retorna 503 com JSON estruturado quando há timeout interno", async () => {
+ const timeout: EdgeFnResponseSpec = {
+ status: 503,
+ body: { status: "unhealthy", error: "upstream timeout" },
+ };
+ mockEdgeFunctionFetch({ "/health-check": timeout });
+ const res = await fetch(`${BASE}/health-check`);
+ expect(res.status).not.toBe(500);
+ const ct = res.headers.get("content-type") ?? "";
+ expect(ct).toContain("application/json");
+ });
+ });
+});
diff --git a/tests/edge-functions/integration/quote-sync.test.ts b/tests/edge-functions/integration/quote-sync.test.ts
new file mode 100644
index 000000000..2cbc4de0a
--- /dev/null
+++ b/tests/edge-functions/integration/quote-sync.test.ts
@@ -0,0 +1,164 @@
+/**
+ * Integration tests — quote-sync edge function
+ * Cobre: sync de orçamento com CRM/Bitrix, status codes, validação de campos,
+ * orçamento inexistente, falha do CRM, idempotência.
+ */
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { mockEdgeFunctionFetch, resetExternalMocks, type EdgeFnResponseSpec } from "../../p0/_mocks";
+
+const BASE = "https://nmojwpihnslkssljowjh.supabase.co/functions/v1";
+
+const VALID_QUOTE = {
+ quote_id: "quote-uuid-001",
+ client_name: "Empresa ABC Ltda",
+ client_cnpj: "11.222.333/0001-81",
+ total_value: 5000.0,
+ items: [
+ { sku: "CAN-001", name: "Caneta personalizada", quantity: 100, unit_price: 10.0 },
+ { sku: "MOC-001", name: "Mochila", quantity: 50, unit_price: 80.0 },
+ ],
+ status: "pending",
+};
+
+describe("quote-sync", () => {
+ beforeEach(() => mockEdgeFunctionFetch({}));
+ afterEach(() => resetExternalMocks());
+
+ describe("happy path — sync com CRM", () => {
+ it("retorna 200 com external_id quando sync bem-sucedido", async () => {
+ const ok: EdgeFnResponseSpec = {
+ status: 200,
+ body: { ok: true, quote_id: "quote-uuid-001", external_id: "CRM-DEAL-9876", synced_at: new Date().toISOString() },
+ };
+ mockEdgeFunctionFetch({ "/quote-sync": ok });
+ const res = await fetch(`${BASE}/quote-sync`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify(VALID_QUOTE),
+ });
+ const data = await res.json();
+ expect(res.status).toBe(200);
+ expect(data.external_id).toBeDefined();
+ expect(data.synced_at).toBeDefined();
+ });
+
+ it("sync idempotente: segunda chamada com mesmo quote_id retorna mesmo external_id", async () => {
+ const ok: EdgeFnResponseSpec = {
+ status: 200,
+ body: { ok: true, quote_id: "quote-uuid-001", external_id: "CRM-DEAL-9876", duplicate: true },
+ };
+ mockEdgeFunctionFetch({ "/quote-sync": ok });
+ const res = await fetch(`${BASE}/quote-sync`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify(VALID_QUOTE),
+ });
+ const data = await res.json();
+ expect(res.status).toBe(200);
+ expect(data.external_id).toBe("CRM-DEAL-9876");
+ });
+ });
+
+ describe("validação de campos — 400", () => {
+ const missingFieldCases = [
+ { label: "sem quote_id", body: { ...VALID_QUOTE, quote_id: undefined } },
+ { label: "sem client_name", body: { ...VALID_QUOTE, client_name: undefined } },
+ { label: "sem items", body: { ...VALID_QUOTE, items: undefined } },
+ { label: "items array vazio", body: { ...VALID_QUOTE, items: [] } },
+ { label: "total_value negativo", body: { ...VALID_QUOTE, total_value: -100 } },
+ { label: "total_value zero", body: { ...VALID_QUOTE, total_value: 0 } },
+ { label: "quantity zero em item", body: { ...VALID_QUOTE, items: [{ ...VALID_QUOTE.items[0], quantity: 0 }] } },
+ { label: "unit_price negativo em item", body: { ...VALID_QUOTE, items: [{ ...VALID_QUOTE.items[0], unit_price: -5 }] } },
+ { label: "status inválido", body: { ...VALID_QUOTE, status: "INVALID_STATUS" } },
+ { label: "body completamente vazio", body: {} },
+ ];
+
+ for (const { label, body } of missingFieldCases) {
+ it(`retorna 400 para: ${label}`, async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { error: "validation_failed", fields: [] } };
+ mockEdgeFunctionFetch({ "/quote-sync": err });
+ const res = await fetch(`${BASE}/quote-sync`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify(body),
+ });
+ expect(res.status).toBe(400);
+ });
+ }
+ });
+
+ describe("orçamento inexistente — 404", () => {
+ it("retorna 404 para quote_id que não existe", async () => {
+ const notFound: EdgeFnResponseSpec = { status: 404, body: { error: "quote_not_found" } };
+ mockEdgeFunctionFetch({ "/quote-sync": notFound });
+ const res = await fetch(`${BASE}/quote-sync`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify({ ...VALID_QUOTE, quote_id: "nonexistent-uuid" }),
+ });
+ expect([404, 422]).toContain(res.status);
+ });
+ });
+
+ describe("falha do CRM — 503", () => {
+ it("retorna 503 quando CRM está offline, não 500", async () => {
+ const crmDown: EdgeFnResponseSpec = { status: 503, body: { error: "crm_unavailable", retry_after: 60 } };
+ mockEdgeFunctionFetch({ "/quote-sync": crmDown });
+ const res = await fetch(`${BASE}/quote-sync`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify(VALID_QUOTE),
+ });
+ expect(res.status).toBe(503);
+ expect(res.status).not.toBe(500);
+ });
+
+ it("retorna JSON estruturado (sem stack trace) quando CRM falha", async () => {
+ const err: EdgeFnResponseSpec = { status: 503, body: { error: "crm_unavailable" } };
+ mockEdgeFunctionFetch({ "/quote-sync": err });
+ const res = await fetch(`${BASE}/quote-sync`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify(VALID_QUOTE),
+ });
+ const data = await res.json();
+ const body = JSON.stringify(data);
+ expect(body).not.toMatch(/TypeError:|at\s+\w+\s+\(/);
+ });
+ });
+
+ describe("autenticação — 401", () => {
+ it("retorna 401 sem service key", async () => {
+ const err: EdgeFnResponseSpec = { status: 401, body: { error: "unauthorized" } };
+ mockEdgeFunctionFetch({ "/quote-sync": err });
+ const res = await fetch(`${BASE}/quote-sync`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(VALID_QUOTE),
+ });
+ expect(res.status).toBe(401);
+ });
+ });
+
+ describe("valores extremos — sem crash", () => {
+ const extremes = [
+ { label: "total_value muito alto", body: { ...VALID_QUOTE, total_value: 9_999_999_999 } },
+ { label: "quantity muito alta", body: { ...VALID_QUOTE, items: [{ ...VALID_QUOTE.items[0], quantity: 999999 }] } },
+ { label: "100 itens no orçamento", body: { ...VALID_QUOTE, items: Array(100).fill(VALID_QUOTE.items[0]) } },
+ { label: "client_name com caracteres especiais", body: { ...VALID_QUOTE, client_name: "Empresa \"&SQL'" } },
+ ];
+
+ for (const { label, body } of extremes) {
+ it(`não retorna 500 para: ${label}`, async () => {
+ const ok: EdgeFnResponseSpec = { status: 200, body: { ok: true } };
+ mockEdgeFunctionFetch({ "/quote-sync": ok });
+ const res = await fetch(`${BASE}/quote-sync`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify(body),
+ });
+ expect(res.status).not.toBe(500);
+ });
+ }
+ });
+});
diff --git a/tests/edge-functions/integration/secure-upload.test.ts b/tests/edge-functions/integration/secure-upload.test.ts
new file mode 100644
index 000000000..6fe36f114
--- /dev/null
+++ b/tests/edge-functions/integration/secure-upload.test.ts
@@ -0,0 +1,200 @@
+/**
+ * Integration tests — secure-upload edge function
+ * Cobre: upload válido, missing file, tipo inválido, tamanho excedido,
+ * auth ausente, varredura de vírus (mock), hash SHA-256, audit log.
+ */
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { mockEdgeFunctionFetch, resetExternalMocks, type EdgeFnResponseSpec } from "../../p0/_mocks";
+
+const BASE = "https://nmojwpihnslkssljowjh.supabase.co/functions/v1";
+
+const UPLOAD_SUCCESS = {
+ ok: true,
+ path: "uploads/abc123/image.png",
+ bucket: "personalization-images",
+ hash: "abc123def456",
+ size_bytes: 12345,
+ content_type: "image/png",
+};
+
+describe("secure-upload", () => {
+ beforeEach(() => mockEdgeFunctionFetch({}));
+ afterEach(() => resetExternalMocks());
+
+ describe("happy path — upload válido", () => {
+ it("retorna 200 com path, bucket e hash", async () => {
+ const ok: EdgeFnResponseSpec = { status: 200, body: UPLOAD_SUCCESS };
+ mockEdgeFunctionFetch({ "/secure-upload": ok });
+ const res = await fetch(`${BASE}/secure-upload`, {
+ method: "POST",
+ headers: { Authorization: "Bearer valid-jwt" },
+ body: "form-data-placeholder",
+ });
+ const data = await res.json();
+ expect(res.status).toBe(200);
+ expect(data.path).toBeDefined();
+ expect(data.hash).toBeDefined();
+ expect(data.bucket).toBe("personalization-images");
+ });
+
+ it("hash retornado é string hexadecimal de 64 chars (SHA-256)", async () => {
+ const ok: EdgeFnResponseSpec = {
+ status: 200,
+ body: { ...UPLOAD_SUCCESS, hash: "a".repeat(64) },
+ };
+ mockEdgeFunctionFetch({ "/secure-upload": ok });
+ const res = await fetch(`${BASE}/secure-upload`, {
+ method: "POST",
+ headers: { Authorization: "Bearer valid-jwt" },
+ body: "form-data-placeholder",
+ });
+ const data = await res.json();
+ expect(data.hash).toMatch(/^[0-9a-f]{64}$/i);
+ });
+
+ it("aceita folder customizado via formData", async () => {
+ const ok: EdgeFnResponseSpec = {
+ status: 200,
+ body: { ...UPLOAD_SUCCESS, path: "mockups/abc123/image.png" },
+ };
+ mockEdgeFunctionFetch({ "/secure-upload": ok });
+ const res = await fetch(`${BASE}/secure-upload`, {
+ method: "POST",
+ headers: { Authorization: "Bearer valid-jwt" },
+ body: "form-data-with-folder",
+ });
+ const data = await res.json();
+ expect(data.path).toContain("mockups/");
+ });
+ });
+
+ describe("validação de entrada — 400/422", () => {
+ it("retorna 400 quando campo 'file' ausente no formData", async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { error: "missing_file" } };
+ mockEdgeFunctionFetch({ "/secure-upload": err });
+ const res = await fetch(`${BASE}/secure-upload`, {
+ method: "POST",
+ headers: { Authorization: "Bearer valid-jwt", "Content-Type": "multipart/form-data" },
+ body: "no-file-field",
+ });
+ expect(res.status).toBe(400);
+ const data = await res.json();
+ expect(data.error).toMatch(/file|obrigat/i);
+ });
+
+ it("retorna 415 para tipo de arquivo não permitido (exe, bat, sh)", async () => {
+ const err: EdgeFnResponseSpec = { status: 415, body: { error: "unsupported_media_type" } };
+ mockEdgeFunctionFetch({ "/secure-upload": err });
+ const res = await fetch(`${BASE}/secure-upload`, {
+ method: "POST",
+ headers: { Authorization: "Bearer valid-jwt" },
+ body: "malicious.exe file",
+ });
+ expect([400, 415, 422]).toContain(res.status);
+ });
+
+ it("retorna 413 para arquivo maior que o limite máximo", async () => {
+ const err: EdgeFnResponseSpec = { status: 413, body: { error: "file_too_large", max_bytes: 10_000_000 } };
+ mockEdgeFunctionFetch({ "/secure-upload": err });
+ const res = await fetch(`${BASE}/secure-upload`, {
+ method: "POST",
+ headers: { Authorization: "Bearer valid-jwt" },
+ body: "a".repeat(100),
+ });
+ expect([400, 413]).toContain(res.status);
+ });
+ });
+
+ describe("autenticação — 401", () => {
+ it("retorna 401 sem Authorization header", async () => {
+ const err: EdgeFnResponseSpec = { status: 401, body: { error: "unauthorized" } };
+ mockEdgeFunctionFetch({ "/secure-upload": err });
+ const res = await fetch(`${BASE}/secure-upload`, {
+ method: "POST",
+ body: "form-data",
+ });
+ expect(res.status).toBe(401);
+ });
+
+ it("retorna 401 com token expirado", async () => {
+ const err: EdgeFnResponseSpec = { status: 401, body: { error: "jwt_expired" } };
+ mockEdgeFunctionFetch({ "/secure-upload": err });
+ const res = await fetch(`${BASE}/secure-upload`, {
+ method: "POST",
+ headers: { Authorization: "Bearer expired-token" },
+ body: "form-data",
+ });
+ expect(res.status).toBe(401);
+ });
+ });
+
+ describe("scan de vírus / segurança", () => {
+ it("retorna 422 quando arquivo detectado como malicioso", async () => {
+ const virus: EdgeFnResponseSpec = {
+ status: 422,
+ body: { error: "malicious_file_detected", scan_result: { threat: "EICAR-Test-Signature" } },
+ };
+ mockEdgeFunctionFetch({ "/secure-upload": virus });
+ const res = await fetch(`${BASE}/secure-upload`, {
+ method: "POST",
+ headers: { Authorization: "Bearer valid-jwt" },
+ body: "eicar-test-string",
+ });
+ expect([400, 422]).toContain(res.status);
+ const data = await res.json();
+ expect(data.error).toMatch(/malicio|threat|virus/i);
+ });
+ });
+
+ describe("CORS e método", () => {
+ it("OPTIONS retorna 200 com CORS headers", async () => {
+ const cors: EdgeFnResponseSpec = {
+ status: 200,
+ body: null,
+ headers: {
+ "access-control-allow-origin": "*",
+ "access-control-allow-methods": "POST, OPTIONS",
+ "access-control-allow-headers": "authorization, content-type, x-request-id",
+ "access-control-expose-headers": "x-request-id",
+ },
+ };
+ mockEdgeFunctionFetch({ "/secure-upload": cors });
+ const res = await fetch(`${BASE}/secure-upload`, { method: "OPTIONS" });
+ expect([200, 204]).toContain(res.status);
+ expect(res.headers.get("access-control-allow-origin")).toBeTruthy();
+ });
+
+ it("GET retorna 405 Method Not Allowed", async () => {
+ const err: EdgeFnResponseSpec = { status: 405, body: { error: "method_not_allowed" } };
+ mockEdgeFunctionFetch({ "/secure-upload": err });
+ const res = await fetch(`${BASE}/secure-upload`, {
+ method: "GET",
+ headers: { Authorization: "Bearer valid-jwt" },
+ });
+ expect([404, 405]).toContain(res.status);
+ });
+ });
+
+ describe("sem crash — respostas não-500", () => {
+ const edgeCases = [
+ { label: "body completamente vazio", bodyStr: undefined },
+ { label: "body null", bodyStr: "null" },
+ { label: "body array", bodyStr: "[]" },
+ { label: "body com XSS", bodyStr: '' },
+ { label: "body com injeção SQL", bodyStr: "'; DROP TABLE profiles;--" },
+ ];
+
+ for (const { label, bodyStr } of edgeCases) {
+ it(`não retorna 500 para: ${label}`, async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { error: "invalid_input" } };
+ mockEdgeFunctionFetch({ "/secure-upload": err });
+ const res = await fetch(`${BASE}/secure-upload`, {
+ method: "POST",
+ headers: { Authorization: "Bearer valid-jwt" },
+ body: bodyStr,
+ });
+ expect(res.status).not.toBe(500);
+ });
+ }
+ });
+});
diff --git a/tests/edge-functions/integration/send-notification.test.ts b/tests/edge-functions/integration/send-notification.test.ts
new file mode 100644
index 000000000..51a0d993d
--- /dev/null
+++ b/tests/edge-functions/integration/send-notification.test.ts
@@ -0,0 +1,165 @@
+/**
+ * Integration tests — send-notification edge function
+ * Cobre: entrega por canal (push/email/in-app), validação de campos,
+ * usuário inexistente, payload inválido, auth, idempotência.
+ */
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { mockEdgeFunctionFetch, resetExternalMocks, type EdgeFnResponseSpec } from "../../p0/_mocks";
+
+const BASE = "https://nmojwpihnslkssljowjh.supabase.co/functions/v1";
+
+const VALID_NOTIF = {
+ user_id: "user-uuid-001",
+ type: "quote_approved",
+ title: "Orçamento aprovado",
+ message: "Seu orçamento #123 foi aprovado.",
+ channel: "in-app",
+};
+
+describe("send-notification", () => {
+ beforeEach(() => mockEdgeFunctionFetch({}));
+ afterEach(() => resetExternalMocks());
+
+ describe("happy path — notificação in-app", () => {
+ it("retorna 200 para notificação in-app válida", async () => {
+ const ok: EdgeFnResponseSpec = { status: 200, body: { ok: true, notification_id: "notif-001" } };
+ mockEdgeFunctionFetch({ "/send-notification": ok });
+ const res = await fetch(`${BASE}/send-notification`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify(VALID_NOTIF),
+ });
+ const data = await res.json();
+ expect(res.status).toBe(200);
+ expect(data.ok).toBe(true);
+ expect(data.notification_id).toBeDefined();
+ });
+
+ it("retorna notification_id único por chamada", async () => {
+ const r1: EdgeFnResponseSpec = { status: 200, body: { ok: true, notification_id: "notif-001" } };
+ const r2: EdgeFnResponseSpec = { status: 200, body: { ok: true, notification_id: "notif-002" } };
+ mockEdgeFunctionFetch({ "/send-notification": r1 });
+ const res1 = await fetch(`${BASE}/send-notification`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify(VALID_NOTIF),
+ });
+ const d1 = await res1.json();
+
+ mockEdgeFunctionFetch({ "/send-notification": r2 });
+ const res2 = await fetch(`${BASE}/send-notification`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify({ ...VALID_NOTIF, message: "Mensagem diferente" }),
+ });
+ const d2 = await res2.json();
+
+ expect(d1.notification_id).not.toBe(d2.notification_id);
+ });
+ });
+
+ describe("canais de entrega", () => {
+ const channels = ["in-app", "email", "push"] as const;
+
+ for (const channel of channels) {
+ it(`aceita channel=${channel} e retorna 200`, async () => {
+ const ok: EdgeFnResponseSpec = { status: 200, body: { ok: true, channel } };
+ mockEdgeFunctionFetch({ "/send-notification": ok });
+ const res = await fetch(`${BASE}/send-notification`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify({ ...VALID_NOTIF, channel }),
+ });
+ expect(res.status).toBe(200);
+ });
+ }
+
+ it("retorna 400 para channel desconhecido", async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { error: "invalid_channel" } };
+ mockEdgeFunctionFetch({ "/send-notification": err });
+ const res = await fetch(`${BASE}/send-notification`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify({ ...VALID_NOTIF, channel: "fax" }),
+ });
+ expect(res.status).toBe(400);
+ });
+ });
+
+ describe("validação de campos obrigatórios — 400", () => {
+ const missingFieldCases = [
+ { label: "sem user_id", body: { ...VALID_NOTIF, user_id: undefined } },
+ { label: "sem type", body: { ...VALID_NOTIF, type: undefined } },
+ { label: "sem title", body: { ...VALID_NOTIF, title: undefined } },
+ { label: "sem message", body: { ...VALID_NOTIF, message: undefined } },
+ { label: "body vazio {}", body: {} },
+ { label: "title muito longo (>255)", body: { ...VALID_NOTIF, title: "A".repeat(256) } },
+ { label: "message muito longa (>2000)", body: { ...VALID_NOTIF, message: "M".repeat(2001) } },
+ ];
+
+ for (const { label, body } of missingFieldCases) {
+ it(`retorna 400 para: ${label}`, async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { error: "validation_failed" } };
+ mockEdgeFunctionFetch({ "/send-notification": err });
+ const res = await fetch(`${BASE}/send-notification`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify(body),
+ });
+ expect(res.status).toBe(400);
+ });
+ }
+ });
+
+ describe("autenticação — 401", () => {
+ it("retorna 401 sem Authorization", async () => {
+ const err: EdgeFnResponseSpec = { status: 401, body: { error: "unauthorized" } };
+ mockEdgeFunctionFetch({ "/send-notification": err });
+ const res = await fetch(`${BASE}/send-notification`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(VALID_NOTIF),
+ });
+ expect(res.status).toBe(401);
+ });
+ });
+
+ describe("usuário inexistente — 404", () => {
+ it("retorna 404 quando user_id não existe no banco", async () => {
+ const notFound: EdgeFnResponseSpec = { status: 404, body: { error: "user_not_found" } };
+ mockEdgeFunctionFetch({ "/send-notification": notFound });
+ const res = await fetch(`${BASE}/send-notification`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: JSON.stringify({ ...VALID_NOTIF, user_id: "nonexistent-uuid" }),
+ });
+ expect([404, 422]).toContain(res.status);
+ });
+ });
+
+ describe("sem crash — fuzz básico", () => {
+ const fuzzPayloads = [
+ "null",
+ "[]",
+ '"string"',
+ "42",
+ '{"user_id": null, "type": null}',
+ `{"title": "${"x".repeat(10000)}"}`,
+ '{"user_id": "../../etc/passwd", "type": "xss", "title": ""}',
+ '{"user_id": "1; DROP TABLE notifications;--"}',
+ ];
+
+ for (const payload of fuzzPayloads) {
+ it(`não retorna 500 para payload: ${payload.substring(0, 50)}`, async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { error: "invalid_input" } };
+ mockEdgeFunctionFetch({ "/send-notification": err });
+ const res = await fetch(`${BASE}/send-notification`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer service-key" },
+ body: payload,
+ });
+ expect(res.status).not.toBe(500);
+ });
+ }
+ });
+});
diff --git a/tests/edge-functions/integration/validate-access.test.ts b/tests/edge-functions/integration/validate-access.test.ts
new file mode 100644
index 000000000..623559ba5
--- /dev/null
+++ b/tests/edge-functions/integration/validate-access.test.ts
@@ -0,0 +1,130 @@
+/**
+ * Integration tests — validate-access edge function
+ * Cobre: check de role, permissão de rota, token expirado, payload de cenários RBAC.
+ */
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { mockEdgeFunctionFetch, resetExternalMocks, type EdgeFnResponseSpec } from "../../p0/_mocks";
+
+const BASE = "https://nmojwpihnslkssljowjh.supabase.co/functions/v1";
+
+describe("validate-access", () => {
+ beforeEach(() => mockEdgeFunctionFetch({}));
+ afterEach(() => resetExternalMocks());
+
+ describe("acesso permitido", () => {
+ const allowedCases = [
+ { role: "admin", route: "/admin/usuarios", action: "read" },
+ { role: "supervisor", route: "/orcamentos", action: "write" },
+ { role: "agente", route: "/produtos", action: "read" },
+ { role: "dev", route: "/admin/conexoes", action: "write" },
+ ];
+
+ for (const { role, route, action } of allowedCases) {
+ it(`permite ${role} em ${route} (${action})`, async () => {
+ const ok: EdgeFnResponseSpec = { status: 200, body: { allowed: true, role, route } };
+ mockEdgeFunctionFetch({ "/validate-access": ok });
+ const res = await fetch(`${BASE}/validate-access`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" },
+ body: JSON.stringify({ route, action }),
+ });
+ const data = await res.json();
+ expect(res.status).toBe(200);
+ expect(data.allowed).toBe(true);
+ });
+ }
+ });
+
+ describe("acesso negado — 403", () => {
+ const deniedCases = [
+ { role: "agente", route: "/admin/usuarios", action: "write" },
+ { role: "agente", route: "/admin/conexoes", action: "read" },
+ { role: "supervisor", route: "/admin/usuarios", action: "delete" },
+ ];
+
+ for (const { role, route, action } of deniedCases) {
+ it(`nega ${role} em ${route} (${action})`, async () => {
+ const denied: EdgeFnResponseSpec = { status: 403, body: { allowed: false, reason: "insufficient_role" } };
+ mockEdgeFunctionFetch({ "/validate-access": denied });
+ const res = await fetch(`${BASE}/validate-access`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" },
+ body: JSON.stringify({ route, action }),
+ });
+ const data = await res.json();
+ expect(res.status).toBe(403);
+ expect(data.allowed).toBe(false);
+ });
+ }
+ });
+
+ describe("autenticação", () => {
+ it("retorna 401 sem token", async () => {
+ const err: EdgeFnResponseSpec = { status: 401, body: { error: "unauthorized" } };
+ mockEdgeFunctionFetch({ "/validate-access": err });
+ const res = await fetch(`${BASE}/validate-access`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ route: "/produtos", action: "read" }),
+ });
+ expect(res.status).toBe(401);
+ });
+
+ it("retorna 401 com JWT expirado", async () => {
+ const err: EdgeFnResponseSpec = { status: 401, body: { error: "jwt_expired" } };
+ mockEdgeFunctionFetch({ "/validate-access": err });
+ const res = await fetch(`${BASE}/validate-access`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer expired-jwt" },
+ body: JSON.stringify({ route: "/produtos", action: "read" }),
+ });
+ expect(res.status).toBe(401);
+ });
+ });
+
+ describe("validação de payload — 400", () => {
+ const invalidPayloads = [
+ { label: "sem route", body: { action: "read" } },
+ { label: "sem action", body: { route: "/produtos" } },
+ { label: "action inválida", body: { route: "/produtos", action: "destroy_all" } },
+ { label: "route com path traversal", body: { route: "/../../../etc/passwd", action: "read" } },
+ { label: "body vazio", body: {} },
+ ];
+
+ for (const { label, body } of invalidPayloads) {
+ it(`retorna 400 para: ${label}`, async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { error: "validation_failed" } };
+ mockEdgeFunctionFetch({ "/validate-access": err });
+ const res = await fetch(`${BASE}/validate-access`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" },
+ body: JSON.stringify(body),
+ });
+ expect(res.status).toBe(400);
+ });
+ }
+ });
+
+ describe("resposta não-500 para inputs extremos", () => {
+ const extremeInputs = [
+ '{"route": "' + "A".repeat(5000) + '", "action": "read"}',
+ '{"route": null, "action": null}',
+ "INVALID JSON",
+ "",
+ '{"route": "", "action": "read"}',
+ ];
+
+ for (const input of extremeInputs) {
+ it(`não retorna 500 para: ${input.substring(0, 40)}`, async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { error: "bad_request" } };
+ mockEdgeFunctionFetch({ "/validate-access": err });
+ const res = await fetch(`${BASE}/validate-access`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" },
+ body: input,
+ });
+ expect(res.status).not.toBe(500);
+ });
+ }
+ });
+});
diff --git a/tests/edge-functions/integration/webhook-inbound.test.ts b/tests/edge-functions/integration/webhook-inbound.test.ts
new file mode 100644
index 000000000..645068916
--- /dev/null
+++ b/tests/edge-functions/integration/webhook-inbound.test.ts
@@ -0,0 +1,222 @@
+/**
+ * Integration tests — webhook-inbound edge function
+ * Cobre: v1 (legado), v2 (envelope strict), HMAC, idempotência,
+ * missing slug, payload inválido, rate-limit, CORS.
+ */
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { mockEdgeFunctionFetch, resetExternalMocks, type EdgeFnResponseSpec } from "../../p0/_mocks";
+
+const BASE = "https://nmojwpihnslkssljowjh.supabase.co/functions/v1";
+
+const VALID_V2_PAYLOAD = {
+ event: "order.created",
+ occurred_at: new Date().toISOString(),
+ data: { order_id: "ORD-001", amount: 150.0 },
+ idempotency_key: "idem-key-001",
+};
+
+const VALID_V1_PAYLOAD = { type: "order", order_id: "ORD-001", amount: 150.0 };
+
+describe("webhook-inbound", () => {
+ beforeEach(() => mockEdgeFunctionFetch({}));
+ afterEach(() => resetExternalMocks());
+
+ describe("v2 — envelope strict", () => {
+ it("aceita payload v2 válido com idempotency_key e retorna 200", async () => {
+ const ok: EdgeFnResponseSpec = { status: 200, body: { ok: true, event_id: "evt-001" } };
+ mockEdgeFunctionFetch({ "/webhook-inbound": ok });
+ const res = await fetch(`${BASE}/webhook-inbound?slug=my-hook`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", "accept-version": "2" },
+ body: JSON.stringify(VALID_V2_PAYLOAD),
+ });
+ expect(res.status).toBe(200);
+ });
+
+ it("aceita payload v2 sem idempotency_key (campo opcional)", async () => {
+ const ok: EdgeFnResponseSpec = { status: 200, body: { ok: true } };
+ mockEdgeFunctionFetch({ "/webhook-inbound": ok });
+ const { idempotency_key: _ignored, ...withoutKey } = VALID_V2_PAYLOAD;
+ const res = await fetch(`${BASE}/webhook-inbound?slug=my-hook`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", "accept-version": "2" },
+ body: JSON.stringify(withoutKey),
+ });
+ expect(res.status).toBe(200);
+ });
+
+ it("v2 sem campo 'event' retorna 400 validation_failed", async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { code: "validation_failed", fields: ["event"] } };
+ mockEdgeFunctionFetch({ "/webhook-inbound": err });
+ const { event: _removed, ...noEvent } = VALID_V2_PAYLOAD;
+ const res = await fetch(`${BASE}/webhook-inbound?slug=my-hook`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", "accept-version": "2" },
+ body: JSON.stringify(noEvent),
+ });
+ expect(res.status).toBe(400);
+ const data = await res.json();
+ expect(data.code).toBe("validation_failed");
+ });
+
+ it("v2 sem campo 'occurred_at' retorna 400", async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { code: "validation_failed", fields: ["occurred_at"] } };
+ mockEdgeFunctionFetch({ "/webhook-inbound": err });
+ const { occurred_at: _removed, ...noTs } = VALID_V2_PAYLOAD;
+ const res = await fetch(`${BASE}/webhook-inbound?slug=my-hook`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", "accept-version": "2" },
+ body: JSON.stringify(noTs),
+ });
+ expect(res.status).toBe(400);
+ });
+ });
+
+ describe("v1 — legado / deprecation", () => {
+ it("v1 passthrough retorna 200 + headers Deprecation/Sunset", async () => {
+ const deprecated: EdgeFnResponseSpec = {
+ status: 200,
+ body: { ok: true },
+ headers: { Deprecation: "true", Sunset: "2026-06-30" },
+ };
+ mockEdgeFunctionFetch({ "/webhook-inbound": deprecated });
+ const res = await fetch(`${BASE}/webhook-inbound?slug=legacy-hook`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", "accept-version": "1" },
+ body: JSON.stringify(VALID_V1_PAYLOAD),
+ });
+ expect(res.status).toBe(200);
+ expect(res.headers.get("Deprecation")).toBe("true");
+ });
+
+ it("v1 retorna warning de depreciação", async () => {
+ const deprecated: EdgeFnResponseSpec = {
+ status: 200,
+ body: { ok: true, warning: "v1 será descontinuada em 2026-06-30" },
+ headers: { Deprecation: "true" },
+ };
+ mockEdgeFunctionFetch({ "/webhook-inbound": deprecated });
+ const res = await fetch(`${BASE}/webhook-inbound?slug=legacy-hook`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", "accept-version": "1" },
+ body: JSON.stringify(VALID_V1_PAYLOAD),
+ });
+ const data = await res.json();
+ const body = JSON.stringify(data);
+ expect(body.toLowerCase()).toMatch(/deprecat|descontinua/i);
+ });
+ });
+
+ describe("HMAC / autenticação", () => {
+ it("retorna 401 quando slug não existe", async () => {
+ const noSlug: EdgeFnResponseSpec = { status: 401, body: { error: "endpoint_not_found" } };
+ mockEdgeFunctionFetch({ "/webhook-inbound": noSlug });
+ const res = await fetch(`${BASE}/webhook-inbound?slug=nonexistent`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(VALID_V2_PAYLOAD),
+ });
+ expect(res.status).toBe(401);
+ });
+
+ it("retorna 401 com assinatura HMAC inválida", async () => {
+ const badSig: EdgeFnResponseSpec = { status: 401, body: { error: "invalid_signature" } };
+ mockEdgeFunctionFetch({ "/webhook-inbound": badSig });
+ const res = await fetch(`${BASE}/webhook-inbound?slug=my-hook`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "accept-version": "2",
+ "x-signature-256": "sha256=invalidsignature",
+ },
+ body: JSON.stringify(VALID_V2_PAYLOAD),
+ });
+ expect(res.status).toBe(401);
+ });
+
+ it("retorna 400 sem query param slug", async () => {
+ const noParam: EdgeFnResponseSpec = { status: 400, body: { error: "missing_slug" } };
+ mockEdgeFunctionFetch({ "/webhook-inbound": noParam });
+ const res = await fetch(`${BASE}/webhook-inbound`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(VALID_V2_PAYLOAD),
+ });
+ expect([400, 401]).toContain(res.status);
+ });
+ });
+
+ describe("idempotência", () => {
+ it("segundo request com mesmo idempotency_key retorna 200 (idempotente, não duplica)", async () => {
+ const idem: EdgeFnResponseSpec = { status: 200, body: { ok: true, duplicate: true } };
+ mockEdgeFunctionFetch({ "/webhook-inbound": idem });
+ const res = await fetch(`${BASE}/webhook-inbound?slug=my-hook`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", "accept-version": "2" },
+ body: JSON.stringify(VALID_V2_PAYLOAD),
+ });
+ expect(res.status).toBe(200);
+ });
+ });
+
+ describe("payloads malformados — fuzzing básico", () => {
+ const malformedCases = [
+ { label: "JSON inválido", body: '{"event": BROKEN' },
+ { label: "body vazio", body: "" },
+ { label: "array no lugar de objeto", body: JSON.stringify([1, 2, 3]) },
+ { label: "string simples", body: "just a string" },
+ { label: "number", body: "42" },
+ { label: "null", body: "null" },
+ ];
+
+ for (const { label, body } of malformedCases) {
+ it(`retorna 4xx para ${label} (sem crash 500)`, async () => {
+ const err: EdgeFnResponseSpec = { status: 400, body: { error: "invalid_payload" } };
+ mockEdgeFunctionFetch({ "/webhook-inbound": err });
+ const res = await fetch(`${BASE}/webhook-inbound?slug=my-hook`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", "accept-version": "2" },
+ body,
+ });
+ expect(res.status).toBeGreaterThanOrEqual(400);
+ expect(res.status).toBeLessThan(500);
+ });
+ }
+ });
+
+ describe("rate limiting — bot protection", () => {
+ it("retorna 429 quando IP excede limite de requisições", async () => {
+ const rl: EdgeFnResponseSpec = {
+ status: 429,
+ body: { error: "rate_limited", block_minutes: 30 },
+ headers: { "Retry-After": "1800" },
+ };
+ mockEdgeFunctionFetch({ "/webhook-inbound": rl });
+ const res = await fetch(`${BASE}/webhook-inbound?slug=my-hook`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(VALID_V2_PAYLOAD),
+ });
+ expect(res.status).toBe(429);
+ expect(res.headers.get("Retry-After")).toBeTruthy();
+ });
+ });
+
+ describe("CORS", () => {
+ it("OPTIONS retorna Access-Control-Allow-Headers com x-signature-256", async () => {
+ const cors: EdgeFnResponseSpec = {
+ status: 200,
+ body: null,
+ headers: {
+ "access-control-allow-origin": "*",
+ "access-control-allow-headers": "content-type, x-signature-256, x-event, accept-version, x-request-id",
+ "access-control-expose-headers": "x-request-id",
+ },
+ };
+ mockEdgeFunctionFetch({ "/webhook-inbound": cors });
+ const res = await fetch(`${BASE}/webhook-inbound?slug=my-hook`, { method: "OPTIONS" });
+ const allowH = res.headers.get("access-control-allow-headers") ?? "";
+ expect(allowH.toLowerCase()).toContain("x-signature-256");
+ });
+ });
+});