Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslint-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
7 changes: 3 additions & 4 deletions .github/workflows/e2e-flows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
109 changes: 0 additions & 109 deletions tests/edge-functions/integration/product-webhook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
98 changes: 0 additions & 98 deletions tests/edge-functions/integration/semantic-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]] } };
Expand Down
Loading