""" 批次作業測試 (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