120 lines
4.2 KiB
Python
120 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
import io
|
|
from datetime import date
|
|
from decimal import Decimal
|
|
|
|
from app.extensions import db
|
|
from app.models import CostParticipant, Entry, InAppNotification, Month, User
|
|
|
|
|
|
def test_share_logic_with_external_guest(app):
|
|
month = Month.query.filter_by(label="2026-04").first()
|
|
service = app.extensions["saldo.share_service"]
|
|
shared_value = next(item for item in month.entry_values if item.entry.name == "Streaming 1")
|
|
result = service.calculate_entry_shares(shared_value)
|
|
guest_share = next(item for item in result["shares"] if item["participant_name"] == "Gast")
|
|
assert guest_share["amount"] == Decimal("5.99")
|
|
assert result["external_total"] == guest_share["amount"]
|
|
|
|
|
|
def test_external_guest_shares_do_not_reduce_distribution_pool(app):
|
|
month = Month.query.filter_by(label="2026-04").first()
|
|
service = app.extensions["saldo.month_service"]
|
|
shared_value = next(item for item in month.entry_values if item.entry.name == "Streaming 1")
|
|
summary = service.compute_summary(month)
|
|
|
|
assert shared_value.entry.share_rules
|
|
assert summary.fixed_costs < summary.total_costs
|
|
|
|
|
|
def test_push_reminder_logic_creates_in_app_notifications(app):
|
|
notification_service = app.extensions["saldo.notification_service"]
|
|
count = notification_service.run_monthly_checks(date(2026, 4, 27))
|
|
assert count >= 1
|
|
assert InAppNotification.query.count() >= 1
|
|
|
|
|
|
def test_setup_route_can_create_first_admin(empty_client, empty_app):
|
|
response = empty_client.post(
|
|
"/auth/setup",
|
|
data={
|
|
"username": "setup-admin",
|
|
"display_name": "Setup Admin",
|
|
"email": "setup-admin@example.invalid",
|
|
"password": "supersecret",
|
|
"password_confirm": "supersecret",
|
|
},
|
|
follow_redirects=True,
|
|
)
|
|
|
|
with empty_app.app_context():
|
|
user = User.query.filter_by(username="setup-admin").first()
|
|
assert user is not None
|
|
assert user.role == "admin"
|
|
|
|
assert response.status_code == 200
|
|
assert "2026-04" in response.get_data(as_text=True) or "2026-05" in response.get_data(as_text=True)
|
|
|
|
|
|
def test_admin_route_requires_admin(client, app, editor_user):
|
|
client.post(
|
|
"/auth/login",
|
|
data={"username": editor_user.username, "password": "testpass"},
|
|
follow_redirects=True,
|
|
)
|
|
response = client.get("/admin/")
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_admin_can_access_admin_route(logged_in_client):
|
|
response = logged_in_client.get("/admin/")
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_admin_can_create_entry_and_backfill_existing_month(logged_in_client, app):
|
|
from app.models import Category, Entry, Month, MonthlyEntryValue
|
|
|
|
category = Category.query.filter_by(slug="haushalt").first()
|
|
month = Month.query.filter_by(label="2026-04").first()
|
|
before = MonthlyEntryValue.query.filter_by(month_id=month.id).count()
|
|
response = logged_in_client.post(
|
|
"/admin/entries",
|
|
data={
|
|
"category_id": category.id,
|
|
"name": "Tierfutter",
|
|
"slug": "tierfutter",
|
|
"default_amount": "45.00",
|
|
"amount_type": "fixed",
|
|
"sort_order": "99",
|
|
"description": "",
|
|
"is_active": "on",
|
|
},
|
|
follow_redirects=True,
|
|
)
|
|
assert response.status_code == 200
|
|
entry = Entry.query.filter_by(slug="tierfutter").first()
|
|
assert entry is not None
|
|
assert MonthlyEntryValue.query.filter_by(month_id=month.id).count() == before + 1
|
|
|
|
|
|
def test_participant_avatar_can_be_uploaded(logged_in_client, app):
|
|
response = logged_in_client.post(
|
|
"/planning/2026-04/participants",
|
|
data={
|
|
"name": "Mika",
|
|
"is_external": "on",
|
|
"return_dialog": "split-people-dialog",
|
|
"avatar_file": (io.BytesIO(b"fake-image-bytes"), "mika.png"),
|
|
},
|
|
content_type="multipart/form-data",
|
|
follow_redirects=True,
|
|
)
|
|
|
|
participant = CostParticipant.query.filter_by(name="Mika").first()
|
|
|
|
assert response.status_code == 200
|
|
assert participant is not None
|
|
assert participant.avatar_url is not None
|
|
assert participant.avatar_url.startswith("/media/avatars/")
|