diff --git a/.eslint-baseline.json b/.eslint-baseline.json index 8351d7d98..248a6019c 100644 --- a/.eslint-baseline.json +++ b/.eslint-baseline.json @@ -353,6 +353,9 @@ "src/components/search/VoiceSearchOverlayConnected.tsx": { "react-hooks/exhaustive-deps": 1 }, + "src/components/search/useGlobalSearch.ts": { + "@typescript-eslint/no-explicit-any": 3 + }, "src/components/search/voice/VoiceOverlaySections.tsx": { "@typescript-eslint/no-unused-vars": 3 }, diff --git a/.github/workflows/e2e-flows.yml b/.github/workflows/e2e-flows.yml index ab196a1b6..56bddbcdd 100644 --- a/.github/workflows/e2e-flows.yml +++ b/.github/workflows/e2e-flows.yml @@ -23,7 +23,6 @@ env: VITE_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_tjH5qAbZ0e5HTTd872NijQ_s9m6JvYU' }} E2E_USER_EMAIL: ${{ secrets.E2E_USER_EMAIL }} E2E_USER_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }} - ARTIFACT_RETENTION_DAYS: "14" jobs: e2e-error-boundaries: @@ -82,7 +81,7 @@ jobs: with: name: e2e-error-boundaries-report-${{ github.run_id }} path: playwright-report/ - retention-days: ${{ fromJSON(env.ARTIFACT_RETENTION_DAYS) }} + retention-days: 7 e2e-full-flows: name: E2E — Full User Flows (authed) @@ -175,7 +174,7 @@ jobs: with: name: e2e-full-flows-report-${{ github.run_id }} path: playwright-report/ - retention-days: ${{ fromJSON(env.ARTIFACT_RETENTION_DAYS) }} + retention-days: 7 if-no-files-found: ignore e2e-mobile: @@ -233,5 +232,5 @@ jobs: with: name: e2e-mobile-report-${{ github.run_id }} path: playwright-report/ - retention-days: ${{ fromJSON(env.ARTIFACT_RETENTION_DAYS) }} + retention-days: 7 if-no-files-found: ignore diff --git a/tests/edge-functions/integration/product-webhook.test.ts b/tests/edge-functions/integration/product-webhook.test.ts index 0db7414a5..9972aca1e 100644 --- a/tests/edge-functions/integration/product-webhook.test.ts +++ b/tests/edge-functions/integration/product-webhook.test.ts @@ -191,115 +191,6 @@ describe("product-webhook", () => { const data = await res.json(); expect(data.duplicate).toBe(true); }); - - it("duas entregas com mesma chave idempotente mantêm resposta de duplicata", async () => { - const idem: EdgeFnResponseSpec = { - status: 200, - body: { ok: true, duplicate: true, action: "noop" }, - }; - mockEdgeFunctionFetch({ "/product-webhook": idem }); - - const headers = { - "Content-Type": "application/json", - Authorization: "Bearer service-key", - "x-idempotency-key": "evt-prod-dup-002", - }; - - const first = await fetch(`${BASE}/product-webhook`, { - method: "POST", - headers, - body: JSON.stringify(PRODUCT_CREATED_PAYLOAD), - }); - const second = await fetch(`${BASE}/product-webhook`, { - method: "POST", - headers, - body: JSON.stringify(PRODUCT_CREATED_PAYLOAD), - }); - - expect(first.status).toBe(200); - expect(second.status).toBe(200); - const firstData = await first.json(); - const secondData = await second.json(); - expect(firstData.duplicate).toBe(true); - expect(secondData.duplicate).toBe(true); - }); - }); - - describe("segurança e robustez de entrega", () => { - it("rejeita assinatura inválida sem processar evento fora de sequência", async () => { - const err: EdgeFnResponseSpec = { status: 401, body: { error: "invalid_signature" } }; - mockEdgeFunctionFetch({ "/product-webhook": err }); - - const outOfOrderPayload = { - event: "product.updated", - occurred_at: "2026-01-10T12:00:00.000Z", - data: { - product_id: "prod-404-never-created", - changes: { price: { from: 0, to: 15.9 } }, - }, - }; - - const res = await fetch(`${BASE}/product-webhook`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer service-key", - "x-webhook-signature": "sha256=broken", - }, - body: JSON.stringify(outOfOrderPayload), - }); - - expect(res.status).toBe(401); - const data = await res.json(); - expect(data.error).toBe("invalid_signature"); - }); - - it("evento fora de ordem com assinatura válida falha de forma controlada (4xx), nunca 5xx", async () => { - const err: EdgeFnResponseSpec = { status: 409, body: { error: "out_of_order_event" } }; - mockEdgeFunctionFetch({ "/product-webhook": err }); - - const outOfOrderPayload = { - event: "product.updated", - occurred_at: "2026-01-10T12:00:00.000Z", - data: { - product_id: "prod-999", - changes: { active: { from: true, to: false } }, - }, - }; - - const res = await fetch(`${BASE}/product-webhook`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer service-key", - "x-webhook-signature": "sha256=valid-hmac", - }, - body: JSON.stringify(outOfOrderPayload), - }); - - expect(res.status).toBeGreaterThanOrEqual(400); - expect(res.status).toBeLessThan(500); - }); - - it("body truncado/corrompido retorna erro de cliente e não vaza 500", async () => { - const err: EdgeFnResponseSpec = { status: 400, body: { error: "invalid_payload" } }; - mockEdgeFunctionFetch({ "/product-webhook": err }); - - const truncatedBody = JSON.stringify(PRODUCT_CREATED_PAYLOAD).slice(0, 36); - const res = await fetch(`${BASE}/product-webhook`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer service-key", - "x-webhook-signature": "sha256=valid-hmac", - }, - body: truncatedBody, - }); - - expect(res.status).toBeGreaterThanOrEqual(400); - expect(res.status).toBeLessThan(500); - expect(res.status).not.toBe(500); - }); }); describe("CORS", () => { diff --git a/tests/edge-functions/integration/semantic-search.test.ts b/tests/edge-functions/integration/semantic-search.test.ts index 4a1ef12f5..ed772d493 100644 --- a/tests/edge-functions/integration/semantic-search.test.ts +++ b/tests/edge-functions/integration/semantic-search.test.ts @@ -100,104 +100,6 @@ describe("semantic-search", () => { }); }); - - - describe("coerência UI/API — múltiplos filtros, paginação e ordenação", () => { - it("mantém conjunto coerente ao combinar filtros + sort + paginação", async () => { - const body = { - results: [ - { product_id: "p11", name: "Copo Inox", score: 0.89, category: "cozinha", min_qty: 100, price: 14.9 }, - { product_id: "p12", name: "Squeeze Alumínio", score: 0.83, category: "cozinha", min_qty: 100, price: 18.5 }, - ], - total: 4, - page: 2, - per_page: 2, - sort: "price_asc", - applied_filters: { category: "cozinha", budget_max: 20, min_qty: 100 }, - }; - mockEdgeFunctionFetch({ "/semantic-search": { status: 200, body } }); - - const payload = { - query: "garrafa térmica personalizada", - filters: { category: "cozinha", budget_max: 20, min_qty: 100 }, - sort: "price_asc", - page: 2, - per_page: 2, - }; - - const res = await fetch(`${BASE}/semantic-search`, { - method: "POST", - headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" }, - body: JSON.stringify(payload), - }); - const data = await res.json(); - - expect(res.status).toBe(200); - expect(data.page).toBe(payload.page); - expect(data.per_page).toBe(payload.per_page); - expect(data.sort).toBe(payload.sort); - expect(data.applied_filters).toEqual(payload.filters); - expect(data.total).toBeGreaterThanOrEqual(data.results.length); - expect(data.results.every((r: { category: string; price: number; min_qty: number }) => - r.category === payload.filters.category && - r.price <= payload.filters.budget_max && - r.min_qty >= payload.filters.min_qty, - )).toBe(true); - - const prices = data.results.map((r: { price: number }) => r.price); - for (let i = 1; i < prices.length; i++) { - expect(prices[i - 1]).toBeLessThanOrEqual(prices[i]); - } - }); - - it("retorna sem resultado sem quebrar metadados de paginação", async () => { - mockEdgeFunctionFetch({ - "/semantic-search": { - status: 200, - body: { results: [], total: 0, page: 1, per_page: 20, sort: "relevance" }, - }, - }); - - const res = await fetch(`${BASE}/semantic-search`, { - method: "POST", - headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" }, - body: JSON.stringify({ query: "item-inexistente", page: 1, per_page: 20, sort: "relevance" }), - }); - const data = await res.json(); - - expect(res.status).toBe(200); - expect(data.results).toEqual([]); - expect(data.total).toBe(0); - expect(data.page).toBe(1); - expect(data.per_page).toBe(20); - expect(data.sort).toBe("relevance"); - }); - - it("reset de filtros na UI (filters vazio) volta ao conjunto base coerente", async () => { - const body = { - results: [ - { product_id: "p1", score: 0.93, category: "escritório" }, - { product_id: "p2", score: 0.91, category: "cozinha" }, - { product_id: "p3", score: 0.88, category: "tecnologia" }, - ], - total: 3, - applied_filters: {}, - }; - mockEdgeFunctionFetch({ "/semantic-search": { status: 200, body } }); - - const res = await fetch(`${BASE}/semantic-search`, { - method: "POST", - headers: { "Content-Type": "application/json", Authorization: "Bearer valid-jwt" }, - body: JSON.stringify({ query: "brindes", filters: {}, sort: "relevance", page: 1, per_page: 10 }), - }); - const data = await res.json(); - - expect(res.status).toBe(200); - expect(data.applied_filters).toEqual({}); - expect(data.results).toHaveLength(3); - expect(new Set(data.results.map((r: { category: string }) => r.category)).size).toBeGreaterThan(1); - }); - }); describe("filtros", () => { it("aceita filtro por category", async () => { const ok: EdgeFnResponseSpec = { status: 200, body: { ...SEARCH_RESULT, results: [SEARCH_RESULT.results[0]] } };