The new "Book Downloads" group in Settings holds a Calibre template for the file name of a direct download. Without a template, the name stays the title. The name is also the title if the template fails or gives no name for a book.
483 lines
23 KiB
Python
483 lines
23 KiB
Python
"""Downloads and builds work with uploads or library matching disabled."""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
from test_api import H
|
|
from test_api import client as client
|
|
from test_deliver import STORY_URL, add_story, rebuild, site, update
|
|
from test_deliver import jobs as jobs
|
|
|
|
from bindery import config, orchestrator
|
|
from bindery.cwa import Book
|
|
from bindery.ingest import IngestError
|
|
from bindery.jobs import DownloadMode, Job
|
|
from bindery.store import Store
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def isolated(client, jobs, monkeypatch, tmp_path):
|
|
monkeypatch.setattr(config, "INGEST_DIR", tmp_path / "ingest")
|
|
config.INGEST_DIR.mkdir()
|
|
monkeypatch.setattr(config, "TMP_DIR", tmp_path / "tmp")
|
|
config.TMP_DIR.mkdir()
|
|
monkeypatch.setattr(config, "CWA_URL", "")
|
|
monkeypatch.setattr(config, "CALIBRE_DB_PATH", None)
|
|
monkeypatch.setattr(orchestrator, "cwa", Mock(side_effect=AssertionError("Unexpected CWA request")))
|
|
monkeypatch.setattr(orchestrator, "_find_book", Mock(side_effect=AssertionError("Unexpected book search")))
|
|
|
|
|
|
@pytest.fixture
|
|
def library(isolated, monkeypatch):
|
|
monkeypatch.setattr(config, "CWA_URL", "https://cwa.invalid")
|
|
client = Mock()
|
|
client.get_book.return_value = Book(7, "uuid-7", "Test Story").to_dict()
|
|
monkeypatch.setattr(orchestrator, "cwa", Mock(return_value=client))
|
|
monkeypatch.setattr(orchestrator, "_find_book", Mock(return_value=None))
|
|
|
|
|
|
@pytest.mark.parametrize("enabled", [False, True])
|
|
def test_app_starts_without_any_cwa_configuration(tmp_path, enabled):
|
|
env = {
|
|
**os.environ,
|
|
"CONFIG_DIR": str(tmp_path / "standalone"),
|
|
"TMP_DIR": str(tmp_path / "builds"),
|
|
"AUTH_METHOD": "none",
|
|
"BINDERY_START_THREADS": "false",
|
|
"URL_BASE": "",
|
|
"CWA_ENABLED": str(enabled),
|
|
}
|
|
for name in ("INGEST_DIR", "CWA_URL", "CWA_DB_PATH", "CALIBRE_DB_PATH"):
|
|
env.pop(name, None)
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
"from bindery.web import app; "
|
|
"client = app.test_client(); "
|
|
"assert client.get('/api/health').status_code == 200; "
|
|
"settings = client.get('/api/settings').json; "
|
|
"assert settings['pause_uploads'] is True; "
|
|
"assert settings['ingest_available'] is False; "
|
|
"assert settings['link_books'] is False; "
|
|
"assert settings['linking_available'] is False",
|
|
],
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
assert "ERROR" not in result.stderr
|
|
# Bindery names what is missing, until the user says that there is no CWA.
|
|
for text in ("WARNING", "INGEST_DIR is not set", "CWA_URL and CALIBRE_DB_PATH are not set", "CWA_ENABLED=false"):
|
|
assert (text in result.stderr) is enabled
|
|
if not enabled:
|
|
assert "CWA" not in result.stderr and "INGEST" not in result.stderr
|
|
|
|
|
|
@pytest.fixture
|
|
def cwa_off(monkeypatch):
|
|
monkeypatch.setattr(config, "CWA_ENABLED", False)
|
|
|
|
|
|
def test_without_cwa_and_an_ingest_folder_a_job_only_builds_a_changed_book(monkeypatch, cwa_off):
|
|
monkeypatch.setattr(config, "INGEST_DIR", None)
|
|
site(monkeypatch, [(1, "One", "First chapter")])
|
|
job = Job(mode=DownloadMode.NEW, url=STORY_URL)
|
|
assert orchestrator._run_site_job(job).endswith("Built the book")
|
|
story_id = job.story_id
|
|
story = orchestrator.store.get_story(story_id)
|
|
assert (story["status"], story["message"], story["ingested_at"]) == ("ok", "Built the book", None)
|
|
assert orchestrator.store.get_setting(story_id, "upload_held") is None
|
|
assert len(list(config.TMP_DIR.glob(f"story-{story_id}-*.epub"))) == 1
|
|
assert update(story_id).startswith("No changes")
|
|
assert orchestrator._run_rebuild(Job(mode=DownloadMode.REBUILD, story_id=story_id)) == "No changes"
|
|
assert rebuild(story_id) == "Built the book"
|
|
site(monkeypatch, [(1, "One", "First chapter"), (2, "Two", "Second chapter")])
|
|
assert update(story_id).endswith("Built the book")
|
|
orchestrator.cwa.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize("saved_id", [None, 7])
|
|
def test_without_cwa_the_ingest_folder_gets_each_changed_book_as_a_plain_epub(cwa_off, saved_id):
|
|
# A pause, a hold and a saved book ID are from a time with CWA. They do not apply.
|
|
orchestrator.save_settings(pause_uploads=True, hold_new_links=True)
|
|
story_id = add_story(calibre_id=saved_id, calibre_uuid="saved-uuid" if saved_id else None)
|
|
orchestrator._link_changed(story_id)
|
|
orchestrator.store.set_setting(story_id, "upload_held", True)
|
|
assert rebuild(story_id) == "Sent to ingest folder"
|
|
assert [p.suffix for p in config.INGEST_DIR.iterdir()] == [".epub"]
|
|
assert orchestrator.store.get_story(story_id)["ingested_at"] is None
|
|
assert orchestrator.store.get_setting(story_id, "upload_held") is None
|
|
assert orchestrator._run_rebuild(Job(mode=DownloadMode.REBUILD, story_id=story_id)) == "No changes"
|
|
assert len(list(config.INGEST_DIR.iterdir())) == 1
|
|
orchestrator.cwa.assert_not_called()
|
|
|
|
|
|
def test_without_cwa_a_missing_ingest_folder_is_still_an_error(monkeypatch, tmp_path, cwa_off):
|
|
monkeypatch.setattr(config, "INGEST_DIR", tmp_path / "missing")
|
|
story_id = add_story()
|
|
with pytest.raises(IngestError, match="does not exist"):
|
|
rebuild(story_id)
|
|
# The book counts as sent only when it is in the folder.
|
|
assert orchestrator.store.get_setting(story_id, "sent_key") is None
|
|
|
|
|
|
def test_cwa_off_shows_in_the_config_and_refuses_cwa_logins(client, monkeypatch, cwa_off):
|
|
assert client.get("/api/config").json["cwa_enabled"] is False
|
|
monkeypatch.setattr(config, "AUTH_METHOD", "cwa")
|
|
monkeypatch.setattr(config, "CWA_DB_PATH", "/cwa/app.db")
|
|
response = client.get("/api/stories")
|
|
assert response.status_code == 503
|
|
assert response.json["error"].startswith("AUTH_METHOD=cwa needs CWA, but CWA_ENABLED is false.")
|
|
|
|
|
|
def test_without_cwa_the_download_template_uses_the_story_id(client, cwa_off):
|
|
assert client.get("/api/config").json["download_fields"]["id"] == "The Bindery story ID"
|
|
story_id = add_story(calibre_id=77)
|
|
client.put("/api/settings", json={"download_template": "{title} {id}"}, headers=H)
|
|
response = client.get(f"/api/stories/{story_id}/epub")
|
|
response.close()
|
|
assert f'filename="My Title {story_id}.epub"' in response.headers["Content-Disposition"]
|
|
|
|
|
|
def test_link_setting_defaults_on_and_persists(client, monkeypatch, tmp_path, library):
|
|
assert client.get("/api/settings").json["link_books"] is True
|
|
response = client.put("/api/settings", json={"link_books": False}, headers=H)
|
|
assert response.status_code == 200
|
|
assert response.json["link_books"] is False
|
|
monkeypatch.setattr(orchestrator, "store", Store(tmp_path / "bindery.db"))
|
|
assert client.get("/api/settings").json["link_books"] is False
|
|
response = client.put("/api/settings", json={"hold_new_links": True}, headers=H)
|
|
assert response.json["link_books"] is False
|
|
assert client.put("/api/settings", json={"link_books": True}, headers=H).json["link_books"] is True
|
|
|
|
|
|
@pytest.mark.parametrize("url", ["", "https://cwa.invalid"])
|
|
@pytest.mark.parametrize("database", [False, True])
|
|
def test_linking_requires_either_library_source(client, monkeypatch, tmp_path, url, database):
|
|
monkeypatch.setattr(config, "CWA_URL", url)
|
|
monkeypatch.setattr(config, "CALIBRE_DB_PATH", tmp_path / "metadata.db" if database else None)
|
|
available = bool(url or database)
|
|
settings = client.get("/api/settings").json
|
|
assert settings["linking_available"] is available
|
|
assert settings["link_books"] is available
|
|
settings = client.put("/api/settings", json={"link_books": True}, headers=H).json
|
|
assert settings["linking_available"] is available
|
|
assert settings["link_books"] is available
|
|
|
|
|
|
@pytest.mark.parametrize("saved", [None, False, True])
|
|
def test_missing_sources_force_linking_off_without_changing_preferences(client, monkeypatch, tmp_path, jobs, saved):
|
|
monkeypatch.setattr(config, "CWA_DB_PATH", tmp_path / "app.db")
|
|
orchestrator.store.set_setting(None, "link_books", saved)
|
|
orchestrator.store.set_setting(None, "hold_new_links", True)
|
|
story_id = add_story()
|
|
orchestrator.store.set_setting(story_id, "upload_held", True)
|
|
for requested in (True, False):
|
|
response = client.put("/api/settings", json={"link_books": requested}, headers=H)
|
|
assert response.status_code == 200
|
|
assert response.json["link_books"] is False
|
|
assert response.json["linking_available"] is False
|
|
assert response.json["hold_new_links"] is True
|
|
assert orchestrator.store.get_setting(None, "link_books") is saved
|
|
assert jobs.get_next() is None
|
|
monkeypatch.setattr(config, "CALIBRE_DB_PATH", tmp_path / "metadata.db")
|
|
assert client.get("/api/settings").json["linking_available"] is True
|
|
assert client.get("/api/settings").json["link_books"] is (saved is not False)
|
|
|
|
|
|
@pytest.mark.parametrize("value", ["false", 0, [], {}])
|
|
def test_link_setting_requires_a_boolean(client, value):
|
|
response = client.put("/api/settings", json={"link_books": value}, headers=H)
|
|
assert response.status_code == 400
|
|
assert client.get("/api/settings").json["link_books"] is False
|
|
|
|
|
|
@pytest.mark.parametrize("paused", [False, True])
|
|
def test_missing_ingest_forces_pause_without_changing_saved_preference(client, monkeypatch, tmp_path, paused):
|
|
orchestrator.save_settings(pause_uploads=paused)
|
|
monkeypatch.setattr(config, "INGEST_DIR", None)
|
|
settings = client.get("/api/settings").json
|
|
assert settings["pause_uploads"] is True
|
|
assert settings["ingest_available"] is False
|
|
for requested in (False, True):
|
|
response = client.put("/api/settings", json={"pause_uploads": requested}, headers=H)
|
|
assert response.status_code == 200
|
|
assert response.json["pause_uploads"] is True
|
|
monkeypatch.setattr(config, "INGEST_DIR", tmp_path / "enabled")
|
|
assert client.get("/api/settings").json["pause_uploads"] is paused
|
|
|
|
|
|
@pytest.mark.parametrize("existing", [False, True])
|
|
@pytest.mark.parametrize("link_books", [False, True])
|
|
def test_build_without_ingest_succeeds_and_keeps_the_book_local(monkeypatch, tmp_path, existing, link_books, library):
|
|
orchestrator.save_settings(link_books=link_books)
|
|
monkeypatch.setattr(config, "INGEST_DIR", None)
|
|
story_id = add_story(ingested_at="2026-01-01" if existing else None)
|
|
assert rebuild(story_id) == "Not sent. INGEST_DIR is not set."
|
|
story = orchestrator.store.get_story(story_id)
|
|
assert story["status"] == "ok"
|
|
assert story["ingested_at"] == ("2026-01-01" if existing else None)
|
|
assert list(config.TMP_DIR.glob(f"story-{story_id}-*.epub"))
|
|
assert not list((tmp_path / "ingest").iterdir())
|
|
details = orchestrator.story_details(story_id)
|
|
assert details["pause_uploads"] is True
|
|
assert details["story"]["upload_held"] is True
|
|
assert details["library_error"] is None
|
|
|
|
|
|
@pytest.mark.parametrize("configured", [False, True])
|
|
@pytest.mark.parametrize("saved_id", [None, 7])
|
|
@pytest.mark.parametrize("paused", [False, True])
|
|
def test_linking_off_exports_books_and_respects_pause(monkeypatch, tmp_path, configured, saved_id, paused):
|
|
if configured:
|
|
monkeypatch.setattr(config, "CWA_URL", "https://cwa.invalid")
|
|
monkeypatch.setattr(config, "CALIBRE_DB_PATH", tmp_path / "unreadable.db")
|
|
orchestrator.save_settings(link_books=False, pause_uploads=paused)
|
|
story_id = add_story(calibre_id=saved_id, calibre_uuid="saved-uuid", ingested_at="2026-01-01")
|
|
message = rebuild(story_id)
|
|
assert orchestrator.store.get_story(story_id)["status"] == "ok"
|
|
assert orchestrator.story_details(story_id)["library_error"] is None
|
|
orchestrator.cwa.assert_not_called()
|
|
orchestrator._find_book.assert_not_called()
|
|
if not paused:
|
|
if saved_id is not None:
|
|
assert message == "Updated library book 7"
|
|
manifest = next(config.INGEST_DIR.glob("*.cwa.json"))
|
|
assert json.loads(manifest.read_text()) == {"action": "add_format", "book_id": 7}
|
|
else:
|
|
assert message == "Sent to ingest folder"
|
|
assert not list(config.INGEST_DIR.glob("*.cwa.json"))
|
|
assert len(list(config.INGEST_DIR.glob("*.epub"))) == 1
|
|
assert orchestrator.store.get_setting(story_id, "upload_held") is None
|
|
else:
|
|
assert message.startswith("Not sent.")
|
|
assert not list(config.INGEST_DIR.iterdir())
|
|
assert orchestrator.store.get_setting(story_id, "upload_held") is True
|
|
|
|
|
|
def test_disabling_linking_releases_approval_holds_for_saved_ids(jobs, library):
|
|
orchestrator.save_settings(hold_new_links=True)
|
|
story_id = add_story(calibre_id=7, calibre_uuid="uuid-7", ingested_at="2026-01-01")
|
|
orchestrator._link_changed(story_id)
|
|
assert "Approve link" in rebuild(story_id)
|
|
orchestrator.save_settings(link_books=False)
|
|
assert orchestrator.get_settings()["hold_new_links"] is True
|
|
assert orchestrator.story_details(story_id)["story"]["needs_approval"] is False
|
|
job = jobs.get_next()
|
|
assert job.story_id == story_id
|
|
assert orchestrator._run_rebuild(job) == "Updated library book 7"
|
|
|
|
|
|
@pytest.mark.parametrize("link_books", [True, False])
|
|
def test_story_response_reports_linking_and_approval_settings(client, link_books, library):
|
|
orchestrator.save_settings(link_books=link_books, hold_new_links=True)
|
|
story_id = add_story()
|
|
orchestrator._link_changed(story_id)
|
|
response = client.get(f"/api/stories/{story_id}")
|
|
assert response.status_code == 200
|
|
assert response.json["link_books"] is link_books
|
|
assert response.json["story"]["needs_approval"] is link_books
|
|
|
|
|
|
def test_enabling_linking_restores_the_saved_approval_preference(library):
|
|
orchestrator.save_settings(link_books=False, hold_new_links=True)
|
|
story_id = add_story(calibre_id=7, calibre_uuid="uuid-7", ingested_at="2026-01-01")
|
|
orchestrator._link_changed(story_id)
|
|
assert orchestrator.story_details(story_id)["story"]["needs_approval"] is False
|
|
orchestrator.save_settings(link_books=True)
|
|
assert orchestrator.get_settings()["hold_new_links"] is True
|
|
assert orchestrator.story_details(story_id)["story"]["needs_approval"] is True
|
|
assert "Approve link" in rebuild(story_id)
|
|
assert not list(config.INGEST_DIR.iterdir())
|
|
|
|
|
|
def test_linking_off_exports_every_rebuild_without_an_id_or_approval():
|
|
orchestrator.save_settings(link_books=False, hold_new_links=True)
|
|
story_id = add_story()
|
|
orchestrator._link_changed(story_id)
|
|
for _ in range(2):
|
|
assert rebuild(story_id) == "Sent to ingest folder"
|
|
assert orchestrator.store.get_story(story_id)["ingested_at"] is not None
|
|
assert len(list(config.INGEST_DIR.glob("*.epub"))) == 2
|
|
assert not list(config.INGEST_DIR.glob("*.cwa.json"))
|
|
assert orchestrator.store.get_setting(story_id, "upload_held") is None
|
|
|
|
|
|
def test_disabling_linking_releases_an_unlinked_book_without_approval(jobs, library):
|
|
orchestrator.save_settings(hold_new_links=True)
|
|
story_id = add_story()
|
|
orchestrator._link_changed(story_id)
|
|
assert "Approve as new book" in rebuild(story_id)
|
|
orchestrator.save_settings(link_books=False)
|
|
assert orchestrator.list_stories()[0]["approval_held"] is False
|
|
assert orchestrator._run_rebuild(jobs.get_next()) == "Sent to ingest folder"
|
|
assert orchestrator.store.get_setting(story_id, "upload_held") is None
|
|
|
|
|
|
@pytest.mark.parametrize("standalone", [False, True])
|
|
def test_new_story_download_skips_library_matching(monkeypatch, standalone):
|
|
if standalone:
|
|
monkeypatch.setattr(config, "INGEST_DIR", None)
|
|
else:
|
|
monkeypatch.setattr(config, "CWA_URL", "https://cwa.invalid")
|
|
orchestrator.save_settings(link_books=False)
|
|
site(monkeypatch, [(1, "One", "First chapter")])
|
|
job = Job(mode=DownloadMode.NEW, url=STORY_URL)
|
|
message = orchestrator._run_site_job(job)
|
|
assert ("Not sent." if standalone else "Sent to ingest folder") in message
|
|
assert len(orchestrator.store.list_chapters(job.story_id)) == 1
|
|
assert orchestrator.store.get_story(job.story_id)["status"] == "ok"
|
|
assert list(config.TMP_DIR.glob(f"story-{job.story_id}-*.epub"))
|
|
orchestrator.cwa.assert_not_called()
|
|
orchestrator._find_book.assert_not_called()
|
|
|
|
|
|
def test_site_updates_export_without_a_library_or_saved_id(monkeypatch):
|
|
assert orchestrator.get_settings()["link_books"] is False
|
|
site(monkeypatch, [(1, "One", "First chapter")])
|
|
job = Job(mode=DownloadMode.NEW, url=STORY_URL)
|
|
assert "Sent to ingest folder" in orchestrator._run_site_job(job)
|
|
site(monkeypatch, [(1, "One", "First chapter"), (2, "Two", "Second chapter")])
|
|
assert "Sent to ingest folder" in orchestrator._run_site_job(Job(mode=DownloadMode.NEW, story_id=job.story_id))
|
|
assert len(orchestrator.store.list_chapters(job.story_id)) == 2
|
|
assert len(list(config.INGEST_DIR.glob("*.epub"))) == 2
|
|
assert not list(config.INGEST_DIR.glob("*.cwa.json"))
|
|
orchestrator.cwa.assert_not_called()
|
|
orchestrator._find_book.assert_not_called()
|
|
|
|
|
|
def test_saved_id_without_uuid_works_without_library_configuration():
|
|
story_id = add_story(calibre_id=7, ingested_at="2026-01-01")
|
|
assert rebuild(story_id) == "Updated library book 7"
|
|
orchestrator.cwa.assert_not_called()
|
|
orchestrator._find_book.assert_not_called()
|
|
|
|
|
|
def test_paused_unlinked_update_does_not_raise_not_found(monkeypatch):
|
|
monkeypatch.setattr(config, "CWA_URL", "https://cwa.invalid")
|
|
orchestrator._find_book.return_value = None
|
|
orchestrator._find_book.side_effect = None
|
|
orchestrator.save_settings(pause_uploads=True)
|
|
story_id = add_story(ingested_at="2026-01-01")
|
|
assert rebuild(story_id) == "Not sent. Turn off pause to send."
|
|
assert orchestrator.store.get_story(story_id)["status"] == "ok"
|
|
assert not list(config.INGEST_DIR.iterdir())
|
|
|
|
|
|
def test_enabling_linking_rebuilds_held_books_and_matches_again(monkeypatch, jobs, tmp_path):
|
|
monkeypatch.setattr(config, "CWA_URL", "https://cwa.invalid")
|
|
orchestrator.save_settings(link_books=False, pause_uploads=True)
|
|
story_id = add_story()
|
|
assert "Turn off pause" in rebuild(story_id)
|
|
downloaded = tmp_path / "library.epub"
|
|
downloaded.touch()
|
|
orchestrator._find_book.side_effect = None
|
|
orchestrator._find_book.return_value = (Book(7, "uuid-7", "Test Story"), downloaded)
|
|
orchestrator.save_settings(link_books=True, pause_uploads=False)
|
|
job = jobs.get_next()
|
|
assert job.mode == DownloadMode.REBUILD
|
|
assert job.story_id == story_id
|
|
assert orchestrator._run_rebuild(job) == "Updated library book 7"
|
|
assert orchestrator.store.get_story(story_id)["calibre_id"] == 7
|
|
assert orchestrator.store.get_setting(story_id, "upload_held") is None
|
|
|
|
|
|
@pytest.mark.parametrize("link_books", [False, True])
|
|
@pytest.mark.parametrize("hold_new_links", [False, True])
|
|
@pytest.mark.parametrize("paused", [False, True])
|
|
@pytest.mark.parametrize("saved_id", [None, 7])
|
|
def test_upload_settings_matrix(client, link_books, hold_new_links, paused, saved_id, library):
|
|
orchestrator.save_settings(link_books=link_books, hold_new_links=hold_new_links, pause_uploads=paused)
|
|
story_id = add_story(
|
|
calibre_id=saved_id,
|
|
calibre_uuid="uuid-7" if saved_id else None,
|
|
ingested_at="2026-01-01" if saved_id else None,
|
|
)
|
|
orchestrator._link_changed(story_id)
|
|
assert client.get("/api/stories").json[0]["approval_held"] is False
|
|
message = rebuild(story_id)
|
|
pending = paused or (link_books and hold_new_links)
|
|
assert message.startswith("Not sent.") is pending
|
|
listed = client.get("/api/stories").json[0]
|
|
assert listed["upload_held"] is pending
|
|
assert listed["approval_held"] is (link_books and hold_new_links and not paused)
|
|
assert bool(orchestrator.store.get_setting(story_id, "upload_held")) is pending
|
|
assert bool(list(config.INGEST_DIR.iterdir())) is (not pending)
|
|
|
|
|
|
@pytest.mark.parametrize("missing_ingest", [False, True])
|
|
def test_approval_while_paused_preserves_pending_upload_until_resume(
|
|
client, monkeypatch, jobs, missing_ingest, library
|
|
):
|
|
ingest = config.INGEST_DIR
|
|
orchestrator.save_settings(hold_new_links=True, pause_uploads=True)
|
|
if missing_ingest:
|
|
monkeypatch.setattr(config, "INGEST_DIR", None)
|
|
story_id = add_story()
|
|
orchestrator._link_changed(story_id)
|
|
rebuild(story_id)
|
|
response = client.post(f"/api/stories/{story_id}/approve", json={"uuid": None}, headers=H)
|
|
assert response.status_code == 200
|
|
assert response.json["job"] is None
|
|
assert response.json["story"]["needs_approval"] is False
|
|
assert response.json["story"]["upload_held"] is True
|
|
assert jobs.get_next() is None
|
|
assert client.get("/api/stories").json[0]["approval_held"] is False
|
|
assert orchestrator.store.get_setting(story_id, "upload_held") is True
|
|
monkeypatch.setattr(config, "INGEST_DIR", ingest)
|
|
orchestrator.save_settings(pause_uploads=False)
|
|
job = jobs.get_next()
|
|
assert job.story_id == story_id
|
|
assert orchestrator._run_rebuild(job) == "Sent to CWA"
|
|
assert orchestrator.store.get_setting(story_id, "upload_held") is None
|
|
assert client.get("/api/stories").json[0]["approval_held"] is False
|
|
|
|
|
|
def test_resuming_uploads_does_not_bypass_approval(client, jobs, library):
|
|
orchestrator.save_settings(hold_new_links=True, pause_uploads=True)
|
|
story_id = add_story(calibre_id=7, calibre_uuid="uuid-7", ingested_at="2026-01-01")
|
|
orchestrator._link_changed(story_id)
|
|
rebuild(story_id)
|
|
assert client.get("/api/stories").json[0]["approval_held"] is False
|
|
orchestrator.save_settings(pause_uploads=False)
|
|
assert client.get("/api/stories").json[0]["approval_held"] is True
|
|
assert "Approve link" in orchestrator._run_rebuild(jobs.get_next())
|
|
assert orchestrator.store.get_setting(story_id, "upload_held") is True
|
|
assert not list(config.INGEST_DIR.iterdir())
|
|
|
|
|
|
def test_missing_library_connection_is_not_an_approval_hold(client, monkeypatch):
|
|
orchestrator.save_settings(hold_new_links=True)
|
|
story_id = add_story(ingested_at="2026-01-01")
|
|
orchestrator._link_changed(story_id)
|
|
assert rebuild(story_id) == "Sent to ingest folder"
|
|
assert client.get("/api/stories").json[0]["approval_held"] is False
|
|
details = client.get(f"/api/stories/{story_id}").json
|
|
assert details["story"]["upload_held"] is False
|
|
assert details["story"]["needs_approval"] is False
|
|
assert details["link_books"] is False
|
|
assert details["ingest_available"] is True
|
|
monkeypatch.setattr(config, "INGEST_DIR", None)
|
|
assert client.get(f"/api/stories/{story_id}").json["ingest_available"] is False
|
|
monkeypatch.setattr(config, "CWA_URL", "https://cwa.invalid")
|
|
assert client.get(f"/api/stories/{story_id}").json["link_books"] is True
|
|
|
|
|
|
@pytest.mark.parametrize("missing_ingest", [False, True])
|
|
def test_releasing_other_holds_does_not_queue_uploads_while_paused(monkeypatch, jobs, missing_ingest, library):
|
|
orchestrator.save_settings(link_books=False, hold_new_links=True, pause_uploads=True)
|
|
story_id = add_story()
|
|
rebuild(story_id)
|
|
if missing_ingest:
|
|
monkeypatch.setattr(config, "INGEST_DIR", None)
|
|
orchestrator.save_settings(link_books=True, hold_new_links=False)
|
|
assert jobs.get_next() is None
|
|
assert orchestrator.get_settings()["pause_uploads"] is True
|