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 操作流程
- 用戶訪問登入頁面 (
/login) - 輸入 Email:
staff@florist.com - 輸入密碼:
Password123! - 點擊「登入」按鈕
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
}
Cookie 設定
- 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"
}
前端行為
- 儲存
accessToken至 Redux Store - 儲存
user資訊至 Redux Store - Toast 通知顯示「登入成功」(綠色,2 秒)
- 重定向至首頁 (
/) - 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