From 0a7b77dc0f9208b11b1c742032161c905150da18 Mon Sep 17 00:00:00 2001 From: ThomasTDS Date: Thu, 8 Oct 2026 16:05:20 -0300 Subject: [PATCH 1/2] Adiciona validacao de schema nas respostas da API (Zod) Novo pages/api.schemas.ts com schemas construidos a partir de respostas reais coletadas manualmente (nao a partir da documentacao), incluindo estruturas aninhadas como category em produtos. ApiPage agora usa Schema.parse() no corpo de cada resposta antes das asserticoes de conteudo - valida a estrutura inteira, nao so os poucos campos ja checados manualmente, entao uma mudanca de contrato em campo nao testado diretamente tambem derruba o teste, com mensagem apontando exatamente qual campo e por que. Removida a interface Product manual duplicada em api.page.ts, agora inferida do schema. Validado quebrando um schema de proposito (price como number em vez de string) e confirmando o erro detalhado do Zod antes de reverter. --- README.md | 2 +- docs/criterios-aceite/api.md | 2 ++ package-lock.json | 13 ++++++- package.json | 3 +- pages/api.page.ts | 36 ++++++++++---------- pages/api.schemas.ts | 66 ++++++++++++++++++++++++++++++++++++ 6 files changed, 100 insertions(+), 22 deletions(-) create mode 100644 pages/api.schemas.ts diff --git a/README.md b/README.md index b45ab80..783cd5a 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ No CI, esse relatório é publicado automaticamente no **[GitHub Pages](https:// - Acessibilidade (passivo, com [axe-core](https://github.com/dequelabs/axe-core-npm)): verifica violações `critical`/`serious` nas páginas de login e produtos. Como a aplicação sob teste é de terceiros, o cenário não quebra o build — as violações encontradas são anexadas ao relatório HTML para documentação, no mesmo espírito da suíte de segurança. -- Verificação de API: chama diretamente a API pública do automationexercise.com (`/api/productsList`, `/api/brandsList`, `/api/searchProduct`), sem passar pela interface, validando estrutura da resposta, presença de dados esperados e rejeição de método HTTP não suportado. +- Verificação de API: chama diretamente a API pública do automationexercise.com (produtos, marcas, busca, login, conta de usuário), sem passar pela interface. Toda resposta é validada contra um schema ([Zod](https://zod.dev/), em `pages/api.schemas.ts`) construído a partir de respostas reais — não só os campos usados nas asserções, a estrutura inteira. Se a API mudar um campo que nenhum teste checa diretamente, o schema ainda pega. --- diff --git a/docs/criterios-aceite/api.md b/docs/criterios-aceite/api.md index 9c65709..2753222 100644 --- a/docs/criterios-aceite/api.md +++ b/docs/criterios-aceite/api.md @@ -3,6 +3,8 @@ Cenários executáveis: `features/api.feature`. > Estes cenários chamam a API REST pública do site diretamente (sem passar pela interface), validando o contrato da resposta. A API do Automation Exercise sempre responde HTTP 200 no nível de transporte — o resultado real da operação vem no campo `responseCode` do corpo JSON (200 para sucesso, 405 para método não suportado). Os critérios abaixo refletem esse comportamento específico da API, não uma convenção genérica de REST. +> +> Toda resposta é validada contra um schema ([Zod](https://zod.dev/), em `pages/api.schemas.ts`) construído a partir de respostas reais coletadas manualmente — não só os campos citados nos critérios abaixo, mas a estrutura inteira (tipo e presença de cada campo, incluindo objetos aninhados como `category` em produtos). Se o formato da resposta mudar, o teste falha com uma mensagem apontando exatamente qual campo não bateu, mesmo que esse campo não seja mencionado explicitamente em nenhum critério aqui. ## História de Usuário diff --git a/package-lock.json b/package-lock.json index d757e27..1cc52f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,8 @@ "prettier": "^3.9.6", "ts-node": "^10.9.2", "typescript": "^5.9.3", - "typescript-eslint": "^8.70.0" + "typescript-eslint": "^8.70.0", + "zod": "^4.6.5" }, "engines": { "node": ">=22" @@ -4899,6 +4900,16 @@ "engines": { "node": ">=18" } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 64e2517..bdfed91 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "prettier": "^3.9.6", "ts-node": "^10.9.2", "typescript": "^5.9.3", - "typescript-eslint": "^8.70.0" + "typescript-eslint": "^8.70.0", + "zod": "^4.6.5" } } diff --git a/pages/api.page.ts b/pages/api.page.ts index 0b93949..f41c1dd 100644 --- a/pages/api.page.ts +++ b/pages/api.page.ts @@ -1,16 +1,15 @@ import { APIResponse, Page, expect } from '@playwright/test'; import { fakerPT_BR } from '@faker-js/faker'; +import { + ProductsListResponseSchema, + BrandsListResponseSchema, + MessageResponseSchema, + UserDetailResponseSchema, +} from './api.schemas'; const BASE_URL = process.env.BASE_URL ?? 'https://automationexercise.com/'; const TEST_ACCOUNT_PASSWORD = 'Teste@123'; -interface Product { - id: number; - name: string; - price: string; - brand: string; -} - export class ApiPage { readonly page: Page; @@ -35,34 +34,33 @@ export class ApiPage { async assertProductsListContains(productName: string) { const response = await this.getProductsList(); expect(response.status()).toBe(200); - const body = await response.json(); + const body = ProductsListResponseSchema.parse(await response.json()); expect(body.responseCode).toBe(200); - const found = (body.products as Product[]).some((product) => product.name === productName); + const found = body.products.some((product) => product.name === productName); expect(found).toBe(true); } async assertBrandsListNotEmpty() { const response = await this.getBrandsList(); expect(response.status()).toBe(200); - const body = await response.json(); + const body = BrandsListResponseSchema.parse(await response.json()); expect(body.responseCode).toBe(200); - expect(Array.isArray(body.brands)).toBe(true); expect(body.brands.length).toBeGreaterThan(0); } async assertSearchResultsContain(term: string, productName: string) { const response = await this.searchProducts(term); expect(response.status()).toBe(200); - const body = await response.json(); + const body = ProductsListResponseSchema.parse(await response.json()); expect(body.responseCode).toBe(200); - const found = (body.products as Product[]).some((product) => product.name === productName); + const found = body.products.some((product) => product.name === productName); expect(found).toBe(true); } async assertProductsListRejectsPost() { const response = await this.page.request.post(BASE_URL + 'api/productsList'); expect(response.status()).toBe(200); - const body = await response.json(); + const body = MessageResponseSchema.parse(await response.json()); expect(body.responseCode).toBe(405); } @@ -90,7 +88,7 @@ export class ApiPage { form: { email, password }, }); expect(response.status()).toBe(200); - const body = await response.json(); + const body = MessageResponseSchema.parse(await response.json()); expect(body.responseCode).toBe(200); expect(body.message).toBe('User exists!'); } @@ -100,7 +98,7 @@ export class ApiPage { form: { email, password }, }); expect(response.status()).toBe(200); - const body = await response.json(); + const body = MessageResponseSchema.parse(await response.json()); expect(body.responseCode).toBe(404); expect(body.message).toBe('User not found!'); } @@ -130,7 +128,7 @@ export class ApiPage { }, }); expect(response.status()).toBe(200); - const body = await response.json(); + const body = MessageResponseSchema.parse(await response.json()); expect(body.responseCode).toBe(201); } @@ -139,7 +137,7 @@ export class ApiPage { form: { email, password: TEST_ACCOUNT_PASSWORD }, }); expect(response.status()).toBe(200); - const body = await response.json(); + const body = MessageResponseSchema.parse(await response.json()); expect(body.responseCode).toBe(200); } @@ -156,7 +154,7 @@ export class ApiPage { BASE_URL + 'api/getUserDetailByEmail?email=' + encodeURIComponent(email) ); expect(response.status()).toBe(200); - const body = await response.json(); + const body = UserDetailResponseSchema.parse(await response.json()); expect(body.responseCode).toBe(200); expect(body.user.email.toLowerCase()).toBe(email.toLowerCase()); } diff --git a/pages/api.schemas.ts b/pages/api.schemas.ts new file mode 100644 index 0000000..ac27fff --- /dev/null +++ b/pages/api.schemas.ts @@ -0,0 +1,66 @@ +import { z } from 'zod'; + +// Schemas baseados em respostas reais da API do automationexercise.com, +// coletadas manualmente antes de escrever este arquivo (não inventados a +// partir da documentação). `.parse()` falha o teste com uma mensagem +// detalhada de qual campo não bate, se o contrato da API mudar. + +export const ProductSchema = z.object({ + id: z.number(), + name: z.string(), + price: z.string(), + brand: z.string(), + category: z.object({ + usertype: z.object({ + usertype: z.string(), + }), + category: z.string(), + }), +}); + +export const ProductsListResponseSchema = z.object({ + responseCode: z.number(), + products: z.array(ProductSchema), +}); + +export const BrandSchema = z.object({ + id: z.number(), + brand: z.string(), +}); + +export const BrandsListResponseSchema = z.object({ + responseCode: z.number(), + brands: z.array(BrandSchema), +}); + +// Resposta genérica no formato {responseCode, message}, usada por +// verifyLogin, createAccount, deleteAccount e pela rejeição de método +// HTTP não suportado. +export const MessageResponseSchema = z.object({ + responseCode: z.number(), + message: z.string(), +}); + +export const UserDetailResponseSchema = z.object({ + responseCode: z.number(), + user: z.object({ + id: z.number(), + name: z.string(), + email: z.string(), + title: z.string(), + birth_day: z.string(), + birth_month: z.string(), + birth_year: z.string(), + first_name: z.string(), + last_name: z.string(), + company: z.string(), + address1: z.string(), + address2: z.string(), + country: z.string(), + state: z.string(), + city: z.string(), + zipcode: z.string(), + }), +}); + +export type Product = z.infer; From 623e81c1c6bda0f847f9ba98efbd605400885959 Mon Sep 17 00:00:00 2001 From: ThomasTDS Date: Thu, 8 Oct 2026 16:09:07 -0300 Subject: [PATCH 2/2] Corrige 2 vulnerabilidades criticas em handlebars (transitiva via allure) GitHub reportou 2 CVEs criticos (injecao de JavaScript via confusao de tipo no AST do compile, GHSA-8r5x-fm3f-whwj e GHSA-p8wg-vrv2-v86f) e 1 moderado em handlebars 4.7.9, puxado pelo Allure Report pra renderizar seus proprios templates de relatorio. Como o Allure ainda nao atualizou sua propria dependencia, forcei handlebars para 4.7.10 via overrides no package.json - resolve os 3 alertas sem quebrar nada (relatorio Allure gerado e conferido normalmente apos o bump). --- package-lock.json | 8 ++++---- package.json | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1cc52f8..bb44f38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2886,13 +2886,13 @@ } }, "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz", + "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.5", + "minimist": "^1.2.8", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" diff --git a/package.json b/package.json index bdfed91..15beb40 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,9 @@ "engines": { "node": ">=22" }, + "overrides": { + "handlebars": "^4.7.10" + }, "lint-staged": { "*.ts": [ "eslint --fix",