Mục lục
- Mục Tiêu Bài Học
- Bài Toán Multi-Role
- Kiến Trúc Tổng Quan
- Cú Pháp Config — Multi-Role Projects
- Setup File — auth.setup.ts
- Role Hierarchy — Admin / User / Guest
- Guest Role — Empty State
- RBAC Test Pattern
- Cross-Role Permission Boundary Test
- Folder Structure
- So Sánh Với Single-Role
- Gitignore
- Limitation
- 4 Pitfall
- Quiz
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- Hiểu tại sao multi-role auth cần cấu trúc khác với single-role.
- Khai báo nhiều project trong
playwright.config.ts, mỗi project dùng storageState của một role. - Viết setup file tạo auth state cho từng role trong cùng một file.
- Cấu hình Guest role không có file storageState.
- Áp dụng
testMatchđể phân tách test file theo role. - Viết test kiểm tra permission boundary (RBAC) giữa các role.
- Tránh 4 pitfall phổ biến khi setup multi-role.
Bài Toán Multi-Role
Hầu hết ứng dụng thực tế có nhiều hơn một loại user. Một dashboard quản trị điển hình có ít nhất ba role:
- Admin — toàn quyền: tạo/sửa/xóa users, thay đổi settings hệ thống, xem logs.
- User — quyền giới hạn: xem và chỉnh sửa data của chính mình, không truy cập được admin panel.
- Guest — chưa đăng nhập: chỉ truy cập public pages, signup, login form.
Test suite cần phủ cả ba role vì mỗi role có behavior khác nhau. Test chạy bằng admin state để kiểm tra CRUD users; test chạy bằng user state để kiểm tra view own data; test chạy bằng guest state để kiểm tra redirect khi chưa đăng nhập.
Nếu chỉ có một storageState dùng chung cho mọi test, bạn không thể kiểm tra được:
- User truy cập admin panel → phải bị chặn.
- Guest truy cập trang cần auth → phải bị redirect về login.
- Admin thấy controls mà user không thấy.
Multi-role auth giải quyết điều này bằng cách cấp mỗi project Playwright một storageState riêng của role tương ứng.
Kiến Trúc Tổng Quan
Multi-role auth trong Playwright xây trên ba thành phần:
- Mỗi role = 1 storageState file riêng — admin lưu vào
playwright/.auth/admin.json, user lưu vàoplaywright/.auth/user.json. Guest không có file (dùng empty state). - 1 setup project login từng role — project setup chạy
auth.setup.ts, file này chứa nhiềusetup()call, mỗi call login một role và ghi storageState ra file. - Mỗi role = 1 main project riêng — project admin dùng
storageState: 'playwright/.auth/admin.json'và chỉ chạy file test khớp vớitestMatch: /admin\..*/. Tương tự cho user và guest.
Flow thực thi:
1. Project 'setup' → auth.setup.ts chạy
→ login admin → ghi admin.json
→ login user → ghi user.json
2. Projects 'admin', → chạy song song (sau khi setup xong)
'user', 'guest' admin project: load admin.json
user project: load user.json
guest project: empty state (no file)
Bài 102 sẽ đi sâu vào cách tổ chức và quản lý các storageState file riêng lẻ. Bài này tập trung vào kiến trúc tổng thể và cách khai báo config.
Cú Pháp Config — Multi-Role Projects
Config dưới đây khai báo một setup project và ba main project, mỗi project cho một role:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// Project setup: chạy auth.setup.ts, không dùng storageState
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
// Project admin: load admin.json, chỉ chạy admin.*.spec.ts
{
name: 'admin',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['setup'],
testMatch: /admin\..*\.spec\.ts/,
},
// Project user: load user.json, chỉ chạy user.*.spec.ts
{
name: 'user',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
testMatch: /user\..*\.spec\.ts/,
},
// Project guest: empty state, chỉ chạy guest.*.spec.ts
{
name: 'guest',
use: {
...devices['Desktop Chrome'],
storageState: { cookies: [], origins: [] },
},
// guest không depend setup vì không cần login
testMatch: /guest\..*\.spec\.ts/,
},
],
});
Vài điểm cần lưu ý:
dependencies: ['setup']— chỉ admin và user mới cần dependency này vì phải đợi fileadmin.jsonvàuser.jsonđược tạo. Guest không cần.testMatchtheo pattern — mỗi project chỉ chạy test file match pattern của role mình. Nếu không cótestMatch, project sẽ chạy tất cả spec file với sai storageState.- Guest dùng inline empty state —
storageState: { cookies: [], origins: [] }là cách khai báo "không có auth" trực tiếp trong config, không cần tạo file JSON trống.
Setup File — auth.setup.ts
Một file auth.setup.ts chứa nhiều setup() call — mỗi call login một role và lưu state:
// tests/auth.setup.ts
import { test as setup } from '@playwright/test';
import path from 'path';
const ADMIN_STATE = path.resolve('playwright/.auth/admin.json');
const USER_STATE = path.resolve('playwright/.auth/user.json');
setup('authenticate admin', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill(process.env.ADMIN_PASSWORD!);
await page.getByRole('button', { name: 'Login' }).click();
// Đợi redirect đến trang chỉ admin mới vào được
await page.waitForURL('/admin');
// Lưu toàn bộ cookies + localStorage vào file
await page.context().storageState({ path: ADMIN_STATE });
});
setup('authenticate user', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill(process.env.USER_PASSWORD!);
await page.getByRole('button', { name: 'Login' }).click();
// User được redirect đến dashboard thường
await page.waitForURL('/dashboard');
await page.context().storageState({ path: USER_STATE });
});
// Guest không cần setup — empty state khai báo thẳng trong config
Hai setup() call trong cùng file chạy tuần tự. Khi cả hai xong, file admin.json và user.json tồn tại trên disk — lúc này các main project mới bắt đầu chạy.
Lưu ý về waitForURL: dùng URL đến trang mà chỉ role đó mới vào được sau login thành công. Cách này xác nhận login đã hoàn tất và cookie/session đã được set trước khi gọi storageState(). Nếu waitForURL fail, setup throw và các main project không chạy.
Credentials phải đến từ environment variable (process.env.ADMIN_PASSWORD), không hardcode trong code. Bài 105 sẽ đề cập cách quản lý credentials trong CI.
Role Hierarchy — Admin / User / Guest
Ba role điển hình trong ứng dụng và scope test tương ứng:
| Role | Auth state | Scope test chính |
|---|---|---|
| Admin | admin.json |
CRUD users, system settings, logs, ban user |
| User | user.json |
View own profile, edit own data, không vào được admin panel |
| Guest | Empty (no file) | Public pages, signup, login form, redirect khi truy cập protected route |
Phân tách test theo role còn giúp tăng tốc khi chạy subset: npx playwright test --project=admin chỉ chạy test admin mà không cần setup user hay guest. CI pipeline có thể song song ba project trên ba worker riêng.
Guest Role — Empty State
Guest là role không có auth — context phải sạch hoàn toàn, không có cookie hay localStorage nào từ session trước. Có hai cách khai báo:
Cách 1 — Inline object trong config (khuyến nghị):
{
name: 'guest',
use: {
storageState: { cookies: [], origins: [] },
},
testMatch: /guest\..*\.spec\.ts/,
}
Cách 2 — Không khai báo storageState (mặc định empty):
{
name: 'guest',
// Không có use.storageState → context bắt đầu hoàn toàn trống
testMatch: /guest\..*\.spec\.ts/,
}
Cả hai cách đều cho context không có cookies. Tuy nhiên, cách 1 được ưu tiên vì ý định rõ ràng hơn — đọc config biết ngay guest không có auth, không phải do lập trình viên quên khai báo.
Khi nào cần guest project? Bất cứ khi nào app có behavior khác nhau cho user chưa đăng nhập: landing page, signup flow, login form validation, public API endpoints, SEO routes. Những test này không nên chạy trong admin hay user project vì context đã có session — redirect behavior sẽ khác.
RBAC Test Pattern
RBAC (Role-Based Access Control) test kiểm tra hai chiều: role có quyền làm gì và không được làm gì. Mỗi chiều thuộc project của role đó.
Admin test — kiểm tra quyền có:
// tests/admin.dashboard.spec.ts
// Project 'admin' dùng admin.json → page đã ở trạng thái admin session
import { test, expect } from '@playwright/test';
test('admin can delete user', async ({ page }) => {
await page.goto('/admin/users');
// Kiểm tra Delete button visible cho admin
const deleteBtn = page.getByRole('button', { name: 'Delete' }).first();
await expect(deleteBtn).toBeVisible();
await deleteBtn.click();
await expect(page.getByText('User deleted')).toBeVisible();
});
test('admin can access system settings', async ({ page }) => {
await page.goto('/admin/settings');
await expect(page).toHaveURL('/admin/settings');
await expect(page.getByRole('heading', { name: 'System Settings' })).toBeVisible();
});
User test — kiểm tra quyền không có:
// tests/user.profile.spec.ts
// Project 'user' dùng user.json → page ở trạng thái user session
import { test, expect } from '@playwright/test';
test('user cannot see delete button on user list', async ({ page }) => {
// User có thể thấy danh sách users (nếu app cho phép)
await page.goto('/users');
// Nhưng không có quyền Delete
const deleteBtn = page.getByRole('button', { name: 'Delete' });
await expect(deleteBtn).not.toBeVisible();
});
test('user can edit own profile', async ({ page }) => {
await page.goto('/profile');
const editBtn = page.getByRole('button', { name: 'Edit Profile' });
await expect(editBtn).toBeVisible();
});
Guest test — kiểm tra redirect:
// tests/guest.signup.spec.ts
// Project 'guest' dùng empty state
import { test, expect } from '@playwright/test';
test('guest is redirected to login when accessing protected route', async ({ page }) => {
await page.goto('/dashboard');
// App redirect về /login khi chưa đăng nhập
await expect(page).toHaveURL(/\/login/);
});
test('guest can view landing page', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});
Cross-Role Permission Boundary Test
Permission boundary test kiểm tra việc một role truy cập vào vùng của role khác bị chặn đúng cách. Đây là phần quan trọng nhất của RBAC test.
// tests/user.permissions.spec.ts
// Chạy trong project 'user' → dùng user storageState
import { test, expect } from '@playwright/test';
test('user cannot access admin panel', async ({ page }) => {
await page.goto('/admin');
// Phải bị chặn — redirect về /forbidden hoặc /login
await expect(page).toHaveURL(/forbidden|login/);
});
test('user cannot access admin user management', async ({ page }) => {
await page.goto('/admin/users');
await expect(page).toHaveURL(/forbidden|login/);
});
test('user cannot call admin API directly', async ({ request }) => {
const response = await request.delete('/api/users/123');
// API phải trả 403 khi user thường gọi admin endpoint
expect(response.status()).toBe(403);
});
Test API trực tiếp qua request fixture (không qua UI) phát hiện lỗ hổng server-side authorization — trường hợp UI ẩn button nhưng API vẫn cho phép gọi.
Lưu ý về testMatch: file user.permissions.spec.ts phải đặt tên theo pattern user.* để chạy trong project user. Nếu đặt nhầm tên không match, test sẽ không chạy — hoặc tệ hơn, chạy trong sai project với sai auth state.
Folder Structure
Cấu trúc thư mục cho multi-role setup:
playwright/.auth/ ← gitignored
├── admin.json ← tạo bởi auth.setup.ts
└── user.json ← tạo bởi auth.setup.ts
← (guest = không có file)
tests/
├── auth.setup.ts ← login admin + user, ghi .json
├── admin.dashboard.spec.ts ← test admin-only features
├── admin.users.spec.ts ← test user management
├── user.profile.spec.ts ← test user own data
├── user.permissions.spec.ts ← test user bị chặn đúng chỗ
└── guest.signup.spec.ts ← test public pages + redirect
Quy ước đặt tên file:
admin.*— test chạy với admin state.user.*— test chạy với user state.guest.*— test chạy với empty state.auth.setup.ts— chạy bởi project setup (testMatch: /.*\.setup\.ts/).
Quy ước này phải nhất quán với testMatch pattern trong config. Nếu thay đổi pattern sau (ví dụ từ admin.* thành *-admin), phải rename tất cả file spec tương ứng.
So Sánh Với Single-Role
| Tiêu chí | Single-role | Multi-role |
|---|---|---|
| Số storageState file | 1 | N (1 per authenticated role) |
| Số setup call | 1 | N (1 per authenticated role) |
| Số main project | 1 (hoặc nhiều browser) | N role × M browser |
| testMatch | Thường không cần (mọi spec đều chạy) | Bắt buộc để phân tách file theo role |
| RBAC test | Không thể test permission boundary | Test được đầy đủ cả chiều cho phép và chặn |
| Setup time | Ngắn (1 login) | Dài hơn (N login, chạy tuần tự trong setup) |
| Token expire | 1 file cần refresh | N file, mỗi file có thể expire độc lập |
Multi-role không phải lúc nào cũng cần thiết. Nếu app chỉ có một loại authenticated user và không có RBAC, single-role với storageState đơn giản là đủ. Multi-role phù hợp khi app có ít nhất hai role có quyền khác nhau và bạn cần kiểm tra permission boundary giữa chúng.
Gitignore
Thư mục playwright/.auth/ chứa cookies và session tokens — không được commit vào repository:
# .gitignore
playwright/.auth/
File JSON trong thư mục này chứa cookies của tài khoản test. Nếu commit vào repo, bất kỳ ai có quyền đọc repo đều có thể dùng token đó để đăng nhập vào môi trường test (hoặc staging nếu dùng chung credentials). Trên CI, thư mục này được tạo mới mỗi run — không cần cache giữa các run trừ khi muốn tối ưu thời gian setup.
Limitation
- Setup complexity tăng tuyến tính theo số role — 3 role → 3 setup calls, 3 state files, 3 main projects, 3 testMatch pattern. Khi số role tăng (ví dụ manager, moderator, viewer), config và setup file dài hơn. Cần quy ước đặt tên nghiêm túc để tránh nhầm lẫn.
- Token expire độc lập per role — session admin có thể hết hạn trong khi session user vẫn còn. Khi chạy CI về đêm, tất cả token đều có thể expire. Cần cơ chế refresh hoặc tạo mới state trước mỗi run. Bài 104 sẽ đề cập API-based login để giảm thời gian setup.
- Naming convention quan trọng —
testMatchdựa trên tên file. Một file spec đặt sai tên sẽ không được chạy trong project đúng, hoặc chạy trong sai project. Lỗi này im lặng — không có warning nếu spec file không match bất kỳ testMatch nào. - Setup tuần tự — các
setup()call trongauth.setup.tschạy tuần tự. Nếu app login chậm (SSO redirect, 2FA), setup time tăng theo số role. Với 5 role mỗi login mất 3 giây → setup tốn 15 giây trước khi test bắt đầu.
4 Pitfall
Pitfall 1 — Test admin dùng nhầm user state
// SAI — project admin trỏ vào file sai
{
name: 'admin',
use: { storageState: 'playwright/.auth/user.json' }, // ← lỗi đánh máy
testMatch: /admin\..*\.spec\.ts/,
}
Test admin chạy với user session → admin page redirect về forbidden → test fail với thông báo sai lệch ("expected URL to contain /admin, got /forbidden"). Lỗi trông giống permission được setup đúng, thực ra là config sai. Kiểm tra path storageState khớp với tên role của project.
Pitfall 2 — Quên khai báo testMatch
// SAI — không có testMatch
{
name: 'admin',
use: { storageState: 'playwright/.auth/admin.json' },
dependencies: ['setup'],
// testMatch bị bỏ sót
}
Project admin chạy tất cả spec file trong thư mục test — kể cả guest.signup.spec.ts và user.profile.spec.ts — nhưng với admin storageState. Guest test kiểm tra redirect sẽ fail vì admin đã đăng nhập rồi. Luôn khai báo testMatch khi dùng multi-role setup.
Pitfall 3 — Role state expire giữa các test trong CI
Setup chạy lúc 00:00, test bắt đầu lúc 00:05. Session expire sau 30 phút. Test suite chạy hết 45 phút → test sau 00:30 fail với lỗi "not authenticated" dù setup đã thành công. Token expire mid-run không báo lỗi rõ ràng — chỉ thấy test fail với redirect về login page.
Giải pháp ngắn hạn: tăng session timeout trong môi trường test. Giải pháp dài hạn: dùng API login (bài 104) với token có thời hạn dài hơn, hoặc refresh token tự động trong setup.
Pitfall 4 — Guest project vô tình nhận admin state
// playwright.config.ts — use global storageState
export default defineConfig({
use: {
storageState: 'playwright/.auth/admin.json', // ← global config
},
projects: [
{ name: 'guest', testMatch: /guest\..*\.spec\.ts/ },
// Guest project không override storageState
// → kế thừa admin state từ global use
],
});
Global use.storageState được kế thừa bởi tất cả project không override. Guest project chạy với admin state — mọi redirect test đều fail vì đã đăng nhập. Luôn khai báo storageState tường minh trong từng project khi dùng multi-role, tránh dùng global use.storageState.
Quiz
Câu 1. Tại sao guest project không cần dependencies: ['setup']?
Đáp án
Project setup có nhiệm vụ login và ghi storageState file ra disk. Guest project dùng empty state — không cần file nào từ setup. Nếu khai báo dependencies: ['setup'], guest project vẫn đợi setup xong mới chạy — không sai về mặt kỹ thuật, nhưng lãng phí thời gian vì không cần state gì từ setup. Bỏ dependency giúp guest test có thể bắt đầu ngay lập tức song song với setup.
Câu 2. Config dưới đây có vấn đề gì?
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'admin',
use: { storageState: 'playwright/.auth/admin.json' },
dependencies: ['setup'],
},
{
name: 'user',
use: { storageState: 'playwright/.auth/user.json' },
dependencies: ['setup'],
},
]
Đáp án
Cả hai project admin và user đều thiếu testMatch. Playwright sẽ chạy tất cả spec file trong cả hai project. Một spec file sẽ chạy hai lần — một lần với admin state, một lần với user state. Test kiểm tra permission boundary sẽ cho kết quả không nhất quán: cùng một assertion có thể pass trong một project và fail trong project kia. Phải thêm testMatch: /admin\..*\.spec\.ts/ và testMatch: /user\..*\.spec\.ts/ cho từng project tương ứng.
Câu 3. Trong auth.setup.ts, tại sao cần waitForURL trước khi gọi storageState()?
Đáp án
Login thường là async — server nhận credentials, kiểm tra, tạo session, set cookie, rồi redirect. Nếu gọi storageState() ngay sau click Login mà chưa đợi redirect hoàn tất, cookies có thể chưa được set đầy đủ vào context. waitForURL('/admin') đảm bảo server đã xử lý xong và tất cả cookies/localStorage của session đã được ghi vào context trước khi capture state.
Câu 4. Dự án thêm role "Moderator" — cần thay đổi gì trong setup?
Đáp án
Cần: (1) thêm setup('authenticate moderator', ...) vào auth.setup.ts để login và ghi playwright/.auth/moderator.json; (2) thêm project mới trong config với name: 'moderator', storageState: 'playwright/.auth/moderator.json', dependencies: ['setup'], testMatch: /moderator\..*\.spec\.ts/; (3) tạo spec file theo convention moderator.*.spec.ts cho test của role mới.
Câu 5. Test sau chạy trong project nào và kiểm tra điều gì?
// file: user.admin-access.spec.ts
test('user cannot reach /admin/users', async ({ page }) => {
await page.goto('/admin/users');
await expect(page).toHaveURL(/forbidden|login/);
});
Đáp án
File khớp pattern user\..*\.spec\.ts → chạy trong project user với user.json storageState. Test kiểm tra permission boundary: user đã đăng nhập truy cập vào admin endpoint → phải bị chặn và redirect về /forbidden hoặc /login. Đây là test chiều "không được làm" của role user — cần chạy với user state, không phải admin state (nếu chạy với admin state, điều kiện trên sẽ fail vì admin có quyền vào /admin/users).
Bài Tiếp Theo
Bài 102 đi sâu vào quản lý storageState file per role: cách đặt tên, tổ chức thư mục, refresh khi expire, và share state giữa các worker.
