Skip to content

Commit 2c389f3

Browse files
committed
Initial commit: API QA automation with Playwright
1 parent 0400996 commit 2c389f3

11 files changed

Lines changed: 330 additions & 0 deletions

File tree

.github/workflows/tests.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: API Tests (Playwright)
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
13+
- uses: actions/setup-node@v4
14+
with:
15+
node-version: "20"
16+
cache: "npm"
17+
18+
- run: npm ci
19+
- run: npx playwright install --with-deps
20+
- run: npm test

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
playwright-report/
3+
test-results/
4+
.env
5+
.DS_Store

package-lock.json

Lines changed: 94 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "api-playwright-qa",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "playwright test",
8+
"test:ui": "playwright test --ui",
9+
"report": "playwright show-report"
10+
},
11+
"keywords": [],
12+
"author": "",
13+
"license": "ISC",
14+
"type": "commonjs",
15+
"devDependencies": {
16+
"@playwright/test": "^1.57.0",
17+
"typescript": "^5.9.3"
18+
}
19+
}

playwright.config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineConfig } from "@playwright/test";
2+
3+
export default defineConfig({
4+
testDir: "./tests",
5+
timeout: 30_000,
6+
retries: process.env.CI ? 1 : 0,
7+
use: {
8+
baseURL: "https://dummyjson.com",
9+
},
10+
reporter: [["html", { open: "never" }], ["list"]],
11+
});

tests/auth.me.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { test, expect } from "@playwright/test";
2+
import { ApiClient } from "../utils/apiClient";
3+
4+
test.describe("Auth API (DummyJSON)", () => {
5+
test("GET /auth/me returns user profile when authorized (token)", async ({ request, baseURL }) => {
6+
const api = new ApiClient(request, baseURL!);
7+
8+
// 1) Login to get token
9+
const loginRes = await api.post("/auth/login", {
10+
username: "emilys",
11+
password: "emilyspass",
12+
expiresInMins: 30,
13+
});
14+
15+
expect(loginRes.status()).toBe(200);
16+
await api.assertJson(loginRes);
17+
18+
const loginBody = await loginRes.json();
19+
expect(loginBody).toHaveProperty("accessToken");
20+
const token = loginBody.accessToken as string;
21+
expect(token.length).toBeGreaterThan(10);
22+
23+
// 2) Call protected endpoint with Bearer token
24+
const meRes = await api.get("/auth/me", {
25+
Authorization: `Bearer ${token}`,
26+
});
27+
28+
expect(meRes.status()).toBe(200);
29+
await api.assertJson(meRes);
30+
31+
const meBody = await meRes.json();
32+
33+
// 3) Assertions that matter for QA (shape + consistency)
34+
expect(meBody).toHaveProperty("id");
35+
expect(meBody).toHaveProperty("username");
36+
expect(typeof meBody.id).toBe("number");
37+
expect(typeof meBody.username).toBe("string");
38+
39+
// Optional: confirm same username (if API returns it in login payload)
40+
// DummyJSON обычно возвращает username в логине — если есть, проверим
41+
if (loginBody.username) {
42+
expect(meBody.username).toBe(loginBody.username);
43+
}
44+
});
45+
46+
test("GET /auth/me fails without token (negative)", async ({ request, baseURL }) => {
47+
const api = new ApiClient(request, baseURL!);
48+
49+
const res = await api.get("/auth/me");
50+
expect(res.ok()).toBeFalsy();
51+
// обычно 401, но пусть будет устойчиво:
52+
expect([400, 401, 403]).toContain(res.status());
53+
});
54+
});

