"""FC-6.2 — promote a post to a series + add a post as a chapter + browse list.""" import pytest from backend.app.models import Artist, ImageRecord, Post, TagKind from backend.app.services.series_service import SeriesService from backend.app.services.tag_service import TagService pytestmark = pytest.mark.integration _SEQ = 0 async def _artist(db, name): a = Artist(name=name, slug=name.lower().replace(" ", "-")) db.add(a) await db.flush() return a async def _post(db, artist, title, ext): p = Post(artist_id=artist.id, external_post_id=ext, post_title=title) db.add(p) await db.flush() return p async def _post_images(db, post, artist, n): global _SEQ ids = [] for _ in range(n): _SEQ += 1 rec = ImageRecord( path=f"/tmp/fc_ps_{_SEQ}.png", sha256=f"{_SEQ:064d}", size_bytes=1, mime="image/png", origin="downloaded", primary_post_id=post.id, artist_id=artist.id, ) db.add(rec) await db.flush() ids.append(rec.id) return ids @pytest.mark.asyncio async def test_promote_post_to_series(db): svc = SeriesService(db) artist = await _artist(db, "Bikupan") post = await _post(db, artist, "My Comic pages 1-3", "pp1") imgs = await _post_images(db, post, artist, 3) out = await svc.promote_post_to_series(post.id) assert out["added"] == 3 assert out["name"] == "My Comic pages 1-3" data = await svc.list_pages(out["series_tag_id"]) assert len(data["chapters"]) == 1 ch = data["chapters"][0] assert ch["stated_page_start"] == 1 assert ch["stated_page_end"] == 3 assert [p["image_id"] for p in ch["pages"]] == imgs # capture order assert [p["stated_page"] for p in ch["pages"]] == [1, 2, 3] @pytest.mark.asyncio async def test_promote_requires_images(db): svc = SeriesService(db) artist = await _artist(db, "Empty Artist") post = await _post(db, artist, "No images", "pp2") from backend.app.services.series_service import SeriesError with pytest.raises(SeriesError): await svc.promote_post_to_series(post.id) @pytest.mark.asyncio async def test_add_post_as_chapter_slots_by_stated_page(db): svc = SeriesService(db) artist = await _artist(db, "Story Artist") sid = (await TagService(db).find_or_create("Story", TagKind.series)).id # An existing chapter stating pages 9-12. await svc.create_chapter(sid, stated_page_start=9, stated_page_end=12) # A post stating pages 1-4 should slot BEFORE the 9-12 chapter. post = await _post(db, artist, "Story pages 1-4", "pp3") await _post_images(db, post, artist, 4) out = await svc.add_post_as_chapter(sid, post.id) assert out["added"] == 4 data = await svc.list_pages(sid) chapters = data["chapters"] assert chapters[0]["stated_page_start"] == 1 # new one is first assert chapters[1]["stated_page_start"] == 9 # gap 5-8 flagged between them assert data["gaps"][0]["start"] == 5 assert data["gaps"][0]["end"] == 8 @pytest.mark.asyncio async def test_list_series_cards(db): svc = SeriesService(db) artist = await _artist(db, "Card Artist") post = await _post(db, artist, "Card Comic pages 1-2", "pp4") await _post_images(db, post, artist, 2) out = await svc.promote_post_to_series(post.id) rows = await svc.list_series() card = next(r for r in rows if r["id"] == out["series_tag_id"]) assert card["name"] == "Card Comic pages 1-2" assert card["chapter_count"] == 1 assert card["page_count"] == 2 assert card["artist_name"] == "Card Artist" assert card["cover_thumbnail_url"] assert card["has_gap"] is False # artist filter keeps it; a different artist id drops it. assert any(r["id"] == out["series_tag_id"] for r in await svc.list_series(artist_id=artist.id)) assert all(r["id"] != out["series_tag_id"] for r in await svc.list_series(artist_id=artist.id + 99999)) # --- FC-6.4: stated_part, part_gaps, source_post label --------------------- @pytest.mark.asyncio async def test_update_chapter_sets_and_clears_stated_part(db): svc = SeriesService(db) sid = (await TagService(db).find_or_create("Part Series", TagKind.series)).id ch = await svc.create_chapter(sid) # Set the installment to Part 2 (chapter_number stays its positional value). await svc.update_chapter(sid, ch["id"], stated_part=2, set_part=True) data = await svc.list_pages(sid) assert data["chapters"][0]["stated_part"] == 2 assert data["chapters"][0]["chapter_number"] == 1 # Clearing it writes NULL back. await svc.update_chapter(sid, ch["id"], stated_part=None, set_part=True) data = await svc.list_pages(sid) assert data["chapters"][0]["stated_part"] is None @pytest.mark.asyncio async def test_part_gaps_flagged_from_stated_part(db): svc = SeriesService(db) sid = (await TagService(db).find_or_create("Gappy Series", TagKind.series)).id c1 = await svc.create_chapter(sid) c3 = await svc.create_chapter(sid) await svc.update_chapter(sid, c1["id"], stated_part=1, set_part=True) await svc.update_chapter(sid, c3["id"], stated_part=3, set_part=True) data = await svc.list_pages(sid) assert len(data["part_gaps"]) == 1 gap = data["part_gaps"][0] assert gap["after_chapter_id"] == c1["id"] assert gap["start"] == 2 assert gap["end"] == 2 @pytest.mark.asyncio async def test_source_post_label_when_pages_share_one_post(db): svc = SeriesService(db) artist = await _artist(db, "Src Artist") post = await _post(db, artist, "Source Comic pages 1-2", "pp5") await _post_images(db, post, artist, 2) out = await svc.promote_post_to_series(post.id) data = await svc.list_pages(out["series_tag_id"]) sp = data["chapters"][0]["source_post"] assert sp is not None assert sp["id"] == post.id assert sp["title"] == "Source Comic pages 1-2"