release: publish saldo 0.1.0
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app import create_app
|
||||
from app.extensions import db
|
||||
from app.models import NotificationPreference, User
|
||||
from app.seed import seed_data, seed_demo_data
|
||||
|
||||
|
||||
class TestConfig:
|
||||
TESTING = True
|
||||
SECRET_KEY = "test-secret"
|
||||
DATA_DIR = Path(tempfile.mkdtemp())
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite://"
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
VAPID_PUBLIC_KEY = ""
|
||||
VAPID_PRIVATE_KEY = ""
|
||||
VAPID_CLAIMS = {"sub": "mailto:test@example.com"}
|
||||
ALLOCATION_TARGET_RULES = {
|
||||
"sparen": {"recommended_pct": 0.18, "min_pct": 0.15, "max_pct": 0.20, "label": "Sparen"},
|
||||
"urlaub": {"recommended_pct": 0.06, "min_pct": 0.05, "max_pct": 0.08, "label": "Urlaub"},
|
||||
"freizeit": {"recommended_pct": 0.07, "min_pct": 0.05, "max_pct": 0.10, "label": "Freizeit"},
|
||||
}
|
||||
DEFAULT_PERSONAL_SPLIT_DESI_PCT = 50.0
|
||||
STRONG_INCOME_CHANGE_THRESHOLD = 150.0
|
||||
APP_NAME = "Saldo Test"
|
||||
CSRF_ENABLED = False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
app = create_app(TestConfig)
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
seed_data()
|
||||
seed_demo_data()
|
||||
yield app
|
||||
db.session.remove()
|
||||
db.drop_all()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def empty_app():
|
||||
app = create_app(TestConfig)
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
seed_data()
|
||||
yield app
|
||||
db.session.remove()
|
||||
db.drop_all()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def empty_client(empty_app):
|
||||
return empty_app.test_client()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
return app.test_client()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_user(app):
|
||||
return User.query.filter_by(username="admin").first()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def editor_user(app):
|
||||
return User.query.filter_by(username="mitglied1").first()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def logged_in_client(client, admin_user):
|
||||
response = client.post(
|
||||
"/auth/login",
|
||||
data={"username": admin_user.username, "password": "testpass"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
return client
|
||||
@@ -0,0 +1,171 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from app.extensions import db
|
||||
from app.models import Account, AllocationSuggestion, Month
|
||||
from app.utils.users import personal_account_names
|
||||
|
||||
|
||||
def test_comparison_to_previous_month(app):
|
||||
service = app.extensions["saldo.month_service"]
|
||||
april = Month.query.filter_by(label="2026-04").first()
|
||||
may = service.copy_month(april, 2026, 5, auto_created=True)
|
||||
may.incomes[0].amount = Decimal("3300.00")
|
||||
may.entry_values[0].planned_amount = Decimal("950.00")
|
||||
db.session.commit()
|
||||
summary = service.compute_summary(may)
|
||||
assert summary.deltas["income_delta"] == Decimal("200.00")
|
||||
assert summary.deltas["cost_delta"] == Decimal("30.00")
|
||||
|
||||
|
||||
def test_suggestion_logic_reacts_to_income_change(app):
|
||||
service = app.extensions["saldo.month_service"]
|
||||
month = Month.query.filter_by(label="2026-04").first()
|
||||
month.incomes[0].amount = Decimal("3500.00")
|
||||
service.refresh_suggestions(month, "Test")
|
||||
db.session.commit()
|
||||
suggestions = AllocationSuggestion.query.filter_by(month_id=month.id).all()
|
||||
assert len(suggestions) == 5
|
||||
sparen = next(item for item in suggestions if item.target_account.slug == "sparen")
|
||||
urlaub = next(item for item in suggestions if item.target_account.slug == "urlaub")
|
||||
freizeit = next(item for item in suggestions if item.target_account.slug == "freizeit")
|
||||
assert sparen.suggested_amount > Decimal("0.00")
|
||||
assert urlaub.suggested_amount > Decimal("0.00")
|
||||
assert freizeit.suggested_amount > Decimal("0.00")
|
||||
|
||||
|
||||
def test_accepting_suggestions_updates_allocations(app):
|
||||
service = app.extensions["saldo.month_service"]
|
||||
allocation_service = app.extensions["saldo.allocation_service"]
|
||||
month = Month.query.filter_by(label="2026-04").first()
|
||||
service.refresh_suggestions(month, "Test")
|
||||
allocation_service.accept_all(month)
|
||||
service.sync_distribution_entries_from_allocations(month)
|
||||
db.session.commit()
|
||||
assert all(item.source == "accepted_suggestion" for item in month.allocations if not item.is_locked)
|
||||
flo_allocation = next(item for item in month.allocations if item.target_account.slug == "persoenlich-flo")
|
||||
flo_entry_value = next(
|
||||
item
|
||||
for item in month.entry_values
|
||||
if item.entry.name == "Person 1" and item.entry.category.account.slug == "persoenlich-flo"
|
||||
)
|
||||
assert flo_entry_value.planned_amount == flo_allocation.amount
|
||||
|
||||
|
||||
def test_personal_distribution_entry_updates_matching_allocation(app):
|
||||
service = app.extensions["saldo.month_service"]
|
||||
month = Month.query.filter_by(label="2026-04").first()
|
||||
flo_value = next(
|
||||
item
|
||||
for item in month.entry_values
|
||||
if item.entry.name == "Person 1" and item.entry.category.account.slug == "persoenlich-flo"
|
||||
)
|
||||
flo_value.planned_amount = Decimal("333.33")
|
||||
|
||||
changed = service.sync_distribution_allocation_from_entry(
|
||||
month,
|
||||
flo_value.entry,
|
||||
mark_manual=True,
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
flo_allocation = next(item for item in month.allocations if item.target_account.slug == "persoenlich-flo")
|
||||
personal_labels = personal_account_names()
|
||||
assert changed is True
|
||||
assert flo_allocation.amount == Decimal("333.33")
|
||||
assert flo_allocation.label == personal_labels["persoenlich-flo"]
|
||||
assert flo_allocation.source == "manual"
|
||||
|
||||
|
||||
def test_personal_split_changes_personal_suggestions(app):
|
||||
service = app.extensions["saldo.month_service"]
|
||||
month = Month.query.filter_by(label="2026-04").first()
|
||||
month.personal_split_desi_pct = Decimal("40.00")
|
||||
|
||||
service.refresh_suggestions(month, "Split angepasst")
|
||||
db.session.commit()
|
||||
|
||||
flo_suggestion = next(item for item in month.suggestions if item.target_account.slug == "persoenlich-flo")
|
||||
desi_suggestion = next(item for item in month.suggestions if item.target_account.slug == "persoenlich-desi")
|
||||
|
||||
assert flo_suggestion.suggested_amount > desi_suggestion.suggested_amount
|
||||
|
||||
|
||||
def test_manual_priority_accounts_leave_remaining_suggestions_for_personal_only(app):
|
||||
service = app.extensions["saldo.month_service"]
|
||||
month = Month.query.filter_by(label="2026-04").first()
|
||||
|
||||
service.refresh_suggestions(month, "Neu berechnen")
|
||||
db.session.commit()
|
||||
|
||||
allocations = {item.target_account.slug: item.amount for item in month.allocations}
|
||||
suggestions = {item.target_account.slug: item.suggested_amount for item in month.suggestions}
|
||||
summary = service.compute_summary(month)
|
||||
|
||||
assert suggestions["sparen"] >= allocations["sparen"]
|
||||
assert suggestions["urlaub"] >= allocations["urlaub"]
|
||||
assert suggestions["freizeit"] >= allocations["freizeit"]
|
||||
assert suggestions["persoenlich-flo"] == allocations["persoenlich-flo"]
|
||||
assert suggestions["persoenlich-desi"] == allocations["persoenlich-desi"]
|
||||
assert summary.remainder == Decimal("0.00")
|
||||
|
||||
|
||||
def test_personal_allocation_is_reduced_when_fixed_costs_rise(app):
|
||||
service = app.extensions["saldo.month_service"]
|
||||
month = Month.query.filter_by(label="2026-04").first()
|
||||
fixed_cost_value = next(item for item in month.entry_values if item.entry.name == "Miete")
|
||||
original_fixed_cost = fixed_cost_value.planned_amount
|
||||
original_total = sum(
|
||||
item.amount
|
||||
for item in month.allocations
|
||||
if item.target_account.slug in {"persoenlich-flo", "persoenlich-desi"}
|
||||
)
|
||||
|
||||
fixed_cost_value.planned_amount = original_fixed_cost + Decimal("50.00")
|
||||
service.refresh_suggestions(month, "Fixkosten gestiegen")
|
||||
db.session.commit()
|
||||
|
||||
personal_total = sum(
|
||||
item.amount
|
||||
for item in month.allocations
|
||||
if item.target_account.slug in {"persoenlich-flo", "persoenlich-desi"}
|
||||
)
|
||||
summary = service.compute_summary(month)
|
||||
|
||||
assert personal_total == original_total - Decimal("50.00")
|
||||
assert summary.remainder == Decimal("0.00")
|
||||
|
||||
|
||||
def test_personal_allocation_can_fall_to_zero_before_remainder_turns_negative(app):
|
||||
service = app.extensions["saldo.month_service"]
|
||||
month = Month.query.filter_by(label="2026-04").first()
|
||||
fixed_cost_value = next(item for item in month.entry_values if item.entry.name == "Miete")
|
||||
|
||||
fixed_cost_value.planned_amount = Decimal("9999.99")
|
||||
service.refresh_suggestions(month, "Defizit")
|
||||
db.session.commit()
|
||||
|
||||
flo_allocation = next(item for item in month.allocations if item.target_account.slug == "persoenlich-flo")
|
||||
desi_allocation = next(item for item in month.allocations if item.target_account.slug == "persoenlich-desi")
|
||||
summary = service.compute_summary(month)
|
||||
|
||||
assert flo_allocation.amount == Decimal("0.00")
|
||||
assert desi_allocation.amount == Decimal("0.00")
|
||||
assert summary.remainder < Decimal("0.00")
|
||||
|
||||
|
||||
def test_seeded_distribution_entries_are_marked_as_allocation_targets(app):
|
||||
month = Month.query.filter_by(label="2026-04").first()
|
||||
target_entries = {
|
||||
item.entry.name: item.entry.is_allocation_target
|
||||
for item in month.entry_values
|
||||
if item.entry.name in {"Sparziel", "Reisebudget", "Freizeitbudget", "Person 1", "Person 2"}
|
||||
}
|
||||
|
||||
assert target_entries["Sparziel"] is True
|
||||
assert target_entries["Reisebudget"] is True
|
||||
assert target_entries["Freizeitbudget"] is True
|
||||
assert target_entries["Person 1"] is True
|
||||
assert target_entries["Person 2"] is True
|
||||
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from app.extensions import db
|
||||
from app.models import Month
|
||||
|
||||
|
||||
def test_auto_creates_missing_month(app):
|
||||
service = app.extensions["saldo.month_service"]
|
||||
month = service.ensure_month(date(2026, 5, 1))
|
||||
assert month.label == "2026-05"
|
||||
assert month.auto_created is True
|
||||
|
||||
|
||||
def test_copy_previous_month_keeps_values(app):
|
||||
service = app.extensions["saldo.month_service"]
|
||||
april = Month.query.filter_by(label="2026-04").first()
|
||||
copied = service.copy_month(april, 2026, 6, auto_created=False)
|
||||
db.session.commit()
|
||||
assert copied.label == "2026-06"
|
||||
assert len(copied.entry_values) == len(april.entry_values)
|
||||
assert copied.incomes[0].amount == april.incomes[0].amount
|
||||
|
||||
|
||||
def test_remainder_calculation(app):
|
||||
service = app.extensions["saldo.month_service"]
|
||||
month = Month.query.filter_by(label="2026-04").first()
|
||||
summary = service.compute_summary(month)
|
||||
assert summary.total_income == Decimal("5550.00")
|
||||
assert summary.remainder == summary.total_income - summary.total_costs
|
||||
@@ -0,0 +1,178 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from app.extensions import db
|
||||
from app.models import AllocationSuggestion, Category, CommunityAccount, Month, MonthlyEntryValue
|
||||
|
||||
|
||||
def test_health_route(client):
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
assert response.json["status"] == "ok"
|
||||
|
||||
|
||||
def test_first_run_redirects_to_setup(empty_client):
|
||||
response = empty_client.get("/", follow_redirects=False)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert "/auth/setup" in response.headers["Location"]
|
||||
|
||||
|
||||
def test_current_month_is_available_after_login(logged_in_client):
|
||||
response = logged_in_client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert b"2026-04" in response.data or b"2026-05" in response.data
|
||||
assert b"Dauerauftr\xc3\xa4ge pr\xc3\xbcfen" in response.data
|
||||
assert b"Extern mitzuteilen" in response.data
|
||||
|
||||
|
||||
def test_analytics_route_uses_new_cost_focused_sections(logged_in_client):
|
||||
response = logged_in_client.get("/analytics")
|
||||
content = response.get_data(as_text=True)
|
||||
assert response.status_code == 200
|
||||
assert "Kategorien im Monat" in content
|
||||
assert "Kosten nach Zuordnung" in content
|
||||
assert "Budgets im Monatsverlauf" in content
|
||||
assert "Größte Einträge im Monat" in content
|
||||
assert "Sparkonten" in content
|
||||
assert "category-chart-back" in content
|
||||
assert "entry-drilldown-chart" not in content
|
||||
assert '"Pers\\u00f6nliche Auszahlung"' in content
|
||||
assert "Person A" in content
|
||||
assert "Person B" in content
|
||||
|
||||
|
||||
def test_planning_detail_refreshes_stale_suggestions_after_distribution_sync(logged_in_client):
|
||||
month = Month.query.filter_by(label="2026-04").first()
|
||||
sparen_value = next(
|
||||
item for item in month.entry_values if item.entry.name == "Sparziel"
|
||||
)
|
||||
sparen_allocation = next(
|
||||
item for item in month.allocations if item.target_account.slug == "sparen"
|
||||
)
|
||||
sparen_value.planned_amount = Decimal("0.00")
|
||||
sparen_allocation.amount = Decimal("1539.20")
|
||||
for suggestion in month.suggestions:
|
||||
suggestion.suggested_amount = Decimal("0.00")
|
||||
db.session.commit()
|
||||
|
||||
response = logged_in_client.get("/planning/2026-04")
|
||||
|
||||
db.session.refresh(sparen_value)
|
||||
refreshed_suggestions = AllocationSuggestion.query.filter_by(month_id=month.id).all()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert sparen_value.planned_amount == Decimal("1539.20")
|
||||
assert sum((item.suggested_amount for item in refreshed_suggestions), Decimal("0.00")) > Decimal("0.00")
|
||||
|
||||
|
||||
def test_community_entry_can_be_updated_via_annual_amount(app, logged_in_client):
|
||||
month = Month.query.filter_by(label="2026-04").first()
|
||||
value = next(
|
||||
item for item in month.entry_values if item.entry.name == "Miete"
|
||||
)
|
||||
|
||||
response = logged_in_client.post(
|
||||
f"/planning/{month.label}/entry",
|
||||
data={
|
||||
"value_id": value.id,
|
||||
"return_dialog": f"category-dialog-{value.entry.category_id}",
|
||||
"entry_name": value.entry.name,
|
||||
"category_id": value.entry.category_id,
|
||||
"planned_amount": "",
|
||||
"annual_amount": "1800.00",
|
||||
"benefit_scope": value.entry.benefit_scope,
|
||||
"note": value.note or "",
|
||||
},
|
||||
)
|
||||
|
||||
updated_value = db.session.get(MonthlyEntryValue, value.id)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert updated_value.planned_amount == Decimal("150.00")
|
||||
|
||||
|
||||
def test_distribution_dialog_shows_direct_budget_form(logged_in_client):
|
||||
response = logged_in_client.get("/planning/2026-04")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b"Budget direkt anpassen" in response.data
|
||||
assert b"Sparkonto" in response.data
|
||||
|
||||
|
||||
def test_planning_shows_budgets_and_community_accounts(logged_in_client):
|
||||
response = logged_in_client.get("/planning/2026-04")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b"Budgets" in response.data
|
||||
assert b"Gemeinschaftskonten" in response.data
|
||||
assert b"Privatkonto 1" in response.data
|
||||
assert b"Privatkonto 2" in response.data
|
||||
|
||||
|
||||
def test_community_account_can_assign_budget_categories(logged_in_client):
|
||||
community_account = CommunityAccount.query.filter_by(slug="hauptkonto").first()
|
||||
category = Category.query.filter_by(slug="wohnen").first()
|
||||
|
||||
response = logged_in_client.post(
|
||||
f"/planning/2026-04/community-accounts/{community_account.id}",
|
||||
data={
|
||||
"name": community_account.name,
|
||||
"description": community_account.description or "",
|
||||
"category_ids": [str(category.id)],
|
||||
},
|
||||
)
|
||||
|
||||
db.session.refresh(category)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert category.community_account_id == community_account.id
|
||||
|
||||
|
||||
def test_community_account_rejects_budget_assigned_to_other_account(logged_in_client):
|
||||
primary_account = CommunityAccount.query.filter_by(slug="hauptkonto").first()
|
||||
secondary_response = logged_in_client.post(
|
||||
"/planning/2026-04/community-accounts",
|
||||
data={"name": "Fixkostenkonto", "description": ""},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert secondary_response.status_code == 200
|
||||
secondary_account = CommunityAccount.query.filter_by(slug="fixkostenkonto").first()
|
||||
category = Category.query.filter_by(slug="wohnen").first()
|
||||
category.community_account_id = primary_account.id
|
||||
db.session.commit()
|
||||
|
||||
response = logged_in_client.post(
|
||||
f"/planning/2026-04/community-accounts/{secondary_account.id}",
|
||||
data={
|
||||
"name": secondary_account.name,
|
||||
"description": secondary_account.description or "",
|
||||
"category_ids": [str(category.id)],
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
db.session.refresh(category)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert category.community_account_id == primary_account.id
|
||||
assert b"bereits anderen Konten zugewiesen" in response.data
|
||||
|
||||
|
||||
def test_community_account_can_be_deleted_and_unassigns_budgets(logged_in_client):
|
||||
community_account = CommunityAccount.query.filter_by(slug="hauptkonto").first()
|
||||
category = Category.query.filter_by(slug="wohnen").first()
|
||||
category.community_account_id = community_account.id
|
||||
db.session.commit()
|
||||
|
||||
response = logged_in_client.post(
|
||||
f"/planning/2026-04/community-accounts/{community_account.id}/delete"
|
||||
)
|
||||
|
||||
db.session.refresh(category)
|
||||
db.session.refresh(community_account)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert community_account.is_active is False
|
||||
assert category.community_account_id is None
|
||||
@@ -0,0 +1,119 @@
|
||||
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/")
|
||||
Reference in New Issue
Block a user