บทที่ 10 · Part 2 — Test Architecture

Authentication and Authorization

reuse authenticated state อย่างปลอดภัย ทดสอบหลาย role และพิสูจน์ 401, 403 กับ resource isolation

Authentication and Authorization

การ login ผ่าน UI ก่อนทุก test ทำ suite ช้าและโหลด identity service โดยไม่จำเป็น แต่การแชร์ session file ไฟล์เดียวให้ทุก role เสี่ยงทั้ง state collision และ secret leakage Authentication setup ต้องเร็วโดยไม่ทำให้ authorization evidence อ่อนลง

จบบทนี้คุณจะ

สร้าง auth setup project และ storageState ต่อ role เก็บ state อย่างปลอดภัย เลือก account strategy ตาม server-side mutation และทดสอบ 401, 403, hidden controls กับ resource isolation แยกกัน

Authentication ไม่เท่ากับ Authorization

  • Authentication พิสูจน์ว่า request มาจาก identity ใด
  • Authorization ตัดสินว่า identity นั้นทำ action ต่อ resource นี้ได้หรือไม่

การ reuse authenticated state ช่วย test อื่นข้ามขั้น login แต่ login flow เองยังต้องมี browser test สั้น ๆ ส่วน authorization ต้องเรียก server boundary จริง ไม่พอที่ปุ่ม Admin ถูกซ่อน เพราะผู้ใช้ยังส่ง HTTP request เองได้

Auth Setup Project

สร้างไฟล์ที่ถูก ignore เช่น playwright/.auth/learner.json:

// tests/auth/learner.setup.ts
import { expect, test as setup } from '@playwright/test'

const authFile = 'playwright/.auth/learner.json'

setup('authenticate learner', async ({ page }) => {
  await page.goto('/sign-in')
  await page.getByRole('textbox', { name: 'Email' }).fill(process.env.E2E_LEARNER_EMAIL!)
  await page.getByLabel('Password').fill(process.env.E2E_LEARNER_PASSWORD!)
  await page.getByRole('button', { name: 'Sign in' }).click()

  await expect(page).toHaveURL('/workshops')
  await expect(page.getByRole('button', { name: 'Account menu' })).toBeVisible()
  await page.context().storageState({ path: authFile })
})

Config dependencies ทำให้ setup รันก่อน project ที่ใช้ state:

projects: [
  {
    name: 'auth-learner',
    testMatch: /learner\.setup\.ts/,
  },
  {
    name: 'learner-chromium',
    testMatch: /tests\/ui\/learner\/.*\.spec\.ts/,
    use: {
      ...devices['Desktop Chrome'],
      storageState: 'playwright/.auth/learner.json',
    },
    dependencies: ['auth-learner'],
  },
]

Project dependencies แสดง setup ใน report และ trace ได้ชัดกว่า hidden global setup ตาม authentication guide State file อาจมี cookies/headers ที่ impersonate account จึงต้อง .gitignore, เก็บเป็น short-lived CI artifact เท่าที่จำเป็น และไม่ส่งเข้า chat/ticket

Shared Account หรือ Account ต่อ Worker

แชร์ state/account เดียวได้เมื่อ tests ไม่เปลี่ยน server-side state ของ account หรือ mutations ทั้งหมดมี unique resources ที่ไม่ชนกัน ถ้า tests แก้ profile, quota, inbox หรือ permissions ให้ใช้ account ต่อ parallel worker และ authenticate worker-scoped fixture

const workerAccount = accounts[testInfo.parallelIndex]

Account pool ต้องมีจำนวนพอ, credential rotation, least privilege และ cleanup owner อย่าแก้ปัญหา collision ด้วย workers: 1 ถ้า suite ออกแบบ isolate ได้ การลด worker เป็น mitigation ชั่วคราวและต้องบันทึกเหตุผล

API Authentication สำหรับ Setup

บางระบบ login ผ่าน API ได้เร็วกว่า UI แล้วบันทึก state:

setup('authenticate organizer through the session API', async ({ request }) => {
  const response = await request.post('/api/session', {
    data: {
      email: process.env.E2E_ORGANIZER_EMAIL,
      password: process.env.E2E_ORGANIZER_PASSWORD,
    },
  })
  expect(response.status()).toBe(200)
  await request.storageState({ path: 'playwright/.auth/organizer.json' })
})

