Commits: 8

Use dependency injection for DB connection

This is a refactoring that touches all endpoints, and a bit of initialization code.

It enables the user registration test to pass by isolating the test case. Technically the get_db_path dependency provider is being overridden with a unique path for each test run. That way inserting a user doesn't conflict with previous runs, as they are inserted into different databases.

index 0e41417..963af57 100644
--- a/main.py
+++ b/main.py
@@ -19,8 +19,6 @@ from passlib.context import CryptContext
=from pydantic import BaseModel, Field, field_validator
=
=
-DB_PATH = Path(os.environ.get("jaas_db_path") or "./jokes.db").expanduser().resolve()
-
=JWT_SECRET_KEY = os.environ.get("jaas_jwt_secret_key")
=JWT_ALGORITHM = "HS256"
=JWT_TTL_MINUTES = 30
@@ -35,20 +33,52 @@ security = OAuth2PasswordBearer(tokenUrl="token")
=password_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
=
=
+def get_db_path() -> Path:
+    """A dependable providing a path to the SQLite .db file
+
+    Can be overridden in tests to isolate different cases.
+    """
+    return Path(os.environ.get("jaas_db_path") or "./jokes.db").expanduser().resolve()
+
+
+DBPath = Annotated[Path, Depends(get_db_path)]
+
+
+def get_database(path: DBPath) -> sqlite3.Connection:
+    """A dependable providing a configured DB connection"""
+    with sqlite3.connect(path) as db:
+        db.row_factory = sqlite3.Row
+        db.execute("Pragma foreign_keys = on")
+        yield db
+
+
+Database = Annotated[sqlite3.Connection, Depends(get_database)]
+
+
=async def app_lifespan(app: FastAPI):
-    logger.info(f"Using the database at {DB_PATH}")
-    if not os.path.exists(DB_PATH):
+    # Below is a homegrown dependency injection
+    #
+    # We need this to initialize a mock database during tests, but the standard
+    # FastAPI dependency injection apparently doesn't work in lifecycle hook.
+    # This solution seems a bit smelly. E.g. transient dependencies would not be
+    # taken care of. Is there a more elegant way to inject dependencies into the
+    # lifespan function?
+    _get_db_path = app.dependency_overrides.get(get_db_path) or get_db_path
+    db_path = _get_db_path()
+
+    logger.info(f"Using the database at {db_path}")
+    if not os.path.exists(db_path):
=        init_sql_path = Path(__file__).joinpath("../init.sql").expanduser().resolve()
=        logger.info(
-            f"Database does not exist at {DB_PATH}. Initializing with {init_sql_path}."
+            f"Database does not exist at {db_path}. Initializing with {init_sql_path}."
=        )
=        init_sql = init_sql_path.read_text(encoding="utf-8")
=        try:
-            with sqlite3.connect(DB_PATH) as db:
+            with sqlite3.connect(db_path) as db:
=                db.executescript(init_sql)
=        except Exception as exception:
=            logger.warn("Initializing database failed. Rolling back")
-            os.remove(DB_PATH)
+            os.remove(db_path)
=            raise exception
=
=    yield
@@ -79,7 +109,12 @@ class Token(BaseModel):
=    token_type: str = "bearer"
=
=
-async def get_current_user(token: Annotated[str, Depends(security)]):
+# TODO: Use type aliases for nicer dependency injection
+#
+#       See <https://fastapi.tiangolo.com/tutorial/dependencies/#share-annotated-dependencies>
+
+
+async def get_current_user(token: Annotated[str, Depends(security)], db: Database):
=    """Provide the current user if logged in, otherwise err.
=
=    It's a dependency that can be used on endpoints that require
@@ -89,70 +124,65 @@ async def get_current_user(token: Annotated[str, Depends(security)]):
=    token_data = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])
=    username = token_data.get("sub")
=
-    with sqlite3.connect(DB_PATH) as db:
-        db.row_factory = sqlite3.Row
-        cursor = db.cursor()
-        joker = cursor.execute(
-            """
-            Select
-                joker.id,
-                joker.name
-            from
-                joker
-            where
-                joker.name = :username
-            ;
-            """,
-            {"username": username},
-        ).fetchone()
+    cursor = db.cursor()
+    joker = cursor.execute(
+        """
+        Select
+            joker.id,
+            joker.name
+        from
+            joker
+        where
+            joker.name = :username
+        ;
+        """,
+        {"username": username},
+    ).fetchone()
=
-        if joker is None:
-            raise HTTPException(
-                status_code=400,
-                detail="Invalid joker",
-                headers={"WWW-Authenticate": "Bearer"},
-            )
+    if joker is None:
+        raise HTTPException(
+            status_code=400,
+            detail="Invalid joker",
+            headers={"WWW-Authenticate": "Bearer"},
+        )
=
-        return Joker(**joker)
+    return Joker(**joker)
=
=
=@app.post("/token")
-async def authenticate(form: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token:
+async def authenticate(
+    form: Annotated[OAuth2PasswordRequestForm, Depends()], db: Database
+) -> Token:
=    """Get a token based on username and password."""
-    with sqlite3.connect(DB_PATH) as db:
-        db.row_factory = sqlite3.Row
-        cursor = db.cursor()
-        joker = cursor.execute(
-            """
-            Select
-                joker.id,
-                joker.name,
-                joker.password
-            from
-                joker
-            where
-                joker.name = :username
-            ;
-            """,
-            {"username": form.username},
-        ).fetchone()
+    cursor = db.cursor()
+    joker = cursor.execute(
+        """
+        Select
+            joker.id,
+            joker.name,
+            joker.password
+        from
+            joker
+        where
+            joker.name = :username
+        ;
+        """,
+        {"username": form.username},
+    ).fetchone()
=
-        if joker is None:
-            raise HTTPException(
-                status_code=400, detail="Incorrect username or password"
-            )
+    if joker is None:
+        raise HTTPException(status_code=400, detail="Incorrect username or password")
=
-        if not password_context.verify(form.password, joker["password"]):
-            raise HTTPException(
-                status_code=400, detail="Incorrect username or password"
-            )
+    if not password_context.verify(form.password, joker["password"]):
+        raise HTTPException(status_code=400, detail="Incorrect username or password")
=
-        ttl = timedelta(minutes=JWT_TTL_MINUTES)
-        exp = datetime.now(timezone.utc) + ttl
-        encoded = jwt.encode(
-            {"exp": exp, "sub": joker["name"]}, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM
-        )
-        return Token(access_token=encoded)
+    # TODO: Separate the pure part below and write some unit tests
+    ttl = timedelta(minutes=JWT_TTL_MINUTES)
+    exp = datetime.now(timezone.utc) + ttl
+    encoded = jwt.encode(
+        {"exp": exp, "sub": joker["name"]}, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM
+    )
+    return Token(access_token=encoded)
=
=
=# TODO: Validation: password and name length, not blank, etc.
@@ -168,36 +198,34 @@ class JokerRegistration(BaseModel):
=
=
=@app.post("/jokers/", status_code=201)
-def register_joker(registration: JokerRegistration) -> Joker:
+def register_joker(registration: JokerRegistration, db: Database) -> Joker:
=    password = password_context.hash(registration.password)
-    with sqlite3.connect(DB_PATH) as db:
-        db.row_factory = sqlite3.Row
-        cursor = db.cursor()
-        try:
-            joker = cursor.execute(
-                """
-                Insert into joker (
-                    name,
-                    password
-                ) values (
-                    :name,
-                    :password
-                )
-                returning id, name
-                ;
-                """,
-                {"name": registration.name, "password": password},
-            ).fetchone()
-
-            return Joker(**joker)
-
-        except sqlite3.IntegrityError as error:
-            if error.args[0] == "UNIQUE constraint failed: joker.name":
-                raise HTTPException(
-                    status_code=409, detail=f"Name already taken: {registration.name}"
-                )
-            else:
-                raise error
+    cursor = db.cursor()
+    try:
+        joker = cursor.execute(
+            """
+            Insert into joker (
+                name,
+                password
+            ) values (
+                :name,
+                :password
+            )
+            returning id, name
+            ;
+            """,
+            {"name": registration.name, "password": password},
+        ).fetchone()
+
+        return Joker(**joker)
+
+    except sqlite3.IntegrityError as error:
+        if error.args[0] == "UNIQUE constraint failed: joker.name":
+            raise HTTPException(
+                status_code=409, detail=f"Name already taken: {registration.name}"
+            )
+        else:
+            raise error
=
=
=@app.get("/me")
@@ -207,41 +235,37 @@ def get_current_joker(user: Annotated[Joker, Depends(get_current_user)]) -> Joke
=
=
=@app.get("/me/laughs")
-def get_laughs(user: Annotated[Joker, Depends(get_current_user)]) -> list[Joke]:
+def get_laughs(
+    user: Annotated[Joker, Depends(get_current_user)], db: Database
+) -> list[Joke]:
=    """List of jokes user laughed at."""
-    """List all jokes."""
-    with sqlite3.connect(DB_PATH) as db:
-        db.row_factory = sqlite3.Row
-        db.execute("Pragma foreign_keys = on")
-        cursor = db.cursor()
-        cursor.execute(
-            """
-            Select
-                joke.id,
-                joke.text,
-                joker.id as author_id,
-                joker.name as author_name,
-                (select count(*) from laugh where laugh.joke = joke.id) as laughs
-            from
-                joke
-                join joker on joke.author = joker.id
-                join laugh on joke.id = laugh.joke
-            where
-                laugh.joker = :joker
-            ;
-            """,
-            {"joker": user.id},
-        )
-        return [Joke(**row) for row in cursor.fetchall()]
+    cursor = db.cursor()
+    cursor.execute(
+        """
+        Select
+            joke.id,
+            joke.text,
+            joker.id as author_id,
+            joker.name as author_name,
+            (select count(*) from laugh where laugh.joke = joke.id) as laughs
+        from
+            joke
+            join joker on joke.author = joker.id
+            join laugh on joke.id = laugh.joke
+        where
+            laugh.joker = :joker
+        ;
+        """,
+        {"joker": user.id},
+    )
+    return [Joke(**row) for row in cursor.fetchall()]
=
=
=@app.get("/")
-def get_random_joke() -> Joke:
+def get_random_joke(db: Database) -> Joke:
=    """Get a single random joke."""
-    with sqlite3.connect(DB_PATH) as db:
-        db.row_factory = sqlite3.Row
-        cursor = db.cursor()
-        cursor.execute("""
+    cursor = db.cursor()
+    cursor.execute("""
=        Select
=            joke.id,
=            joke.text,
@@ -255,12 +279,14 @@ def get_random_joke() -> Joke:
=            random()
=        limit 1
=        ;
-        """)
-        return Joke(**cursor.fetchone())
+    """)
+    return Joke(**cursor.fetchone())
=
=
=@app.get("/jokes")
-def list_jokes(sort: str = "laughs", descending: bool = True) -> list[Joke]:
+def list_jokes(
+    db: Database, sort: str = "laughs", descending: bool = True
+) -> list[Joke]:
=    """List all jokes."""
=    logger.info(f"{sort} {descending}")
=
@@ -271,119 +297,112 @@ def list_jokes(sort: str = "laughs", descending: bool = True) -> list[Joke]:
=            detail=f"Invalid sort parameter: '{sort}'. Acceptable values are {sortable_columns}",
=        )
=
-    with sqlite3.connect(DB_PATH) as db:
-        db.row_factory = sqlite3.Row
-        db.execute("Pragma foreign_keys = on")
-        cursor = db.cursor()
-        direction = "desc" if descending else "asc"
-        cursor.execute(
-            f"""
-            Select
-                joke.id,
-                joke.text,
-                joker.id as author_id,
-                joker.name as author_name,
-                (select count(*) from laugh where laugh.joke = joke.id) as laughs
-            from
-                joke
-                join joker on joke.author = joker.id
-            order by {sort} collate nocase {direction}
-            ;
-            """
-        )
-        return [Joke(**row) for row in cursor.fetchall()]
+    cursor = db.cursor()
+    direction = "desc" if descending else "asc"
+    cursor.execute(
+        f"""
+        Select
+            joke.id,
+            joke.text,
+            joker.id as author_id,
+            joker.name as author_name,
+            (select count(*) from laugh where laugh.joke = joke.id) as laughs
+        from
+            joke
+            join joker on joke.author = joker.id
+        order by {sort} collate nocase {direction}
+        ;
+        """
+    )
+    return [Joke(**row) for row in cursor.fetchall()]
=
=
=@app.get("/jokes/{id}")
-def get_joke(id: int) -> Joke:
+def get_joke(db: Database, id: int) -> Joke:
=    """Get a single joke."""
-    with sqlite3.connect(DB_PATH) as db:
-        db.row_factory = sqlite3.Row
-        cursor = db.cursor()
-        joke = cursor.execute(
-            """
-            Select
-                joke.id,
-                joke.text,
-                joker.id as author_id,
-                joker.name as author_name,
-                (select count(*) from laugh where laugh.joke = joke.id) as laughs
-            from
-                joke
-                join joker on joke.author = joker.id
-            where
-                joke.id = :id
-            ;
-            """,
-            {"id": id},
-        ).fetchone()
+    cursor = db.cursor()
+    joke = cursor.execute(
+        """
+        Select
+            joke.id,
+            joke.text,
+            joker.id as author_id,
+            joker.name as author_name,
+            (select count(*) from laugh where laugh.joke = joke.id) as laughs
+        from
+            joke
+            join joker on joke.author = joker.id
+        where
+            joke.id = :id
+        ;
+        """,
+        {"id": id},
+    ).fetchone()
=
-        if joke is None:
-            raise HTTPException(status_code=404)
+    if joke is None:
+        raise HTTPException(status_code=404)
=
-        return Joke(**joke)
+    return Joke(**joke)
=
=
=@app.put("/jokes/{id}/laugh", status_code=201)
-def laugh(id: int, joker: Annotated[Joker, Depends(get_current_user)]) -> Response:
+def laugh(
+    db: Database, id: int, joker: Annotated[Joker, Depends(get_current_user)]
+) -> Response:
=    """Add a laugh to a funny joke."""
-    with sqlite3.connect(DB_PATH) as db:
-        db.row_factory = sqlite3.Row
-        db.execute("Pragma foreign_keys = on")
-        cursor = db.cursor()
+    cursor = db.cursor()
=
-        try:
-            cursor.execute(
-                """
-                Insert into laugh (
-                    joke,
-                    joker
-                ) values (
-                    :joke,
-                    :joker
-                )
-                ;
-                """,
-                {"joke": id, "joker": joker.id},
+    try:
+        cursor.execute(
+            """
+            Insert into laugh (
+                joke,
+                joker
+            ) values (
+                :joke,
+                :joker
=            )
+            ;
+            """,
+            {"joke": id, "joker": joker.id},
+        )
=
-            return Response(status_code=201)
+        return Response(status_code=201)
=
-        except sqlite3.IntegrityError as error:
-            if error.args[0] == "UNIQUE constraint failed: laugh.joke, laugh.joker":
-                raise HTTPException(
-                    status_code=200, detail="You've already laughed at this joke"
-                )
-            elif error.args[0] == "FOREIGN KEY constraint failed":
-                raise HTTPException(
-                    status_code=404,
-                    detail="Probably no such joke. Or maybe you don't exist. Who can tell?",
-                )
-            else:
-                raise error
+    except sqlite3.IntegrityError as error:
+        if error.args[0] == "UNIQUE constraint failed: laugh.joke, laugh.joker":
+            raise HTTPException(
+                status_code=200, detail="You've already laughed at this joke"
+            )
+        elif error.args[0] == "FOREIGN KEY constraint failed":
+            raise HTTPException(
+                status_code=404,
+                detail="Probably no such joke. Or maybe you don't exist. Who can tell?",
+            )
+        else:
+            raise error
=
=
=@app.delete("/jokes/{id}/laugh", status_code=204)
-def unlaugh(id: int, joker: Annotated[Joker, Depends(get_current_user)]) -> Response:
+def unlaugh(
+    db: Database, id: int, joker: Annotated[Joker, Depends(get_current_user)]
+) -> Response:
=    """Let it be known that the joke is not funny anymore."""
-    with sqlite3.connect(DB_PATH) as db:
-        db.row_factory = sqlite3.Row
-        db.execute("Pragma foreign_keys = on")
-        cursor = db.cursor()
-
-        # TODO: Handle a 404 type of situation
-        cursor.execute(
-            """
-            Delete from laugh
-            where
-                joke = :joke and
-                joker = :joker
-            ;
-            """,
-            {"joke": id, "joker": joker.id},
-        )
+    cursor = db.cursor()
+
+    # TODO: Handle a 404 type of situation
+    cursor.execute(
+        """
+        Delete from laugh
+        where
+            joke = :joke and
+            joker = :joker
+        ;
+        """,
+        {"joke": id, "joker": joker.id},
+    )
=
-        return Response(status_code=204)
+    return Response(status_code=204)
=
=
=class JokeSubmission(BaseModel):
@@ -399,26 +418,26 @@ class JokeSubmission(BaseModel):
=
=@app.post("/jokes", status_code=201)
=def new_joke(
-    joke: JokeSubmission, joker: Annotated[Joker, Depends(get_current_user)]
+    db: Database,
+    joke: JokeSubmission,
+    joker: Annotated[Joker, Depends(get_current_user)],
=) -> Response:
=    """Write a new joke."""
-    with sqlite3.connect(DB_PATH) as db:
-        db.row_factory = sqlite3.Row
-        cursor = db.cursor()
-        row = cursor.execute(
-            """
-            Insert into joke (
-                text,
-                author
-            )
-            values (:text, :author)
-            returning id
-            ;
-            """,
-            {"text": joke.text, "author": joker.id},
-        ).fetchone()
+    cursor = db.cursor()
+    row = cursor.execute(
+        """
+        Insert into joke (
+            text,
+            author
+        )
+        values (:text, :author)
+        returning id
+        ;
+        """,
+        {"text": joke.text, "author": joker.id},
+    ).fetchone()
=
-        return Response(headers={"location": f"/jokes/{row['id']}"}, status_code=201)
+    return Response(headers={"location": f"/jokes/{row['id']}"}, status_code=201)
=
=
=if __name__ == "__main__":
index 42a9715..67884b7 100644
--- a/test_rest.py
+++ b/test_rest.py
@@ -3,15 +3,21 @@ import main
=from fastapi.testclient import TestClient
=
=
-# TODO: Use dependency injection for DB connections so it can be mocked during tests
-#
-#       Currently the database file is created in the CWD, and the tests are not
-#       isolated. A unique temporary file should be created for every test.
+@pytest.fixture
+def client(tmp_path):
+    """Setup a test client with a unique DB"""
+    db_path = tmp_path / "test-jokes.db"
=
-client = TestClient(main.app)
+    def get_mock_db_path():
+        return db_path
=
+    main.app.dependency_overrides[main.get_db_path] = get_mock_db_path
=
-def test_get_root():
+    with TestClient(main.app) as client:
+        yield client
+
+
+def test_get_root(client):
=    response = client.get("/")
=    assert response.status_code == 200
=    body = response.json()
@@ -22,7 +28,7 @@ def test_get_root():
=    assert "laughs" in body
=
=
-def test_post_jokers():
+def test_post_jokers(client):
=    response = client.post(
=        "/jokers/", json={"name": "Bob", "password": "Bob is funny!"}
=    )

Write a test for authentication

It depends on a new fixture that registers the joker before they can authenticate. This fixture modifies the test client database. It relies on the fact that PyTest fixtures re-use results from other fixtures, i.e. the client fixture in the test will be the same as the client fixture in registered_joker fixture. See https://docs.pytest.org/en/stable/how-to/fixtures.html#fixtures-can-be-requested-more-than-once-per-test-return-values-are-cached. It's a really nice feature!

This test doesn't currently pass. It uncovered a bug in the POST /token/ endpoint, that is unnecessarily asynchronous and thus can't use SQLite connection injected into it from another thread.

index 67884b7..6362019 100644
--- a/test_rest.py
+++ b/test_rest.py
@@ -17,6 +17,18 @@ def client(tmp_path):
=        yield client
=
=
+@pytest.fixture
+def registered_joker(client, request):
+    """Registers the joker with a client and provides it's record"""
+
+    name = request.node.get_closest_marker("register_name").args[0]
+    password = request.node.get_closest_marker("register_password").args[0]
+
+    response = client.post("/jokers/", json={"name": name, "password": password})
+    assert response.status_code == 201, f"Failed to register user: {response.json()}"
+    yield response.json()
+
+
=def test_get_root(client):
=    response = client.get("/")
=    assert response.status_code == 200
@@ -28,7 +40,7 @@ def test_get_root(client):
=    assert "laughs" in body
=
=
-def test_post_jokers(client):
+def test_post_jokers(client, request):
=    response = client.post(
=        "/jokers/", json={"name": "Bob", "password": "Bob is funny!"}
=    )
@@ -39,3 +51,16 @@ def test_post_jokers(client):
=
=
=# TODO: Test authentication. Use a fixture to register new users before authenticating.
+@pytest.mark.register_name("Alice")
+@pytest.mark.register_password("Buhahaha!")
+def test_post_token(client, registered_joker):
+    name = registered_joker["name"]
+    id = registered_joker["id"]
+
+    response = client.post(
+        "/token", data={"username": "Alice", "password": "Buhahaha!"}
+    )
+
+    assert response.status_code == 200
+    assert "access_token" in response.json()
+    assert response.json().get("token_type") == "bearer"

