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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ Demonstrar, na prática:
OpenAPI - já encontrou e documentou defeitos reais na API sob teste.
- Relatórios de teste em **HTML**, gerados a cada execução do Jest e do
Newman e publicados como artefato do CI.
- Testes de segurança inspirados no **OWASP API Security Top 10**
(autenticação inválida, mass assignment, tratamento de entrada,
observação de rate limiting), com um recorte que nunca toca dado de
terceiros nem faz varredura agressiva contra o sandbox público.

## Estrutura

Expand Down
26 changes: 26 additions & 0 deletions docs/test-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,32 @@ defeito novo, documentado na [issue #9](https://github.com/ThomasTDS/qa-api-swag
respostas `405 Method Not Allowed` sem o header `Allow` exigido pela RFC
9110, em 10 endpoints diferentes.

## Casos implementados - Segurança

Cenários inspirados no OWASP API Security Top 10, com um recorte
deliberadamente seguro: a GoRest é uma API pública compartilhada por todo
mundo que usa a ferramenta, então nenhum teste aqui toca ou tenta acessar
dado que não foi criado pelo próprio teste, nem faz varredura agressiva
contra o serviço. Categorias do Top 10 que exigiriam isso (ex.: autorização
por nível de função administrativa, que a GoRest nem possui) ficam fora de
escopo.

| ID | Endpoint | Cenário | Tipo | Resultado esperado | Arquivo |
| ------ | ---------------------------- | ------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------- | ------------------------------------------------------------------- |
| TC-061 | POST /users | Token de autenticação inválido (não apenas ausente) | Negativo | 401 + `{"message":"Invalid token"}` (mensagem distinta de token ausente) | [security.test.js](../tests/security.test.js) |
| TC-062 | PUT /users/{id} | Token de autenticação inválido | Negativo | 401 | [security.test.js](../tests/security.test.js) |
| TC-063 | DELETE /users/{id} | Token de autenticação inválido | Negativo | 401 | [security.test.js](../tests/security.test.js) |
| TC-064 | GET /users | Rajada de 15 requisições simultâneas (observação de rate limiting) | Observacional | 200 em todas, sem header de rate limit nem 429 - ver issue documentada | [security.test.js](../tests/security.test.js) |
| TC-065 | POST /users | Cliente tenta definir o próprio `id` (mass assignment) | Negativo | 201 + `id` gerado pelo servidor, ignorando o valor enviado | [security.write.test.js](../tests/security.write.test.js) |
| TC-066 | PUT /users/{id} | Cliente tenta alterar o `id` de um registro existente (mass assignment) | Negativo | 200 + `id` original inalterado | [security.write.test.js](../tests/security.write.test.js) |
| TC-067 | POST /users | Caracteres especiais/script no campo `name` | Positivo | 201 + valor armazenado e devolvido integralmente, sem erro 500 | [security.write.test.js](../tests/security.write.test.js) |

Todos os 7 casos (TC-061 a TC-067) foram executados com sucesso contra a API
real, incluindo os autenticados. O TC-064 documenta uma característica (não
necessariamente um defeito) na [issue #43](https://github.com/ThomasTDS/qa-api-swagger/issues/43):
a GoRest não expõe nenhum sinal de rate limiting em endpoints de leitura,
pelo menos até o volume testado.

## Relatórios de teste (HTML)

`npm test` e `npm run postman:run`/`postman:run:read-only` geram relatórios
Expand Down
44 changes: 44 additions & 0 deletions tests/security.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const { createApiClient } = require("../src/apiClient");

const api = createApiClient();
const invalidAuthHeader = { Authorization: "Bearer token-invalido-garbage-123" };

describe("Segurança - autenticação inválida (não apenas ausente)", () => {
test("POST com token inválido retorna 401", async () => {
const res = await api.post(
"/users",
{ name: "x", email: "x@example.test", gender: "male", status: "active" },
{ headers: invalidAuthHeader }
);

expect(res.status).toBe(401);
expect(res.data).toEqual({ message: "Invalid token" });
});

test("PUT com token inválido retorna 401", async () => {
const res = await api.put("/users/1", { status: "inactive" }, { headers: invalidAuthHeader });

expect(res.status).toBe(401);
});

test("DELETE com token inválido retorna 401", async () => {
const res = await api.delete("/users/1", { headers: invalidAuthHeader });

expect(res.status).toBe(401);
});
});

describe("Segurança - rate limiting (observação de característica)", () => {
test("uma rajada pequena de GETs não retorna 429 nem expõe headers de rate limit", async () => {
const burstSize = 15;
const requests = Array.from({ length: burstSize }, () => api.get("/users?page=1"));
const responses = await Promise.all(requests);

expect(responses.every((res) => res.status === 200)).toBe(true);

const hasRateLimitHeaders = responses.some((res) =>
Object.keys(res.headers).some((h) => /ratelimit|retry-after/i.test(h))
);
expect(hasRateLimitHeaders).toBe(false);
});
});
64 changes: 64 additions & 0 deletions tests/security.write.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const { createApiClient } = require("../src/apiClient");

const describeIfToken = process.env.GOREST_TOKEN ? describe : describe.skip;

describeIfToken("Segurança - mass assignment e tratamento de entrada (autenticado)", () => {
let api;

beforeAll(() => {
api = createApiClient({ withAuth: true });
});

function uniqueEmail(prefix) {
return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1e6)}@example.test`;
}

test("POST /users ignora o campo 'id' enviado pelo cliente", async () => {
const res = await api.post("/users", {
id: 1,
name: "QA Portfolio - Mass Assignment",
email: uniqueEmail("mass-assign-post"),
gender: "male",
status: "active",
});

expect(res.status).toBe(201);
expect(res.data.id).not.toBe(1);
expect(res.data.id).toBeGreaterThan(1);

await api.delete(`/users/${res.data.id}`);
});

test("PUT /users/{id} ignora tentativa de alterar o 'id'", async () => {
const created = await api.post("/users", {
name: "QA Portfolio - Mass Assignment PUT",
email: uniqueEmail("mass-assign-put"),
gender: "male",
status: "active",
});
const originalId = created.data.id;

const res = await api.put(`/users/${originalId}`, { id: 1 });

expect(res.status).toBe(200);
expect(res.data.id).toBe(originalId);

await api.delete(`/users/${originalId}`);
});

test("POST /users com caracteres especiais/script no nome é armazenado e devolvido sem corromper a resposta", async () => {
const payload = {
name: "<script>alert(1)</script> O'Brien --",
email: uniqueEmail("injection"),
gender: "male",
status: "active",
};

const res = await api.post("/users", payload);

expect(res.status).toBe(201);
expect(res.data.name).toBe(payload.name);

await api.delete(`/users/${res.data.id}`);
});
});
Loading