You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

257 lines
9.8 KiB

from __future__ import annotations
from datetime import UTC, datetime, timedelta
from decimal import Decimal
import pytest
from app import create_app
from app.extensions import db
from app.models import Account, Household, ShareLink
from app.services import check_share_token, create_share_link
@pytest.fixture
def app():
app = create_app("testing")
with app.app_context():
db.create_all()
yield app
db.session.remove()
db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
def create_account(*, username: str, password: str, role: str) -> Account:
account = Account(username=username, role=role, status="active")
account.set_password(password)
db.session.add(account)
db.session.commit()
return account
def create_household(**overrides: object) -> Household:
household = Household(
household_code=str(overrides.get("household_code", "S001")),
head_name=str(overrides.get("head_name", "分享测试户")),
phone=str(overrides.get("phone", "13800001111")),
side=str(overrides.get("side", "groom_side")),
invite_status=str(overrides.get("invite_status", "invited")),
attendance_status=str(overrides.get("attendance_status", "pending")),
expected_attendee_count=int(str(overrides.get("expected_attendee_count", 3))),
actual_attendee_count=int(str(overrides.get("actual_attendee_count", 0))),
child_count=int(str(overrides.get("child_count", 0))),
red_packet_child_count=int(str(overrides.get("red_packet_child_count", 0))),
total_gift_amount=Decimal(str(overrides.get("total_gift_amount", "0.00"))),
favor_status=str(overrides.get("favor_status", "not_given")),
candy_status=str(overrides.get("candy_status", "not_given")),
child_red_packet_status=str(overrides.get("child_red_packet_status", "not_given")),
note=overrides.get("note", None),
version=int(str(overrides.get("version", 1))),
)
db.session.add(household)
db.session.commit()
return household
def login(client, *, username: str, password: str):
return client.post(
"/auth/login",
data={"username": username, "password": password},
follow_redirects=False,
)
def test_share_token_check_respects_expired_and_revoked(app) -> None:
with app.app_context():
account = create_account(username="admin", password="AdminPass123!", role="admin")
link = create_share_link(created_by=account.id, label="测试", expire_days=1)
ok = check_share_token(link.token)
assert ok.ok is True
link.expires_at = datetime.now(UTC).replace(tzinfo=None) - timedelta(minutes=1)
expired = check_share_token(link.token)
assert expired.ok is False
assert expired.reason == "expired"
link.expires_at = datetime.now(UTC).replace(tzinfo=None) + timedelta(days=1)
link.revoked_at = datetime.now(UTC).replace(tzinfo=None)
revoked = check_share_token(link.token)
assert revoked.ok is False
assert revoked.reason == "revoked"
def test_admin_can_create_and_revoke_share_link(client, app) -> None:
with app.app_context():
create_account(username="admin", password="AdminPass123!", role="admin")
login(client, username="admin", password="AdminPass123!")
page_response = client.get("/admin/share-links")
page = page_response.get_data(as_text=True)
assert page_response.status_code == 200
assert "分享链接" in page
create_response = client.post(
"/admin/share-links",
data={"label": "给亲友", "expire_days": "7"},
follow_redirects=False,
)
assert create_response.status_code == 302
assert create_response.headers["Location"].endswith("/admin/share-links")
with app.app_context():
link = db.session.execute(db.select(ShareLink).order_by(ShareLink.id.desc())).scalar_one_or_none()
assert link is not None
link_id = link.id
token = link.token
revoke_response = client.post(f"/admin/share-links/{link_id}/revoke", follow_redirects=False)
assert revoke_response.status_code == 302
assert revoke_response.headers["Location"].endswith("/admin/share-links")
with app.app_context():
link = db.session.get(ShareLink, link_id)
assert link is not None
assert link.revoked_at is not None
invalid_response = client.get(f"/s/{token}")
assert invalid_response.status_code == 404
def test_share_page_edit_save_returns_search_and_updates_fields(client, app) -> None:
with app.app_context():
account = create_account(username="admin", password="AdminPass123!", role="admin")
household = create_household()
link = create_share_link(created_by=account.id, label="公开", expire_days=7)
db.session.commit()
household_id = household.id
token = link.token
open_response = client.get(f"/s/{token}/households/{household_id}/edit?q=分享")
assert open_response.status_code == 200
edit_page = open_response.get_data(as_text=True)
assert "编辑信息" in edit_page
assert 'id="phone"' in edit_page
save_response = client.post(
f"/s/{token}/households/{household_id}/edit",
data={
"q": "分享",
"version": "1",
"household_code": "S001",
"head_name": "分享测试户",
"side": "groom_side",
"relation_category_option_id": "",
"relation_detail_option_id": "",
"invite_status": "invited",
"expected_attendee_count": "3",
"phone": "13800002222",
"attendance_status": "attending",
"actual_attendee_count": "2",
"child_count": "1",
"red_packet_child_count": "1",
"total_gift_amount": "520.00",
"gift_method_option_id": "",
"gift_scene_option_id": "",
"favor_status": "given",
"candy_status": "given",
"child_red_packet_status": "partial",
"note": "公开页保存",
},
follow_redirects=False,
)
assert save_response.status_code == 302
assert save_response.headers["Location"].endswith(f"/s/{token}?q=%E5%88%86%E4%BA%AB")
with app.app_context():
updated = db.session.get(Household, household_id)
assert updated is not None
assert updated.phone == "13800002222"
assert updated.attendance_status == "attending"
assert updated.actual_attendee_count == 2
assert updated.total_gift_amount == Decimal("520.00")
assert updated.note == "公开页保存"
def test_share_search_page_is_public_and_does_not_expose_create_household_action(client, app) -> None:
with app.app_context():
account = create_account(username="admin", password="AdminPass123!", role="admin")
create_household()
link = create_share_link(created_by=account.id, label="公开", expire_days=7)
db.session.commit()
token = link.token
response = client.get(f"/s/{token}?q=分享")
page = response.get_data(as_text=True)
assert response.status_code == 200
assert "分享搜索" in page
assert "/households/new" not in page
assert "点击编辑" in page
def test_share_search_supports_pinyin_and_partial_results(client, app) -> None:
with app.app_context():
account = create_account(username="admin", password="AdminPass123!", role="admin")
create_household(household_code="S001", head_name="王大爷")
create_household(household_code="S002", head_name="李阿姨")
link = create_share_link(created_by=account.id, label="公开", expire_days=7)
db.session.commit()
token = link.token
pinyin_response = client.get(f"/s/{token}?q=wangdaye")
pinyin_page = pinyin_response.get_data(as_text=True)
assert pinyin_response.status_code == 200
assert "王大爷" in pinyin_page
assert "李阿姨" not in pinyin_page
partial_response = client.get(
f"/s/{token}?q=%E7%8E%8B&partial=results",
headers={"X-HW-Partial": "results"},
)
partial_page = partial_response.get_data(as_text=True)
assert partial_response.status_code == 200
assert 'id="share-search-results-region"' in partial_page
assert "王大爷" in partial_page
assert "<html" not in partial_page
def test_share_search_excludes_soft_deleted_households(client, app) -> None:
with app.app_context():
account = create_account(username="admin", password="AdminPass123!", role="admin")
active = create_household(household_code="S001", head_name="王大爷")
deleted = create_household(household_code="S002", head_name="李阿姨")
deleted.deleted_at = datetime(2026, 4, 12, 10, 0)
link = create_share_link(created_by=account.id, label="公开", expire_days=7)
db.session.commit()
token = link.token
response = client.get(f"/s/{token}?q=%E9%98%BF%E5%A7%A8")
page = response.get_data(as_text=True)
assert response.status_code == 200
assert "李阿姨" not in page
def test_deleted_household_cannot_be_opened_from_share_direct_edit_url(client, app) -> None:
with app.app_context():
account = create_account(username="admin", password="AdminPass123!", role="admin")
household = create_household(household_code="S001", head_name="王大爷")
link = create_share_link(created_by=account.id, label="公开", expire_days=7)
household.deleted_at = datetime(2026, 4, 12, 10, 0)
db.session.commit()
token = link.token
household_id = household.id
response = client.get(f"/s/{token}/households/{household_id}/edit?q=%E7%8E%8B", follow_redirects=True)
page = response.get_data(as_text=True)
assert response.status_code == 200
assert "未找到要修改的户信息。" in page
assert "分享搜索" in page