import os import shutil from datetime import date, timedelta from datetime import datetime as dt from datetime import timezone as tz import pytest from sqlalchemy.ext.asyncio import AsyncSession import hircine import hircine.db as database import hircine.db.models as models import hircine.plugins from hircine.app import schema from hircine.enums import Category, Censorship, Direction, Language, Layout, Rating @pytest.fixture(scope="session") def anyio_backend(): return "asyncio" def pytest_addoption(parser): parser.addoption( "--sql-echo", action="store_true", help="Enable logging of SQL statements", ) @pytest.fixture def empty_plugins(monkeypatch): monkeypatch.setattr(hircine.plugins, "scraper_registry", {}) monkeypatch.setattr(hircine.plugins, "transformers", []) @pytest.fixture def data(tmpdir, request): file = request.module.__file__ data = os.path.join(os.path.dirname(file), "data") if os.path.isdir(data): shutil.copytree(data, tmpdir, dirs_exist_ok=True) return lambda dir: os.path.join(tmpdir, dir) @pytest.fixture(scope="session") def engine(pytestconfig): yield database.create_engine(":memory:", echo=pytestconfig.option.sql_echo) @pytest.fixture async def session(anyio_backend, engine): async with engine.begin() as conn: await conn.begin_nested() yield AsyncSession(conn, expire_on_commit=False, autoflush=False) await conn.rollback() @pytest.fixture(autouse=True) async def patch_session(anyio_backend, session, engine, monkeypatch): monkeypatch.setattr(hircine.db, "session", lambda: session) @pytest.fixture(scope="session", autouse=True) async def metadata(engine, anyio_backend): await database.initialize(engine) @pytest.fixture def schema_execute(): async def _execute(endpoint, variables=None): return await schema.execute(endpoint, variable_values=variables) return _execute @pytest.fixture def execute(schema_execute): def wrapper(q): async def _execute(): return await schema_execute(q) return _execute return wrapper @pytest.fixture def execute_add(schema_execute): def wrapper(q): async def _execute(input): return await schema_execute(q, {"input": input}) return _execute return wrapper @pytest.fixture def execute_update(schema_execute): def wrapper(q): async def _execute(ids, input): return await schema_execute(q, {"ids": ids, "input": input}) return _execute return wrapper @pytest.fixture def execute_update_single(schema_execute): def wrapper(q): async def _execute(id, input): return await schema_execute(q, {"id": id, "input": input}) return _execute return wrapper @pytest.fixture def execute_delete(schema_execute): def wrapper(q): async def _execute(ids): return await schema_execute(q, {"ids": ids}) return _execute return wrapper @pytest.fixture def execute_id(schema_execute): def wrapper(q): async def _execute(id): return await schema_execute(q, {"id": id}) return _execute return wrapper @pytest.fixture def execute_filter(schema_execute): def wrapper(q): async def _execute(filter=None): return await schema_execute(q, {"filter": filter} if filter else None) return _execute return wrapper @pytest.fixture def execute_sort(schema_execute): def wrapper(q): async def _execute(sort=None): return await schema_execute(q, {"sort": sort} if sort else None) return _execute return wrapper class DB: @staticmethod async def add(model): async with database.session() as s: s.add(model) await s.commit() return model @staticmethod async def add_all(*models): async with database.session() as s: s.add_all(models) await s.commit() return models @staticmethod async def get(modelcls, id, full=False): async with database.session() as s: options = modelcls.load_full() if full else [] model = await s.get(modelcls, id, options=options) return model @staticmethod async def delete(modelcls, id): async with database.session() as s: model = await s.get(modelcls, id) await s.delete(model) await s.commit() return class Response: def __init__(self, response, key=None): assert response.errors is None if key is None: assert response.data is not None assert len(response.data) == 1 key = next(iter(response.data.keys())) assert key in response.data self.data = response.data.get(key) self.errors = response.errors def __getattr__(self, name): assert name in self.data return self.data.get(name) def assert_is(self, typename): assert self.data["__typename"] == typename @pytest.fixture def gen_artist(): def _gen(): yield models.Artist(id=1, name="alan smithee") yield models.Artist(id=2, name="david agnew") yield models.Artist(id=3, name="robin bland") yield models.Artist(id=4, name="robin smith") return _gen() @pytest.fixture def gen_character(): def _gen(): yield models.Character(id=1, name="greta giraffe") yield models.Character(id=2, name="bob bear") yield models.Character(id=3, name="rico rhinoceros") yield models.Character(id=4, name="ziggy zebra") return _gen() @pytest.fixture def gen_circle(): def _gen(): yield models.Circle(id=1, name="archimedes") yield models.Circle(id=2, name="bankoff") yield models.Circle(id=3, name="carlyle") yield models.Circle(id=4, name="ford") return _gen() @pytest.fixture def gen_namespace(): def _gen(): yield models.Namespace(id=1, name="animal", sort_name="animal") yield models.Namespace(id=2, name="human", sort_name="human") return _gen() @pytest.fixture def gen_tag(): def _gen(): yield models.Tag( id=1, name="small", description="barely visible", namespaces=[] ) yield models.Tag( id=2, name="medium", description="mostly average", namespaces=[], ) yield models.Tag(id=3, name="big", description="impressive", namespaces=[]) yield models.Tag( id=4, name="massive", description="what is THAT", namespaces=[] ) return _gen() @pytest.fixture def gen_world(): def _gen(): yield models.World(id=1, name="animal friends") yield models.World(id=2, name="criminanimals") yield models.World(id=3, name="in the nude") yield models.World(id=4, name="wall street") return _gen() @pytest.fixture def gen_image(): def _gen(): yield models.Image( id=1, hash="1bb05614b44bf177589632a51ce216a2", width=3024, height=2106 ) yield models.Image( id=2, hash="77dfd96aee1bc8c36ab7095fcf18f7ff", width=3024, height=2094 ) yield models.Image( id=3, hash="109aac22f29bd361fbfb19f975a1b7f0", width=3019, height=2089 ) yield models.Image( id=4, hash="e18fc95f00a087ff001ecd8675eddd14", width=3024, height=2097 ) yield models.Image( id=5, hash="0e2cd2f176e792a3777710978768bc90", width=1607, height=2259 ) yield models.Image( id=6, hash="64e50730eb842750ebe5417a524b83e6", width=1556, height=2264 ) yield models.Image( id=7, hash="d906ef54788cae72e1a511c9775e6d68", width=1525, height=2259 ) yield models.Image( id=8, hash="0f8ead4a60df09a1dd071617b5d5583b", width=1545, height=2264 ) yield models.Image( id=9, hash="912ccb4350fb17ea1248e26ecfb5d983", width=1607, height=2259 ) yield models.Image( id=10, hash="108edee1b417f022a6d1f999bd32d16d", width=1546, height=2224 ) yield models.Image( id=11, hash="97c0903cb0962741174f264aaa7015d4", width=1528, height=2257 ) yield models.Image( id=12, hash="b5490ad31d2a8910087ba932073b4e52", width=1543, height=2271 ) yield models.Image( id=13, hash="c9ab7febcb81974a992ed1de60c728ba", width=1611, height=2257 ) yield models.Image( id=14, hash="bcfdf22ec17a09cd4f6a0af86e966e8f", width=1553, height=2265 ) yield models.Image( id=15, hash="1f58f4b08bf6f4ca92bd29cbce26241e", width=1526, height=2258 ) yield models.Image( id=16, hash="f87d7e55203b5e7cf9c801db48624ef0", width=1645, height=2262 ) return _gen() @pytest.fixture def gen_page(gen_image): def _gen(): yield models.Page(id=1, index=1, path="001.png", image=next(gen_image)) yield models.Page(id=2, index=2, path="002.png", image=next(gen_image)) yield models.Page(id=3, index=3, path="003.png", image=next(gen_image)) yield models.Page(id=4, index=4, path="004.png", image=next(gen_image)) yield models.Page(id=5, index=1, path="00.jpg", image=next(gen_image)) yield models.Page(id=6, index=2, path="01.jpg", image=next(gen_image)) yield models.Page(id=7, index=3, path="02.jpg", image=next(gen_image)) yield models.Page(id=8, index=4, path="03.jpg", image=next(gen_image)) yield models.Page(id=9, index=1, path="1.jpg", image=next(gen_image)) yield models.Page(id=10, index=2, path="2.jpg", image=next(gen_image)) yield models.Page(id=11, index=3, path="10.jpg", image=next(gen_image)) yield models.Page(id=12, index=4, path="11.jpg", image=next(gen_image)) yield models.Page(id=13, index=1, path="010.png", image=next(gen_image)) yield models.Page(id=14, index=2, path="011.png", image=next(gen_image)) yield models.Page(id=15, index=3, path="012.png", image=next(gen_image)) yield models.Page(id=16, index=4, path="013.png", image=next(gen_image)) return _gen() @pytest.fixture def gen_jumbled_pages(gen_image): def _gen(): yield models.Page(id=101, index=3, path="3.png", image=next(gen_image)) yield models.Page(id=52, index=9, path="9.png", image=next(gen_image)) yield models.Page(id=13, index=2, path="2.png", image=next(gen_image)) yield models.Page(id=258, index=10, path="10.png", image=next(gen_image)) yield models.Page(id=7, index=7, path="7.jpg", image=next(gen_image)) yield models.Page(id=25, index=5, path="5.jpg", image=next(gen_image)) yield models.Page(id=150, index=1, path="1.jpg", image=next(gen_image)) yield models.Page(id=69, index=4, path="4.jpg", image=next(gen_image)) yield models.Page(id=219, index=6, path="6.jpg", image=next(gen_image)) yield models.Page(id=34, index=8, path="8.jpg", image=next(gen_image)) return _gen() @pytest.fixture def gen_jumbled_archive(gen_jumbled_pages): def _gen(): pages = [next(gen_jumbled_pages) for _ in range(10)] yield models.Archive( id=100, hash="4e1243bd22c66e76c2ba9eddc1f91394", path="comics/jumbled.zip", size=32559235, mtime=dt(2002, 1, 23).astimezone(), cover=pages[0].image, pages=pages, page_count=len(pages), ) return _gen() @pytest.fixture def gen_archive(gen_page): def _gen(): pages = [next(gen_page) for _ in range(4)] yield models.Archive( id=1, hash="1d394f66c49ccb1d3c30870904d31bd4", path="comics/archive-01.zip", size=7340032, mtime=dt(2016, 5, 10).astimezone(), cover=pages[0].image, pages=pages, page_count=len(pages), ) pages = [next(gen_page) for _ in range(4)] yield models.Archive( id=2, hash="d7d8929b2e606200e863d390f71b53bb", path="comics/archive-02.zip", size=11335106, mtime=dt(2008, 10, 2, tzinfo=tz(timedelta(hours=+6))), cover=pages[0].image, pages=pages, page_count=len(pages), ) pages = [next(gen_page) for _ in range(4)] yield models.Archive( id=3, hash="02669dbe08c4a5f4820c10b3ff2178fa", path="comics/sub/archive-new.zip", size=51841969, mtime=dt(2005, 11, 17, tzinfo=tz(timedelta(hours=+2))), cover=pages[0].image, pages=pages, page_count=len(pages), ) pages = [next(gen_page) for _ in range(4)] yield models.Archive( id=4, hash="6b2ecf5ceb8befd6d0c1cd353a3df709", path="comics/archive-03.zip", size=13568769, mtime=dt(1999, 5, 8, tzinfo=tz(timedelta(hours=-2))), cover=pages[0].image, pages=pages, page_count=len(pages), ) return _gen() @pytest.fixture def gen_comic( gen_archive, gen_artist, gen_character, gen_circle, gen_world, gen_tag, gen_namespace, ): def _gen(): artists = {a.id: a for a in gen_artist} characters = {c.id: c for c in gen_character} namespaces = {ns.id: ns for ns in gen_namespace} tags = {t.id: t for t in gen_tag} def tag(nid, tid): return models.ComicTag(namespace=namespaces[nid], tag=tags[tid]) archive = next(gen_archive) yield models.Comic( id=1, title="Arid Savannah Adventures", url="file:///home/savannah/adventures", category=Category.MANGA, censorship=Censorship.NONE, date=date(2010, 7, 5), direction=Direction.LEFT_TO_RIGHT, favourite=True, language=Language.EN, layout=Layout.SINGLE, rating=Rating.SAFE, archive=archive, artists=[artists[1], artists[2]], characters=list(characters.values()), circles=[next(gen_circle)], worlds=[next(gen_world)], cover=archive.cover, pages=archive.pages, tags=[ tag(1, 1), tag(1, 2), tag(1, 3), tag(1, 4), ], ) archive = next(gen_archive) yield models.Comic( id=2, title="This Giraffe Stole My Wallet", original_title="Diese Giraffe hat mein Geldbeutel geklaut", url="ftp://crimes.local/giraffes.zip", category=Category.MANGA, censorship=Censorship.BAR, date=date(2002, 2, 17), direction=Direction.LEFT_TO_RIGHT, favourite=False, language=Language.EN, layout=Layout.SINGLE, rating=Rating.QUESTIONABLE, archive=archive, artists=[artists[3]], characters=[characters[1]], circles=[next(gen_circle)], worlds=[next(gen_world)], cover=archive.cover, pages=archive.pages, tags=[ tag(1, 3), tag(2, 1), ], ) archive = next(gen_archive) yield models.Comic( id=3, title="サイのスパ", category=Category.ARTBOOK, censorship=Censorship.MOSAIC, date=date(2017, 5, 3), direction=Direction.RIGHT_TO_LEFT, favourite=False, language=Language.JA, layout=Layout.DOUBLE_OFFSET, rating=Rating.EXPLICIT, archive=archive, artists=[artists[1], artists[4]], characters=[characters[3]], circles=[next(gen_circle)], worlds=[next(gen_world)], cover=archive.cover, pages=archive.pages, tags=[ tag(1, 4), ], ) archive = next(gen_archive) yield models.Comic( id=4, title="In the Company of Vultures", category=Category.DOUJINSHI, date=date(2023, 3, 10), direction=Direction.LEFT_TO_RIGHT, favourite=False, language=Language.EN, layout=Layout.SINGLE, rating=Rating.SAFE, archive=archive, artists=[artists[4]], characters=[characters[4]], circles=[next(gen_circle)], worlds=[next(gen_world)], cover=archive.cover, pages=archive.pages, tags=[ tag(2, 1), tag(2, 2), tag(2, 3), ], ) return _gen() @pytest.fixture def empty_comic(gen_archive): archive = next(gen_archive) yield models.Comic( id=100, title="Hic Sunt Dracones", archive=archive, cover=archive.cover, pages=archive.pages, )