跳至主要内容

Scenario 1: 使用有效憑證登入成功

User Story: US-001: 用戶登入

Given: 系統初始狀態

已註冊用戶

{
"userId": "user-001",
"email": "staff@florist.com",
"passwordHash": "$2b$12$...", // 對應密碼 "Password123!"
"name": "王小明",
"roles": ["ROLE_STAFF"],
"tenantId": "tenant-abc",
"tenantName": "花店 ABC",
"status": "active",
"loginFailedCount": 0,
"lockedUntil": null
}

When: 執行操作

API 請求

POST /api/v1/auth/login HTTP/1.1
Content-Type: application/json

{
"email": "staff@florist.com",
"password": "Password123!",
"rememberMe": false
}

UI 操作流程

  1. 用戶訪問登入頁面 (/login)
  2. 輸入 Email: staff@florist.com
  3. 輸入密碼: Password123!
  4. 點擊「登入」按鈕

Then: 預期結果

系統響應 (200 OK)

{
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c2VyLTAwMSIsInRlbmFudElkIjoidGVuYW50LWFiYyIsImVtYWlsIjoic3RhZmZAZmxvcmlzdC5jb20iLCJyb2xlcyI6WyJST0xFX1NUQUZGIl0sImlhdCI6MTcxNDU3MDAwMCwiZXhwIjoxNzE0NTcwOTAwfQ...",
"refreshToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "user-001",
"email": "staff@florist.com",
"name": "王小明",
"roles": ["ROLE_STAFF"],
"tenantId": "tenant-abc",
"tenantName": "花店 ABC"
},
"expiresIn": 900
}

Access Token Payload (解碼後)

{
"userId": "user-001",
"tenantId": "tenant-abc",
"email": "staff@florist.com",
"roles": ["ROLE_STAFF"],
"iat": 1714570000,
"exp": 1714570900
}
  • refreshToken: HttpOnly Cookie,有效期 7 天
    Set-Cookie: refreshToken={token}; HttpOnly; Secure; SameSite=Strict; Max-Age=604800

數據庫變更

users 表更新

{
"userId": "user-001",
"loginFailedCount": 0, // 重置為 0
"lastLoginAt": "2025-10-31T10:30:00Z",
"lastLoginIp": "192.168.1.100"
}

audit_logs 表新增記錄

{
"id": "log-001",
"tenantId": "tenant-abc",
"userId": "user-001",
"action": "USER_LOGIN",
"resourceType": "User",
"resourceId": "user-001",
"status": "SUCCESS",
"ipAddress": "192.168.1.100",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"timestamp": "2025-10-31T10:30:00Z"
}

前端行為

  1. 儲存 accessToken 至 Redux Store
  2. 儲存 user 資訊至 Redux Store
  3. Toast 通知顯示「登入成功」(綠色,2 秒)
  4. 重定向至首頁 (/)
  5. Header 右上角顯示「王小明 [店員]」

自動化測試範例

Jest + MSW

describe('US-001 Scenario 1: Login with valid credentials', () => {
it('should login successfully and redirect to home', async () => {
const user = userEvent.setup();
render(<LoginPage />);

await user.type(screen.getByLabelText('Email'), 'staff@florist.com');
await user.type(screen.getByLabelText('密碼'), 'Password123!');
await user.click(screen.getByRole('button', { name: '登入' }));

await waitFor(() => {
expect(screen.getByText('登入成功')).toBeInTheDocument();
});

// 驗證重定向
expect(window.location.pathname).toBe('/');
});
});

最後更新: 2025-10-31