diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/bootstrap/test_new_instance.py | 90 | ||||
| -rw-r--r-- | tests/conftest.py | 4 | ||||
| -rw-r--r-- | tests/integration/test_browse.py | 118 | ||||
| -rw-r--r-- | tests/integration/test_pdf.py | 59 | ||||
| -rw-r--r-- | tests/playwright/conftest.py | 6 | ||||
| -rw-r--r-- | tests/playwright/test_transformers.py | 19 | ||||
| -rw-r--r-- | tests/unit/test_pdf.py | 58 | ||||
| -rw-r--r-- | tests/unit/test_util.py | 25 |
8 files changed, 319 insertions, 60 deletions
diff --git a/tests/bootstrap/test_new_instance.py b/tests/bootstrap/test_new_instance.py index dc3076e..05076f4 100644 --- a/tests/bootstrap/test_new_instance.py +++ b/tests/bootstrap/test_new_instance.py @@ -2,6 +2,7 @@ script, as described in the documentation. """ +import configparser import contextlib import logging import os @@ -10,6 +11,8 @@ import subprocess import venv from pathlib import Path +import sqlalchemy + LOGGER = logging.getLogger(__name__) REPO_BASE = Path(__file__).parent.parent.parent @@ -51,31 +54,66 @@ def create_config(config_name: Path): Path("data").mkdir() +def cleanup_database(config_name: Path): + """Connects to the database and ensures everything is reset. + + :param config_name: Path to the config file. + """ + if not config_name.exists(): + return + + parser = configparser.ConfigParser() + parser["DEFAULT"]["here"] = str(config_name.parent) + parser.read(config_name) + + db_url = parser["app:main"]["sqlalchemy.url"] + engine = sqlalchemy.create_engine(db_url) + + match engine.name: + case "sqlite": + pass + case "postgresql": + with engine.connect() as connection: + connection.execute(sqlalchemy.text("DROP SCHEMA public CASCADE;")) + connection.execute(sqlalchemy.text("CREATE SCHEMA public;")) + connection.commit() + + def test_setup_via_fietsupdate(tmpdir): with chdir(tmpdir): - # We create a new temporary virtual environment with a fresh install, just - # to be sure there's as little interference as possible. - LOGGER.info("Installing Fietsboek into clean env") - binaries_path = install_fietsboek(tmpdir / "venv") - - LOGGER.info("Creating a test configuration") - create_config(Path("testing.ini")) - - # Try to run the migrations - subprocess.check_call( - [binaries_path / "fietsupdate", "update", "-c", "testing.ini", "-f"] - ) - - # Also try to add an administrator - subprocess.check_call([ - binaries_path / "fietsctl", - "user", - "add", - "-c", "testing.ini", - "--email", "foobar@example.com", - "--name", "Foo Bar", - "--password", "raboof", - "--admin", - ]) - - assert True + try: + # We create a new temporary virtual environment with a fresh install, just + # to be sure there's as little interference as possible. + LOGGER.info("Installing Fietsboek into clean env") + binaries_path = install_fietsboek(tmpdir / "venv") + + LOGGER.info("Installing additional SQL engines") + subprocess.check_call( + [binaries_path / "pip", "install", "psycopg2"] + ) + + LOGGER.info("Creating a test configuration") + create_config(Path("testing.ini")) + + # Try to run the migrations + subprocess.check_call( + [binaries_path / "fietsupdate", "update", "-c", "testing.ini", "-f"] + ) + + # Also try to add an administrator + subprocess.check_call([ + binaries_path / "fietsctl", + "user", + "add", + "-c", "testing.ini", + "--email", "foobar@example.com", + "--name", "Foo Bar", + "--password", "raboof", + "--admin", + ]) + + assert True + finally: + # Clean up the database. This is important with anything but SQLite, as + # the tables would otherwise persist and interfere with the other tests. + cleanup_database(Path("testing.ini")) diff --git a/tests/conftest.py b/tests/conftest.py index cd74b0b..b49dad2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,6 +52,7 @@ def dbengine(app_settings, ini_file): yield engine + engine.dispose() Base.metadata.drop_all(bind=engine) alembic.command.stamp(alembic_cfg, None, purge=True) @@ -82,6 +83,7 @@ def _cleanup_data(app_settings): def app(app_settings, dbengine, tmp_path_factory): app_settings["fietsboek.data_dir"] = str(tmp_path_factory.mktemp("data")) logging.getLogger().setLevel(logging.DEBUG) + logging.getLogger("matplotlib").setLevel(logging.INFO) return main({}, dbengine=dbengine, **app_settings) @pytest.fixture @@ -189,7 +191,7 @@ def logged_in(testapp, route_path, dbsession, tm): tm.abort() with tm: - user = models.User(email='foo@barre.com', is_verified=True) + user = models.User(name="Feu Barre", email='foo@barre.com', is_verified=True) user.set_password("foobar") dbsession.add(user) dbsession.flush() diff --git a/tests/integration/test_browse.py b/tests/integration/test_browse.py index 875821d..1b96e2e 100644 --- a/tests/integration/test_browse.py +++ b/tests/integration/test_browse.py @@ -3,8 +3,10 @@ import zipfile from contextlib import contextmanager from datetime import datetime +from sqlalchemy import delete, inspect + from testutils import load_gpx_asset -from fietsboek import models +from fietsboek import convert, models from fietsboek.models.track import Visibility @@ -21,7 +23,10 @@ def added_tracks(tm, dbsession, owner, data_manager): # objects to the database. tm.abort() + path = convert.smart_convert(load_gpx_asset("Teasi_1.gpx.gz")).path() + tracks = [] + track_ids = [] with tm: track = models.Track( owner=owner, @@ -35,8 +40,10 @@ def added_tracks(tm, dbsession, owner, data_manager): track.date = datetime(2022, 3, 14, 9, 26, 54) dbsession.add(track) dbsession.flush() - data_manager.initialize(track.id).compress_gpx(load_gpx_asset("MyTourbook_1.gpx.gz")) + track.fast_set_path(path) + data_manager.initialize(track.id) tracks.append(track) + track_ids.append(track.id) track = models.Track( owner=owner, @@ -50,14 +57,18 @@ def added_tracks(tm, dbsession, owner, data_manager): track.date = datetime(2022, 10, 29, 13, 37, 11) dbsession.add(track) dbsession.flush() - data_manager.initialize(track.id).compress_gpx(load_gpx_asset("Teasi_1.gpx.gz")) + track.fast_set_path(path) + track.ensure_cache(path) + dbsession.add(track.cache) + data_manager.initialize(track.id) tracks.append(track) + track_ids.append(track.id) tm.begin() tm.doom() try: - yield tracks + yield track_ids finally: tm.abort() with tm: @@ -67,26 +78,113 @@ def added_tracks(tm, dbsession, owner, data_manager): tm.doom() +@contextmanager +def a_lot_of_tracks(tm, dbsession, owner, data_manager): + """Adds some tracks to the database session. + + This function should be used as a context manager and it ensures that the + added tracks are deleted again after the test, to make a clean slate for + the next test. + """ + # The normal transaction is "doomed", so we need to abort it, start a fresh + # one, and then explicitely commit it, otherwise we will not persist the + # objects to the database. + tm.abort() + + gpx_data = load_gpx_asset("MyTourbook_1.gpx.gz") + skel = convert.smart_convert(gpx_data) + path = skel.path() + + tracks = [] + track_ids = [] + with tm: + for index in range(50): + track = models.Track( + owner=owner, + title=f"Traxi {index}", + visibility=Visibility.PUBLIC, + description="One of many", + badges=[], + link_secret="foobar", + tagged_people=[], + ) + track.date = datetime(2022 - index, 3, 14, 9, 26, 59) + dbsession.add(track) + dbsession.flush() + track.fast_set_path(path) + track.ensure_cache(path) + dbsession.add(track.cache) + tracks.append(track) + track_ids.append(track.id) + data_manager.initialize(track.id) + + tm.begin() + tm.doom() + + try: + yield track_ids + finally: + tm.abort() + table = inspect(models.track.TrackPoint).tables[0] + with tm: + for track_id in track_ids: + dbsession.execute(delete(table).where(table.c.track_id == track_id)) + dbsession.execute( + delete(models.TrackCache).where(models.TrackCache.track_id == track_id) + ) + dbsession.execute(delete(models.Track).where(models.Track.id == track_id)) + tm.begin() + tm.doom() + + def test_browse(testapp, dbsession, route_path, logged_in, tm, data_manager): # pylint: disable=too-many-positional-arguments # Ensure there are some tracks in the database with added_tracks(tm, dbsession, logged_in, data_manager): # Now go to the browse page - browse = testapp.get(route_path('browse')) + browse = testapp.get(route_path("browse")) assert "Foobar" in browse.text assert "Barfoo" in browse.text +def test_browse_paged(testapp, dbsession, route_path, logged_in, tm, data_manager): + # pylint: disable=too-many-positional-arguments + with a_lot_of_tracks(tm, dbsession, logged_in, data_manager): + page_1 = testapp.get(route_path("browse", _query=[("page", 1)])) + assert "Traxi 0" in page_1.text + assert "Traxi 10" in page_1.text + assert "Traxi 20" not in page_1.text + assert "Traxi 30" not in page_1.text + assert "Traxi 40" not in page_1.text + + page_2 = testapp.get(route_path("browse", _query=[("page", 2)])) + assert "Traxi 0" not in page_2.text + assert "Traxi 10" not in page_2.text + assert "Traxi 20" in page_2.text + assert "Traxi 30" in page_2.text + assert "Traxi 40" not in page_2.text + + page_3 = testapp.get(route_path("browse", _query=[("page", 3)])) + assert "Traxi 0" not in page_3.text + assert "Traxi 10" not in page_3.text + assert "Traxi 20" not in page_3.text + assert "Traxi 30" not in page_3.text + assert "Traxi 40" in page_3.text + + def test_archive(testapp, dbsession, route_path, logged_in, tm, data_manager): # pylint: disable=too-many-positional-arguments - with added_tracks(tm, dbsession, logged_in, data_manager): + with added_tracks(tm, dbsession, logged_in, data_manager) as tracks: archive = testapp.get( - route_path('track-archive', _query=[("track_id[]", "1"), ("track_id[]", "2")]) + route_path( + "track-archive", + _query=[("track_id[]", tracks[0]), ("track_id[]", tracks[1])], + ) ) result = io.BytesIO(archive.body) - with zipfile.ZipFile(result, 'r') as zipped: + with zipfile.ZipFile(result, "r") as zipped: assert len(zipped.namelist()) == 2 - assert "track_1.gpx" in zipped.namelist() - assert "track_2.gpx" in zipped.namelist() + assert f"track_{tracks[0]}.gpx" in zipped.namelist() + assert f"track_{tracks[1]}.gpx" in zipped.namelist() diff --git a/tests/integration/test_pdf.py b/tests/integration/test_pdf.py new file mode 100644 index 0000000..29cda02 --- /dev/null +++ b/tests/integration/test_pdf.py @@ -0,0 +1,59 @@ +from contextlib import contextmanager +from datetime import datetime + +from testutils import load_gpx_asset +from fietsboek import convert, models +from fietsboek.models.track import Visibility + + +@contextmanager +def a_track(tm, dbsession, owner, data_manager): + """Adds some tracks to the database session. + + This function should be used as a context manager and it ensures that the + added tracks are deleted again after the test, to make a clean slate for + the next test. + """ + # The normal transaction is "doomed", so we need to abort it, start a fresh + # one, and then explicitely commit it, otherwise we will not persist the + # objects to the database. + tm.abort() + + with tm: + track = models.Track( + owner=owner, + title="Goober", + visibility=Visibility.PUBLIC, + description="A bar'd track", + badges=[], + link_secret="raboof", + tagged_people=[], + ) + track.date = datetime(2027, 3, 14, 9, 26, 54) + track.set_path(convert.smart_convert(load_gpx_asset("MyTourbook_1.gpx.gz")).path()) + dbsession.add(track) + dbsession.flush() + data_manager.initialize(track.id) + track_id = track.id + + tm.begin() + tm.doom() + + try: + yield track_id + finally: + tm.abort() + with tm: + dbsession.delete(track) + data_manager.purge(track_id) + tm.begin() + tm.doom() + + +def test_pdf(testapp, dbsession, route_path, logged_in, tm, data_manager): + # pylint: disable=too-many-positional-arguments + # Ensure there are some tracks in the database + with a_track(tm, dbsession, logged_in, data_manager) as track_id: + pdf = testapp.get(route_path("track-pdf", track_id=track_id)) + + assert pdf diff --git a/tests/playwright/conftest.py b/tests/playwright/conftest.py index e914b01..adf5ef3 100644 --- a/tests/playwright/conftest.py +++ b/tests/playwright/conftest.py @@ -57,7 +57,11 @@ def dbaccess(app): through and the running WSGI app cannot read them. """ session_factory = app.registry["dbsession_factory"] - return session_factory() + factory = session_factory() + + yield factory + + factory.close() class Helper: diff --git a/tests/playwright/test_transformers.py b/tests/playwright/test_transformers.py index fc89afb..d4e3456 100644 --- a/tests/playwright/test_transformers.py +++ b/tests/playwright/test_transformers.py @@ -26,7 +26,7 @@ def test_transformer_zero_elevation_disabled(page: Page, playwright_helper, tmp_ # Expect early (here and in the other tests) to ensure that the backend has # caught up with executing the transformer. Otherwise it might happen that # we read the database while the request is not finished yet. - expect(page.locator("#detailsUphill")).to_contain_text("167.7 m") + expect(page.locator("#detailsUphill")).to_contain_text("167.79 m") new_track_id = int(page.url.rsplit("/", 1)[1]) track = dbaccess.execute(select(models.Track).filter_by(id=new_track_id)).scalar_one() @@ -90,7 +90,7 @@ def test_transformer_steep_slope_disabled(page: Page, playwright_helper, tmp_pat page.locator(".btn", has_text="Upload").click() - expect(page.locator("#detailsUphill")).to_contain_text("61.54 m") + expect(page.locator("#detailsUphill")).to_contain_text("64.4 m") new_track_id = int(page.url.rsplit("/", 1)[1]) track = dbaccess.execute(select(models.Track).filter_by(id=new_track_id)).scalar_one() @@ -111,11 +111,11 @@ def test_transformer_steep_slope_enabled(page: Page, playwright_helper, tmp_path page.locator(".btn", has_text="Upload").click() - expect(page.locator("#detailsUphill")).to_contain_text("1.2 m") + expect(page.locator("#detailsUphill")).to_contain_text("2.4 m") new_track_id = int(page.url.rsplit("/", 1)[1]) track = dbaccess.execute(select(models.Track).filter_by(id=new_track_id)).scalar_one() - assert track.cache.uphill < 2 + assert track.cache.uphill < 3 def test_transformer_steep_slope_edited(page: Page, playwright_helper, tmp_path, dbaccess): @@ -137,14 +137,14 @@ def test_transformer_steep_slope_edited(page: Page, playwright_helper, tmp_path, page.locator(".btn", has_text="Save").click() - expect(page.locator("#detailsUphill")).to_contain_text("1.2 m") + expect(page.locator("#detailsUphill")).to_contain_text("2.4 m") track_id = int(page.url.rsplit("/", 1)[1]) track = dbaccess.execute(select(models.Track).filter_by(id=track_id)).scalar_one() - assert track.cache.uphill < 2 + assert track.cache.uphill < 3 -def test_transformer_elevation_jump_enabled(page: Page, playwright_helper, tmp_path, data_manager): +def test_transformer_elevation_jump_enabled(page: Page, playwright_helper, tmp_path, dbaccess): playwright_helper.login() page.goto("/") @@ -161,9 +161,10 @@ def test_transformer_elevation_jump_enabled(page: Page, playwright_helper, tmp_p page.locator(".alert", has_text="Upload successful").wait_for() new_track_id = int(page.url.rsplit("/", 1)[1]) - data = data_manager.open(new_track_id) - gpx = gpxpy.parse(data.decompress_gpx()) + gpx = gpxpy.parse( + dbaccess.execute(select(models.Track).filter_by(id=new_track_id)).scalar_one().gpx_xml() + ) points = iter(gpx.walk(only_points=True)) next(points) for prev_point, point in zip(gpx.walk(only_points=True), points): diff --git a/tests/unit/test_pdf.py b/tests/unit/test_pdf.py new file mode 100644 index 0000000..aafa717 --- /dev/null +++ b/tests/unit/test_pdf.py @@ -0,0 +1,58 @@ +import pytest + +from fietsboek import pdf + + +@pytest.mark.parametrize("value, expected", [ + ('', '""'), + ('a', '"\\u{61}"'), + ('FOO', '"\\u{46}\\u{4f}\\u{4f}"'), + ('äß', '"\\u{e4}\\u{df}"'), + ('"', '"\\u{22}"'), + ("'", '"\\u{27}"'), +]) +def test_typst_string(value, expected): + assert pdf.typst_string(value) == expected + + +@pytest.mark.parametrize("value, expected", [ + ("foo", "foo"), + ("*foo*", "\\*foo\\*"), + ("#strong[foo]", "\\#strong\\[foo\\]"), + ("= foo", "\\= foo"), + ("par 1\n\npar 2", "par 1\n\npar 2"), +]) +def test_typst_escape(value, expected): + assert pdf.typst_escape(value) == expected + + +@pytest.mark.parametrize("md_source, typst_source", [ + ("*foo*", "#emph[foo]\n\n"), + ("**foo**", "#strong[foo]\n\n"), + ("***foo***", "#strong[#emph[foo]]\n\n"), + ("[Teksd](https://link)", + '#link("\\u{68}\\u{74}\\u{74}\\u{70}\\u{73}\\u{3a}' + '\\u{2f}\\u{2f}\\u{6c}\\u{69}\\u{6e}\\u{6b}")[Teksd]\n\n'), + ("""\ +# Uperschrift + +Teksd""", """\ +#heading(level: 1)[Uperschrift] +Teksd\n\n"""), + ("""\ +* Eitem 1 +* Eitem 2""", """\ +#list( +[Eitem 1], +[Eitem 2], +)"""), + ("""\ +1. Eitem 1 +1. Eitem 2""", """\ +#enum( +[Eitem 1], +[Eitem 2], +)"""), +]) +def test_md_to_typst(md_source, typst_source): + assert pdf.md_to_typst(md_source) == typst_source diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index 6dc8e7d..cc92058 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -71,19 +71,6 @@ def test_guess_gpx_timezone(gpx_file, offset): assert timezone.utcoffset(None) == offset -@pytest.mark.parametrize('gpx_file', [ - 'Teasi_1.gpx.gz', - 'MyTourbook_1.gpx.gz', - 'Synthetic_WT2.gpx.gz', - 'Synthetic_BRouter_1.gpx.gz', -]) -def test_tour_metadata(gpx_file): - # Here we simply make sure that we do not crash the metadata extraction - # function. - gpx_data = load_gpx_asset(gpx_file) - assert util.tour_metadata(gpx_data) is not None - - @pytest.mark.parametrize('mps, kph', [(1, 3.6), (10, 36)]) def test_mps_to_kph(mps, kph): assert util.mps_to_kph(mps) == pytest.approx(kph, 0.1) @@ -112,3 +99,15 @@ def test_tile_url(app_request): assert "{y}" in route_url assert "{z}" in route_url assert "bobby" in route_url + + +@pytest.mark.parametrize("value, expected", [ + ("", b""), + ("foo", b"foo"), + ("<foo>", b"<foo>"), + ("foo bar", b"foo bar"), + ("</gpx>", b"</gpx>"), + ("äÖß", b"äÖß"), +]) +def test_xml_escape(value, expected): + assert util.xml_escape(value) == expected |
