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="
x
", ) 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="y
", ) 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="x
", ) 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