scholarr/tests/integration/test_multi_user_schema.py
2026-02-17 14:51:25 +01:00

241 lines
7.5 KiB
Python

import pytest
from sqlalchemy import text
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
async def _insert_user(db_session: AsyncSession, email: str) -> int:
result = await db_session.execute(
text(
"""
INSERT INTO users (email, password_hash)
VALUES (:email, :password_hash)
RETURNING id
"""
),
{"email": email, "password_hash": "argon2id$placeholder"},
)
return int(result.scalar_one())
@pytest.mark.integration
@pytest.mark.db
@pytest.mark.schema
@pytest.mark.asyncio
async def test_scholar_id_uniqueness_is_scoped_to_user(db_session: AsyncSession) -> None:
user_a = await _insert_user(db_session, "owner-a@example.com")
user_b = await _insert_user(db_session, "owner-b@example.com")
await db_session.commit()
await db_session.execute(
text(
"""
INSERT INTO scholar_profiles (user_id, scholar_id, display_name)
VALUES (:user_id, :scholar_id, :display_name)
"""
),
{"user_id": user_a, "scholar_id": "abcDEF123456", "display_name": "Alpha"},
)
await db_session.execute(
text(
"""
INSERT INTO scholar_profiles (user_id, scholar_id, display_name)
VALUES (:user_id, :scholar_id, :display_name)
"""
),
{"user_id": user_b, "scholar_id": "abcDEF123456", "display_name": "Beta"},
)
await db_session.commit()
with pytest.raises(IntegrityError):
await db_session.execute(
text(
"""
INSERT INTO scholar_profiles (user_id, scholar_id, display_name)
VALUES (:user_id, :scholar_id, :display_name)
"""
),
{"user_id": user_a, "scholar_id": "abcDEF123456", "display_name": "Gamma"},
)
await db_session.commit()
await db_session.rollback()
@pytest.mark.integration
@pytest.mark.db
@pytest.mark.schema
@pytest.mark.asyncio
async def test_user_settings_allow_only_one_row_per_user(db_session: AsyncSession) -> None:
user_id = await _insert_user(db_session, "settings-owner@example.com")
await db_session.commit()
await db_session.execute(
text(
"""
INSERT INTO user_settings (user_id, auto_run_enabled, run_interval_minutes, request_delay_seconds)
VALUES (:user_id, :auto_run_enabled, :run_interval_minutes, :request_delay_seconds)
"""
),
{
"user_id": user_id,
"auto_run_enabled": True,
"run_interval_minutes": 60,
"request_delay_seconds": 10,
},
)
await db_session.commit()
with pytest.raises(IntegrityError):
await db_session.execute(
text(
"""
INSERT INTO user_settings (user_id, auto_run_enabled, run_interval_minutes, request_delay_seconds)
VALUES (:user_id, :auto_run_enabled, :run_interval_minutes, :request_delay_seconds)
"""
),
{
"user_id": user_id,
"auto_run_enabled": False,
"run_interval_minutes": 30,
"request_delay_seconds": 5,
},
)
await db_session.commit()
await db_session.rollback()
@pytest.mark.integration
@pytest.mark.db
@pytest.mark.schema
@pytest.mark.asyncio
async def test_crawl_run_requires_owner_user(db_session: AsyncSession) -> None:
with pytest.raises(IntegrityError):
await db_session.execute(
text(
"""
INSERT INTO crawl_runs (user_id, trigger_type, status)
VALUES (:user_id, :trigger_type, :status)
"""
),
{"user_id": None, "trigger_type": "manual", "status": "running"},
)
await db_session.commit()
await db_session.rollback()
@pytest.mark.integration
@pytest.mark.db
@pytest.mark.schema
@pytest.mark.asyncio
async def test_read_state_is_isolated_across_users(db_session: AsyncSession) -> None:
user_a = await _insert_user(db_session, "reader-a@example.com")
user_b = await _insert_user(db_session, "reader-b@example.com")
scholar_a = await db_session.execute(
text(
"""
INSERT INTO scholar_profiles (user_id, scholar_id, display_name)
VALUES (:user_id, :scholar_id, :display_name)
RETURNING id
"""
),
{"user_id": user_a, "scholar_id": "qwerty123456", "display_name": "Reader A"},
)
scholar_a_id = int(scholar_a.scalar_one())
scholar_b = await db_session.execute(
text(
"""
INSERT INTO scholar_profiles (user_id, scholar_id, display_name)
VALUES (:user_id, :scholar_id, :display_name)
RETURNING id
"""
),
{"user_id": user_b, "scholar_id": "zxcvbn654321", "display_name": "Reader B"},
)
scholar_b_id = int(scholar_b.scalar_one())
publication = await db_session.execute(
text(
"""
INSERT INTO publications (fingerprint_sha256, title_raw, title_normalized, year)
VALUES (:fingerprint_sha256, :title_raw, :title_normalized, :year)
RETURNING id
"""
),
{
"fingerprint_sha256": "f" * 64,
"title_raw": "A Shared Paper",
"title_normalized": "a shared paper",
"year": 2026,
},
)
publication_id = int(publication.scalar_one())
run_a = await db_session.execute(
text(
"""
INSERT INTO crawl_runs (user_id, trigger_type, status)
VALUES (:user_id, :trigger_type, :status)
RETURNING id
"""
),
{"user_id": user_a, "trigger_type": "manual", "status": "success"},
)
run_a_id = int(run_a.scalar_one())
run_b = await db_session.execute(
text(
"""
INSERT INTO crawl_runs (user_id, trigger_type, status)
VALUES (:user_id, :trigger_type, :status)
RETURNING id
"""
),
{"user_id": user_b, "trigger_type": "manual", "status": "success"},
)
run_b_id = int(run_b.scalar_one())
await db_session.execute(
text(
"""
INSERT INTO scholar_publications (scholar_profile_id, publication_id, is_read, first_seen_run_id)
VALUES (:scholar_profile_id, :publication_id, :is_read, :first_seen_run_id)
"""
),
{
"scholar_profile_id": scholar_a_id,
"publication_id": publication_id,
"is_read": False,
"first_seen_run_id": run_a_id,
},
)
await db_session.execute(
text(
"""
INSERT INTO scholar_publications (scholar_profile_id, publication_id, is_read, first_seen_run_id)
VALUES (:scholar_profile_id, :publication_id, :is_read, :first_seen_run_id)
"""
),
{
"scholar_profile_id": scholar_b_id,
"publication_id": publication_id,
"is_read": True,
"first_seen_run_id": run_b_id,
},
)
await db_session.commit()
result = await db_session.execute(
text(
"""
SELECT sp.user_id, spp.is_read
FROM scholar_publications spp
JOIN scholar_profiles sp ON sp.id = spp.scholar_profile_id
ORDER BY sp.user_id
"""
)
)
rows = result.all()
assert rows == [(user_a, False), (user_b, True)]