Files
xiaoxia-saas/tests/unit/test_duplication_api_enqueue.py
CI Bot 481f8776b8
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 25s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 26s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m29s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m35s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m43s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m0s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m9s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m50s
AI Code Review / AI Code Review (pull_request) Successful in 3m43s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 7m24s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 11s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 8m8s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 16s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 26s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 291h42m10s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 291h42m12s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 291h42m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 291h42m13s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 291h49m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 291h49m11s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 291h49m9s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 291h49m35s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 291h49m36s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 291h49m37s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 291h49m41s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 291h49m43s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 291h49m41s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 291h49m43s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 291h49m41s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 291h49m44s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 292h16m35s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 292h23m35s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 292h24m9s
style: auto-format with black + isort + prettier [skip ci-format-check]
2026-09-04 03:34:57 +00:00

168 lines
5.9 KiB
Python

"""#1679 查重 API enqueue + repository.update 单测。
覆盖:
1. POST /duplication/upload 上传成功后调用 send_task 入队 worker。
2. POST /duplication/records/{id}/retry 重置后调用 send_task 入队 worker。
3. SQLAlchemyDuplicationRecordRepository.update 会调用 session.commit。
"""
from __future__ import annotations
import io
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "apps" / "api"))
sys.path.insert(0, str(ROOT / "packages"))
# ── 通用 fixtures ────────────────────────────────────────────────
def _make_auth_user(user_id: str = "user-1"):
from app.auth import AuthenticatedUser
from packages.domain.entities import User
user = User(id=user_id, email="u@example.com", display_name="Test User", username="u")
return AuthenticatedUser(user=user)
def _make_record(record_id: str = "rec-1", status: str = "pending", user_id: str = "user-1"):
from packages.domain.duplication import DuplicationRecord
record = DuplicationRecord.create(
user_id=user_id,
filename="demo.mp4",
file_size=2048,
storage_key="duplication/abc/demo.mp4",
)
# 覆盖生成的 id,方便断言
record.id = record_id
record.status = status
return record
@pytest.fixture()
def mock_repo():
return MagicMock()
@pytest.fixture()
def mock_storage():
return MagicMock()
@pytest.fixture()
def app(mock_repo, mock_storage):
"""构造带依赖 override 的 FastAPI app,避免拉起完整 DB / 认证栈。"""
from unittest.mock import MagicMock as _MagicMock
from unittest.mock import patch as _patch
from app.api.routes.duplication import router as duplication_router
from app.auth import AuthenticatedUser, get_current_user
from app.core.storage import get_storage_service
from app.dependencies import get_duplication_repository
from fastapi import FastAPI
test_app = FastAPI()
test_app.include_router(duplication_router, prefix="/duplication")
test_app.dependency_overrides[get_current_user] = lambda: _make_auth_user()
test_app.dependency_overrides[get_duplication_repository] = lambda: mock_repo
test_app.dependency_overrides[get_storage_service] = lambda: mock_storage
# Mock get_settings so OSS_DIRECT_UPLOAD_MAX_MB is available
mock_settings = _MagicMock()
mock_settings.OSS_DIRECT_UPLOAD_MAX_MB = 100
with _patch("app.config.get_settings", return_value=mock_settings):
yield test_app
# ── 1. 上传接口 enqueue ──────────────────────────────────────────
def test_upload_enqueue_calls_celery_task(app, mock_repo):
"""POST /duplication/upload 成功创建记录后,必须调用 send_task 入队 worker。"""
from fastapi.testclient import TestClient
record = _make_record("rec-upload", status="pending")
mock_repo.create.return_value = record
with patch("app.api.routes.duplication.celery_app") as mock_celery:
mock_celery.send_task = MagicMock(return_value=MagicMock(id="task-xyz"))
client = TestClient(app)
# 使用 in-memory bytes,避免真实写盘
response = client.post(
"/duplication/upload",
files={"file": ("demo.mp4", io.BytesIO(b"\x00\x00\x00\x00fake"), "video/mp4")},
)
assert response.status_code == 200, response.text
body = response.json()
assert body["id"] == "rec-upload"
# 关键断言:send_task 被调用,且参数包含 worker.process_duplication_check
mock_celery.send_task.assert_called_once_with("worker.process_duplication_check", args=["rec-upload"])
# ── 2. 重试接口 enqueue ──────────────────────────────────────────
def test_retry_enqueue_calls_celery_task(app, mock_repo):
"""POST /duplication/records/{id}/retry 重置后必须调用 send_task 入队 worker。"""
from fastapi.testclient import TestClient
record = _make_record("rec-retry", status="failed")
mock_repo.get.return_value = record
mock_repo.update.return_value = record
with patch("app.api.routes.duplication.celery_app") as mock_celery:
mock_celery.send_task = MagicMock(return_value=MagicMock(id="task-xyz"))
client = TestClient(app)
response = client.post("/duplication/records/rec-retry/retry")
assert response.status_code == 200, response.text
body = response.json()
assert body["id"] == "rec-retry"
# 关键断言:send_task 被调用
mock_celery.send_task.assert_called_once_with("worker.process_duplication_check", args=["rec-retry"])
# ── 3. repository.update 调用 session.commit ─────────────────────
def test_repository_update_calls_session_commit():
"""SQLAlchemyDuplicationRecordRepository.update 必须在末尾调用 session.commit。"""
from packages.adapters.sqlalchemy_impl.duplication_repository import (
SQLAlchemyDuplicationRecordRepository,
)
from packages.domain.duplication import DuplicationRecord
session = MagicMock()
# 模拟 query().filter().first() 返回一个 model 实例
model = MagicMock()
model.id = "rec-1"
query_proxy = MagicMock()
query_proxy.filter.return_value.first.return_value = model
session.query.return_value = query_proxy
repo = SQLAlchemyDuplicationRecordRepository(session)
record = DuplicationRecord.create(
user_id="user-1",
filename="demo.mp4",
file_size=1024,
storage_key="duplication/abc/demo.mp4",
)
record.id = "rec-1"
repo.update(record)
# 关键断言:session.commit 被调用一次
session.commit.assert_called_once()