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>
175 lines
6.4 KiB
Python
175 lines
6.4 KiB
Python
"""
|
||
批次作業測試 (6.4)
|
||
測試各批次作業的邏輯
|
||
"""
|
||
import datetime
|
||
import pytest
|
||
from unittest.mock import patch, MagicMock
|
||
|
||
|
||
class TestBatchLog:
|
||
"""BatchLog model 測試"""
|
||
|
||
def test_create_batch_log(self, db):
|
||
"""成功建立批次日誌"""
|
||
from app.models.batch_log import BatchLog
|
||
log = BatchLog(
|
||
batch_name="test_batch",
|
||
status="success",
|
||
message="測試批次執行成功",
|
||
started_at=datetime.datetime.utcnow(),
|
||
finished_at=datetime.datetime.utcnow(),
|
||
duration_seconds=5,
|
||
)
|
||
db.add(log)
|
||
db.commit()
|
||
db.refresh(log)
|
||
|
||
assert log.id is not None
|
||
assert log.batch_name == "test_batch"
|
||
assert log.status == "success"
|
||
|
||
def test_batch_log_repr(self, db):
|
||
"""BatchLog repr 格式"""
|
||
from app.models.batch_log import BatchLog
|
||
log = BatchLog(
|
||
batch_name="daily_quota_check",
|
||
status="success",
|
||
started_at=datetime.datetime(2026, 2, 18, 2, 0, 0),
|
||
)
|
||
db.add(log)
|
||
db.commit()
|
||
assert "daily_quota_check" in repr(log)
|
||
assert "success" in repr(log)
|
||
|
||
|
||
class TestDailyQuotaCheck:
|
||
"""每日配額檢查批次測試"""
|
||
|
||
def test_run_with_empty_db(self, db):
|
||
"""空資料庫時應能正常執行"""
|
||
# side_effect 使每次呼叫 get_db() 都回傳新 iter,避免 iterator 用盡問題
|
||
with patch("app.db.session.get_db", side_effect=lambda: iter([db])):
|
||
from app.batch.daily_quota_check import run_daily_quota_check
|
||
result = run_daily_quota_check()
|
||
|
||
assert result["status"] == "success"
|
||
assert result["summary"]["email_checked"] == 0
|
||
assert result["summary"]["drive_checked"] == 0
|
||
|
||
def test_run_with_email_accounts(self, db, sample_employee):
|
||
"""有郵件帳號時應正確計數"""
|
||
from app.models.email_account import EmailAccount
|
||
# 建立測試郵件帳號
|
||
account = EmailAccount(
|
||
tenant_id=1,
|
||
employee_id=sample_employee.id,
|
||
email_address="test.user@porscheworld.tw",
|
||
quota_mb=5120,
|
||
is_active=True,
|
||
)
|
||
db.add(account)
|
||
db.commit()
|
||
|
||
with patch("app.db.session.get_db", side_effect=lambda: iter([db])):
|
||
from app.batch.daily_quota_check import run_daily_quota_check
|
||
result = run_daily_quota_check()
|
||
|
||
assert result["status"] == "success"
|
||
assert result["summary"]["email_checked"] == 1
|
||
|
||
|
||
class TestSyncKeycloakUsers:
|
||
"""Keycloak 同步批次測試"""
|
||
|
||
def test_run_with_no_keycloak_connection(self, db, sample_employee):
|
||
"""Keycloak 無法連線時,batch 應以 failed 狀態記錄"""
|
||
with patch("app.db.session.get_db", side_effect=lambda: iter([db])):
|
||
with patch("app.services.keycloak_admin_client.get_keycloak_admin_client") as mock_kc:
|
||
# 模擬 Keycloak 連線失敗
|
||
mock_client = MagicMock()
|
||
mock_client.get_user_by_username.return_value = None
|
||
mock_kc.return_value = mock_client
|
||
|
||
from app.batch.sync_keycloak_users import run_sync_keycloak_users
|
||
result = run_sync_keycloak_users()
|
||
|
||
# 員工在 Keycloak 中不存在 → 記為 not_found_in_keycloak
|
||
assert result["status"] == "success"
|
||
assert result["summary"]["not_found_in_keycloak"] >= 0
|
||
|
||
def test_sync_disabled_employee(self, db, sample_employee):
|
||
"""離職員工應同步停用 Keycloak"""
|
||
# 將員工設為離職
|
||
sample_employee.status = "terminated"
|
||
db.commit()
|
||
|
||
with patch("app.db.session.get_db", side_effect=lambda: iter([db])):
|
||
with patch("app.services.keycloak_admin_client.get_keycloak_admin_client") as mock_kc:
|
||
mock_client = MagicMock()
|
||
# 模擬 Keycloak 中帳號仍是 enabled
|
||
mock_client.get_user_by_username.return_value = {
|
||
"id": "kc-uuid-123",
|
||
"enabled": True,
|
||
}
|
||
mock_client.update_user.return_value = True
|
||
mock_kc.return_value = mock_client
|
||
|
||
from app.batch.sync_keycloak_users import run_sync_keycloak_users
|
||
result = run_sync_keycloak_users()
|
||
|
||
# 應同步停用
|
||
assert result["summary"]["synced"] == 1
|
||
mock_client.update_user.assert_called_once_with(
|
||
"kc-uuid-123", {"enabled": False}
|
||
)
|
||
|
||
|
||
class TestArchiveAuditLogs:
|
||
"""審計日誌歸檔批次測試"""
|
||
|
||
def test_archive_no_old_logs(self, db):
|
||
"""沒有舊日誌時應正常執行"""
|
||
with patch("app.db.session.get_db", side_effect=lambda: iter([db])):
|
||
from app.batch.archive_audit_logs import run_archive_audit_logs
|
||
result = run_archive_audit_logs(dry_run=True)
|
||
|
||
assert result["status"] == "success"
|
||
assert result["archived"] == 0
|
||
|
||
def test_archive_old_logs_dry_run(self, db, sample_employee):
|
||
"""dry_run 模式下不刪除資料"""
|
||
from app.models.audit_log import AuditLog
|
||
import json
|
||
# 建立 91 天前的舊日誌
|
||
old_date = datetime.datetime.utcnow() - datetime.timedelta(days=91)
|
||
for i in range(3):
|
||
log = AuditLog(
|
||
tenant_id=1,
|
||
action="test_action",
|
||
resource_type="employee",
|
||
resource_id=sample_employee.id,
|
||
performed_by="test_system",
|
||
performed_at=old_date,
|
||
details=json.dumps({"test": i}),
|
||
)
|
||
db.add(log)
|
||
db.commit()
|
||
|
||
# 確認有 3 筆舊日誌
|
||
count_before = db.query(AuditLog).count()
|
||
assert count_before == 3
|
||
|
||
with patch("app.db.session.get_db", side_effect=lambda: iter([db])):
|
||
with patch("os.makedirs"):
|
||
with patch("builtins.open", MagicMock()):
|
||
with patch("csv.DictWriter") as mock_writer:
|
||
mock_writer.return_value.__enter__ = MagicMock()
|
||
mock_writer.return_value.__exit__ = MagicMock()
|
||
from app.batch.archive_audit_logs import run_archive_audit_logs
|
||
result = run_archive_audit_logs(dry_run=True)
|
||
|
||
# dry_run 模式下不刪除
|
||
assert result["archived"] == 3
|
||
assert result["deleted"] == 0
|