Files
company-ai-platform/tests/test_feishu_inbound_reliability.py
JiuContinent eb8267ed18 ```
feat(feishu): 添加飞书入站事件inbox和混合数据库协调功能

- 实现飞书入站事件持久化inbox机制,支持状态管理、租约锁定和重试退避
- 添加混合数据库基线协调工具,确保平台PostgreSQL结构安全对齐
- 增加运行组件心跳检测和readiness就绪检查机制
- 实现app_ticket事件的安全轮换和验证处理
- 添加生产环境运行编排和fail-closed安全机制
- 支持webhook快速确认和长连接独立进程处理
- 完善个人数据擦除时的待处理事件清理功能
```
2026-07-27 17:14:37 +08:00

1221 lines
40 KiB
Python
Raw Permalink 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.

import json
import tempfile
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta
from hashlib import sha256
from pathlib import Path
from threading import Event, Lock
from typing import Any
import pytest
from alembic import command
from alembic.config import Config
from fastapi import HTTPException
from sqlalchemy import create_engine, func, select, text
from sqlalchemy.orm import Session, sessionmaker
from app.application.feishu.commands import FeishuCommandService
from app.application.feishu.events import (
FeishuEventService,
_audit_event_metadata,
_identifier_digest,
)
from app.application.feishu.personal_data import FeishuPersonalDataService
from app.application.feishu.results import command_result
from app.core.config import get_settings
from app.core.database import Base
from app.core.utils.time import utc_now
from app.modules.audit.models import AuditLog
from app.modules.feishu.client import FeishuClient
from app.modules.feishu import long_connection
from app.modules.feishu.constants import (
FeishuCommandName,
FeishuEventSource,
FeishuInboundStatus,
FeishuReplyType,
)
from app.modules.feishu.models import FeishuEventReceipt
from app.modules.feishu.services import FeishuInboundService
from app.modules.feishu_users.models import FeishuUser
from app.modules.feishu_users.identifiers import feishu_audit_identity_hash
from app.modules.feishu_users.principal import FeishuPrincipal
from app.modules.feishu_users.services import FeishuIdentityService
@pytest.fixture
def session_factory(
monkeypatch: pytest.MonkeyPatch,
) -> sessionmaker[Session]:
monkeypatch.setenv("FEISHU_USER_FEATURES_ENABLED", "false")
monkeypatch.setenv("FEISHU_VERIFICATION_TOKEN", "verified-token")
get_settings.cache_clear()
database_file = tempfile.NamedTemporaryFile(
prefix="feishu-inbound-",
suffix=".db",
delete=False,
)
database_file.close()
database_path = Path(database_file.name)
engine = create_engine(
f"sqlite:///{database_path.as_posix()}",
connect_args={"check_same_thread": False, "timeout": 5},
)
FeishuEventReceipt.__table__.create(engine)
AuditLog.__table__.create(engine)
FeishuUser.__table__.create(engine)
factory = sessionmaker(bind=engine, expire_on_commit=False)
try:
yield factory
finally:
FeishuUser.__table__.drop(engine)
AuditLog.__table__.drop(engine)
FeishuEventReceipt.__table__.drop(engine)
engine.dispose()
database_path.unlink(missing_ok=True)
get_settings.cache_clear()
@pytest.fixture
def full_session_factory(
monkeypatch: pytest.MonkeyPatch,
) -> sessionmaker[Session]:
monkeypatch.setenv("FEISHU_USER_FEATURES_ENABLED", "true")
monkeypatch.setenv("FEISHU_VERIFICATION_TOKEN", "verified-token")
get_settings.cache_clear()
database_file = tempfile.NamedTemporaryFile(
prefix="feishu-inbound-erasure-",
suffix=".db",
delete=False,
)
database_file.close()
database_path = Path(database_file.name)
engine = create_engine(
f"sqlite:///{database_path.as_posix()}",
connect_args={"check_same_thread": False, "timeout": 5},
)
Base.metadata.create_all(engine)
factory = sessionmaker(bind=engine, expire_on_commit=False)
try:
yield factory
finally:
engine.dispose()
database_path.unlink(missing_ok=True)
get_settings.cache_clear()
def _message_event(
event_id: str,
*,
text: str = "hello",
tenant_key: str = "tenant-a",
open_id: str = "ou-user",
) -> dict[str, Any]:
return {
"schema": "2.0",
"header": {
"event_id": event_id,
"event_type": "im.message.receive_v1",
"tenant_key": tenant_key,
"token": "verified-token",
},
"event": {
"sender": {"sender_id": {"open_id": open_id}},
"message": {
"chat_id": "oc-chat",
"chat_type": "p2p",
"message_id": f"om-{event_id}",
"message_type": "text",
"content": json.dumps({"text": text}),
},
},
}
def _accept(
db: Session,
payload: dict[str, Any],
) -> str:
accepted = FeishuEventService(db).accept_verified_event(
payload,
source=FeishuEventSource.WEBHOOK,
auto_reply=False,
)
assert accepted.event_key is not None
return accepted.event_key
def _expected_identifier_digest(value: str, domain: str) -> str:
digest = sha256(
f"company-ai-platform:feishu:{domain}:v1\0{value}".encode("utf-8")
).hexdigest()
return f"sha256-{digest}"
def _create_erasure_confirmation(
db: Session,
*,
tenant_key: str = "tenant-a",
open_id: str = "ou-user",
) -> tuple[FeishuPrincipal, str]:
user = FeishuUser(
code=f"FSU-ERASURE-{open_id}",
tenant_key=tenant_key,
open_id=open_id,
)
db.add(user)
db.commit()
db.refresh(user)
principal = FeishuPrincipal.from_user(
user,
chat_id="oc-chat",
chat_type="p2p",
)
confirmation = FeishuPersonalDataService(db).request_confirmation(
principal
)
return principal, confirmation.confirmation_code
def test_identifier_metadata_uses_fixed_domain_separated_digests(
session_factory: sessionmaker[Session],
) -> None:
raw_identifier = "shared-raw-identifier"
payload = _message_event(
raw_identifier,
tenant_key=raw_identifier,
open_id=raw_identifier,
)
payload["header"]["app_id"] = raw_identifier
payload["event"]["message"]["chat_id"] = raw_identifier
payload["event"]["message"]["message_id"] = raw_identifier
with session_factory() as db:
_accept(db, payload)
receipt = db.scalar(select(FeishuEventReceipt))
assert receipt is not None
metadata = _audit_event_metadata(
payload,
include_open_id=True,
include_identity_context=True,
)
digests = {
receipt.event_key,
metadata["event_id"],
metadata["message_id"],
metadata["chat_id"],
metadata["open_id"],
metadata["tenant_key"],
metadata["app_id"],
}
assert len(digests) == 7
assert all(
isinstance(value, str)
and len(value) == 71
and value.startswith("sha256-")
for value in digests
)
assert raw_identifier not in json.dumps(metadata)
def test_verified_acceptance_audit_uses_erasable_identity_subject(
full_session_factory: sessionmaker[Session],
) -> None:
with full_session_factory() as db:
_accept(
db,
_message_event(
"erasable-audit-subject",
tenant_key="tenant-audit",
open_id="ou-audit",
),
)
record = db.execute(
select(AuditLog).where(AuditLog.action == "webhook_event")
).scalar_one()
assert record.actor == feishu_audit_identity_hash(
"tenant-audit",
"ou-audit",
)
assert "tenant-audit" not in str(record.request_payload)
assert "ou-audit" not in str(record.request_payload)
def test_inbox_migration_digests_legacy_identifiers_once(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
database_path = tmp_path / "legacy-feishu-receipts.db"
database_url = f"sqlite:///{database_path.as_posix()}"
monkeypatch.setenv("DATABASE_URL", database_url)
get_settings.cache_clear()
config = Config("alembic.ini")
engine = create_engine(database_url)
received_at = datetime(2026, 7, 27, 1, 0)
raw_event_key = "tenant-a:im.message.receive_v1:legacy-event"
existing_event_key = _expected_identifier_digest(
"tenant-a:im.message.receive_v1:already-private",
"event-key",
)
existing_event_id = _expected_identifier_digest(
"already-private",
"event-id",
)
existing_message_id = _expected_identifier_digest(
"om-already-private",
"message-id",
)
try:
command.upgrade(config, "202607270001")
with engine.begin() as connection:
connection.execute(
text(
"""
INSERT INTO feishu_event_receipts (
event_key,
source,
event_id,
message_id,
received_at
)
VALUES (
:event_key,
'webhook',
:event_id,
:message_id,
:received_at
)
"""
),
[
{
"event_key": raw_event_key,
"event_id": "legacy-event",
"message_id": "om-legacy-event",
"received_at": received_at,
},
{
"event_key": existing_event_key,
"event_id": existing_event_id,
"message_id": existing_message_id,
"received_at": received_at,
},
],
)
command.upgrade(config, "head")
with engine.connect() as connection:
rows = list(
connection.execute(
text(
"""
SELECT event_key, event_id, message_id, status, payload
FROM feishu_event_receipts
ORDER BY id
"""
)
).mappings()
)
assert rows[0]["event_key"] == _expected_identifier_digest(
raw_event_key,
"event-key",
)
assert rows[0]["event_id"] == _expected_identifier_digest(
"legacy-event",
"event-id",
)
assert rows[0]["message_id"] == _expected_identifier_digest(
"om-legacy-event",
"message-id",
)
assert rows[1]["event_key"] == existing_event_key
assert rows[1]["event_id"] == existing_event_id
assert rows[1]["message_id"] == existing_message_id
assert all(row["status"] == FeishuInboundStatus.SUCCEEDED for row in rows)
assert all(row["payload"] is None for row in rows)
assert "legacy-event" not in json.dumps(rows, default=str)
finally:
engine.dispose()
get_settings.cache_clear()
def test_verified_intake_is_durable_sanitized_and_does_not_execute_command(
session_factory: sessionmaker[Session],
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
FeishuCommandService,
"handle_text",
lambda *_args, **_kwargs: pytest.fail("intake must not execute commands"),
)
payload = _message_event("durable-intake", text="private question")
payload["event"]["token"] = "nested-secret"
with session_factory() as db:
accepted = FeishuEventService(db).accept_verified_event(
payload,
source=FeishuEventSource.WEBHOOK,
auto_reply=False,
)
record = db.scalar(select(FeishuEventReceipt))
assert accepted.response["accepted"] is True
assert accepted.response["handled"] is False
assert accepted.should_dispatch is True
assert record is not None
assert record.status == FeishuInboundStatus.PENDING
assert record.next_attempt_at is not None
serialized = json.dumps(record.payload)
assert "verified-token" not in serialized
assert "nested-secret" not in serialized
assert "private question" in serialized
def test_concurrent_duplicate_workers_execute_only_once(
session_factory: sessionmaker[Session],
) -> None:
with session_factory() as db:
event_key = _accept(db, _message_event("concurrent-once"))
started = Event()
release = Event()
counter_lock = Lock()
execution_count = 0
def handler(
_payload: dict[str, Any],
_source: str,
_auto_reply: bool,
_event_key: str,
) -> dict[str, Any]:
nonlocal execution_count
with counter_lock:
execution_count += 1
started.set()
assert release.wait(timeout=5)
return {"ok": True}
def process(worker_id: str) -> str:
with session_factory() as db:
return FeishuInboundService(db).process(
event_key,
handler_factory=lambda _db: handler,
worker_id=worker_id,
).record.status
with ThreadPoolExecutor(max_workers=2) as executor:
first = executor.submit(process, "worker-one")
assert started.wait(timeout=5)
second = executor.submit(process, "worker-two")
release.set()
assert first.result(timeout=5) == FeishuInboundStatus.SUCCEEDED
assert second.result(timeout=5) == FeishuInboundStatus.SUCCEEDED
assert execution_count == 1
with session_factory() as db:
record = db.scalar(select(FeishuEventReceipt))
assert record is not None
assert record.attempt_count == 1
assert record.payload is None
def test_handler_exceeding_lease_is_not_executed_concurrently(
session_factory: sessionmaker[Session],
monkeypatch: pytest.MonkeyPatch,
) -> None:
with session_factory() as db:
event_key = _accept(db, _message_event("slow-handler-lease"))
claimed_at = utc_now()
handler_started = Event()
release_handler = Event()
replacement_claim_started = Event()
counter_lock = Lock()
execution_count = 0
original_claim = FeishuInboundService._claim
def observed_claim(
self: FeishuInboundService,
receipt_id: int,
lock_owner: str,
current: datetime,
**kwargs: Any,
) -> bool:
if lock_owner.startswith("replacement:"):
replacement_claim_started.set()
return original_claim(
self,
receipt_id,
lock_owner,
current,
**kwargs,
)
def handler(
_payload: dict[str, Any],
_source: str,
_auto_reply: bool,
_event_key: str,
) -> dict[str, bool]:
nonlocal execution_count
with counter_lock:
execution_count += 1
handler_started.set()
assert release_handler.wait(timeout=5)
return {"ok": True}
def process(worker_id: str, current: datetime) -> str:
with session_factory() as db:
return FeishuInboundService(db, lease_seconds=1).process(
event_key,
handler_factory=lambda _db: handler,
now=current,
worker_id=worker_id,
).record.status
monkeypatch.setattr(FeishuInboundService, "_claim", observed_claim)
with ThreadPoolExecutor(max_workers=2) as executor:
original = executor.submit(process, "original", claimed_at)
assert handler_started.wait(timeout=5)
replacement = executor.submit(
process,
"replacement",
claimed_at + timedelta(seconds=2),
)
assert replacement_claim_started.wait(timeout=5)
assert execution_count == 1
release_handler.set()
assert original.result(timeout=5) == FeishuInboundStatus.SUCCEEDED
assert replacement.result(timeout=5) == FeishuInboundStatus.SUCCEEDED
assert execution_count == 1
with session_factory() as db:
record = db.scalar(
select(FeishuEventReceipt).where(
FeishuEventReceipt.event_key == event_key
)
)
assert record is not None
assert record.attempt_count == 1
def test_failure_retries_with_same_reply_uuid_and_redacted_error(
session_factory: sessionmaker[Session],
monkeypatch: pytest.MonkeyPatch,
) -> None:
seen_uuids: list[str | None] = []
attempt_count = 0
def flaky_handle(
self: FeishuCommandService,
*_args: Any,
**_kwargs: Any,
) -> dict[str, Any]:
nonlocal attempt_count
attempt_count += 1
seen_uuids.append(self.feishu.message_uuid)
if attempt_count == 1:
raise RuntimeError("private prompt must never enter last_error")
return {"command": "fallback_ai"}
monkeypatch.setattr(FeishuCommandService, "handle_text", flaky_handle)
with session_factory() as db:
service = FeishuEventService(db)
event_key = _accept(db, _message_event("retry-stable-uuid"))
first = service.process_inbound_event(event_key, worker_id="first")
assert first.record.status == FeishuInboundStatus.RETRY
assert first.record.last_error == "RuntimeError"
assert "private prompt" not in str(first.record.last_error)
first.record.next_attempt_at = utc_now()
db.commit()
second = service.process_inbound_event(event_key, worker_id="second")
assert second.record.status == FeishuInboundStatus.SUCCEEDED
assert second.record.payload is None
duplicate = service.process_inbound_event(event_key, worker_id="duplicate")
assert duplicate.record.status == FeishuInboundStatus.SUCCEEDED
assert attempt_count == 2
assert len(seen_uuids) == 2
assert seen_uuids[0] == seen_uuids[1]
assert seen_uuids[0] is not None
assert seen_uuids[0].startswith("inbound-")
def test_command_writes_and_receipt_success_commit_atomically(
session_factory: sessionmaker[Session],
monkeypatch: pytest.MonkeyPatch,
) -> None:
with session_factory() as db:
event_key = _accept(db, _message_event("atomic-command"))
service = FeishuInboundService(db)
original_mark_success = FeishuInboundService._mark_success
fail_before_commit = True
def mark_success(
atomic_db: Session,
key: str,
lock_owner: str,
current: datetime,
) -> None:
nonlocal fail_before_commit
if fail_before_commit:
fail_before_commit = False
raise RuntimeError("simulated crash before atomic commit")
original_mark_success(atomic_db, key, lock_owner, current)
def handler_factory(atomic_db: Session) -> Any:
def handler(*_args: Any) -> dict[str, bool]:
atomic_db.add(
AuditLog(
actor="atomic-test",
source="test",
action="atomic-side-effect",
)
)
atomic_db.commit()
return {"ok": True}
return handler
monkeypatch.setattr(
FeishuInboundService,
"_mark_success",
staticmethod(mark_success),
)
first = service.process(
event_key,
handler_factory=handler_factory,
worker_id="first",
)
assert first.record.status == FeishuInboundStatus.RETRY
assert (
db.scalar(
select(func.count())
.select_from(AuditLog)
.where(AuditLog.action == "atomic-side-effect")
)
== 0
)
first.record.next_attempt_at = utc_now()
db.commit()
second = service.process(
event_key,
handler_factory=handler_factory,
worker_id="second",
)
assert second.record.status == FeishuInboundStatus.SUCCEEDED
assert (
db.scalar(
select(func.count())
.select_from(AuditLog)
.where(AuditLog.action == "atomic-side-effect")
)
== 1
)
def test_reply_outbox_does_not_rerun_command_after_post_send_crash(
session_factory: sessionmaker[Session],
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("FEISHU_APP_ID", "cli_test_app")
monkeypatch.setenv("FEISHU_APP_SECRET", "test-secret-value")
get_settings.cache_clear()
command_runs = 0
send_attempts: list[tuple[str, str | None]] = []
def handle_text(
self: FeishuCommandService,
_text: str,
chat_id: str | None = None,
actor: str = "feishu",
auto_reply: bool = True,
**_kwargs: Any,
) -> dict[str, Any]:
nonlocal command_runs
command_runs += 1
content = f"SUB-{command_runs}"
self.db.add(
AuditLog(
actor="reply-outbox-test",
source="test",
action="reply-outbox-command",
)
)
self.db.commit()
response = (
self.feishu.send_text(
content,
receive_id=chat_id,
actor=actor,
)
if auto_reply
else None
)
return command_result(
FeishuCommandName.SUBSCRIPTION_CREATE,
FeishuReplyType.TEXT,
"订阅",
content,
response,
)
def send_text(
_self: FeishuClient,
text_value: str,
_receive_id: str | None = None,
_receive_id_type: str = "chat_id",
uuid: str | None = None,
tenant_key: str | None = None,
) -> dict[str, Any]:
del tenant_key
send_attempts.append((text_value, uuid))
return {"code": 0, "data": {"message_id": "om-reply"}}
original_mark_reply_success = FeishuInboundService._mark_reply_success
crash_after_send = True
def mark_reply_success(
atomic_db: Session,
key: str,
lock_owner: str,
current: datetime,
) -> None:
nonlocal crash_after_send
if crash_after_send:
crash_after_send = False
raise RuntimeError("simulated crash after provider accepted reply")
original_mark_reply_success(
atomic_db,
key,
lock_owner,
current,
)
monkeypatch.setattr(FeishuCommandService, "handle_text", handle_text)
monkeypatch.setattr(FeishuClient, "send_text", send_text)
monkeypatch.setattr(
FeishuInboundService,
"_mark_reply_success",
staticmethod(mark_reply_success),
)
with session_factory() as db:
accepted = FeishuEventService(db).accept_verified_event(
_message_event("reply-post-send-crash", text="订阅 每天 09:00提醒我"),
source=FeishuEventSource.WEBHOOK,
auto_reply=True,
)
assert accepted.event_key is not None
event_key = accepted.event_key
first = FeishuEventService(db).process_inbound_event(
event_key,
worker_id="first",
)
assert first.record.status == FeishuInboundStatus.SUCCEEDED
assert first.record.reply_status == FeishuInboundStatus.RETRY
assert first.record.reply_payload is not None
assert command_runs == 1
assert len(send_attempts) == 1
assert send_attempts[0][0] == "SUB-1"
first.record.reply_next_attempt_at = utc_now()
db.commit()
FeishuInboundService(db).dispatch_reply(
event_key,
worker_id="reply-retry",
)
db.expire_all()
completed = db.scalar(
select(FeishuEventReceipt).where(
FeishuEventReceipt.event_key == event_key
)
)
assert completed is not None
assert completed.reply_status == FeishuInboundStatus.SUCCEEDED
assert completed.reply_payload is None
assert command_runs == 1
assert len(send_attempts) == 2
assert send_attempts[0] == send_attempts[1]
assert send_attempts[0][1] is not None
assert send_attempts[0][1].startswith("inbound-")
assert (
db.scalar(
select(func.count())
.select_from(AuditLog)
.where(AuditLog.action == "reply-outbox-command")
)
== 1
)
def test_reply_outbox_defers_image_and_card_until_command_commit(
session_factory: sessionmaker[Session],
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("FEISHU_APP_ID", "cli_test_app")
monkeypatch.setenv("FEISHU_APP_SECRET", "test-secret-value")
get_settings.cache_clear()
image_uploads: list[bytes] = []
sent_cards: list[tuple[dict[str, Any], str | None]] = []
command_runs = 0
def handle_text(
self: FeishuCommandService,
_text: str,
chat_id: str | None = None,
actor: str = "feishu",
auto_reply: bool = True,
**_kwargs: Any,
) -> dict[str, Any]:
nonlocal command_runs
command_runs += 1
response = None
if auto_reply:
image = self.feishu.upload_image(b"stable-chart", actor=actor)
image_key = (image.get("data") or {}).get("image_key")
card = self.feishu.build_basic_card(
"可靠卡片",
["同一事务提交后发送"],
image_key=image_key,
)
response = self.feishu.send_card(
card,
receive_id=chat_id,
actor=actor,
)
return command_result(
FeishuCommandName.MARKET_OVERVIEW,
FeishuReplyType.CARD,
"可靠卡片",
"同一事务提交后发送",
response,
["同一事务提交后发送"],
)
def upload_image(
_self: FeishuClient,
image: bytes,
_filename: str = "lifecycle-report.png",
tenant_key: str | None = None,
) -> dict[str, Any]:
del tenant_key
image_uploads.append(image)
return {"code": 0, "data": {"image_key": "img-stable"}}
def send_card(
_self: FeishuClient,
card: dict[str, Any],
_receive_id: str | None = None,
_receive_id_type: str = "chat_id",
uuid: str | None = None,
tenant_key: str | None = None,
) -> dict[str, Any]:
del tenant_key
sent_cards.append((card, uuid))
return {"code": 0, "data": {"message_id": "om-card"}}
original_mark_success = FeishuInboundService._mark_success
fail_before_commit = True
def mark_success(
atomic_db: Session,
key: str,
lock_owner: str,
current: datetime,
) -> None:
nonlocal fail_before_commit
if fail_before_commit:
fail_before_commit = False
raise RuntimeError("simulated crash before command commit")
original_mark_success(atomic_db, key, lock_owner, current)
monkeypatch.setattr(FeishuCommandService, "handle_text", handle_text)
monkeypatch.setattr(FeishuClient, "upload_image", upload_image)
monkeypatch.setattr(FeishuClient, "send_card", send_card)
monkeypatch.setattr(
FeishuInboundService,
"_mark_success",
staticmethod(mark_success),
)
with session_factory() as db:
accepted = FeishuEventService(db).accept_verified_event(
_message_event("reply-card-commit"),
source=FeishuEventSource.WEBHOOK,
auto_reply=True,
)
assert accepted.event_key is not None
event_key = accepted.event_key
first = FeishuEventService(db).process_inbound_event(
event_key,
worker_id="first",
)
assert first.record.status == FeishuInboundStatus.RETRY
assert image_uploads == []
assert sent_cards == []
first.record.next_attempt_at = utc_now()
db.commit()
second = FeishuEventService(db).process_inbound_event(
event_key,
worker_id="second",
)
assert second.record.status == FeishuInboundStatus.SUCCEEDED
assert second.record.reply_status == FeishuInboundStatus.SUCCEEDED
assert command_runs == 2
assert image_uploads == [b"stable-chart"]
assert len(sent_cards) == 1
assert sent_cards[0][1] is not None
assert sent_cards[0][0]["elements"][0]["img_key"] == "img-stable"
def test_expired_lease_is_reclaimed_and_terminal_failure_clears_payload(
session_factory: sessionmaker[Session],
) -> None:
current = datetime(2026, 7, 27, 2, 0)
with session_factory() as db:
event_key = _accept(db, _message_event("expired-lease"))
record = db.scalar(
select(FeishuEventReceipt).where(
FeishuEventReceipt.event_key == event_key
)
)
assert record is not None
record.status = FeishuInboundStatus.PROCESSING
record.attempt_count = 1
record.locked_by = "dead-worker"
record.locked_until = current - timedelta(seconds=1)
db.commit()
recovered = FeishuInboundService(db).process(
event_key,
handler_factory=lambda _db: (
lambda *_args: {"ok": True}
),
now=current,
worker_id="replacement",
)
assert recovered.record.status == FeishuInboundStatus.SUCCEEDED
assert recovered.record.attempt_count == 2
failed_key = _accept(db, _message_event("terminal-failure"))
service = FeishuInboundService(db)
pending_failure = db.scalar(
select(FeishuEventReceipt).where(
FeishuEventReceipt.event_key == failed_key
)
)
assert pending_failure is not None
assert pending_failure.next_attempt_at is not None
next_time = pending_failure.next_attempt_at + timedelta(microseconds=1)
for _ in range(4):
failed = service.process(
failed_key,
handler_factory=lambda _db: (
lambda *_args: (_ for _ in ()).throw(
RuntimeError("secret")
)
),
now=next_time,
)
next_time = (
failed.record.next_attempt_at + timedelta(microseconds=1)
if failed.record.next_attempt_at is not None
else next_time
)
assert failed.record.status == FeishuInboundStatus.FAILED
assert failed.record.attempt_count == 4
assert failed.record.payload is None
assert failed.record.last_error == "RuntimeError"
def test_erasure_clears_other_pending_identity_payloads(
session_factory: sessionmaker[Session],
) -> None:
with session_factory() as db:
current_key = _accept(db, _message_event("erase-current"))
other_key = _accept(db, _message_event("erase-other"))
reply_key = _accept(db, _message_event("erase-pending-reply"))
untouched_key = _accept(
db,
_message_event(
"erase-unrelated",
tenant_key="tenant-b",
open_id="ou-other",
),
)
reply_record = db.scalar(
select(FeishuEventReceipt).where(
FeishuEventReceipt.event_key == reply_key
)
)
assert reply_record is not None
reply_record.status = FeishuInboundStatus.SUCCEEDED
reply_record.payload = None
reply_record.reply_status = FeishuInboundStatus.PENDING
reply_record.reply_next_attempt_at = utc_now()
reply_record.reply_payload = {
"version": 1,
"identity": {
"tenant_key": "tenant-a",
"open_id": "ou-user",
},
"identity_fence_required": False,
"operations": [
{
"kind": "text",
"text": "must not be sent after erasure",
"receive_id": "oc-chat",
"receive_id_type": "chat_id",
}
],
}
db.commit()
cleared = FeishuInboundService(db).erase_identity_payloads(
tenant_key="tenant-a",
open_id="ou-user",
exclude_event_key=current_key,
)
db.commit()
records = {
item.event_key: item
for item in db.execute(select(FeishuEventReceipt)).scalars()
}
assert cleared == 2
assert records[current_key].payload is not None
assert records[other_key].status == FeishuInboundStatus.FAILED
assert records[other_key].payload is None
assert records[reply_key].reply_status == FeishuInboundStatus.FAILED
assert records[reply_key].reply_payload is None
assert records[untouched_key].payload is not None
def test_erasure_fences_already_claimed_event_before_identity_deletion(
full_session_factory: sessionmaker[Session],
monkeypatch: pytest.MonkeyPatch,
) -> None:
lock_owner = "old-worker:claimed"
with full_session_factory() as db:
_principal, confirmation_code = _create_erasure_confirmation(db)
old_payload = _message_event("old-processing-event")
old_key = _accept(db, old_payload)
deletion_key = _accept(
db,
_message_event(
"delete-identity-event",
text=f"确认忘记我 {confirmation_code}",
),
)
old_record = db.scalar(
select(FeishuEventReceipt).where(
FeishuEventReceipt.event_key == old_key
)
)
assert old_record is not None
old_record.status = FeishuInboundStatus.PROCESSING
old_record.attempt_count = 1
old_record.locked_by = lock_owner
old_record.locked_until = utc_now() + timedelta(minutes=5)
db.commit()
erasure_has_identity_fence = Event()
allow_erasure = Event()
old_worker_attempting = Event()
old_handler_calls = 0
original_erase = FeishuInboundService.erase_identity_payloads
def blocking_erase(
self: FeishuInboundService,
**kwargs: Any,
) -> int:
erasure_has_identity_fence.set()
assert allow_erasure.wait(timeout=5)
return original_erase(self, **kwargs)
def process_deletion() -> str:
with full_session_factory() as db:
return FeishuEventService(db).process_inbound_event(
deletion_key,
worker_id="deletion",
).record.status
def resume_claimed_event() -> int:
nonlocal old_handler_calls
old_worker_attempting.set()
def handler_factory(atomic_db: Session) -> Any:
def handler(*_args: Any) -> dict[str, bool]:
nonlocal old_handler_calls
old_handler_calls += 1
FeishuIdentityService(atomic_db).resolve_or_register(
tenant_key="tenant-a",
open_id="ou-user",
)
return {"ok": True}
return handler
with full_session_factory() as db:
service = FeishuInboundService(db)
try:
service._execute_atomically(
event_key=old_key,
lock_owner=lock_owner,
current=utc_now(),
payload=old_payload,
source=FeishuEventSource.WEBHOOK,
auto_reply=False,
handler_factory=handler_factory,
)
except HTTPException as exc:
return exc.status_code
return 200
monkeypatch.setattr(
FeishuInboundService,
"erase_identity_payloads",
blocking_erase,
)
with ThreadPoolExecutor(max_workers=2) as executor:
deletion = executor.submit(process_deletion)
assert erasure_has_identity_fence.wait(timeout=5)
old_worker = executor.submit(resume_claimed_event)
assert old_worker_attempting.wait(timeout=5)
assert not old_worker.done()
allow_erasure.set()
assert deletion.result(timeout=5) == FeishuInboundStatus.SUCCEEDED
assert old_worker.result(timeout=5) == 409
assert old_handler_calls == 0
with full_session_factory() as db:
assert db.scalar(select(FeishuUser)) is None
old_record = db.scalar(
select(FeishuEventReceipt).where(
FeishuEventReceipt.event_key == old_key
)
)
deletion_record = db.scalar(
select(FeishuEventReceipt).where(
FeishuEventReceipt.event_key == deletion_key
)
)
assert old_record is not None
assert old_record.status == FeishuInboundStatus.FAILED
assert old_record.payload is None
assert deletion_record is not None
assert deletion_record.status == FeishuInboundStatus.SUCCEEDED
monkeypatch.setattr(
FeishuIdentityService,
"resolve_or_register",
lambda *_args, **_kwargs: pytest.fail(
"a terminal erased event must not recreate its identity"
),
)
retry = FeishuEventService(db).process_inbound_event(
old_key,
worker_id="old-retry",
)
assert retry.record.status == FeishuInboundStatus.FAILED
assert db.scalar(select(FeishuUser)) is None
def test_completed_erasure_event_duplicate_does_not_recreate_identity(
full_session_factory: sessionmaker[Session],
monkeypatch: pytest.MonkeyPatch,
) -> None:
with full_session_factory() as db:
_principal, confirmation_code = _create_erasure_confirmation(db)
payload = _message_event(
"completed-delete-event",
text=f"确认忘记我 {confirmation_code}",
)
event_key = _accept(db, payload)
first = FeishuEventService(db).process_inbound_event(
event_key,
worker_id="first-delete",
)
assert first.record.status == FeishuInboundStatus.SUCCEEDED
assert db.scalar(select(FeishuUser)) is None
monkeypatch.setattr(
FeishuIdentityService,
"resolve_or_register",
lambda *_args, **_kwargs: pytest.fail(
"a completed deletion duplicate must not recreate its identity"
),
)
duplicate = FeishuEventService(db).process_inbound_event(
event_key,
worker_id="duplicate-delete",
)
accepted_duplicate = FeishuEventService(db).accept_verified_event(
payload,
source=FeishuEventSource.WEBHOOK,
auto_reply=False,
)
assert duplicate.record.status == FeishuInboundStatus.SUCCEEDED
assert accepted_duplicate.should_dispatch is False
assert db.scalar(select(FeishuUser)) is None
def test_long_connection_dispatches_only_when_durable_queue_is_enabled(
session_factory: sessionmaker[Session],
monkeypatch: pytest.MonkeyPatch,
) -> None:
payload = _message_event("long-connection-inbox")
queued: list[str] = []
monkeypatch.setattr(long_connection, "_sdk_event_to_payload", lambda _event: payload)
monkeypatch.setattr(long_connection, "SessionLocal", session_factory)
monkeypatch.setattr(
long_connection,
"enqueue_feishu_inbound_event",
lambda event_key, **_kwargs: queued.append(event_key),
)
long_connection._handle_verified_sdk_event(object())
with session_factory() as db:
record = db.scalar(select(FeishuEventReceipt))
assert record is not None
assert queued == []
assert record.source == FeishuEventSource.LONG_CONNECTION
assert record.status == FeishuInboundStatus.PENDING
queued_payload = _message_event("long-connection-queued")
monkeypatch.setattr(
long_connection,
"_sdk_event_to_payload",
lambda _event: queued_payload,
)
monkeypatch.setenv("TASK_QUEUE_ENABLED", "true")
get_settings.cache_clear()
long_connection._handle_verified_sdk_event(object())
with session_factory() as db:
queued_record = db.scalar(
select(FeishuEventReceipt).where(
FeishuEventReceipt.event_id
== _identifier_digest(
"long-connection-queued",
"event-id",
)
)
)
assert queued_record is not None
assert queued == [queued_record.event_key]