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:
127
backend/tests/test_permissions_api.py
Normal file
127
backend/tests/test_permissions_api.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
系統權限管理 API 測試 (6.2)
|
||||
測試 /api/v1/permissions/ CRUD 操作
|
||||
"""
|
||||
import pytest
|
||||
|
||||
|
||||
class TestPermissionsAPI:
|
||||
"""系統權限 API 測試"""
|
||||
|
||||
def test_get_permissions_empty(self, client):
|
||||
"""空資料庫回傳空列表"""
|
||||
response = client.get("/api/v1/permissions/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 0
|
||||
|
||||
def test_get_available_systems(self, client):
|
||||
"""取得可授權系統清單"""
|
||||
response = client.get("/api/v1/permissions/systems")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "systems" in data
|
||||
assert "gitea" in data["systems"]
|
||||
assert "keycloak" in data["systems"]
|
||||
assert "access_levels" in data
|
||||
assert "admin" in data["access_levels"]
|
||||
|
||||
def test_create_permission_success(self, client, sample_employee):
|
||||
"""成功建立權限"""
|
||||
payload = {
|
||||
"employee_id": sample_employee.id,
|
||||
"system_name": "gitea",
|
||||
"access_level": "user",
|
||||
}
|
||||
response = client.post("/api/v1/permissions/", json=payload)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["system_name"] == "gitea"
|
||||
assert data["access_level"] == "user"
|
||||
assert data["employee_id"] == sample_employee.id
|
||||
|
||||
def test_create_permission_duplicate(self, client, sample_employee):
|
||||
"""重複建立同系統權限應回傳 400"""
|
||||
payload = {
|
||||
"employee_id": sample_employee.id,
|
||||
"system_name": "gitea",
|
||||
"access_level": "user",
|
||||
}
|
||||
# 第一次建立
|
||||
client.post("/api/v1/permissions/", json=payload)
|
||||
# 第二次建立同系統
|
||||
response = client.post("/api/v1/permissions/", json=payload)
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_create_permission_invalid_system(self, client, sample_employee):
|
||||
"""無效系統名稱應回傳 422"""
|
||||
payload = {
|
||||
"employee_id": sample_employee.id,
|
||||
"system_name": "invalid_system",
|
||||
"access_level": "user",
|
||||
}
|
||||
response = client.post("/api/v1/permissions/", json=payload)
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_get_employee_permissions(self, client, sample_employee):
|
||||
"""取得員工所有權限"""
|
||||
# 先建立兩個權限
|
||||
for system in ["gitea", "portainer"]:
|
||||
client.post("/api/v1/permissions/", json={
|
||||
"employee_id": sample_employee.id,
|
||||
"system_name": system,
|
||||
"access_level": "user",
|
||||
})
|
||||
|
||||
response = client.get(f"/api/v1/permissions/employees/{sample_employee.id}/permissions")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 2
|
||||
systems = [p["system_name"] for p in data]
|
||||
assert "gitea" in systems
|
||||
assert "portainer" in systems
|
||||
|
||||
def test_update_permission(self, client, sample_employee):
|
||||
"""更新權限層級"""
|
||||
# 建立權限
|
||||
create_resp = client.post("/api/v1/permissions/", json={
|
||||
"employee_id": sample_employee.id,
|
||||
"system_name": "gitea",
|
||||
"access_level": "user",
|
||||
})
|
||||
perm_id = create_resp.json()["id"]
|
||||
|
||||
# 更新為 admin
|
||||
response = client.put(f"/api/v1/permissions/{perm_id}", json={"access_level": "admin"})
|
||||
assert response.status_code == 200
|
||||
assert response.json()["access_level"] == "admin"
|
||||
|
||||
def test_delete_permission(self, client, sample_employee):
|
||||
"""刪除權限"""
|
||||
create_resp = client.post("/api/v1/permissions/", json={
|
||||
"employee_id": sample_employee.id,
|
||||
"system_name": "gitea",
|
||||
"access_level": "user",
|
||||
})
|
||||
perm_id = create_resp.json()["id"]
|
||||
|
||||
response = client.delete(f"/api/v1/permissions/{perm_id}")
|
||||
assert response.status_code == 200
|
||||
|
||||
# 確認已刪除
|
||||
get_resp = client.get(f"/api/v1/permissions/{perm_id}")
|
||||
assert get_resp.status_code == 404
|
||||
|
||||
def test_batch_create_permissions(self, client, sample_employee):
|
||||
"""批次建立權限"""
|
||||
payload = {
|
||||
"employee_id": sample_employee.id,
|
||||
"permissions": [
|
||||
{"system_name": "gitea", "access_level": "user"},
|
||||
{"system_name": "portainer", "access_level": "readonly"},
|
||||
],
|
||||
}
|
||||
response = client.post("/api/v1/permissions/batch", json=payload)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert len(data) == 2
|
||||
Reference in New Issue
Block a user