diff --git a/.gitignore b/.gitignore index 7be8fe0..eb17c0a 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,4 @@ instance/ # uv specific .uvcache/ +/db_fixtures/ diff --git a/dev-server.sh b/dev-server.sh index 2c8e0c2..7227bdc 100644 --- a/dev-server.sh +++ b/dev-server.sh @@ -1,4 +1,7 @@ if [ -z "${APS_username}" ]; then export APS_username="testuser" fi +if [ -z "${APS_display_name}" ]; then + export APS_display_name="Dr. T. Estuser" +fi fastapi dev src/allmende_payment_system/app.py \ No newline at end of file diff --git a/justfile b/justfile index edc12dc..0e6e74d 100644 --- a/justfile +++ b/justfile @@ -1,3 +1,8 @@ lint: uv run isort test src - uv run black test src \ No newline at end of file + uv run black test src + +reset_db: + rm -f aps_db.db + uv run python -c "from allmende_payment_system.database import create_tables; create_tables()" + for file in db_fixtures/*.sql; do sqlite3 aps_db.db < "$file"; done \ No newline at end of file diff --git a/src/allmende_payment_system/app.py b/src/allmende_payment_system/app.py index 62135b1..a854e34 100644 --- a/src/allmende_payment_system/app.py +++ b/src/allmende_payment_system/app.py @@ -4,12 +4,18 @@ from typing import Annotated from fastapi import Depends, FastAPI, HTTPException, Request from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates +from sqlalchemy.orm import Session + +from allmende_payment_system.database import SessionLocal, ensure_user async def get_user(request: Request) -> dict: if username := os.environ.get("APS_username", None): - return {"username": username} + return { + "username": username, + "display_name": os.environ.get("APS_display_name", "Missing Display Name"), + } if "ynh_user" not in request.headers: raise HTTPException(status_code=401, detail="Missing ynh_user header") return {"username": request.headers["ynh_user"]} @@ -28,6 +34,21 @@ app.mount( ) +def get_session() -> Session: + db = SessionLocal() + try: + yield db + finally: + db.close() + + +SessionDep = Annotated[Session, Depends(get_session)] + + @app.get("/") -async def landing_page(request: Request): - return templates.TemplateResponse("index.html.jinja", {"request": request}) +async def landing_page(request: Request, user_info: UserDep, session: SessionDep): + user = ensure_user(user_info, session) + print(f"User {user.username} ({user.display_name}) accessed landing page") + return templates.TemplateResponse( + "index.html.jinja", context={"request": request, "user": user} + ) diff --git a/src/allmende_payment_system/database.py b/src/allmende_payment_system/database.py index 3139eec..2ed4cec 100644 --- a/src/allmende_payment_system/database.py +++ b/src/allmende_payment_system/database.py @@ -1,6 +1,29 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker +from sqlalchemy import create_engine, select +from sqlalchemy.orm import Session, sessionmaker + +from allmende_payment_system.models import User SQLALCHEMY_DATABASE_URL = "sqlite:///./aps_db.db" engine = create_engine(SQLALCHEMY_DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def create_tables(): + from allmende_payment_system.models import Base + + Base.metadata.create_all(bind=engine) + + +def ensure_user(user_info: dict, session: Session) -> User: + statement = select(User).where(User.username == user_info["username"]) + + if user := session.scalars(statement).one_or_none(): + return user + + user = User( + username=user_info["username"], display_name=user_info.get("display_name") + ) + session.add(user) + session.commit() + + return user diff --git a/src/allmende_payment_system/models.py b/src/allmende_payment_system/models.py index 64d53aa..b276981 100644 --- a/src/allmende_payment_system/models.py +++ b/src/allmende_payment_system/models.py @@ -19,6 +19,10 @@ class Account(Base): back_populates="accounts", ) + @property + def balance(self): + return 141.23 + user_account_association = Table( TABLE_PREFIX + "user_account_association", diff --git a/src/allmende_payment_system/templates/index.html.jinja b/src/allmende_payment_system/templates/index.html.jinja index 5045302..1bb5bb4 100644 --- a/src/allmende_payment_system/templates/index.html.jinja +++ b/src/allmende_payment_system/templates/index.html.jinja @@ -1,7 +1,75 @@ + {% extends "base.html.jinja" %} {% block content %} -
-

Welcome to My App

-

This is your landing page content. Replace this with your actual content.

+ +
+

Meine Konten

