Files
banban/mini-program/tests/test_dao.py

448 lines
15 KiB
Python

import os
import pytest
os.environ["DB_TYPE"] = "sqlite"
os.environ["DB_PATH"] = ":memory:"
def test_sqlite_connection():
"""Test SQLite database works."""
from sqlalchemy import create_engine, text
from app.models import Base
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(bind=engine)
with engine.connect() as conn:
conn.execute(text("INSERT INTO parents (openid, nickname, status) VALUES ('test', 'Test', 1)"))
conn.commit()
result = conn.execute(text("SELECT * FROM parents")).fetchall()
assert len(result) == 1
def test_parent_dao():
"""Test ParentDAO."""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.models import Base
from app.dao.parent import ParentDAO
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
db = Session()
dao = ParentDAO(db)
user_id = dao.create("test_openid", None, "Test User")
assert user_id > 0
parent = dao.get_by_id(user_id)
assert parent["openid"] == "test_openid"
def test_child_dao():
"""Test ChildDAO."""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.models import Base
from app.dao.child import ChildDAO
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
db = Session()
dao = ChildDAO(db)
child_id = dao.create(1, "Test Child")
assert child_id > 0
child = dao.get_by_id(child_id)
assert child["child_name"] == "Test Child"
def test_child_service_create_creates_parent_child_conversation():
"""Creating a child should also create a parent-child conversation."""
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from app.models import Base
from app.service.child import ChildService
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
db = Session()
db.execute(text("INSERT INTO parents (openid, nickname, status) VALUES ('p_child_service', 'Parent A', 1)"))
db.commit()
user_id = int(db.execute(text("SELECT user_id FROM parents WHERE openid = 'p_child_service'")).scalar_one())
service = ChildService(db)
child = service.create(user_id, "Kid Service")
conversation = (
db.execute(
text(
"""
SELECT
conversation_type,
participant_a_type,
participant_a_id,
participant_b_type,
participant_b_id,
pair_key,
status,
last_seq,
message_count
FROM im_conversations
WHERE conversation_type = 2
AND pair_key = :pair_key
LIMIT 1
"""
),
{"pair_key": f"{child['child_id']}:{user_id}"},
)
.mappings()
.first()
)
assert conversation is not None
assert int(conversation["conversation_type"]) == 2
assert int(conversation["participant_a_type"]) == 2
assert str(conversation["participant_a_id"]) == str(child["child_id"])
assert int(conversation["participant_b_type"]) == 1
assert str(conversation["participant_b_id"]) == str(user_id)
assert int(conversation["status"]) == 1
assert int(conversation["last_seq"]) == 0
assert int(conversation["message_count"]) == 0
def test_parent_child_message_reuses_precreated_conversation():
"""Parent message creation should reuse the conversation created with the child."""
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from app.models import Base
from app.schemas.im import ParentChildMessageCreateRequest
from app.service.child import ChildService
from app.service.im import create_parent_child_message
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
db = Session()
db.execute(text("INSERT INTO parents (openid, nickname, status) VALUES ('p_msg_reuse', 'Parent B', 1)"))
db.commit()
user_id = int(db.execute(text("SELECT user_id FROM parents WHERE openid = 'p_msg_reuse'")).scalar_one())
service = ChildService(db)
child = service.create(user_id, "Kid Reuse")
child_id = int(child["child_id"])
precreated_conversation_id = int(
db.execute(
text(
"""
SELECT id
FROM im_conversations
WHERE conversation_type = 2
AND pair_key = :pair_key
LIMIT 1
"""
),
{"pair_key": f"{child_id}:{user_id}"},
).scalar_one()
)
payload = ParentChildMessageCreateRequest(
content_type=1,
content_text="hello child",
client_msg_id="parent-msg-reuse-001",
)
result = create_parent_child_message(
db,
parent_user_id=user_id,
child_id=child_id,
payload=payload,
)
assert result.idempotent is False
assert result.conversation_id == precreated_conversation_id
duplicate = create_parent_child_message(
db,
parent_user_id=user_id,
child_id=child_id,
payload=payload,
)
assert duplicate.idempotent is True
assert duplicate.conversation_id == precreated_conversation_id
conversation = (
db.execute(
text(
"""
SELECT last_seq, message_count, last_message_preview
FROM im_conversations
WHERE id = :conversation_id
"""
),
{"conversation_id": precreated_conversation_id},
)
.mappings()
.first()
)
assert conversation is not None
assert int(conversation["last_seq"]) == 1
assert int(conversation["message_count"]) == 1
assert conversation["last_message_preview"] == "hello child"
conversation_count = int(
db.execute(
text(
"""
SELECT COUNT(*)
FROM im_conversations
WHERE conversation_type = 2
AND pair_key = :pair_key
"""
),
{"pair_key": f"{child_id}:{user_id}"},
).scalar_one()
)
assert conversation_count == 1
def test_binding_dao():
"""Test BindingDAO."""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.models import Base
from app.dao.binding import BindingDAO
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
db = Session()
dao = BindingDAO(db)
token = dao.start_bind(1, "device_123", 1)
assert len(token) == 36
def test_confirm_bind_upserts_parent_child_relation():
"""Confirm bind should create parent-child relation when missing."""
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from app.models import Base
from app.dao.binding import BindingDAO
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
db = Session()
db.execute(text("INSERT INTO parents (openid, status) VALUES ('p_bind_confirm', 1)"))
db.execute(text("INSERT INTO children (child_name, child_gender, status) VALUES ('Kid Confirm', 2, 1)"))
db.commit()
user_id = int(db.execute(text("SELECT user_id FROM parents WHERE openid = 'p_bind_confirm'")).scalar_one())
child_id = int(
db.execute(text("SELECT child_id FROM children WHERE child_name = 'Kid Confirm'")).scalar_one()
)
dao = BindingDAO(db)
token = dao.start_bind(user_id, "device_confirm_001", child_id)
session = dao.get_session(token, user_id)
assert session is not None
dao.confirm_bind(int(session["id"]), "device_confirm_001", child_id, user_id)
relation = db.execute(
text(
"""
SELECT status
FROM parent_child_relations
WHERE user_id = :user_id AND child_id = :child_id
"""
),
{"user_id": user_id, "child_id": child_id},
).scalar_one_or_none()
assert relation == 1
def test_direct_bind_reactivates_parent_child_relation():
"""Direct bind should reactivate an existing disabled parent-child relation."""
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from app.models import Base
from app.dao.binding import BindingDAO
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
db = Session()
db.execute(text("INSERT INTO parents (openid, status) VALUES ('p_bind_direct', 1)"))
db.execute(text("INSERT INTO children (child_name, child_gender, status) VALUES ('Kid Direct', 2, 1)"))
db.commit()
user_id = int(db.execute(text("SELECT user_id FROM parents WHERE openid = 'p_bind_direct'")).scalar_one())
child_id = int(db.execute(text("SELECT child_id FROM children WHERE child_name = 'Kid Direct'")).scalar_one())
db.execute(
text(
"""
INSERT INTO parent_child_relations (user_id, child_id, relation_type, is_primary, status)
VALUES (:user_id, :child_id, 9, 0, 0)
"""
),
{"user_id": user_id, "child_id": child_id},
)
db.commit()
dao = BindingDAO(db)
dao.direct_bind("device_direct_001", child_id, user_id)
relation = db.execute(
text(
"""
SELECT status
FROM parent_child_relations
WHERE user_id = :user_id AND child_id = :child_id
"""
),
{"user_id": user_id, "child_id": child_id},
).scalar_one()
assert relation == 1
def test_direct_bind_without_child_then_set_child():
"""Direct bind should support empty child first and assign child later."""
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from app.models import Base
from app.dao.binding import BindingDAO
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
db = Session()
db.execute(text("INSERT INTO parents (openid, status) VALUES ('p_bind_later', 1)"))
db.execute(text("INSERT INTO children (child_name, child_gender, status) VALUES ('Kid Later', 2, 1)"))
db.commit()
user_id = int(db.execute(text("SELECT user_id FROM parents WHERE openid = 'p_bind_later'")).scalar_one())
child_id = int(db.execute(text("SELECT child_id FROM children WHERE child_name = 'Kid Later'")).scalar_one())
dao = BindingDAO(db)
dao.direct_bind("device_later_001", None, user_id)
current = dao.get_current_by_user(user_id)
assert current is not None
assert current["device_id"] == "device_later_001"
assert current["child_id"] is None
changed = dao.set_binding_child("device_later_001", child_id, user_id)
assert changed is True
current = dao.get_current_by_user(user_id)
assert current is not None
assert int(current["child_id"]) == child_id
relation = db.execute(
text(
"""
SELECT status
FROM parent_child_relations
WHERE user_id = :user_id AND child_id = :child_id
"""
),
{"user_id": user_id, "child_id": child_id},
).scalar_one()
assert relation == 1
def test_update_my_relation_switches_primary():
"""Relation update should switch primary parent on the same child."""
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from app.models import Base
from app.dao.relation import RelationDAO
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
db = Session()
db.execute(text("INSERT INTO parents (openid, status) VALUES ('p_relation_1', 1)"))
db.execute(text("INSERT INTO parents (openid, status) VALUES ('p_relation_2', 1)"))
db.execute(text("INSERT INTO children (child_name, child_gender, status) VALUES ('Kid Relation', 2, 1)"))
db.commit()
user_id_1 = int(db.execute(text("SELECT user_id FROM parents WHERE openid = 'p_relation_1'")).scalar_one())
user_id_2 = int(db.execute(text("SELECT user_id FROM parents WHERE openid = 'p_relation_2'")).scalar_one())
child_id = int(db.execute(text("SELECT child_id FROM children WHERE child_name = 'Kid Relation'")).scalar_one())
db.execute(
text(
"""
INSERT INTO parent_child_relations (user_id, child_id, relation_type, is_primary, status)
VALUES (:user_id, :child_id, :relation_type, :is_primary, 1)
"""
),
{"user_id": user_id_1, "child_id": child_id, "relation_type": 9, "is_primary": 0},
)
db.execute(
text(
"""
INSERT INTO parent_child_relations (user_id, child_id, relation_type, is_primary, status)
VALUES (:user_id, :child_id, :relation_type, :is_primary, 1)
"""
),
{"user_id": user_id_2, "child_id": child_id, "relation_type": 9, "is_primary": 1},
)
db.commit()
dao = RelationDAO(db)
updated = dao.update_my_relation(
child_id=child_id,
user_id=user_id_1,
relation_type=2,
is_primary=True,
)
assert updated is not None
assert int(updated["relation_type"]) == 2
assert int(updated["is_primary"]) == 1
assert int(updated["status"]) == 1
other_primary = int(
db.execute(
text(
"""
SELECT is_primary
FROM parent_child_relations
WHERE child_id = :child_id
AND user_id = :user_id
"""
),
{"child_id": child_id, "user_id": user_id_2},
).scalar_one()
)
assert other_primary == 0
if __name__ == "__main__":
test_sqlite_connection()
test_parent_dao()
test_child_dao()
test_child_service_create_creates_parent_child_conversation()
test_parent_child_message_reuses_precreated_conversation()
test_binding_dao()
test_confirm_bind_upserts_parent_child_relation()
test_direct_bind_reactivates_parent_child_relation()
test_direct_bind_without_child_then_set_child()
test_update_my_relation_switches_primary()
print("All DAO tests passed!")