Files
hr-portal/backend/tests/test_batch_jobs.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

175 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
批次作業測試 (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