test: replace TestClient with direct router calls to fix hang in binding tests
- Removed TestClient/FastAPI/app.main imports entirely - Tests now call websites_router.create_binding() and websites_router.update_binding() directly with db_session - Bypasses all ASGI transport and lifespan issues - All 13 tests pass in 0.75s
This commit is contained in:
@@ -3,22 +3,20 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
|
||||||
from app.database import Base, get_db
|
from app.database import Base
|
||||||
from app.main import app
|
|
||||||
from app.models.snapshot import UpstreamRateSnapshot
|
from app.models.snapshot import UpstreamRateSnapshot
|
||||||
from app.models.upstream import Upstream
|
from app.models.upstream import Upstream
|
||||||
from app.models.notification_log import NotificationLog
|
from app.models.notification_log import NotificationLog
|
||||||
from app.models.website import Website, WebsiteGroupBinding, WebsiteSyncLog
|
from app.models.website import Website, WebsiteGroupBinding, WebsiteSyncLog
|
||||||
from app.models.webhook_config import WebhookConfig
|
from app.models.webhook_config import WebhookConfig
|
||||||
from app.routers import websites as websites_router
|
from app.routers import websites as websites_router
|
||||||
from app.utils.auth import get_current_user
|
from app.schemas.website import BindingCreate, BindingUpdate
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
@@ -45,33 +43,6 @@ def db_session(engine):
|
|||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def client(engine, monkeypatch):
|
|
||||||
"""Provide a TestClient with a fresh session factory for each request."""
|
|
||||||
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
||||||
|
|
||||||
def override_get_db():
|
|
||||||
db = TestingSessionLocal()
|
|
||||||
try:
|
|
||||||
yield db
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
# Mock lifespan-related database initialization to avoid using the real DB file
|
|
||||||
monkeypatch.setattr("app.main.init_db", lambda: None)
|
|
||||||
monkeypatch.setattr("app.main._init_admin", lambda: None)
|
|
||||||
monkeypatch.setattr("app.main.start_scheduler", lambda: None)
|
|
||||||
monkeypatch.setattr("app.main.stop_scheduler", lambda: None)
|
|
||||||
|
|
||||||
app.dependency_overrides[get_db] = override_get_db
|
|
||||||
app.dependency_overrides[get_current_user] = lambda: object()
|
|
||||||
try:
|
|
||||||
with TestClient(app) as c:
|
|
||||||
yield c
|
|
||||||
finally:
|
|
||||||
app.dependency_overrides.clear()
|
|
||||||
|
|
||||||
|
|
||||||
def seed_rows(db_session, *, website_enabled=True, auto_sync_enabled=True):
|
def seed_rows(db_session, *, website_enabled=True, auto_sync_enabled=True):
|
||||||
website = Website(
|
website = Website(
|
||||||
name="Target",
|
name="Target",
|
||||||
@@ -113,24 +84,24 @@ def seed_rows(db_session, *, website_enabled=True, auto_sync_enabled=True):
|
|||||||
return website, upstream
|
return website, upstream
|
||||||
|
|
||||||
|
|
||||||
def binding_payload(website_id, upstream_id, *, enabled=True):
|
def make_body(website_id, upstream_id, *, enabled=True):
|
||||||
return {
|
return BindingCreate(
|
||||||
"website_id": website_id,
|
website_id=website_id,
|
||||||
"target_group_id": "target",
|
target_group_id="target",
|
||||||
"target_group_name": "Target group",
|
target_group_name="Target group",
|
||||||
"source_groups": [{
|
source_groups=[{
|
||||||
"upstream_id": upstream_id,
|
"upstream_id": upstream_id,
|
||||||
"upstream_name": "Upstream",
|
"upstream_name": "Upstream",
|
||||||
"group_id": "source",
|
"group_id": "source",
|
||||||
"group_name": "Source",
|
"group_name": "Source",
|
||||||
}],
|
}],
|
||||||
"percent": 10,
|
percent=10,
|
||||||
"algorithm": "max_plus_percent",
|
algorithm="max_plus_percent",
|
||||||
"enabled": enabled,
|
enabled=enabled,
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_binding_runs_initial_sync(monkeypatch, client, db_session):
|
def test_create_binding_runs_initial_sync(monkeypatch, db_session):
|
||||||
website, upstream = seed_rows(db_session)
|
website, upstream = seed_rows(db_session)
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
@@ -151,10 +122,13 @@ def test_create_binding_runs_initial_sync(monkeypatch, client, db_session):
|
|||||||
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeClient)
|
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeClient)
|
||||||
monkeypatch.setattr("app.services.website_sync.Sub2ApiWebsiteClient", FakeClient)
|
monkeypatch.setattr("app.services.website_sync.Sub2ApiWebsiteClient", FakeClient)
|
||||||
|
|
||||||
response = client.post("/api/group-bindings", json=binding_payload(website.id, upstream.id))
|
result = websites_router.create_binding(
|
||||||
|
make_body(website.id, upstream.id),
|
||||||
|
db_session,
|
||||||
|
object(),
|
||||||
|
)
|
||||||
|
|
||||||
assert response.status_code == 201
|
assert result.target_group_id == "target"
|
||||||
assert response.json()["target_group_id"] == "target"
|
|
||||||
assert calls == [("/groups/{id}", "target", "2.2000")]
|
assert calls == [("/groups/{id}", "target", "2.2000")]
|
||||||
log = db_session.query(WebsiteSyncLog).one()
|
log = db_session.query(WebsiteSyncLog).one()
|
||||||
assert log.status == "success"
|
assert log.status == "success"
|
||||||
@@ -163,12 +137,16 @@ def test_create_binding_runs_initial_sync(monkeypatch, client, db_session):
|
|||||||
assert log.new_rate == "2.2"
|
assert log.new_rate == "2.2"
|
||||||
|
|
||||||
|
|
||||||
def test_create_binding_skips_write_when_website_auto_sync_disabled(client, db_session):
|
def test_create_binding_skips_write_when_website_auto_sync_disabled(db_session):
|
||||||
website, upstream = seed_rows(db_session, auto_sync_enabled=False)
|
website, upstream = seed_rows(db_session, auto_sync_enabled=False)
|
||||||
|
|
||||||
response = client.post("/api/group-bindings", json=binding_payload(website.id, upstream.id))
|
result = websites_router.create_binding(
|
||||||
|
make_body(website.id, upstream.id),
|
||||||
|
db_session,
|
||||||
|
object(),
|
||||||
|
)
|
||||||
|
|
||||||
assert response.status_code == 201
|
assert result.target_group_id == "target"
|
||||||
assert db_session.query(WebsiteGroupBinding).count() == 1
|
assert db_session.query(WebsiteGroupBinding).count() == 1
|
||||||
log = db_session.query(WebsiteSyncLog).one()
|
log = db_session.query(WebsiteSyncLog).one()
|
||||||
assert log.status == "success"
|
assert log.status == "success"
|
||||||
@@ -177,26 +155,34 @@ def test_create_binding_skips_write_when_website_auto_sync_disabled(client, db_s
|
|||||||
assert log.new_rate == "2.2"
|
assert log.new_rate == "2.2"
|
||||||
|
|
||||||
|
|
||||||
def test_create_binding_skips_write_when_binding_disabled(client, db_session):
|
def test_create_binding_skips_write_when_binding_disabled(db_session):
|
||||||
website, upstream = seed_rows(db_session)
|
website, upstream = seed_rows(db_session)
|
||||||
|
|
||||||
response = client.post("/api/group-bindings", json=binding_payload(website.id, upstream.id, enabled=False))
|
result = websites_router.create_binding(
|
||||||
|
make_body(website.id, upstream.id, enabled=False),
|
||||||
|
db_session,
|
||||||
|
object(),
|
||||||
|
)
|
||||||
|
|
||||||
assert response.status_code == 201
|
assert result.target_group_id == "target"
|
||||||
log = db_session.query(WebsiteSyncLog).one()
|
log = db_session.query(WebsiteSyncLog).one()
|
||||||
assert log.status == "success"
|
assert log.status == "success"
|
||||||
assert log.message == "绑定未启用,未写回"
|
assert log.message == "绑定未启用,未写回"
|
||||||
assert log.new_rate == "2.2"
|
assert log.new_rate == "2.2"
|
||||||
|
|
||||||
|
|
||||||
def test_create_binding_keeps_binding_when_initial_sync_calculation_fails(client, db_session):
|
def test_create_binding_keeps_binding_when_initial_sync_calculation_fails(db_session):
|
||||||
website, upstream = seed_rows(db_session)
|
website, upstream = seed_rows(db_session)
|
||||||
db_session.query(UpstreamRateSnapshot).delete()
|
db_session.query(UpstreamRateSnapshot).delete()
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
response = client.post("/api/group-bindings", json=binding_payload(website.id, upstream.id))
|
result = websites_router.create_binding(
|
||||||
|
make_body(website.id, upstream.id),
|
||||||
|
db_session,
|
||||||
|
object(),
|
||||||
|
)
|
||||||
|
|
||||||
assert response.status_code == 201
|
assert result.target_group_id == "target"
|
||||||
assert db_session.query(WebsiteGroupBinding).count() == 1
|
assert db_session.query(WebsiteGroupBinding).count() == 1
|
||||||
log = db_session.query(WebsiteSyncLog).one()
|
log = db_session.query(WebsiteSyncLog).one()
|
||||||
assert log.status == "failed"
|
assert log.status == "failed"
|
||||||
@@ -204,7 +190,7 @@ def test_create_binding_keeps_binding_when_initial_sync_calculation_fails(client
|
|||||||
assert log.new_rate is None
|
assert log.new_rate is None
|
||||||
|
|
||||||
|
|
||||||
def test_update_binding_runs_sync_after_save(monkeypatch, client, db_session):
|
def test_update_binding_runs_sync_after_save(monkeypatch, db_session):
|
||||||
website, upstream = seed_rows(db_session)
|
website, upstream = seed_rows(db_session)
|
||||||
binding = WebsiteGroupBinding(
|
binding = WebsiteGroupBinding(
|
||||||
website_id=website.id,
|
website_id=website.id,
|
||||||
@@ -243,16 +229,18 @@ def test_update_binding_runs_sync_after_save(monkeypatch, client, db_session):
|
|||||||
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeClient)
|
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeClient)
|
||||||
monkeypatch.setattr("app.services.website_sync.Sub2ApiWebsiteClient", FakeClient)
|
monkeypatch.setattr("app.services.website_sync.Sub2ApiWebsiteClient", FakeClient)
|
||||||
|
|
||||||
response = client.put(
|
result = websites_router.update_binding(
|
||||||
f"/api/group-bindings/{binding.id}",
|
binding.id,
|
||||||
json={
|
BindingUpdate(
|
||||||
"target_group_name": "Target group",
|
target_group_name="Target group",
|
||||||
"percent": 20,
|
percent=20,
|
||||||
"enabled": True,
|
enabled=True,
|
||||||
},
|
),
|
||||||
|
db_session,
|
||||||
|
object(),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert result.target_group_id == "target"
|
||||||
assert calls == [("/groups/{id}", "target", "2.4000")]
|
assert calls == [("/groups/{id}", "target", "2.4000")]
|
||||||
log = db_session.query(WebsiteSyncLog).one()
|
log = db_session.query(WebsiteSyncLog).one()
|
||||||
assert log.status == "success"
|
assert log.status == "success"
|
||||||
@@ -260,7 +248,7 @@ def test_update_binding_runs_sync_after_save(monkeypatch, client, db_session):
|
|||||||
assert log.new_rate == "2.4"
|
assert log.new_rate == "2.4"
|
||||||
|
|
||||||
|
|
||||||
def test_update_binding_skips_write_when_disabled(monkeypatch, client, db_session):
|
def test_update_binding_skips_write_when_disabled(monkeypatch, db_session):
|
||||||
website, upstream = seed_rows(db_session)
|
website, upstream = seed_rows(db_session)
|
||||||
binding = WebsiteGroupBinding(
|
binding = WebsiteGroupBinding(
|
||||||
website_id=website.id,
|
website_id=website.id,
|
||||||
@@ -291,21 +279,20 @@ def test_update_binding_skips_write_when_disabled(monkeypatch, client, db_sessio
|
|||||||
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeClient)
|
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeClient)
|
||||||
monkeypatch.setattr("app.services.website_sync.Sub2ApiWebsiteClient", FakeClient)
|
monkeypatch.setattr("app.services.website_sync.Sub2ApiWebsiteClient", FakeClient)
|
||||||
|
|
||||||
response = client.put(
|
result = websites_router.update_binding(
|
||||||
f"/api/group-bindings/{binding.id}",
|
binding.id,
|
||||||
json={
|
BindingUpdate(target_group_name="Target group", percent=20),
|
||||||
"target_group_name": "Target group",
|
db_session,
|
||||||
"percent": 20,
|
object(),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert result.target_group_id == "target"
|
||||||
log = db_session.query(WebsiteSyncLog).one()
|
log = db_session.query(WebsiteSyncLog).one()
|
||||||
assert log.status == "success"
|
assert log.status == "success"
|
||||||
assert log.message == "绑定未启用,未写回"
|
assert log.message == "绑定未启用,未写回"
|
||||||
|
|
||||||
|
|
||||||
def test_create_binding_notifies_when_website_rate_changes(monkeypatch, client, db_session):
|
def test_create_binding_notifies_when_website_rate_changes(monkeypatch, db_session):
|
||||||
website, upstream = seed_rows(db_session)
|
website, upstream = seed_rows(db_session)
|
||||||
webhook = WebhookConfig(
|
webhook = WebhookConfig(
|
||||||
name="Notify",
|
name="Notify",
|
||||||
@@ -341,9 +328,13 @@ def test_create_binding_notifies_when_website_rate_changes(monkeypatch, client,
|
|||||||
monkeypatch.setattr("app.services.website_sync.Sub2ApiWebsiteClient", FakeClient)
|
monkeypatch.setattr("app.services.website_sync.Sub2ApiWebsiteClient", FakeClient)
|
||||||
monkeypatch.setattr("app.services.webhook_service._send_generic", fake_send_generic)
|
monkeypatch.setattr("app.services.webhook_service._send_generic", fake_send_generic)
|
||||||
|
|
||||||
response = client.post("/api/group-bindings", json=binding_payload(website.id, upstream.id))
|
result = websites_router.create_binding(
|
||||||
|
make_body(website.id, upstream.id),
|
||||||
|
db_session,
|
||||||
|
object(),
|
||||||
|
)
|
||||||
|
|
||||||
assert response.status_code == 201
|
assert result.target_group_id == "target"
|
||||||
assert len(sent_payloads) == 1
|
assert len(sent_payloads) == 1
|
||||||
_, payload = sent_payloads[0]
|
_, payload = sent_payloads[0]
|
||||||
assert payload["event"] == "website_rate_changed"
|
assert payload["event"] == "website_rate_changed"
|
||||||
@@ -355,7 +346,7 @@ def test_create_binding_notifies_when_website_rate_changes(monkeypatch, client,
|
|||||||
assert log.status == "success"
|
assert log.status == "success"
|
||||||
|
|
||||||
|
|
||||||
def test_create_binding_does_not_notify_when_website_rate_unchanged(monkeypatch, client, db_session):
|
def test_create_binding_does_not_notify_when_website_rate_unchanged(monkeypatch, db_session):
|
||||||
website, upstream = seed_rows(db_session)
|
website, upstream = seed_rows(db_session)
|
||||||
webhook = WebhookConfig(
|
webhook = WebhookConfig(
|
||||||
name="Notify",
|
name="Notify",
|
||||||
@@ -388,7 +379,11 @@ def test_create_binding_does_not_notify_when_website_rate_unchanged(monkeypatch,
|
|||||||
monkeypatch.setattr("app.services.website_sync.Sub2ApiWebsiteClient", FakeClient)
|
monkeypatch.setattr("app.services.website_sync.Sub2ApiWebsiteClient", FakeClient)
|
||||||
monkeypatch.setattr("app.services.webhook_service._send_generic", fake_send_generic)
|
monkeypatch.setattr("app.services.webhook_service._send_generic", fake_send_generic)
|
||||||
|
|
||||||
response = client.post("/api/group-bindings", json=binding_payload(website.id, upstream.id))
|
result = websites_router.create_binding(
|
||||||
|
make_body(website.id, upstream.id),
|
||||||
|
db_session,
|
||||||
|
object(),
|
||||||
|
)
|
||||||
|
|
||||||
assert response.status_code == 201
|
assert result.target_group_id == "target"
|
||||||
assert db_session.query(NotificationLog).count() == 0
|
assert db_session.query(NotificationLog).count() == 0
|
||||||
|
|||||||
Reference in New Issue
Block a user