+
+ {% if user.accounts|length > 0 %} + {% for account in user.accounts %} +
+
+
+
{{ account.name }}
+

+ {{ '%.2f' | format(account.balance) }} € +

+ +
+
+
+ {% endfor %} + {% else %} +
+
+ Keine Konten verfügbar +
+
+ {% endif %} +
+
+ + +
+
+

Latest Transactions

+ View All +
+ +
+
+ {% if transactions and transactions|length > 0 %} + {% for transaction in transactions[:5] %} +
+
+
{{ transaction.product_name | default('Transaction') }}
+ + {{ transaction.date | default('') }} + {% if transaction.description %} + · {{ transaction.description }} + {% endif %} + +
+
+ + {{ '%+.2f' | format(transaction.amount | default(0)) }} € + + {% if transaction.quantity %} +
{{ transaction.quantity }} {{ transaction.unit | default('pcs') }}
+ {% endif %} +
+
+ {% endfor %} + {% else %} +
+

No transactions yet

+ Your transactions will appear here +
+ {% endif %} +
+
{% endblock %} \ No newline at end of file diff --git a/test/conftest.py b/test/conftest.py index 15dff24..95d8be6 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,30 +1,35 @@ -import pytest -from fastapi import Request -from fastapi.testclient import TestClient +import os -from allmende_payment_system.app import create_app +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from allmende_payment_system.app import app +from allmende_payment_system.models import Base @pytest.fixture(scope="session") def client(): - app = create_app() - - async def add_ynh_headers(request: Request, call_next): - username = request.headers.get("APS-TEST-username", "test") - # This seems to work although headers are immutable - # If this ever turns out to be a problem, we can use request.state instead, - # but will have to modify app.get_user - request.headers._list.append((b"ynh_user", username.encode("utf-8"))) - - response = await call_next(request) - return response - - app.middleware("http")(add_ynh_headers) - + os.environ["APS_username"] = "test" return TestClient(app) @pytest.fixture(scope="session") def unauthorized_client(): - app = create_app() + os.environ.pop("APS_username", None) return TestClient(app) + + +@pytest.fixture +def test_db(): + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(bind=engine) # Create tables + TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + # Provide a session and the engine + db = TestingSessionLocal() + try: + yield db + finally: + db.close() diff --git a/test/test_auth.py b/test/test_auth.py index dae3e81..b5d9625 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -6,7 +6,6 @@ def test_unauthorized_access(unauthorized_client): assert response.status_code == 401 -def test_unauthorized_access(unauthorized_client): - response = unauthorized_client.get("/") - print(response.text) - assert response.status_code == 401 +def test_authorized_access(client): + response = client.get("/") + assert response.status_code == 200 diff --git a/test/test_database.py b/test/test_database.py new file mode 100644 index 0000000..0edfdef --- /dev/null +++ b/test/test_database.py @@ -0,0 +1,19 @@ +from sqlalchemy import func, select + +from allmende_payment_system.database import ensure_user +from allmende_payment_system.models import User + + +def test_ensure_user(test_db): + + user_info = {"username": "test", "display_name": "Test User"} + user = ensure_user(user_info, test_db) + + assert user.username == "test" + + test_db.commit() + + assert test_db.scalar(select(func.count()).select_from(User)) == 1 + + user = ensure_user(user_info, test_db) + assert test_db.scalar(select(func.count()).select_from(User)) == 1 diff --git a/test/test_models.py b/test/test_models.py index 2431d44..d5825e2 100644 --- a/test/test_models.py +++ b/test/test_models.py @@ -1,35 +1,16 @@ -import pytest -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -from allmende_payment_system.models import Account, Base, User +from allmende_payment_system.models import Account, User -# Create an in-memory SQLite database -@pytest.fixture -def in_memory_db(): - engine = create_engine("sqlite:///:memory:") - Base.metadata.create_all(bind=engine) # Create tables - TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - - # Provide a session and the engine - db = TestingSessionLocal() - try: - yield db - finally: - db.close() - - -def test_user_model(in_memory_db): +def test_user_model(test_db): user = User(username="test", display_name="Test User") - in_memory_db.add(user) - in_memory_db.commit() + test_db.add(user) + test_db.commit() assert user.id is not None account = Account(name="Test Account") account.users.append(user) - in_memory_db.add(account) - in_memory_db.commit() + test_db.add(account) + test_db.commit() assert len(user.accounts) == 1