Fix authentication (POST /token/)

The authentication was implemented as an async function (for no good reason), and it was preventing it from using SQLite connection injected.

index 963af57..afc6850 100644
--- a/main.py
+++ b/main.py
@@ -150,7 +150,7 @@ async def get_current_user(token: Annotated[str, Depends(security)], db: Databas
=
=
=@app.post("/token")
-async def authenticate(
+def authenticate(
=    form: Annotated[OAuth2PasswordRequestForm, Depends()], db: Database
=) -> Token:
=    """Get a token based on username and password."""

Fix a warning about lifespan

It needs to be decorated with contextlib.asynccontextmanager for some reason.

index afc6850..d85f87c 100644
--- a/main.py
+++ b/main.py
@@ -10,6 +10,7 @@ import sqlite3
=from datetime import datetime, timedelta, timezone
=from pathlib import Path
=from typing import Optional, Annotated
+from contextlib import asynccontextmanager
=
=import jwt
=import uvicorn
@@ -55,6 +56,7 @@ def get_database(path: DBPath) -> sqlite3.Connection:
=Database = Annotated[sqlite3.Connection, Depends(get_database)]
=
=
+@asynccontextmanager
=async def app_lifespan(app: FastAPI):
=    # Below is a homegrown dependency injection
=    #

Fix warnings about unused markers

I merged name and password markers into a single credentials dict. It's more elegant that way. Then to fix the warning, this new marker is registered in pytest.ini file.

new file mode 100644
index 0000000..0906a51
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+markers =
+    credentials: used for registering or authenticating a user
index 6362019..0abe3dc 100644
--- a/test_rest.py
+++ b/test_rest.py
@@ -21,10 +21,11 @@ def client(tmp_path):
=def registered_joker(client, request):
=    """Registers the joker with a client and provides it's record"""
=
-    name = request.node.get_closest_marker("register_name").args[0]
-    password = request.node.get_closest_marker("register_password").args[0]
+    credentials = request.node.get_closest_marker("credentials").kwargs
+    assert "name" in credentials
+    assert "password" in credentials
=
-    response = client.post("/jokers/", json={"name": name, "password": password})
+    response = client.post("/jokers/", json=credentials)
=    assert response.status_code == 201, f"Failed to register user: {response.json()}"
=    yield response.json()
=
@@ -51,8 +52,7 @@ def test_post_jokers(client, request):
=
=
=# TODO: Test authentication. Use a fixture to register new users before authenticating.
-@pytest.mark.register_name("Alice")
-@pytest.mark.register_password("Buhahaha!")
+@pytest.mark.credentials(name="Alice", password="Buhahaha!")
=def test_post_token(client, registered_joker):
=    name = registered_joker["name"]
=    id = registered_joker["id"]

Write a comment about dependencies from flake.nix

Initially (and eventually in the future) I want this project ot be compatible with standard, modern Python tooling (which I guess at this time is uv), but being a Nix user and running out of time to deploy it, I opted to manage Python dependencies using Nix. In the future I'd like to make it work both as a Nix flake and as a standalone Python project.

For context: https://discourse.nixos.org/t/uv2nix-build-develop-python-projects-using-uv-with-nix/58563/11?u=tad-lispy

index 6793d0a..e8bdd96 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,6 +5,9 @@ description = "Joke as a Service"
=readme = "README.md"
=license = "GPL-3.0-only"
=requires-python = ">=3.13"
+# FIXME: The dependencies are currently managed with flake.nix
+#
+#        The section below is redundant and most likely outdated
=dependencies = [
=    "fastapi[standard] (>=0.116.1,<0.117.0)",
=    "pyjwt (>=2.10.1,<3.0.0)",

Remove unused variables from tests

index 4236bd1..ec54422 100644
--- a/test_joker_registration_validation.py
+++ b/test_joker_registration_validation.py
@@ -44,7 +44,5 @@ def test_blank_password_registration(input, good):
=    if good:
=        JokerRegistration(**payload)
=    else:
-        with pytest.raises(
-            ValidationError, match="A password cannot be blank"
-        ) as exception_info:
+        with pytest.raises(ValidationError, match="A password cannot be blank"):
=            JokerRegistration(**payload)
index 0abe3dc..e643b03 100644
--- a/test_rest.py
+++ b/test_rest.py
@@ -54,11 +54,8 @@ def test_post_jokers(client, request):
=# TODO: Test authentication. Use a fixture to register new users before authenticating.
=@pytest.mark.credentials(name="Alice", password="Buhahaha!")
=def test_post_token(client, registered_joker):
-    name = registered_joker["name"]
-    id = registered_joker["id"]
-
=    response = client.post(
-        "/token", data={"username": "Alice", "password": "Buhahaha!"}
+        "/token", data={"username": registered_joker["name"], "password": "Buhahaha!"}
=    )
=
=    assert response.status_code == 200

Remove a stale TODO comment

index e643b03..7450a12 100644
--- a/test_rest.py
+++ b/test_rest.py
@@ -51,7 +51,6 @@ def test_post_jokers(client, request):
=    assert "name" in body
=
=
-# TODO: Test authentication. Use a fixture to register new users before authenticating.
=@pytest.mark.credentials(name="Alice", password="Buhahaha!")
=def test_post_token(client, registered_joker):
=    response = client.post(