完善设备端体验与微信校验支持
This commit is contained in:
354
talkingq-url/tests/test_family_display_name.py
Normal file
354
talkingq-url/tests/test_family_display_name.py
Normal file
@@ -0,0 +1,354 @@
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from banban.dao.family import FAMILY_ROLE_MEMBER, FAMILY_ROLE_OWNER, FAMILY_STATUS_ACTIVE, FamilyDAO
|
||||
from banban.service.family import FamilyService
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self):
|
||||
self.commits = 0
|
||||
self.rollbacks = 0
|
||||
self.closed = False
|
||||
|
||||
async def commit(self):
|
||||
self.commits += 1
|
||||
|
||||
async def rollback(self):
|
||||
self.rollbacks += 1
|
||||
|
||||
async def close(self):
|
||||
self.closed = True
|
||||
|
||||
|
||||
class SqliteMappingResult:
|
||||
def __init__(self, rows):
|
||||
self.rows = rows
|
||||
|
||||
def all(self):
|
||||
return self.rows
|
||||
|
||||
def first(self):
|
||||
return self.rows[0] if self.rows else None
|
||||
|
||||
|
||||
class SqliteResult:
|
||||
def __init__(self, rows, rowcount):
|
||||
self.rows = rows
|
||||
self.rowcount = rowcount
|
||||
|
||||
def mappings(self):
|
||||
return SqliteMappingResult(self.rows)
|
||||
|
||||
def scalar(self):
|
||||
if not self.rows:
|
||||
return None
|
||||
return next(iter(self.rows[0].values()))
|
||||
|
||||
def scalar_one_or_none(self):
|
||||
return self.scalar()
|
||||
|
||||
|
||||
class SqliteAsyncSession:
|
||||
def __init__(self):
|
||||
self.conn = sqlite3.connect(":memory:")
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
|
||||
async def execute(self, statement, params=None):
|
||||
cursor = self.conn.execute(str(statement), params or {})
|
||||
rows = [dict(row) for row in cursor.fetchall()] if cursor.description else []
|
||||
return SqliteResult(rows, cursor.rowcount)
|
||||
|
||||
|
||||
def _create_family_dao_session() -> SqliteAsyncSession:
|
||||
session = SqliteAsyncSession()
|
||||
conn = session.conn
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE parents (
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
nickname TEXT,
|
||||
avatar_url TEXT,
|
||||
status INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE device_bindings (
|
||||
device_id TEXT PRIMARY KEY,
|
||||
owner_user_id INTEGER NOT NULL,
|
||||
child_id INTEGER,
|
||||
status INTEGER NOT NULL,
|
||||
bound_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE device_family_members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_id TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
role INTEGER NOT NULL,
|
||||
display_name TEXT,
|
||||
status INTEGER NOT NULL,
|
||||
invited_by_user_id INTEGER,
|
||||
joined_at TEXT NOT NULL,
|
||||
removed_at TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE (device_id, user_id)
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.executemany(
|
||||
"INSERT INTO parents (user_id, nickname, avatar_url, status) VALUES (?, ?, ?, 1)",
|
||||
[
|
||||
(10, "妈妈微信", None),
|
||||
(20, "爸爸微信", None),
|
||||
(77, "姑姑微信", None),
|
||||
(88, "小明微信", None),
|
||||
],
|
||||
)
|
||||
conn.executemany(
|
||||
"INSERT INTO device_bindings (device_id, owner_user_id, child_id, status, bound_at) VALUES (?, ?, ?, 1, ?)",
|
||||
[
|
||||
("TalkingQ_family_a", 10, 1001, "2026-06-08 10:00:00"),
|
||||
("TalkingQ_family_b", 20, 1002, "2026-06-08 10:00:00"),
|
||||
],
|
||||
)
|
||||
conn.executemany(
|
||||
"""
|
||||
INSERT INTO device_family_members (
|
||||
device_id,
|
||||
user_id,
|
||||
role,
|
||||
display_name,
|
||||
status,
|
||||
invited_by_user_id,
|
||||
joined_at,
|
||||
removed_at,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
|
||||
""",
|
||||
[
|
||||
("TalkingQ_family_a", 10, FAMILY_ROLE_OWNER, None, FAMILY_STATUS_ACTIVE, None, "2026-06-08 10:00:00", "2026-06-08 10:00:00", "2026-06-08 10:00:00"),
|
||||
("TalkingQ_family_a", 88, FAMILY_ROLE_MEMBER, None, FAMILY_STATUS_ACTIVE, 10, "2026-06-08 10:01:00", "2026-06-08 10:01:00", "2026-06-08 10:01:00"),
|
||||
("TalkingQ_family_a", 77, FAMILY_ROLE_MEMBER, None, FAMILY_STATUS_ACTIVE, 10, "2026-06-08 10:02:00", "2026-06-08 10:02:00", "2026-06-08 10:02:00"),
|
||||
("TalkingQ_family_b", 20, FAMILY_ROLE_OWNER, None, FAMILY_STATUS_ACTIVE, None, "2026-06-08 10:00:00", "2026-06-08 10:00:00", "2026-06-08 10:00:00"),
|
||||
("TalkingQ_family_b", 88, FAMILY_ROLE_MEMBER, None, FAMILY_STATUS_ACTIVE, 20, "2026-06-08 10:01:00", "2026-06-08 10:01:00", "2026-06-08 10:01:00"),
|
||||
],
|
||||
)
|
||||
conn.commit()
|
||||
return session
|
||||
|
||||
|
||||
def _row_by_user(rows, user_id):
|
||||
return next(row for row in rows if int(row["user_id"]) == user_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_member_display_name_allows_owner(monkeypatch):
|
||||
service = FamilyService()
|
||||
db_session = FakeSession()
|
||||
updates = []
|
||||
|
||||
async def fake_get_session():
|
||||
return db_session
|
||||
|
||||
async def fake_get_binding_for_owner(self, device_id, user_id):
|
||||
assert device_id == "TalkingQ_device001"
|
||||
assert user_id == 99
|
||||
return {"device_id": device_id, "owner_user_id": user_id}
|
||||
|
||||
async def fake_update_member_display_name(self, **kwargs):
|
||||
updates.append(kwargs)
|
||||
return {
|
||||
"user_id": 88,
|
||||
"nickname": "奶奶",
|
||||
"display_name": "奶奶",
|
||||
"account_nickname": "微信昵称",
|
||||
"avatar_url": None,
|
||||
"role": 2,
|
||||
"is_owner": 0,
|
||||
"joined_at": None,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(service, "get_session", fake_get_session)
|
||||
monkeypatch.setattr("banban.service.family.FamilyDAO.get_binding_for_owner", fake_get_binding_for_owner)
|
||||
monkeypatch.setattr("banban.service.family.FamilyDAO.update_member_display_name", fake_update_member_display_name)
|
||||
|
||||
result = await service.update_member_display_name(
|
||||
device_id="TalkingQ_device001",
|
||||
member_user_id=88,
|
||||
display_name=" 奶奶 ",
|
||||
user_id=99,
|
||||
)
|
||||
|
||||
assert result["display_name"] == "奶奶"
|
||||
assert updates == [
|
||||
{
|
||||
"device_id": "TalkingQ_device001",
|
||||
"member_user_id": 88,
|
||||
"owner_user_id": 99,
|
||||
"display_name": "奶奶",
|
||||
}
|
||||
]
|
||||
assert db_session.commits == 1
|
||||
assert db_session.rollbacks == 0
|
||||
assert db_session.closed is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_member_display_name_rejects_non_owner(monkeypatch):
|
||||
service = FamilyService()
|
||||
db_session = FakeSession()
|
||||
updates = []
|
||||
|
||||
async def fake_get_session():
|
||||
return db_session
|
||||
|
||||
async def fake_get_binding_for_owner(self, device_id, user_id):
|
||||
return None
|
||||
|
||||
async def fake_update_member_display_name(self, **kwargs):
|
||||
updates.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(service, "get_session", fake_get_session)
|
||||
monkeypatch.setattr("banban.service.family.FamilyDAO.get_binding_for_owner", fake_get_binding_for_owner)
|
||||
monkeypatch.setattr("banban.service.family.FamilyDAO.update_member_display_name", fake_update_member_display_name)
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await service.update_member_display_name(
|
||||
device_id="TalkingQ_device001",
|
||||
member_user_id=88,
|
||||
display_name="奶奶",
|
||||
user_id=77,
|
||||
)
|
||||
|
||||
assert exc.value.status_code == 403
|
||||
assert updates == []
|
||||
assert db_session.commits == 0
|
||||
assert db_session.rollbacks == 1
|
||||
assert db_session.closed is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_member_display_name_allows_clearing(monkeypatch):
|
||||
service = FamilyService()
|
||||
db_session = FakeSession()
|
||||
updates = []
|
||||
|
||||
async def fake_get_session():
|
||||
return db_session
|
||||
|
||||
async def fake_get_binding_for_owner(self, device_id, user_id):
|
||||
return {"device_id": device_id, "owner_user_id": user_id}
|
||||
|
||||
async def fake_update_member_display_name(self, **kwargs):
|
||||
updates.append(kwargs)
|
||||
return {
|
||||
"user_id": 88,
|
||||
"nickname": "微信昵称",
|
||||
"display_name": None,
|
||||
"account_nickname": "微信昵称",
|
||||
"avatar_url": None,
|
||||
"role": 2,
|
||||
"is_owner": 0,
|
||||
"joined_at": None,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(service, "get_session", fake_get_session)
|
||||
monkeypatch.setattr("banban.service.family.FamilyDAO.get_binding_for_owner", fake_get_binding_for_owner)
|
||||
monkeypatch.setattr("banban.service.family.FamilyDAO.update_member_display_name", fake_update_member_display_name)
|
||||
|
||||
result = await service.update_member_display_name(
|
||||
device_id="TalkingQ_device001",
|
||||
member_user_id=88,
|
||||
display_name=" ",
|
||||
user_id=99,
|
||||
)
|
||||
|
||||
assert result["display_name"] is None
|
||||
assert updates[0]["display_name"] is None
|
||||
assert db_session.commits == 1
|
||||
assert db_session.closed is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_member_display_name_rejects_too_long_value():
|
||||
service = FamilyService()
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await service.update_member_display_name(
|
||||
device_id="TalkingQ_device001",
|
||||
member_user_id=88,
|
||||
display_name="名" * 65,
|
||||
user_id=99,
|
||||
)
|
||||
|
||||
assert exc.value.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_display_name_is_scoped_per_family_and_visible_to_every_member_view():
|
||||
session = _create_family_dao_session()
|
||||
dao = FamilyDAO(session)
|
||||
|
||||
updated = await dao.update_member_display_name(
|
||||
device_id="TalkingQ_family_a",
|
||||
member_user_id=88,
|
||||
owner_user_id=10,
|
||||
display_name="奶奶",
|
||||
)
|
||||
|
||||
assert updated is not None
|
||||
assert updated["user_id"] == 88
|
||||
assert updated["display_name"] == "奶奶"
|
||||
assert updated["nickname"] == "奶奶"
|
||||
assert updated["account_nickname"] == "小明微信"
|
||||
|
||||
for viewer_user_id in (10, 88, 77):
|
||||
rows = await dao.list_members(device_id="TalkingQ_family_a", user_id=viewer_user_id)
|
||||
renamed_member = _row_by_user(rows, 88)
|
||||
assert renamed_member["display_name"] == "奶奶"
|
||||
assert renamed_member["nickname"] == "奶奶"
|
||||
assert renamed_member["account_nickname"] == "小明微信"
|
||||
|
||||
for viewer_user_id in (20, 88):
|
||||
rows = await dao.list_members(device_id="TalkingQ_family_b", user_id=viewer_user_id)
|
||||
same_account_in_other_family = _row_by_user(rows, 88)
|
||||
assert same_account_in_other_family["display_name"] is None
|
||||
assert same_account_in_other_family["nickname"] == "小明微信"
|
||||
assert same_account_in_other_family["account_nickname"] == "小明微信"
|
||||
|
||||
parent_row = session.conn.execute("SELECT nickname FROM parents WHERE user_id = ?", (88,)).fetchone()
|
||||
assert parent_row["nickname"] == "小明微信"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cleared_display_name_falls_back_to_account_nickname_from_self_view():
|
||||
session = _create_family_dao_session()
|
||||
dao = FamilyDAO(session)
|
||||
|
||||
await dao.update_member_display_name(
|
||||
device_id="TalkingQ_family_a",
|
||||
member_user_id=88,
|
||||
owner_user_id=10,
|
||||
display_name="奶奶",
|
||||
)
|
||||
cleared = await dao.update_member_display_name(
|
||||
device_id="TalkingQ_family_a",
|
||||
member_user_id=88,
|
||||
owner_user_id=10,
|
||||
display_name=None,
|
||||
)
|
||||
|
||||
assert cleared is not None
|
||||
assert cleared["display_name"] is None
|
||||
assert cleared["nickname"] == "小明微信"
|
||||
assert cleared["account_nickname"] == "小明微信"
|
||||
|
||||
rows = await dao.list_members(device_id="TalkingQ_family_a", user_id=88)
|
||||
self_view = _row_by_user(rows, 88)
|
||||
assert self_view["display_name"] is None
|
||||
assert self_view["nickname"] == "小明微信"
|
||||
Reference in New Issue
Block a user