From 34896029855f58148d69ae165a1ce4f99310b3ac Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sun, 4 Jan 2026 22:47:47 +0100 Subject: start working on tests for the CLI tools --- tests/cli/test_fietsctl.py | 192 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 tests/cli/test_fietsctl.py (limited to 'tests/cli/test_fietsctl.py') diff --git a/tests/cli/test_fietsctl.py b/tests/cli/test_fietsctl.py new file mode 100644 index 0000000..0571d1a --- /dev/null +++ b/tests/cli/test_fietsctl.py @@ -0,0 +1,192 @@ +import os +import re +import secrets +import subprocess +from pathlib import Path + +import pytest +from sqlalchemy import select +from sqlalchemy.orm import Session + +from fietsboek import models +from .conftest import fietsctl_results # pylint: disable=relative-beyond-top-level + + +class CliChecker: + """Result of a CLI tool invocation, with useful methods for tests.""" + + def __init__(self, result): + self.result = result + + @property + def args(self): + """Arguments to the CLI invocation.""" + return self.result.args + + @property + def returncode(self) -> int: + """Return code.""" + return self.result.returncode + + def successful(self) -> bool: + """Whether the invocation was successful (based on the return code).""" + return self.returncode == os.EX_OK + + @property + def stdout(self) -> bytes: + """Standard output.""" + return self.result.stdout or b"" + + def has_line(self, regex: str) -> bool: + """Checks whether the output has a line matching the given regex.""" + pattern = re.compile(regex) + for line in self.stdout.split(b"\n"): + ascii_line = line.decode("ascii", "ignore") + if pattern.search(ascii_line): + return True + return False + + def report(self): + """Formats the output for reporting in pytest.""" + return f"cmd: {self.args} -> {self.returncode}\n" + self.stdout.decode("ascii", "ignore") + + +@pytest.fixture +def fietsctl(app, dbengine, ini_file, data_manager): + """A fietsctl invocation helper.""" + # Ideally, we want to use the ini file. However, the ini doesn't have a data + # dir defined (as we use a temporary path for that), so we need to create a + # new one. + # Ideally², we'd create that ini file in the temporary folder, but that + # gives us the wrong sqlite database (as that uses %(here)). So we could + # either figure out the actual SQLite path and patch that as well, or "give + # up" and create the ini in the correct working dir. + # To do that, we add some random bytes to avoid collisions, and we remove + # the file at the end. + data_dir = data_manager.data_dir + entropy = secrets.token_hex(4) + config_path = Path(ini_file).with_name(f"testing.{entropy}.ini") + assert not config_path.exists(), "file collision - rerun tests!" + config = Path(ini_file).read_text(encoding="ascii") + config = config.replace("# %% fietsboek.data_dir %%", f"fietsboek.data_dir = {data_dir}") + config_path.write_text(config, encoding="ascii") + + def run_inner(args): + cmd = ["fietsctl"] + args + ["-c", str(config_path)] + result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) + checker = CliChecker(result) + fietsctl_results.append(checker) + return checker + + yield run_inner + + config_path.unlink() + + +def test_user_add(fietsctl, dbsession): + res = fietsctl( + ["user", "add", "--email", "foo@bar.com", "--name", "Remy", "--password", "hadley"] + ) + assert res.successful() + + res = fietsctl([ + "user", "add", + "--email", "bar@foo.com", + "--name", "James", + "--password", "wilson", + "--admin", + ]) + assert res.successful() + + user: models.User = ( + dbsession.execute(select(models.User).filter_by(email="foo@bar.com")).scalar_one() + ) + + assert user.name == "Remy" + user.check_password("hadley") + assert user.session_secret + assert not user.is_admin + assert user.is_verified + + user: models.User = ( + dbsession.execute(select(models.User).filter_by(email="bar@foo.com")).scalar_one() + ) + + assert user.name == "James" + user.check_password("wilson") + assert user.session_secret + assert user.is_admin + assert user.is_verified + + +def test_user_list(fietsctl, dbengine): + with Session(dbengine) as session: + user = models.User(name="Shauna", email="vayne@lge.com") + user.set_password("vayne") + user.roll_session_secret() + session.add(user) + + user = models.User(name="Berri", email="tx@ar.rak", is_admin=True, is_verified=True) + user.set_password("txarrak") + user.roll_session_secret() + session.add(user) + + session.commit() + + res = fietsctl(["user", "list"]) + assert res.successful() + assert res.has_line("vayne@lge\\.com .* Shauna") + assert res.has_line("av.*tx@ar\\.rak .* Berri") + + +def test_user_del(fietsctl, dbengine): + with Session(dbengine) as session: + user = models.User(name="Shaun", email="murphy@tg.doc", is_verified=True) + user.set_password("murphy") + user.roll_session_secret() + session.add(user) + + user = models.User(name="Aaron", email="gl@ss.man", is_verified=True) + user.set_password("glassman") + user.roll_session_secret() + session.add(user) + + session.commit() + + res = fietsctl(["user", "del", "-f", "--email", "murphy@tg.doc"]) + assert res.successful() + + with dbengine.connect() as conn: + qry = select(models.User).filter_by(email="gl@ss.man") + row = conn.execute(qry).scalar_one_or_none() + assert row + qry = select(models.User).filter_by(email="murphy@tg.doc") + row = conn.execute(qry).scalar_one_or_none() + assert row is None + + res = fietsctl(["user", "del", "-f", "--email", "gl@ss.man"]) + assert res.successful() + + with dbengine.connect() as conn: + qry = select(models.User).filter_by(email="gl@ss.man") + row = conn.execute(qry).scalar_one_or_none() + assert row is None + + +def test_user_passwd(fietsctl, dbengine): + with Session(dbengine) as session: + user = models.User(name="john", email="dori@n", is_verified=True) + user.set_password("dorian") + user.roll_session_secret() + session.add(user) + session.commit() + user_id = user.id + + res = fietsctl(["user", "passwd", "--email", "dori@n", "--password", "DORIAN"]) + assert res.successful() + + with Session(dbengine) as session: + user = session.get(models.User, user_id) + user.check_password("DORIAN") + with pytest.raises(models.user.PasswordMismatch): + user.check_password("dorian") -- cgit v1.2.3 From 735d8278e95cb1ac1237992bb93ae6187be4923a Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 8 Jan 2026 17:34:54 +0100 Subject: add test for fietsctl user modify --- tests/cli/test_fietsctl.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'tests/cli/test_fietsctl.py') diff --git a/tests/cli/test_fietsctl.py b/tests/cli/test_fietsctl.py index 0571d1a..e1994b6 100644 --- a/tests/cli/test_fietsctl.py +++ b/tests/cli/test_fietsctl.py @@ -190,3 +190,28 @@ def test_user_passwd(fietsctl, dbengine): user.check_password("DORIAN") with pytest.raises(models.user.PasswordMismatch): user.check_password("dorian") + + +def test_user_modify(fietsctl, dbengine): + with Session(dbengine) as session: + user = models.User(name="stephen", email="str@n.ge") + user.set_password("strange") + user.roll_session_secret() + session.add(user) + session.commit() + user_id = user.id + + res = fietsctl([ + "user", "modify", + "--email", "str@n.ge", + "--admin", + "--verified", + "--set-email", "not-str@n.ge", + ]) + assert res.successful() + + with Session(dbengine) as session: + user: models.User = session.get(models.User, user_id) + assert user.is_verified + assert user.is_admin + assert user.email == "not-str@n.ge" -- cgit v1.2.3 From adab3f162ddc642f5fe4fad39d0eeda543c1d2db Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Fri, 9 Jan 2026 17:15:36 +0100 Subject: have a general populate() to fill some data in Especially with all the "list" commands, we now see that we always have to put some data into the instance. This is cumbersome, especially as we get to the track commands. Therefore, I added a method to put some data into the instance -- that also helps to ensure that we properly cascade things if we delete a user, for example. Currently, this method only adds two users and two tracks, but I plan to also add comments and friendship associations. Further, we can think about re-writing the playwright tests to use this population as well, instead of the playwright_helper one. --- tests/cli/test_fietsctl.py | 87 ++++++++++++++-------------------------------- 1 file changed, 27 insertions(+), 60 deletions(-) (limited to 'tests/cli/test_fietsctl.py') diff --git a/tests/cli/test_fietsctl.py b/tests/cli/test_fietsctl.py index e1994b6..6e19f21 100644 --- a/tests/cli/test_fietsctl.py +++ b/tests/cli/test_fietsctl.py @@ -10,6 +10,7 @@ from sqlalchemy.orm import Session from fietsboek import models from .conftest import fietsctl_results # pylint: disable=relative-beyond-top-level +from ..testutils import populate # pylint: disable=relative-beyond-top-level class CliChecker: @@ -119,99 +120,65 @@ def test_user_add(fietsctl, dbsession): assert user.is_verified -def test_user_list(fietsctl, dbengine): - with Session(dbengine) as session: - user = models.User(name="Shauna", email="vayne@lge.com") - user.set_password("vayne") - user.roll_session_secret() - session.add(user) - - user = models.User(name="Berri", email="tx@ar.rak", is_admin=True, is_verified=True) - user.set_password("txarrak") - user.roll_session_secret() - session.add(user) - - session.commit() +def test_user_list(fietsctl, dbengine, data_manager): + populate(dbengine, data_manager) res = fietsctl(["user", "list"]) assert res.successful() - assert res.has_line("vayne@lge\\.com .* Shauna") - assert res.has_line("av.*tx@ar\\.rak .* Berri") + assert res.has_line("av.*jon\\.snow@nw\\.org.*Jon") + assert res.has_line("v.*davos@seaworth\\.com.*Davos") -def test_user_del(fietsctl, dbengine): - with Session(dbengine) as session: - user = models.User(name="Shaun", email="murphy@tg.doc", is_verified=True) - user.set_password("murphy") - user.roll_session_secret() - session.add(user) - - user = models.User(name="Aaron", email="gl@ss.man", is_verified=True) - user.set_password("glassman") - user.roll_session_secret() - session.add(user) +def test_user_del(fietsctl, dbengine, data_manager): + populate(dbengine, data_manager) - session.commit() - - res = fietsctl(["user", "del", "-f", "--email", "murphy@tg.doc"]) + res = fietsctl(["user", "del", "-f", "--email", "jon.snow@nw.org"]) assert res.successful() with dbengine.connect() as conn: - qry = select(models.User).filter_by(email="gl@ss.man") + qry = select(models.User).filter_by(email="davos@seaworth.com") row = conn.execute(qry).scalar_one_or_none() assert row - qry = select(models.User).filter_by(email="murphy@tg.doc") + qry = select(models.User).filter_by(email="jon.snow@nw.org") row = conn.execute(qry).scalar_one_or_none() assert row is None - res = fietsctl(["user", "del", "-f", "--email", "gl@ss.man"]) + res = fietsctl(["user", "del", "-f", "--email", "davos@seaworth.com"]) assert res.successful() with dbengine.connect() as conn: - qry = select(models.User).filter_by(email="gl@ss.man") + qry = select(models.User).filter_by(email="davos@seaworth.com") row = conn.execute(qry).scalar_one_or_none() assert row is None -def test_user_passwd(fietsctl, dbengine): - with Session(dbengine) as session: - user = models.User(name="john", email="dori@n", is_verified=True) - user.set_password("dorian") - user.roll_session_secret() - session.add(user) - session.commit() - user_id = user.id - - res = fietsctl(["user", "passwd", "--email", "dori@n", "--password", "DORIAN"]) +def test_user_passwd(fietsctl, dbengine, data_manager): + ids = populate(dbengine, data_manager) + + res = fietsctl(["user", "passwd", "--email", "jon.snow@nw.org", "--password", "Ghost"]) assert res.successful() with Session(dbengine) as session: - user = session.get(models.User, user_id) - user.check_password("DORIAN") + user = session.get(models.User, ids.jon) + user.check_password("Ghost") with pytest.raises(models.user.PasswordMismatch): - user.check_password("dorian") + user.check_password("ygritte") -def test_user_modify(fietsctl, dbengine): - with Session(dbengine) as session: - user = models.User(name="stephen", email="str@n.ge") - user.set_password("strange") - user.roll_session_secret() - session.add(user) - session.commit() - user_id = user.id +def test_user_modify(fietsctl, dbengine, data_manager): + ids = populate(dbengine, data_manager) res = fietsctl([ "user", "modify", - "--email", "str@n.ge", + "--email", "davos@seaworth.com", "--admin", - "--verified", - "--set-email", "not-str@n.ge", + "--no-verified", + "--set-email", "ser.davos@seaworth.com", ]) assert res.successful() with Session(dbengine) as session: - user: models.User = session.get(models.User, user_id) - assert user.is_verified + user: models.User = session.get(models.User, ids.davos) + assert not user.is_verified assert user.is_admin - assert user.email == "not-str@n.ge" + assert user.email == "ser.davos@seaworth.com" -- cgit v1.2.3 From 8ba285d4d1cccafcf4cb6b7c7cb5c92196d7a75e Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Fri, 9 Jan 2026 17:33:26 +0100 Subject: add tests for fietsctl track list/del --- tests/cli/test_fietsctl.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'tests/cli/test_fietsctl.py') diff --git a/tests/cli/test_fietsctl.py b/tests/cli/test_fietsctl.py index 6e19f21..afc77fe 100644 --- a/tests/cli/test_fietsctl.py +++ b/tests/cli/test_fietsctl.py @@ -182,3 +182,29 @@ def test_user_modify(fietsctl, dbengine, data_manager): assert not user.is_verified assert user.is_admin assert user.email == "ser.davos@seaworth.com" + + +def test_track_list(fietsctl, dbengine, data_manager): + populate(dbengine, data_manager) + + res = fietsctl(["track", "list"]) + assert res.successful() + assert res.has_line("Jon.*Trip around Winterfell") + assert res.has_line("Jon.*Road to Riverrun") + + +def test_track_del(fietsctl, dbengine, data_manager): + ids = populate(dbengine, data_manager) + + res = fietsctl(["track", "del", "-i", str(ids.winterfell)]) + assert not res.successful() + + res = fietsctl(["track", "del", "-i", str(ids.winterfell), "-f"]) + assert res.successful() + + with Session(dbengine) as session: + track = session.get(models.Track, ids.winterfell) + assert track is None + + with pytest.raises(FileNotFoundError): + data_manager.open(ids.winterfell) -- cgit v1.2.3 From 3ba5031a74bc0dc7aab16dc8eb349b871c012a1c Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sat, 10 Jan 2026 16:54:50 +0100 Subject: add tests for fietsctl maintenance-mode --- tests/cli/test_fietsctl.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'tests/cli/test_fietsctl.py') diff --git a/tests/cli/test_fietsctl.py b/tests/cli/test_fietsctl.py index afc77fe..c033b23 100644 --- a/tests/cli/test_fietsctl.py +++ b/tests/cli/test_fietsctl.py @@ -208,3 +208,34 @@ def test_track_del(fietsctl, dbengine, data_manager): with pytest.raises(FileNotFoundError): data_manager.open(ids.winterfell) + + +def test_maintenance_mode_get(fietsctl, data_manager): + res = fietsctl(["maintenance-mode"]) + assert res.successful() + assert res.has_line("Maintenance mode is disabled") + + (data_manager.data_dir / "MAINTENANCE").write_bytes(b"Updating") + res = fietsctl(["maintenance-mode"]) + assert res.successful() + assert res.has_line("enabled: Updating") + + +def test_maintenance_mode_set(fietsctl, data_manager): + assert data_manager.maintenance_mode() is None + + res = fietsctl(["maintenance-mode", "Discombobulating"]) + assert res.successful() + assert data_manager.maintenance_mode() == "Discombobulating" + + res = fietsctl(["maintenance-mode", "Frobnicating"]) + assert res.successful() + assert data_manager.maintenance_mode() == "Frobnicating" + + +def test_maintenance_mode_disable(fietsctl, data_manager): + (data_manager.data_dir / "MAINTENANCE").write_bytes(b"Updating") + + res = fietsctl(["maintenance-mode", "--disable"]) + assert res.successful() + assert data_manager.maintenance_mode() is None -- cgit v1.2.3