Files
hr-portal/backend/tests/test_employees_api.py
Porsche Chen 360533393f feat: HR Portal - Complete Multi-Tenant System with Redis Session Storage
Major Features:
-  Multi-tenant architecture (tenant isolation)
-  Employee CRUD with lifecycle management (onboarding/offboarding)
-  Department tree structure with email domain management
-  Company info management (single-record editing)
-  System functions CRUD (permission management)
-  Email account management (multi-account per employee)
-  Keycloak SSO integration (auth.lab.taipei)
-  Redis session storage (10.1.0.254:6379)
  - Solves Cookie 4KB limitation
  - Cross-system session sharing
  - Sliding expiration (8 hours)
  - Automatic token refresh

Technical Stack:
Backend:
- FastAPI + SQLAlchemy
- PostgreSQL 16 (10.1.0.20:5433)
- Keycloak Admin API integration
- Docker Mailserver integration (SSH)
- Alembic migrations

Frontend:
- Next.js 14 (App Router)
- NextAuth 4 with Keycloak Provider
- Redis session storage (ioredis)
- Tailwind CSS

Infrastructure:
- Redis 7 (10.1.0.254:6379) - Session + Cache
- Keycloak 26.1.0 (auth.lab.taipei)
- Docker Mailserver (10.1.0.254)

Architecture Highlights:
- Session管理由 Keycloak + Redis 統一控制
- 支援多系統 (HR/WebMail/Calendar/Drive/Office) 共享 session
- Token 自動刷新,異質服務整合
- 未來可無縫遷移到雲端

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-23 20:12:43 +08:00

144 lines
5.3 KiB
Python

"""
員工管理 API 測試 (6.2)
測試 /api/v1/employees/ 的 CRUD 操作
"""
import datetime
import pytest
class TestEmployeeListAPI:
"""員工列表 API 測試"""
def test_get_employees_empty(self, client):
"""空資料庫回傳空列表"""
response = client.get("/api/v1/employees/")
assert response.status_code == 200
data = response.json()
assert data["total"] == 0
assert data["items"] == []
def test_get_employees_with_data(self, client, sample_employee):
"""有資料時回傳員工列表"""
response = client.get("/api/v1/employees/")
assert response.status_code == 200
data = response.json()
assert data["total"] == 1
assert len(data["items"]) == 1
assert data["items"][0]["employee_id"] == "EMP001"
def test_get_employees_pagination(self, client, db, sample_business_unit):
"""分頁功能測試"""
from app.models.employee import Employee
# 建立 5 個員工
for i in range(5):
emp = Employee(
tenant_id=1,
employee_id=f"EMP00{i+1}",
username_base=f"user{i+1}",
legal_name=f"員工{i+1}",
hire_date=datetime.date.today(),
status="active",
)
db.add(emp)
db.commit()
# 取第 1 頁,每頁 2 筆
response = client.get("/api/v1/employees/?page=1&page_size=2")
assert response.status_code == 200
data = response.json()
assert data["total"] == 5
assert len(data["items"]) == 2
assert data["total_pages"] == 3
class TestEmployeeCreateAPI:
"""員工建立 API 測試"""
def test_create_employee_success(self, client, sample_business_unit):
"""成功建立員工"""
payload = {
"username_base": "porsche.chen",
"legal_name": "陳保時",
"english_name": "Porsche Chen",
"hire_date": str(datetime.date.today()),
"business_unit_id": sample_business_unit.id,
"job_title": "軟體工程師",
"job_level": "Mid",
"email_quota_mb": 5120,
}
response = client.post("/api/v1/employees/", json=payload)
assert response.status_code == 201
data = response.json()
assert data["username_base"] == "porsche.chen"
assert data["legal_name"] == "陳保時"
assert data["status"] == "active"
assert "employee_id" in data # 自動產生
def test_create_employee_duplicate_username(self, client, sample_employee, sample_business_unit):
"""重複帳號名稱應回傳 400"""
payload = {
"username_base": "test.user", # 與 sample_employee 相同
"legal_name": "另一個用戶",
"hire_date": str(datetime.date.today()),
"business_unit_id": sample_business_unit.id,
"job_title": "工程師",
"job_level": "Junior",
"email_quota_mb": 1024,
}
response = client.post("/api/v1/employees/", json=payload)
assert response.status_code in [400, 409, 422]
def test_create_employee_missing_required_fields(self, client):
"""缺少必填欄位應回傳 422"""
response = client.post("/api/v1/employees/", json={"username_base": "only.this"})
assert response.status_code == 422
class TestEmployeeDetailAPI:
"""員工詳情 API 測試"""
def test_get_employee_success(self, client, sample_employee):
"""成功取得員工詳情"""
response = client.get(f"/api/v1/employees/{sample_employee.id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == sample_employee.id
assert data["employee_id"] == "EMP001"
def test_get_employee_not_found(self, client):
"""不存在的員工 ID 應回傳 404"""
response = client.get("/api/v1/employees/99999")
assert response.status_code == 404
def test_update_employee_success(self, client, sample_employee):
"""成功更新員工資料"""
payload = {"legal_name": "更新後姓名", "phone": "02-12345678"}
response = client.put(f"/api/v1/employees/{sample_employee.id}", json=payload)
assert response.status_code == 200
data = response.json()
assert data["legal_name"] == "更新後姓名"
assert data["phone"] == "02-12345678"
def test_delete_employee_success(self, client, sample_employee):
"""成功刪除 (停用) 員工"""
response = client.delete(f"/api/v1/employees/{sample_employee.id}")
assert response.status_code == 200
class TestEmployeeSearchAPI:
"""員工搜尋 API 測試"""
def test_search_by_name(self, client, sample_employee):
"""依姓名搜尋"""
response = client.get("/api/v1/employees/?search=測試")
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1
def test_filter_by_status(self, client, sample_employee):
"""依狀態篩選"""
response = client.get("/api/v1/employees/?status=active")
assert response.status_code == 200
data = response.json()
assert all(item["status"] == "active" for item in data["items"])