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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
2 changes: 2 additions & 0 deletions docs/criterios-aceite/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 16 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
"engines": {
"node": ">=22"
},
"overrides": {
"handlebars": "^4.7.10"
},
"lint-staged": {
"*.ts": [
"eslint --fix",
Expand Down Expand Up @@ -51,6 +54,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"
}
}
36 changes: 17 additions & 19 deletions pages/api.page.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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);
}

Expand Down Expand Up @@ -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!');
}
Expand All @@ -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!');
}
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -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());
}
Expand Down
66 changes: 66 additions & 0 deletions pages/api.schemas.ts
Original file line number Diff line number Diff line change
@@ -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<typeof ProductSchema>;
Loading