Files
pi-weekly-newspaper/tests/test_issues.py

362 lines
13 KiB
Python
Raw Normal View History

import json
import os
from datetime import date, datetime
from unittest.mock import MagicMock, patch
import requests
from PIL import Image as PILImage
from app import db
from src.models import Article, Image, Issue
import config
def test_regenerate_passes_ordered_image_paths_to_generate_cover(app, client, db, tmp_path):
"""First image per article, in pub_date order, passed to generate_cover."""
os.makedirs(tmp_path, exist_ok=True)
img1 = str(tmp_path / "a1.jpg")
img2 = str(tmp_path / "a2.jpg")
for p in (img1, img2):
PILImage.new("RGB", (100, 100), color="blue").save(p, format="JPEG")
with app.app_context():
a1 = Article(
guid="g1",
title="Earlier Article",
author="A",
pub_date=datetime(2026, 4, 6, 10, 0),
categories=json.dumps(["Government"]),
link="http://example.com/1",
content_html="<p>x</p>",
)
a2 = Article(
guid="g2",
title="Later Article",
author="B",
pub_date=datetime(2026, 4, 7, 10, 0),
categories=json.dumps(["Culture"]),
link="http://example.com/2",
content_html="<p>y</p>",
)
db.session.add_all([a1, a2])
db.session.flush()
db.session.add_all(
[
Image(
article_id=a1.id,
original_url="https://example.com/1.jpg",
local_path=img1,
width=100,
height=100,
),
Image(
article_id=a2.id,
original_url="https://example.com/2.jpg",
local_path=img2,
width=100,
height=100,
),
]
)
id1, id2 = a1.id, a2.id
issue = Issue(
week_start=date(2026, 4, 6),
week_end=date(2026, 4, 12),
cover_method="mosaic",
cover_path=str(tmp_path / "old-cover.jpg"),
epub_path=str(tmp_path / "old.epub"),
article_ids=json.dumps([id2, id1]),
status="published",
)
db.session.add(issue)
db.session.commit()
issue_id = issue.id
mock_cover = MagicMock(return_value=str(tmp_path / "new-cover.jpg"))
mock_epub = MagicMock(return_value=str(tmp_path / "new.epub"))
with patch("src.routes.issues.generate_cover", mock_cover), patch(
"src.routes.issues.build_epub", mock_epub
):
resp = client.post(f"/issues/{issue_id}/regenerate")
assert resp.status_code in (302, 303)
mock_cover.assert_called_once()
args = mock_cover.call_args[0]
assert args[0] == "mosaic"
assert args[4] == ["Earlier Article", "Later Article"]
assert args[5] == [img1, img2]
def test_regenerate_maps_ai_cover_method_to_mosaic(app, client, db, tmp_path):
"""Legacy ai issues use mosaic generation and persist cover_method=mosaic."""
os.makedirs(tmp_path, exist_ok=True)
img1 = str(tmp_path / "a1.jpg")
PILImage.new("RGB", (100, 100), color="blue").save(img1, format="JPEG")
with app.app_context():
a1 = Article(
guid="g-ai",
title="Some Article",
author="A",
pub_date=datetime(2026, 4, 6, 10, 0),
categories=json.dumps(["Government"]),
link="http://example.com/1",
content_html="<p>x</p>",
)
db.session.add(a1)
db.session.flush()
db.session.add(
Image(
article_id=a1.id,
original_url="https://example.com/1.jpg",
local_path=img1,
width=100,
height=100,
)
)
issue = Issue(
week_start=date(2026, 4, 6),
week_end=date(2026, 4, 12),
cover_method="ai",
cover_path=str(tmp_path / "old-cover.jpg"),
epub_path=str(tmp_path / "old.epub"),
article_ids=json.dumps([a1.id]),
status="published",
)
db.session.add(issue)
db.session.commit()
issue_id = issue.id
mock_cover = MagicMock(return_value=str(tmp_path / "new-cover.jpg"))
mock_epub = MagicMock(return_value=str(tmp_path / "new.epub"))
with patch("src.routes.issues.generate_cover", mock_cover), patch(
"src.routes.issues.build_epub", mock_epub
):
client.post(f"/issues/{issue_id}/regenerate")
mock_cover.assert_called_once()
assert mock_cover.call_args[0][0] == "mosaic"
with app.app_context():
updated = db.session.get(Issue, issue_id)
assert updated.cover_method == "mosaic"
def test_push_issue_success(app, client, db, tmp_path):
"""Test successful push to Grimmory API."""
os.makedirs(tmp_path, exist_ok=True)
epub_path = tmp_path / "test.epub"
epub_path.write_text("dummy epub content")
with app.app_context():
issue = Issue(
week_start=date(2026, 4, 6),
week_end=date(2026, 4, 12),
cover_method="mosaic",
cover_path=str(tmp_path / "cover.jpg"),
epub_path=str(epub_path),
article_ids=json.dumps([1]),
status="published",
)
db.session.add(issue)
db.session.commit()
issue_id = issue.id
mock_login_resp = MagicMock()
mock_login_resp.status_code = 200
mock_login_resp.json.return_value = {"token": "test_token"}
mock_libraries_resp = MagicMock()
mock_libraries_resp.status_code = 200
mock_libraries_resp.json.return_value = [{"id": 123, "name": "lib123", "paths": [{"id": 456}]}]
mock_upload_resp = MagicMock()
mock_upload_resp.status_code = 200
def mock_post(url, *args, **kwargs):
if url.endswith("/api/v1/auth/login"):
return mock_login_resp
elif url.endswith("/api/v1/files/upload"):
return mock_upload_resp
return MagicMock(status_code=404)
def mock_get(url, *args, **kwargs):
if url.endswith("/api/v1/libraries"):
return mock_libraries_resp
return MagicMock(status_code=404)
with patch("src.routes.issues.requests.post", side_effect=mock_post) as mock_post_call, \
patch("src.routes.issues.requests.get", side_effect=mock_get) as mock_get_call:
with patch.object(config, "GRIMMORY_URL", "http://grimmory.test"), \
patch.object(config, "GRIMMORY_USERNAME", "admin"), \
patch.object(config, "GRIMMORY_PASSWORD", "password"), \
patch.object(config, "GRIMMORY_LIBRARY_ID", "lib123"):
resp = client.post(f"/issues/{issue_id}/push")
assert resp.status_code in (302, 303)
assert mock_post_call.call_count == 2
assert mock_get_call.call_count == 1
# Check login call
login_args, login_kwargs = mock_post_call.call_args_list[0]
assert login_args[0] == "http://grimmory.test/api/v1/auth/login"
assert login_kwargs["json"] == {"username": "admin", "password": "password"}
# Check libraries call
get_args, get_kwargs = mock_get_call.call_args_list[0]
assert get_args[0] == "http://grimmory.test/api/v1/libraries"
assert get_kwargs["headers"] == {"Authorization": "Bearer test_token"}
# Check upload call
upload_args, upload_kwargs = mock_post_call.call_args_list[1]
assert upload_args[0] == "http://grimmory.test/api/v1/files/upload"
assert upload_kwargs["headers"] == {"Authorization": "Bearer test_token"}
assert upload_kwargs["data"] == {"libraryId": 123, "pathId": 456}
assert "file" in upload_kwargs["files"]
def test_push_issue_missing_config(app, client, db, tmp_path):
"""Test push fails gracefully when config is missing."""
with app.app_context():
issue = Issue(
week_start=date(2026, 4, 6),
week_end=date(2026, 4, 12),
cover_method="mosaic",
cover_path=str(tmp_path / "cover.jpg"),
epub_path=str(tmp_path / "test.epub"),
article_ids=json.dumps([1]),
status="published",
)
db.session.add(issue)
db.session.commit()
issue_id = issue.id
with patch("src.routes.issues.requests.post") as mock_post:
with patch.object(config, "GRIMMORY_URL", None), \
patch.object(config, "GRIMMORY_USERNAME", None), \
patch.object(config, "GRIMMORY_PASSWORD", None):
resp = client.post(f"/issues/{issue_id}/push")
assert resp.status_code in (302, 303)
mock_post.assert_not_called()
def test_push_issue_missing_file(app, client, db, tmp_path):
"""Test push fails gracefully when epub file is missing."""
with app.app_context():
issue = Issue(
week_start=date(2026, 4, 6),
week_end=date(2026, 4, 12),
cover_method="mosaic",
cover_path=str(tmp_path / "cover.jpg"),
epub_path="/nonexistent/path/test.epub",
article_ids=json.dumps([1]),
status="published",
)
db.session.add(issue)
db.session.commit()
issue_id = issue.id
with patch("src.routes.issues.requests.post") as mock_post:
with patch.object(config, "GRIMMORY_URL", "http://grimmory.test"), \
patch.object(config, "GRIMMORY_USERNAME", "admin"), \
patch.object(config, "GRIMMORY_PASSWORD", "password"):
resp = client.post(f"/issues/{issue_id}/push")
assert resp.status_code in (302, 303)
mock_post.assert_not_called()
def test_push_issue_login_error(app, client, db, tmp_path):
"""Test push handles login errors gracefully."""
os.makedirs(tmp_path, exist_ok=True)
epub_path = tmp_path / "test.epub"
epub_path.write_text("dummy epub content")
with app.app_context():
issue = Issue(
week_start=date(2026, 4, 6),
week_end=date(2026, 4, 12),
cover_method="mosaic",
cover_path=str(tmp_path / "cover.jpg"),
epub_path=str(epub_path),
article_ids=json.dumps([1]),
status="published",
)
db.session.add(issue)
db.session.commit()
issue_id = issue.id
mock_login_resp = MagicMock()
mock_login_resp.status_code = 401
mock_login_resp.text = "Unauthorized"
with patch("src.routes.issues.requests.post", return_value=mock_login_resp) as mock_post_call:
with patch.object(config, "GRIMMORY_URL", "http://grimmory.test"), \
patch.object(config, "GRIMMORY_USERNAME", "admin"), \
patch.object(config, "GRIMMORY_PASSWORD", "wrong"):
resp = client.post(f"/issues/{issue_id}/push")
assert resp.status_code in (302, 303)
assert mock_post_call.call_count == 1
login_args, _ = mock_post_call.call_args_list[0]
assert login_args[0] == "http://grimmory.test/api/v1/auth/login"
def test_push_issue_api_error(app, client, db, tmp_path):
"""Test push handles API errors gracefully."""
os.makedirs(tmp_path, exist_ok=True)
epub_path = tmp_path / "test.epub"
epub_path.write_text("dummy epub content")
with app.app_context():
issue = Issue(
week_start=date(2026, 4, 6),
week_end=date(2026, 4, 12),
cover_method="mosaic",
cover_path=str(tmp_path / "cover.jpg"),
epub_path=str(epub_path),
article_ids=json.dumps([1]),
status="published",
)
db.session.add(issue)
db.session.commit()
issue_id = issue.id
mock_login_resp = MagicMock()
mock_login_resp.status_code = 200
mock_login_resp.json.return_value = {"token": "test_token"}
mock_upload_resp = MagicMock()
mock_upload_resp.status_code = 500
mock_upload_resp.text = "Internal Server Error"
def mock_post(url, *args, **kwargs):
if url.endswith("/api/v1/auth/login"):
return mock_login_resp
elif url.endswith("/api/v1/files/upload"):
return mock_upload_resp
return MagicMock(status_code=404)
def mock_get(url, *args, **kwargs):
return MagicMock(status_code=200, json=lambda: [{"id": 123, "name": "lib123", "paths": [{"id": 456}]}])
with patch("src.routes.issues.requests.post", side_effect=mock_post) as mock_post_call, \
patch("src.routes.issues.requests.get", side_effect=mock_get) as mock_get_call:
with patch.object(config, "GRIMMORY_URL", "http://grimmory.test"), \
patch.object(config, "GRIMMORY_USERNAME", "admin"), \
patch.object(config, "GRIMMORY_PASSWORD", "password"), \
patch.object(config, "GRIMMORY_LIBRARY_ID", "lib123"):
resp = client.post(f"/issues/{issue_id}/push")
assert resp.status_code in (302, 303)
assert mock_post_call.call_count == 2