after audit
This commit is contained in:
parent
e8e20637e6
commit
a5165dc3ba
80 changed files with 8489 additions and 7302 deletions
573
tests/integration/test_api_publications.py
Normal file
573
tests/integration/test_api_publications.py
Normal file
|
|
@ -0,0 +1,573 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.main import app
|
||||
from tests.integration.helpers import (
|
||||
api_csrf_headers,
|
||||
insert_user,
|
||||
login_user,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.db
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_publications_list_and_mark_read(db_session: AsyncSession) -> None:
|
||||
user_id = await insert_user(
|
||||
db_session,
|
||||
email="api-pubs@example.com",
|
||||
password="api-password",
|
||||
)
|
||||
|
||||
scholar_result = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO scholar_profiles (user_id, scholar_id, display_name, is_enabled)
|
||||
VALUES (:user_id, :scholar_id, :display_name, true)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"user_id": user_id,
|
||||
"scholar_id": "abcDEF123456",
|
||||
"display_name": "Publication Owner",
|
||||
},
|
||||
)
|
||||
scholar_profile_id = int(scholar_result.scalar_one())
|
||||
|
||||
publication_a = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO publications (
|
||||
fingerprint_sha256,
|
||||
title_raw,
|
||||
title_normalized,
|
||||
citation_count,
|
||||
pdf_url
|
||||
)
|
||||
VALUES (:fingerprint, :title_raw, :title_normalized, 10, :pdf_url)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"fingerprint": f"{user_id:064x}",
|
||||
"title_raw": "Paper A",
|
||||
"title_normalized": "paper a",
|
||||
"pdf_url": "https://example.org/paper-a.pdf",
|
||||
},
|
||||
)
|
||||
publication_a_id = int(publication_a.scalar_one())
|
||||
|
||||
publication_b = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO publications (fingerprint_sha256, title_raw, title_normalized, citation_count)
|
||||
VALUES (:fingerprint, :title_raw, :title_normalized, 4)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"fingerprint": f"{(user_id + 1):064x}",
|
||||
"title_raw": "Paper B",
|
||||
"title_normalized": "paper b",
|
||||
},
|
||||
)
|
||||
publication_b_id = int(publication_b.scalar_one())
|
||||
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO scholar_publications (scholar_profile_id, publication_id, is_read)
|
||||
VALUES
|
||||
(:scholar_profile_id, :publication_a_id, false),
|
||||
(:scholar_profile_id, :publication_b_id, false)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"scholar_profile_id": scholar_profile_id,
|
||||
"publication_a_id": publication_a_id,
|
||||
"publication_b_id": publication_b_id,
|
||||
},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
client = TestClient(app)
|
||||
login_user(client, email="api-pubs@example.com", password="api-password")
|
||||
headers = api_csrf_headers(client)
|
||||
|
||||
list_response = client.get("/api/v1/publications?mode=all")
|
||||
assert list_response.status_code == 200
|
||||
data = list_response.json()["data"]
|
||||
assert data["mode"] == "all"
|
||||
assert data["favorite_only"] is False
|
||||
assert data["total_count"] == 2
|
||||
assert data["unread_count"] == 2
|
||||
assert data["favorites_count"] == 0
|
||||
assert data["latest_count"] == 0
|
||||
assert data["new_count"] == data["latest_count"]
|
||||
assert data["page"] == 1
|
||||
assert data["page_size"] == 100
|
||||
assert data["has_prev"] is False
|
||||
assert data["has_next"] is False
|
||||
assert isinstance(data["publications"], list)
|
||||
assert len(data["publications"]) == 2
|
||||
assert all(bool(item["is_favorite"]) is False for item in data["publications"])
|
||||
pdf_urls = {item["title"]: item["pdf_url"] for item in data["publications"]}
|
||||
assert pdf_urls["Paper A"] == "https://example.org/paper-a.pdf"
|
||||
assert pdf_urls["Paper B"] is None
|
||||
|
||||
latest_response = client.get("/api/v1/publications?mode=latest")
|
||||
assert latest_response.status_code == 200
|
||||
latest_data = latest_response.json()["data"]
|
||||
assert latest_data["mode"] == "latest"
|
||||
assert latest_data["latest_count"] == 0
|
||||
|
||||
unread_response = client.get("/api/v1/publications?mode=unread")
|
||||
assert unread_response.status_code == 200
|
||||
unread_data = unread_response.json()["data"]
|
||||
assert unread_data["mode"] == "unread"
|
||||
assert unread_data["unread_count"] == 2
|
||||
|
||||
alias_response = client.get("/api/v1/publications?mode=new")
|
||||
assert alias_response.status_code == 200
|
||||
alias_data = alias_response.json()["data"]
|
||||
assert alias_data["mode"] == "latest"
|
||||
assert alias_data["latest_count"] == latest_data["latest_count"]
|
||||
assert alias_data["publications"] == latest_data["publications"]
|
||||
|
||||
mark_selected_response = client.post(
|
||||
"/api/v1/publications/mark-read",
|
||||
json={
|
||||
"selections": [
|
||||
{
|
||||
"scholar_profile_id": scholar_profile_id,
|
||||
"publication_id": publication_a_id,
|
||||
}
|
||||
]
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
assert mark_selected_response.status_code == 200
|
||||
assert mark_selected_response.json()["data"]["requested_count"] == 1
|
||||
assert mark_selected_response.json()["data"]["updated_count"] == 1
|
||||
|
||||
read_state = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT publication_id, is_read
|
||||
FROM scholar_publications
|
||||
WHERE scholar_profile_id = :scholar_profile_id
|
||||
ORDER BY publication_id
|
||||
"""
|
||||
),
|
||||
{"scholar_profile_id": scholar_profile_id},
|
||||
)
|
||||
states = {int(row[0]): bool(row[1]) for row in read_state.all()}
|
||||
assert states[publication_a_id] is True
|
||||
assert states[publication_b_id] is False
|
||||
|
||||
mark_response = client.post("/api/v1/publications/mark-all-read", headers=headers)
|
||||
assert mark_response.status_code == 200
|
||||
assert mark_response.json()["data"]["updated_count"] == 1
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.db
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_publications_list_supports_pagination(db_session: AsyncSession) -> None:
|
||||
user_id = await insert_user(
|
||||
db_session,
|
||||
email="api-pubs-paging@example.com",
|
||||
password="api-password",
|
||||
)
|
||||
scholar_result = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO scholar_profiles (user_id, scholar_id, display_name, is_enabled)
|
||||
VALUES (:user_id, :scholar_id, :display_name, true)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"user_id": user_id,
|
||||
"scholar_id": "pagingScholar01",
|
||||
"display_name": "Paging Scholar",
|
||||
},
|
||||
)
|
||||
scholar_profile_id = int(scholar_result.scalar_one())
|
||||
|
||||
publication_ids: list[int] = []
|
||||
for index in range(3):
|
||||
created = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO publications (fingerprint_sha256, title_raw, title_normalized, citation_count)
|
||||
VALUES (:fingerprint, :title_raw, :title_normalized, 1)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"fingerprint": f"{(user_id + 500 + index):064x}",
|
||||
"title_raw": f"Paged Paper {index}",
|
||||
"title_normalized": f"paged paper {index}",
|
||||
},
|
||||
)
|
||||
publication_ids.append(int(created.scalar_one()))
|
||||
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO scholar_publications (scholar_profile_id, publication_id, is_read, is_favorite)
|
||||
VALUES
|
||||
(:scholar_profile_id, :publication_1, false, false),
|
||||
(:scholar_profile_id, :publication_2, false, false),
|
||||
(:scholar_profile_id, :publication_3, false, false)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"scholar_profile_id": scholar_profile_id,
|
||||
"publication_1": publication_ids[0],
|
||||
"publication_2": publication_ids[1],
|
||||
"publication_3": publication_ids[2],
|
||||
},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
client = TestClient(app)
|
||||
login_user(client, email="api-pubs-paging@example.com", password="api-password")
|
||||
|
||||
first_page = client.get("/api/v1/publications?mode=all&page=1&page_size=2")
|
||||
assert first_page.status_code == 200
|
||||
first_data = first_page.json()["data"]
|
||||
assert first_data["total_count"] == 3
|
||||
assert first_data["page"] == 1
|
||||
assert first_data["page_size"] == 2
|
||||
assert first_data["has_prev"] is False
|
||||
assert first_data["has_next"] is True
|
||||
assert len(first_data["publications"]) == 2
|
||||
|
||||
second_page = client.get("/api/v1/publications?mode=all&page=2&page_size=2")
|
||||
assert second_page.status_code == 200
|
||||
second_data = second_page.json()["data"]
|
||||
assert second_data["total_count"] == 3
|
||||
assert second_data["page"] == 2
|
||||
assert second_data["page_size"] == 2
|
||||
assert second_data["has_prev"] is True
|
||||
assert second_data["has_next"] is False
|
||||
assert len(second_data["publications"]) == 1
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.db
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_publications_search_pagination_uses_filtered_total_count(
|
||||
db_session: AsyncSession,
|
||||
) -> None:
|
||||
user_id = await insert_user(
|
||||
db_session,
|
||||
email="api-pubs-search-page@example.com",
|
||||
password="api-password",
|
||||
)
|
||||
scholar_result = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO scholar_profiles (user_id, scholar_id, display_name, is_enabled)
|
||||
VALUES (:user_id, :scholar_id, :display_name, true)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"user_id": user_id,
|
||||
"scholar_id": "searchPagingScholar01",
|
||||
"display_name": "Search Paging Scholar",
|
||||
},
|
||||
)
|
||||
scholar_profile_id = int(scholar_result.scalar_one())
|
||||
titles = ["Alpha Optimization", "Alpha Learning", "Beta Methods"]
|
||||
publication_ids: list[int] = []
|
||||
for index, title in enumerate(titles):
|
||||
created = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO publications (fingerprint_sha256, title_raw, title_normalized, citation_count)
|
||||
VALUES (:fingerprint, :title_raw, :title_normalized, 1)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"fingerprint": f"{(user_id + 900 + index):064x}",
|
||||
"title_raw": title,
|
||||
"title_normalized": title.lower(),
|
||||
},
|
||||
)
|
||||
publication_ids.append(int(created.scalar_one()))
|
||||
for publication_id in publication_ids:
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO scholar_publications (scholar_profile_id, publication_id, is_read, is_favorite)
|
||||
VALUES (:scholar_profile_id, :publication_id, false, false)
|
||||
"""
|
||||
),
|
||||
{"scholar_profile_id": scholar_profile_id, "publication_id": publication_id},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
client = TestClient(app)
|
||||
login_user(client, email="api-pubs-search-page@example.com", password="api-password")
|
||||
|
||||
response = client.get("/api/v1/publications?mode=all&page=1&page_size=2&search=alpha")
|
||||
assert response.status_code == 200
|
||||
data = response.json()["data"]
|
||||
assert int(data["total_count"]) == 2
|
||||
assert data["has_next"] is False
|
||||
assert len(data["publications"]) == 2
|
||||
assert all("alpha" in str(item["title"]).lower() for item in data["publications"])
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.db
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_publications_supports_sort_by_pdf_status(
|
||||
db_session: AsyncSession,
|
||||
) -> None:
|
||||
user_id = await insert_user(
|
||||
db_session,
|
||||
email="api-pubs-pdf-sort@example.com",
|
||||
password="api-password",
|
||||
)
|
||||
scholar_result = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO scholar_profiles (user_id, scholar_id, display_name, is_enabled)
|
||||
VALUES (:user_id, :scholar_id, :display_name, true)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"user_id": user_id,
|
||||
"scholar_id": "pdfSortScholar01",
|
||||
"display_name": "PDF Sort Scholar",
|
||||
},
|
||||
)
|
||||
scholar_profile_id = int(scholar_result.scalar_one())
|
||||
|
||||
resolved_result = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO publications (fingerprint_sha256, title_raw, title_normalized, citation_count, pdf_url)
|
||||
VALUES (:fingerprint, :title_raw, :title_normalized, 1, :pdf_url)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"fingerprint": f"{(user_id + 1300):064x}",
|
||||
"title_raw": "Resolved PDF",
|
||||
"title_normalized": "resolved pdf",
|
||||
"pdf_url": "https://example.org/resolved.pdf",
|
||||
},
|
||||
)
|
||||
resolved_publication_id = int(resolved_result.scalar_one())
|
||||
queued_result = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO publications (fingerprint_sha256, title_raw, title_normalized, citation_count)
|
||||
VALUES (:fingerprint, :title_raw, :title_normalized, 1)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"fingerprint": f"{(user_id + 1301):064x}",
|
||||
"title_raw": "Queued PDF",
|
||||
"title_normalized": "queued pdf",
|
||||
},
|
||||
)
|
||||
queued_publication_id = int(queued_result.scalar_one())
|
||||
failed_result = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO publications (fingerprint_sha256, title_raw, title_normalized, citation_count)
|
||||
VALUES (:fingerprint, :title_raw, :title_normalized, 1)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"fingerprint": f"{(user_id + 1302):064x}",
|
||||
"title_raw": "Failed PDF",
|
||||
"title_normalized": "failed pdf",
|
||||
},
|
||||
)
|
||||
failed_publication_id = int(failed_result.scalar_one())
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO scholar_publications (scholar_profile_id, publication_id, is_read, is_favorite)
|
||||
VALUES
|
||||
(:scholar_profile_id, :resolved_publication_id, false, false),
|
||||
(:scholar_profile_id, :queued_publication_id, false, false),
|
||||
(:scholar_profile_id, :failed_publication_id, false, false)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"scholar_profile_id": scholar_profile_id,
|
||||
"resolved_publication_id": resolved_publication_id,
|
||||
"queued_publication_id": queued_publication_id,
|
||||
"failed_publication_id": failed_publication_id,
|
||||
},
|
||||
)
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO publication_pdf_jobs (publication_id, status, attempt_count)
|
||||
VALUES (:queued_publication_id, 'queued', 1),
|
||||
(:failed_publication_id, 'failed', 2)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"queued_publication_id": queued_publication_id,
|
||||
"failed_publication_id": failed_publication_id,
|
||||
},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
client = TestClient(app)
|
||||
login_user(client, email="api-pubs-pdf-sort@example.com", password="api-password")
|
||||
|
||||
response = client.get("/api/v1/publications?mode=all&sort_by=pdf_status&sort_dir=desc")
|
||||
assert response.status_code == 200
|
||||
publications = response.json()["data"]["publications"]
|
||||
publication_ids = [int(item["publication_id"]) for item in publications]
|
||||
assert publication_ids[0] == resolved_publication_id
|
||||
assert publication_ids[-1] == failed_publication_id
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.db
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_publications_favorite_toggle_and_filter(db_session: AsyncSession) -> None:
|
||||
user_id = await insert_user(
|
||||
db_session,
|
||||
email="api-pubs-favorites@example.com",
|
||||
password="api-password",
|
||||
)
|
||||
scholar_result = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO scholar_profiles (user_id, scholar_id, display_name, is_enabled)
|
||||
VALUES (:user_id, :scholar_id, :display_name, true)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"user_id": user_id,
|
||||
"scholar_id": "favoriteScholar01",
|
||||
"display_name": "Favorite Scholar",
|
||||
},
|
||||
)
|
||||
scholar_profile_id = int(scholar_result.scalar_one())
|
||||
publication_a_result = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO publications (fingerprint_sha256, title_raw, title_normalized, citation_count)
|
||||
VALUES (:fingerprint, :title_raw, :title_normalized, 9)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"fingerprint": f"{(user_id + 101):064x}",
|
||||
"title_raw": "Favorite Target",
|
||||
"title_normalized": "favorite target",
|
||||
},
|
||||
)
|
||||
publication_a_id = int(publication_a_result.scalar_one())
|
||||
publication_b_result = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO publications (fingerprint_sha256, title_raw, title_normalized, citation_count)
|
||||
VALUES (:fingerprint, :title_raw, :title_normalized, 2)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"fingerprint": f"{(user_id + 102):064x}",
|
||||
"title_raw": "Not Favorite",
|
||||
"title_normalized": "not favorite",
|
||||
},
|
||||
)
|
||||
publication_b_id = int(publication_b_result.scalar_one())
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO scholar_publications (scholar_profile_id, publication_id, is_read, is_favorite)
|
||||
VALUES
|
||||
(:scholar_profile_id, :publication_a_id, false, false),
|
||||
(:scholar_profile_id, :publication_b_id, false, false)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"scholar_profile_id": scholar_profile_id,
|
||||
"publication_a_id": publication_a_id,
|
||||
"publication_b_id": publication_b_id,
|
||||
},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
client = TestClient(app)
|
||||
login_user(client, email="api-pubs-favorites@example.com", password="api-password")
|
||||
headers = api_csrf_headers(client)
|
||||
|
||||
set_response = client.post(
|
||||
f"/api/v1/publications/{publication_a_id}/favorite",
|
||||
json={"scholar_profile_id": scholar_profile_id, "is_favorite": True},
|
||||
headers=headers,
|
||||
)
|
||||
assert set_response.status_code == 200
|
||||
set_data = set_response.json()["data"]
|
||||
assert set_data["publication"]["publication_id"] == publication_a_id
|
||||
assert set_data["publication"]["is_favorite"] is True
|
||||
|
||||
favorite_only_response = client.get("/api/v1/publications?mode=all&favorite_only=true")
|
||||
assert favorite_only_response.status_code == 200
|
||||
favorite_only_data = favorite_only_response.json()["data"]
|
||||
assert favorite_only_data["favorite_only"] is True
|
||||
assert favorite_only_data["favorites_count"] == 1
|
||||
assert favorite_only_data["total_count"] == 1
|
||||
assert len(favorite_only_data["publications"]) == 1
|
||||
assert int(favorite_only_data["publications"][0]["publication_id"]) == publication_a_id
|
||||
|
||||
clear_response = client.post(
|
||||
f"/api/v1/publications/{publication_a_id}/favorite",
|
||||
json={"scholar_profile_id": scholar_profile_id, "is_favorite": False},
|
||||
headers=headers,
|
||||
)
|
||||
assert clear_response.status_code == 200
|
||||
clear_data = clear_response.json()["data"]
|
||||
assert clear_data["publication"]["publication_id"] == publication_a_id
|
||||
assert clear_data["publication"]["is_favorite"] is False
|
||||
|
||||
favorite_only_after_clear_response = client.get("/api/v1/publications?mode=all&favorite_only=true")
|
||||
assert favorite_only_after_clear_response.status_code == 200
|
||||
favorite_only_after_clear_data = favorite_only_after_clear_response.json()["data"]
|
||||
assert favorite_only_after_clear_data["favorites_count"] == 0
|
||||
assert favorite_only_after_clear_data["total_count"] == 0
|
||||
assert favorite_only_after_clear_data["publications"] == []
|
||||
|
||||
favorite_state_result = await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT is_favorite
|
||||
FROM scholar_publications
|
||||
WHERE scholar_profile_id = :scholar_profile_id
|
||||
AND publication_id = :publication_id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"scholar_profile_id": scholar_profile_id,
|
||||
"publication_id": publication_a_id,
|
||||
},
|
||||
)
|
||||
assert bool(favorite_state_result.scalar_one()) is False
|
||||
Loading…
Add table
Add a link
Reference in a new issue