"""Exercise the FanFicFare boundary without requests to a story site.""" import zipfile from types import SimpleNamespace from unittest.mock import Mock import pytest from test_build import IMG_URL, PNG, URL, chapters, metahtml from bindery import config, fff @pytest.mark.parametrize("cover", [None, {}, {"url": IMG_URL, "ext": "png", "mime": "image/png", "data": PNG}]) def test_metadata_snapshot_from_adapter(monkeypatch, cover): adapter = fff.get_adapter(URL) adapter.setStoryMetadata(metahtml()) adapter.chapterUrls = chapters() adapter.story.img_store.cover = cover fetch = Mock(return_value=adapter.story) monkeypatch.setattr(adapter, "getStoryMetadataOnly", fetch) snap = fff.fetch_story(adapter) fetch.assert_called_once_with(get_cover=True) assert snap.title == "Test Story" assert snap.author == "Writer" assert snap.site == "www.royalroad.com" assert snap.chapters == [{"url": c["url"], "title": c["title"]} for c in chapters()] assert snap.calibre["authors"] == ["Writer"] assert snap.cover == (cover or None) assert "Test Story" in snap.metahtml @pytest.mark.parametrize( "error,message", [ (fff.exceptions.StoryDoesNotExist(URL), "StoryDoesNotExist"), (fff.exceptions.AccessDenied("private"), "AccessDenied"), (fff.exceptions.FailedToLogin(URL, "reader"), "FailedToLogin"), (fff.exceptions.AdultCheckRequired(URL), "is_adult:true"), ], ) def test_adapter_errors_become_actionable_story_errors(error, message): adapter = SimpleNamespace(getStoryMetadataOnly=Mock(side_effect=error)) with pytest.raises(fff.StoryError, match=message): fff.fetch_story(adapter) def test_downloaded_chapter_restores_original_image_urls(): html = '
Text
'
adapter = SimpleNamespace(getChapterTextNum=Mock(return_value=html))
result = fff.fetch_chapter(adapter, "chapter", 3)
adapter.getChapterTextNum.assert_called_once_with("chapter", 3)
assert 'src="https://images.test/1.png"' in result
for src in ["data:image/png;base64,abc", "failedtoload", "https://images.test/direct.png"]:
markup = f'plain
") == "plain
" def test_collected_images_skip_failed_and_empty_downloads(): image = {"url": "image", "ext": "png", "mime": "image/png", "data": PNG} adapter = SimpleNamespace( story=SimpleNamespace( img_store=SimpleNamespace( infos=[{**image, "newsrc": "images/1.png"}, {**image, "data": b""}, {**image, "newsrc": "failedtoload"}] ) ) ) assert fff.collected_images(adapter) == [image] def test_personal_ini_overrides_and_invalid_input(monkeypatch, tmp_path): ini = tmp_path / "personal.ini" ini.write_text("[overrides]\nis_adult: true\n", encoding="utf-8") monkeypatch.setattr(config, "PERSONAL_INI", ini) configuration = fff.get_configuration(URL) assert configuration.getConfig("is_adult") for text in ["[defaults]\nbroken line\n", "setting: before any section"]: with pytest.raises(ValueError, match="cannot read"): fff.check_personal_ini(text) with pytest.raises(fff.StoryError, match="does not support"): fff.get_configuration("https://unsupported.example/story") def test_adapter_rejects_invalid_story_url(monkeypatch): monkeypatch.setattr( fff.adapters, "getAdapter", Mock(side_effect=fff.exceptions.InvalidStoryURL(URL, "site", "example")) ) with pytest.raises(fff.StoryError): fff.get_adapter(URL) def test_normalization_errors_are_not_site_requests(monkeypatch): monkeypatch.setattr(fff.adapters, "getNormalStoryURLSite", Mock(side_effect=ValueError("bad URL"))) fff.normalize_url.cache_clear() try: assert fff.normalize_url("bad-input") is None finally: fff.normalize_url.cache_clear() def test_empty_chapters_fail_and_stored_cover_is_built(tmp_path): with pytest.raises(fff.StoryError, match="no chapters"): fff.build_epub(URL, metahtml(), [], [], None, [], {}, tmp_path / "empty.epub") cover = {"url": IMG_URL, "ext": "png", "mime": "image/png", "data": PNG} out = tmp_path / "cover.epub" fff.build_epub(URL, metahtml(), chapters()[:1], [], cover, [], {}, out) with zipfile.ZipFile(out) as book: assert any(book.read(n) == PNG for n in book.namelist() if n.endswith(".png"))