CWA_ENABLED now defaults to false. If a CWA variable is set while CWA is off, the start log has a warning that names it. On the Settings page, the alert about INGEST_DIR moves to Book Downloads and shows with CWA on or off. The Uploads to CWA card moves below Book Downloads.
491 lines
23 KiB
Python
491 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", [None, 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),
|
|
}
|
|
# CWA is off unless CWA_ENABLED turns it on.
|
|
for name in (
|
|
"INGEST_DIR",
|
|
"CWA_URL",
|
|
"CWA_DB_PATH",
|
|
"CALIBRE_DB_PATH",
|
|
*(["CWA_ENABLED"] if enabled is None else []),
|
|
):
|
|
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; "
|
|
f"assert client.get('/api/config').json['cwa_enabled'] is {bool(enabled)}; "
|
|
"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 only for a user who turned CWA on.
|
|
for text in ("WARNING", "INGEST_DIR is not set", "CWA_URL and CALIBRE_DB_PATH are not set", "remove CWA_ENABLED"):
|
|
assert (text in result.stderr) is bool(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_ENABLED=true.")
|
|
|
|
|
|
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
|