tests/auth.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { test, expect } from "@playwright/test";
2+
import { ApiClient } from "../utils/apiClient";
3+
4+
test.describe("Auth API (DummyJSON)", () => {
5+
test("POST /auth/login returns accessToken and refreshToken for valid credentials", async ({ request, baseURL }) => {
6+
const api = new ApiClient(request, baseURL!);
7+
8+
const res = await api.post("/auth/login", {
9+
username: "emilys",
10+
password: "emilyspass",
11+
expiresInMins: 30,
12+
});
13+
14+
expect(res.status()).toBe(200);
15+
await api.assertJson(res);
16+
17+
const body = await res.json();
18+
expect(body).toHaveProperty("accessToken");
19+
expect(body).toHaveProperty("refreshToken");
20+
expect(typeof body.accessToken).toBe("string");
21+
expect(typeof body.refreshToken).toBe("string");
22+
expect(body.accessToken.length).toBeGreaterThan(10);
23+
});
24+
25+
test("POST /auth/login fails for invalid credentials (negative)", async ({ request, baseURL }) => {
26+
const api = new ApiClient(request, baseURL!);
27+
28+
const res = await api.post("/auth/login", {
29+
username: "wrong_user",
30+
password: "wrong_pass",
31+
});
32+
33+
// у разных API может быть 400/401 — главное что не OK
34+
expect(res.ok()).toBeFalsy();
35+
});
36+
});

tests/products.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { test, expect } from "@playwright/test";
2+
import { ApiClient } from "../utils/apiClient";
3+
import { expectProductShape } from "../utils/validators";
4+
5+
test.describe("Products API (DummyJSON)", () => {
6+
test("GET /products returns a non-empty products array", async ({ request, baseURL }) => {
7+
const api = new ApiClient(request, baseURL!);
8+
9+
const res = await api.get("/products");
10+
expect(res.status()).toBe(200);
11+
await api.assertJson(res);
12+
13+
const body = await res.json();
14+
expect(Array.isArray(body.products)).toBeTruthy();
15+
expect(body.products.length).toBeGreaterThan(0);
16+
17+
expectProductShape(body.products[0]);
18+
});
19+
20+
test("GET /products/:id returns correct product shape (data-driven)", async ({ request, baseURL }) => {
21+
const api = new ApiClient(request, baseURL!);
22+
const ids = [1, 2, 3, 4];
23+
24+
for (const id of ids) {
25+
const res = await api.get(`/products/${id}`);
26+
expect(res.status(), `id=${id}`).toBe(200);
27+
await api.assertJson(res);
28+
29+
const body = await res.json();
30+
expectProductShape(body);
31+
expect(body.id).toBe(id);
32+
}
33+
});
34+
35+
test("GET /products/:id returns 404 or an error payload for invalid id (negative)", async ({ request, baseURL }) => {
36+
const api = new ApiClient(request, baseURL!);
37+
38+
const res = await api.get("/products/999999");
39+
40+
// Делаем тест устойчивым: либо 404, либо res.ok=false, либо error/message в body
41+
if (res.ok()) {
42+
const body = await res.json();
43+
expect(body).toHaveProperty("message");
44+
} else {
45+
expect([400, 404]).toContain(res.status());
46+
}
47+
});
48+
});

tsconfig.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "CommonJS",
5+
"strict": true,
6+
"esModuleInterop": true,
7+
"skipLibCheck": true
8+
}
9+
}

utils/apiClient.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { APIRequestContext, expect } from "@playwright/test";
2+
3+
export class ApiClient {
4+
constructor(private request: APIRequestContext, private baseURL: string) {}
5+
6+
async get(path: string, headers?: Record<string, string>) {
7+
const res = await this.request.get(this.baseURL + path, { headers });
8+
return res;
9+
}
10+
11+
async post(path: string, data: unknown, headers?: Record<string, string>) {
12+
const res = await this.request.post(this.baseURL + path, { data, headers });
13+
return res;
14+
}
15+
16+
async assertJson(res: any) {
17+
expect(res.ok()).toBeTruthy();
18+
const contentType = res.headers()["content-type"] ?? "";
19+
expect(contentType).toContain("application/json");
20+
}
21+
}

0 commit comments

Comments
 (0)