ใช้เมื่อ API session เป็น supported test path และ cookie scope เหมือน browser Login UI test แยกยังต้องพิสูจน์ form, validation, redirect และ error feedback ที่ผู้ใช้เจอจริง

401 กับ 403 เป็นคนละ Contract

API suite ควรตรวจอย่างน้อย:

test('AUTH-API-001 - returns 401 when no session is supplied', async ({ request }) => {
  const response = await request.post('/api/organizer/workshops', {
    data: { title: 'Unauthorized attempt' },
  })
  expect(response.status()).toBe(401)
  expect((await response.json()).error.code).toBe('AUTHENTICATION_REQUIRED')
})

test('AUTH-API-002 - returns 403 when a learner calls an organizer action', async ({
  learnerApi,
}) => {
  const response = await learnerApi.post('/api/organizer/workshops', {
    data: { title: 'Forbidden attempt' },
  })
  expect(response.status()).toBe(403)
  expect((await response.json()).error.code).toBe('ROLE_FORBIDDEN')
})

401 คือไม่มี/ใช้ credential ไม่ได้ ส่วน 403 คือรู้ identity แล้วแต่ไม่มีสิทธิ์ Clients อาจตอบสนองต่างกัน จึง pin status และ stable machine-readable code ให้ exact

Resource Isolation

Organizer A ไม่ควรแก้ workshop ของ Organizer B และ learner คนหนึ่งไม่ควรอ่าน reservation ของอีกคน สร้าง resource ด้วย identity A แล้วใช้ identity B อ่าน/แก้:

const workshop = await organizerA.createWorkshop(aWorkshop())
const response = await organizerB.patch(`/api/workshops/${workshop.id}`, {
  data: { title: 'Cross-owner edit' },
})

expect(response.status()).toBe(404)
expect((await response.json()).error.code).toBe('WORKSHOP_NOT_FOUND')

ตัวอย่างใช้ 404 เพื่อไม่ยืนยัน existence ข้าม owner แต่ระบบจริงต้องกำหนด contract จาก threat model/product policy อย่าคัดลอก status โดยไม่ตกลงกับ API owner นอกจาก rejection ให้ inquiry ด้วย owner A ยืนยันว่า title เดิมไม่เปลี่ยน

UI Evidence เสริม Server Evidence

test('AUTH-UI-001 - does not offer organizer controls to a learner', async ({ page }) => {
  await page.goto('/workshops')
  await expect(page.getByRole('link', { name: 'Organizer dashboard' })).toHaveCount(0)
  await page.goto('/organizer')
  await expect(page.getByRole('heading', { name: 'Access denied' })).toBeVisible()
})

UI tests พิสูจน์ affordance/route feedback ส่วน API tests พิสูจน์ enforcement ต้องมีทั้งคู่สำหรับ critical boundary

Session Expiry และ Logout

ทดสอบ expired/revoked session ด้วย state ที่สร้างอย่างควบคุม ไม่แก้ cookie bytes แบบเดาสุ่ม Assertion ควรตรวจ redirect, preserved safe return path และไม่มีข้อมูล account เดิมใน cache หลัง logout หาก application ใช้ service worker/client cache ให้ครอบคลุมด้วย

Artifact อาจยึด session ได้

Trace, request log และ storage state อาจมี cookie/token ที่ใช้ impersonate test account Reporter/attachment ต้อง redact headers และกำหนด retention/access การใช้ test account ลด impact แต่ไม่ทำให้ secret กลายเป็น public

รายการตรวจสอบ

  • Auth setup แยกจาก feature tests และยืนยัน post-login state
  • State files ถูก ignore, short-lived และเข้าถึงจำกัด
  • Role/account strategy รองรับ parallel mutation
  • มี 401, 403 และ cross-resource isolation tests
  • UI hidden control ไม่ถูกใช้แทน server enforcement evidence
  • Logout/expiry ล้าง state ที่ผู้ใช้อื่นไม่ควรเห็น

สรุปบทนี้

Reuse authentication เพื่อลด setup cost แต่ authorization ต้องทดสอบทุก boundary ที่สำคัญ State file คือ credential และ isolation ของ account/resource เป็นเงื่อนไขให้ suite รันขนานได้อย่างปลอดภัย

อ่านเพิ่มเติม