codex / tests /test_registration_outlook_selection.py
webhook's picture
Upload 137 files
89419ea verified
Raw History Blame Contribute Delete
17.4 kB
from contextlib import contextmanager
from pathlib import Path
import threading
import asyncio
from types import SimpleNamespace
from src.config.constants import EmailServiceType
from src.core.register import RegistrationResult
from src.database.models import Account, Base, EmailService, RegistrationTask
from src.database.session import DatabaseSessionManager
from src.web.routes import registration as registration_routes
def _make_session_manager(name):
runtime_dir = Path("tests_runtime")
runtime_dir.mkdir(exist_ok=True)
db_path = runtime_dir / name
if db_path.exists():
db_path.unlink()
manager = DatabaseSessionManager(f"sqlite:///{db_path}")
Base.metadata.create_all(bind=manager.engine)
return manager
@contextmanager
def _patched_get_db(manager):
session = manager.SessionLocal()
try:
yield session
finally:
session.close()
def _seed_outlook_services(session):
first = EmailService(
service_type="outlook",
name="Outlook A",
config={
"email": "reserved-a@example.com",
"client_id": "client-a",
"refresh_token": "refresh-a",
},
enabled=True,
priority=0,
)
second = EmailService(
service_type="outlook",
name="Outlook B",
config={
"email": "available-b@example.com",
"client_id": "client-b",
"refresh_token": "refresh-b",
},
enabled=True,
priority=1,
)
session.add_all([first, second])
session.commit()
session.refresh(first)
session.refresh(second)
return first, second
def _seed_failed_outlook_placeholder(session, email, service_name="placeholder"):
account = Account(
email=email,
password="",
email_service="outlook",
email_service_id=str(service_name),
status="failed",
source="register",
extra_data={
"register_failed_reason": "username_rejected_or_existing_account",
},
)
session.add(account)
session.commit()
session.refresh(account)
return account
def _seed_recoverable_outlook_account(session, email, service_name="placeholder"):
account = Account(
email=email,
password="saved-password",
email_service="outlook",
email_service_id=str(service_name),
status="failed",
source="register",
extra_data={
"register_failed_reason": "token_recovery_pending",
"recovery_ready": True,
"account_created": True,
"token_acquired": False,
},
)
session.add(account)
session.commit()
session.refresh(account)
return account
def _patch_registration_selection(monkeypatch, selected):
def fake_create(service_type, config, name=None):
selected["service_type"] = service_type
selected["config"] = dict(config)
selected["name"] = name
return SimpleNamespace(service_type=service_type, config=config, name=name)
def fake_run(self):
return RegistrationResult(success=False, error_message="stop after selection")
monkeypatch.setattr(registration_routes.EmailServiceFactory, "create", fake_create)
monkeypatch.setattr(registration_routes.RegistrationEngine, "run", fake_run)
def test_outlook_execution_uses_task_bound_email_service_id_when_runtime_arg_is_none(monkeypatch):
manager = _make_session_manager("outlook-selection-bound.db")
selected = {}
with manager.session_scope() as session:
first, second = _seed_outlook_services(session)
session.add(
RegistrationTask(
task_uuid="task-bound-email-service",
status="pending",
email_service_id=second.id,
)
)
session.commit()
@contextmanager
def fake_get_db():
with _patched_get_db(manager) as session:
yield session
monkeypatch.setattr(registration_routes, "get_db", fake_get_db)
_patch_registration_selection(monkeypatch, selected)
registration_routes._run_sync_registration_task(
task_uuid="task-bound-email-service",
email_service_type="outlook",
proxy=None,
email_service_config=None,
email_service_id=None,
)
assert selected["service_type"] == EmailServiceType.OUTLOOK
assert selected["config"]["email"] == "available-b@example.com"
assert selected["config"]["client_id"] == "client-b"
def test_outlook_auto_selection_skips_failed_placeholder_account(monkeypatch):
manager = _make_session_manager("outlook-selection-failed-placeholder.db")
selected = {}
with manager.session_scope() as session:
first, second = _seed_outlook_services(session)
_seed_failed_outlook_placeholder(session, "reserved-a@example.com")
session.add(
RegistrationTask(
task_uuid="task-skip-failed-placeholder",
status="pending",
)
)
session.commit()
@contextmanager
def fake_get_db():
with _patched_get_db(manager) as session:
yield session
monkeypatch.setattr(registration_routes, "get_db", fake_get_db)
_patch_registration_selection(monkeypatch, selected)
registration_routes._run_sync_registration_task(
task_uuid="task-skip-failed-placeholder",
email_service_type="outlook",
proxy=None,
email_service_config=None,
email_service_id=None,
)
assert selected["service_type"] == EmailServiceType.OUTLOOK
assert selected["config"]["email"] == "available-b@example.com"
assert selected["config"]["client_id"] == "client-b"
def test_outlook_auto_selection_reports_consumed_accounts_with_clear_error(monkeypatch):
manager = _make_session_manager("outlook-selection-all-consumed.db")
with manager.session_scope() as session:
first, second = _seed_outlook_services(session)
_seed_failed_outlook_placeholder(session, "reserved-a@example.com")
_seed_failed_outlook_placeholder(session, "available-b@example.com")
session.add(
RegistrationTask(
task_uuid="task-all-consumed",
status="pending",
)
)
session.commit()
@contextmanager
def fake_get_db():
with _patched_get_db(manager) as session:
yield session
monkeypatch.setattr(registration_routes, "get_db", fake_get_db)
registration_routes._run_sync_registration_task(
task_uuid="task-all-consumed",
email_service_type="outlook",
proxy=None,
email_service_config=None,
email_service_id=None,
)
with _patched_get_db(manager) as session:
task = session.query(RegistrationTask).filter_by(task_uuid="task-all-consumed").first()
assert task.status == "failed"
assert "已消耗" in (task.error_message or "")
assert "Outlook" in (task.error_message or "")
def test_outlook_bound_consumed_service_is_rejected_before_engine_runs(monkeypatch):
manager = _make_session_manager("outlook-selection-bound-consumed.db")
selected = {}
with manager.session_scope() as session:
first, second = _seed_outlook_services(session)
_seed_failed_outlook_placeholder(session, "available-b@example.com")
session.add(
RegistrationTask(
task_uuid="task-bound-consumed",
status="pending",
email_service_id=second.id,
)
)
session.commit()
@contextmanager
def fake_get_db():
with _patched_get_db(manager) as session:
yield session
monkeypatch.setattr(registration_routes, "get_db", fake_get_db)
def fake_create(service_type, config, name=None):
selected["service_type"] = service_type
selected["config"] = dict(config)
selected["name"] = name
return SimpleNamespace(service_type=service_type, config=config, name=name)
def fake_run(self):
selected["engine_ran"] = True
return RegistrationResult(success=False, error_message="should not be reached")
monkeypatch.setattr(registration_routes.EmailServiceFactory, "create", fake_create)
monkeypatch.setattr(registration_routes.RegistrationEngine, "run", fake_run)
registration_routes._run_sync_registration_task(
task_uuid="task-bound-consumed",
email_service_type="outlook",
proxy=None,
email_service_config=None,
email_service_id=None,
)
with _patched_get_db(manager) as session:
task = session.query(RegistrationTask).filter_by(task_uuid="task-bound-consumed").first()
assert selected == {}
assert task.status == "failed"
assert "已消耗" in (task.error_message or "")
assert "Outlook" in (task.error_message or "")
def test_outlook_auto_selection_skips_reserved_pending_task_and_uses_next_available_service(monkeypatch):
manager = _make_session_manager("outlook-selection-reserved.db")
selected = {}
with manager.session_scope() as session:
first, second = _seed_outlook_services(session)
session.add_all(
[
RegistrationTask(
task_uuid="reserved-task",
status="running",
email_service_id=first.id,
),
RegistrationTask(
task_uuid="selected-task",
status="pending",
),
]
)
session.commit()
@contextmanager
def fake_get_db():
with _patched_get_db(manager) as session:
yield session
monkeypatch.setattr(registration_routes, "get_db", fake_get_db)
_patch_registration_selection(monkeypatch, selected)
registration_routes._run_sync_registration_task(
task_uuid="selected-task",
email_service_type="outlook",
proxy=None,
email_service_config=None,
email_service_id=None,
)
assert selected["service_type"] == EmailServiceType.OUTLOOK
assert selected["config"]["email"] == "available-b@example.com"
assert selected["config"]["client_id"] == "client-b"
def test_outlook_auto_selection_reserves_account_atomically_across_concurrent_tasks(monkeypatch):
manager = _make_session_manager("outlook-selection-concurrent.db")
selections = {}
with manager.session_scope() as session:
first, second = _seed_outlook_services(session)
session.add_all(
[
RegistrationTask(task_uuid="task-1", status="pending"),
RegistrationTask(task_uuid="task-2", status="pending"),
]
)
session.commit()
@contextmanager
def fake_get_db():
with _patched_get_db(manager) as session:
yield session
real_update = registration_routes.crud.update_registration_task
task_1_reached_reservation = threading.Event()
allow_task_1_reservation = threading.Event()
def fake_update_registration_task(db, task_uuid, **kwargs):
if "email_service_id" in kwargs and task_uuid == "task-1":
task_1_reached_reservation.set()
allow_task_1_reservation.wait(timeout=5)
return real_update(db, task_uuid, **kwargs)
def fake_create(service_type, config, name=None):
selections[threading.current_thread().name] = dict(config)
return SimpleNamespace(service_type=service_type, config=config, name=name)
def fake_run(self):
return RegistrationResult(success=False, error_message="stop after selection")
monkeypatch.setattr(registration_routes, "get_db", fake_get_db)
monkeypatch.setattr(registration_routes.crud, "update_registration_task", fake_update_registration_task)
monkeypatch.setattr(registration_routes.EmailServiceFactory, "create", fake_create)
monkeypatch.setattr(registration_routes.RegistrationEngine, "run", fake_run)
errors = []
def run_task(task_uuid, release_after_entry=False):
try:
if release_after_entry:
task_1_reached_reservation.wait(timeout=5)
registration_routes._run_sync_registration_task(
task_uuid=task_uuid,
email_service_type="outlook",
proxy=None,
email_service_config=None,
email_service_id=None,
)
except Exception as exc: # pragma: no cover - test diagnostics
errors.append(exc)
finally:
if release_after_entry:
allow_task_1_reservation.set()
thread_1 = threading.Thread(target=run_task, args=("task-1", False), name="task-1-thread")
thread_2 = threading.Thread(target=run_task, args=("task-2", True), name="task-2-thread")
thread_1.start()
thread_2.start()
thread_1.join(timeout=10)
thread_2.join(timeout=10)
assert not errors
assert selections["task-1-thread"]["email"] == "reserved-a@example.com"
assert selections["task-2-thread"]["email"] == "available-b@example.com"
def test_outlook_auto_selection_keeps_recoverable_account_available(monkeypatch):
manager = _make_session_manager("outlook-selection-recoverable.db")
selected = {}
with manager.session_scope() as session:
_seed_outlook_services(session)
_seed_recoverable_outlook_account(session, "reserved-a@example.com")
session.add(
RegistrationTask(
task_uuid="task-recoverable-selection",
status="pending",
)
)
session.commit()
@contextmanager
def fake_get_db():
with _patched_get_db(manager) as session:
yield session
monkeypatch.setattr(registration_routes, "get_db", fake_get_db)
_patch_registration_selection(monkeypatch, selected)
registration_routes._run_sync_registration_task(
task_uuid="task-recoverable-selection",
email_service_type="outlook",
proxy=None,
email_service_config=None,
email_service_id=None,
)
assert selected["service_type"] == EmailServiceType.OUTLOOK
assert selected["config"]["email"] == "reserved-a@example.com"
def test_outlook_bound_recoverable_service_runs_instead_of_being_rejected(monkeypatch):
manager = _make_session_manager("outlook-selection-bound-recoverable.db")
selected = {}
with manager.session_scope() as session:
first, second = _seed_outlook_services(session)
_seed_recoverable_outlook_account(session, "available-b@example.com")
session.add(
RegistrationTask(
task_uuid="task-bound-recoverable",
status="pending",
email_service_id=second.id,
)
)
session.commit()
@contextmanager
def fake_get_db():
with _patched_get_db(manager) as session:
yield session
monkeypatch.setattr(registration_routes, "get_db", fake_get_db)
def fake_create(service_type, config, name=None):
selected["service_type"] = service_type
selected["config"] = dict(config)
selected["name"] = name
return SimpleNamespace(service_type=service_type, config=config, name=name)
def fake_run(self):
selected["engine_ran"] = True
return RegistrationResult(success=False, error_message="recovery still failed")
monkeypatch.setattr(registration_routes.EmailServiceFactory, "create", fake_create)
monkeypatch.setattr(registration_routes.RegistrationEngine, "run", fake_run)
registration_routes._run_sync_registration_task(
task_uuid="task-bound-recoverable",
email_service_type="outlook",
proxy=None,
email_service_config=None,
email_service_id=None,
)
with _patched_get_db(manager) as session:
task = session.query(RegistrationTask).filter_by(task_uuid="task-bound-recoverable").first()
assert selected["engine_ran"] is True
assert selected["config"]["email"] == "available-b@example.com"
assert task.status == "failed"
assert task.error_message == "recovery still failed"
def test_outlook_batch_skip_registered_keeps_recoverable_accounts(monkeypatch):
manager = _make_session_manager("outlook-batch-recoverable.db")
with manager.session_scope() as session:
first, _second = _seed_outlook_services(session)
first_id = first.id
_seed_recoverable_outlook_account(session, "reserved-a@example.com")
@contextmanager
def fake_get_db():
with _patched_get_db(manager) as session:
yield session
monkeypatch.setattr(registration_routes, "get_db", fake_get_db)
async def fake_batch_runner(*args, **kwargs):
return None
monkeypatch.setattr(registration_routes, "run_outlook_batch_registration", fake_batch_runner)
async def invoke():
from fastapi import BackgroundTasks
return await registration_routes.start_outlook_batch_registration(
registration_routes.OutlookBatchRegistrationRequest(
service_ids=[first_id],
skip_registered=True,
),
BackgroundTasks(),
)
response = asyncio.run(invoke())
assert response.to_register == 1
assert response.service_ids == [first_id]