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>
This commit is contained in:
143
backend/tests/test_employees_api.py
Normal file
143
backend/tests/test_employees_api.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
員工管理 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"])
|
||||
Reference in New Issue
Block a user