2026-07-16 07:00:42 +02:00
|
|
|
import tempfile
|
2026-07-17 19:57:08 +02:00
|
|
|
from pathlib import Path
|
2026-07-16 07:00:42 +02:00
|
|
|
|
2026-07-17 20:02:11 +02:00
|
|
|
from flask_fatima import create_app, load_yaml_config, log_database_summary
|
2026-07-16 07:00:42 +02:00
|
|
|
|
2026-07-17 18:19:42 +02:00
|
|
|
URL_PREFIX = "/fatima"
|
2026-07-16 07:00:42 +02:00
|
|
|
|
2026-07-17 18:19:42 +02:00
|
|
|
|
|
|
|
|
def url(path="/"):
|
|
|
|
|
return f"{URL_PREFIX}{path}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def client(url_prefix=URL_PREFIX):
|
2026-07-16 07:00:42 +02:00
|
|
|
database = tempfile.NamedTemporaryFile(suffix=".sqlite")
|
|
|
|
|
app = create_app({
|
|
|
|
|
"TESTING": True,
|
|
|
|
|
"SECRET_KEY": "test-secret",
|
|
|
|
|
"APP_USERNAME": "user",
|
|
|
|
|
"APP_PASSWORD": "pass",
|
|
|
|
|
"DATABASE": database.name,
|
2026-07-17 17:53:36 +02:00
|
|
|
"URL_PREFIX": url_prefix,
|
2026-07-16 07:00:42 +02:00
|
|
|
})
|
|
|
|
|
test_client = app.test_client()
|
|
|
|
|
test_client._database_file = database
|
|
|
|
|
return test_client
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_calendar_requires_login():
|
2026-07-17 18:19:42 +02:00
|
|
|
response = client().get(url())
|
2026-07-16 07:00:42 +02:00
|
|
|
assert response.status_code == 302
|
2026-07-17 18:19:42 +02:00
|
|
|
assert response.headers["Location"].endswith(url("/login"))
|
2026-07-17 17:53:36 +02:00
|
|
|
|
|
|
|
|
|
2026-07-17 19:57:08 +02:00
|
|
|
def test_database_summary_is_appended_to_log(capsys):
|
|
|
|
|
test_client = client()
|
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
|
|
|
log_path = Path(directory) / "fatima.log"
|
|
|
|
|
log_database_summary(test_client.application.config["DATABASE"], log_path)
|
|
|
|
|
|
|
|
|
|
logged = log_path.read_text(encoding="utf-8")
|
|
|
|
|
assert "Fatima database:" in logged
|
|
|
|
|
assert f"path={test_client.application.config['DATABASE']}" in logged
|
|
|
|
|
assert "amounts=0 | statuses=0 | recorded_dates=0" in logged
|
|
|
|
|
assert "Fatima database:" in capsys.readouterr().out
|
|
|
|
|
|
|
|
|
|
|
2026-07-17 20:02:11 +02:00
|
|
|
def test_database_path_is_resolved_relative_to_config_file():
|
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
|
|
|
config_path = Path(directory) / "production.yaml"
|
|
|
|
|
config_path.write_text(
|
|
|
|
|
"""
|
|
|
|
|
flask:
|
|
|
|
|
secret_key: test-secret
|
|
|
|
|
database: data/calendar.sqlite
|
|
|
|
|
auth:
|
|
|
|
|
username: user
|
|
|
|
|
password: pass
|
|
|
|
|
""",
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
config = load_yaml_config(config_path)
|
|
|
|
|
assert config["DATABASE"] == str(
|
|
|
|
|
(Path(directory) / "data" / "calendar.sqlite").resolve()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-07-17 18:19:42 +02:00
|
|
|
def test_prefix_applies_to_generated_links_and_unprefixed_routes_are_rejected():
|
2026-07-17 17:53:36 +02:00
|
|
|
test_client = client()
|
2026-07-17 18:19:42 +02:00
|
|
|
test_client.post(url("/login"), data={"username": "user", "password": "pass"})
|
|
|
|
|
page = test_client.get(url())
|
2026-07-17 17:53:36 +02:00
|
|
|
|
|
|
|
|
assert b'href="/fatima/static/styles.css"' in page.data
|
|
|
|
|
assert b'src="/fatima/static/calendar.js"' in page.data
|
|
|
|
|
assert b'data-save-url="/fatima/api/choices"' in page.data
|
2026-07-17 18:19:42 +02:00
|
|
|
assert b'href="/fatima/report"' in page.data
|
|
|
|
|
assert page.data.index(b'class="report-button"') < page.data.index(b'class="today-button"')
|
2026-07-17 17:53:36 +02:00
|
|
|
assert test_client.get("/").status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_url_prefix_is_configurable_and_can_be_disabled():
|
|
|
|
|
custom_client = client("/calendar")
|
2026-07-17 18:19:42 +02:00
|
|
|
response = custom_client.get("/calendar/")
|
|
|
|
|
assert response.headers["Location"].endswith("/calendar/login")
|
|
|
|
|
assert custom_client.get(url()).status_code == 404
|
2026-07-17 17:53:36 +02:00
|
|
|
|
|
|
|
|
root_client = client("")
|
|
|
|
|
assert root_client.get("/").headers["Location"].endswith("/login")
|
2026-07-16 07:00:42 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_login_and_logout():
|
|
|
|
|
test_client = client()
|
2026-07-17 18:19:42 +02:00
|
|
|
response = test_client.post(url("/login"), data={"username": "user", "password": "pass"})
|
2026-07-16 07:00:42 +02:00
|
|
|
assert response.status_code == 302
|
2026-07-17 18:19:42 +02:00
|
|
|
assert response.headers["Location"].endswith(url())
|
|
|
|
|
assert test_client.get(url()).status_code == 200
|
|
|
|
|
assert test_client.post(url("/logout")).headers["Location"].endswith(url("/login"))
|
2026-07-16 07:00:42 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_bad_login_shows_error():
|
2026-07-17 18:19:42 +02:00
|
|
|
response = client().post(url("/login"), data={"username": "user", "password": "wrong"})
|
2026-07-16 07:00:42 +02:00
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert b"not correct" in response.data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_choice_is_saved_and_rendered():
|
|
|
|
|
test_client = client()
|
2026-07-17 18:19:42 +02:00
|
|
|
test_client.post(url("/login"), data={"username": "user", "password": "pass"})
|
|
|
|
|
response = test_client.post(url("/api/choices"), json={"date": "2026-07-15", "amount": "27.50"})
|
2026-07-16 07:00:42 +02:00
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json["amount_cents"] == 2750
|
2026-07-17 18:19:42 +02:00
|
|
|
assert b'"2026-07-15": 2750' in test_client.get(url()).data
|
2026-07-16 07:00:42 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_choice_rejects_invalid_amount():
|
|
|
|
|
test_client = client()
|
2026-07-17 18:19:42 +02:00
|
|
|
test_client.post(url("/login"), data={"username": "user", "password": "pass"})
|
|
|
|
|
response = test_client.post(url("/api/choices"), json={"date": "2026-07-15", "amount": "-1"})
|
2026-07-16 07:00:42 +02:00
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_day_status_is_saved_and_rendered():
|
|
|
|
|
test_client = client()
|
2026-07-17 18:19:42 +02:00
|
|
|
test_client.post(url("/login"), data={"username": "user", "password": "pass"})
|
2026-07-16 07:00:42 +02:00
|
|
|
response = test_client.post(
|
2026-07-17 18:19:42 +02:00
|
|
|
url("/api/statuses"),
|
2026-07-16 07:00:42 +02:00
|
|
|
json={"date": "2026-07-15", "status": "vacation"},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json["status"] == "vacation"
|
2026-07-17 18:19:42 +02:00
|
|
|
assert b'"2026-07-15": "vacation"' in test_client.get(url()).data
|
2026-07-16 07:00:42 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_day_status_rejects_unknown_value():
|
|
|
|
|
test_client = client()
|
2026-07-17 18:19:42 +02:00
|
|
|
test_client.post(url("/login"), data={"username": "user", "password": "pass"})
|
2026-07-16 07:00:42 +02:00
|
|
|
response = test_client.post(
|
2026-07-17 18:19:42 +02:00
|
|
|
url("/api/statuses"),
|
2026-07-16 07:00:42 +02:00
|
|
|
json={"date": "2026-07-15", "status": "elsewhere"},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_delete_removes_amount_and_status():
|
|
|
|
|
test_client = client()
|
2026-07-17 18:19:42 +02:00
|
|
|
test_client.post(url("/login"), data={"username": "user", "password": "pass"})
|
|
|
|
|
test_client.post(url("/api/choices"), json={"date": "2026-07-15", "amount": "55"})
|
|
|
|
|
test_client.post(url("/api/statuses"), json={"date": "2026-07-15", "status": "sick"})
|
2026-07-16 07:00:42 +02:00
|
|
|
|
2026-07-17 18:19:42 +02:00
|
|
|
response = test_client.post(url("/api/delete-date"), json={"date": "2026-07-15"})
|
2026-07-16 07:00:42 +02:00
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json["deleted"] is True
|
2026-07-17 18:19:42 +02:00
|
|
|
page = test_client.get(url()).data
|
2026-07-16 07:00:42 +02:00
|
|
|
assert b'"2026-07-15": 5500' not in page
|
|
|
|
|
assert b'"2026-07-15": "sick"' not in page
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_report_groups_money_and_days_by_status():
|
|
|
|
|
from datetime import date
|
|
|
|
|
|
|
|
|
|
test_client = client()
|
2026-07-17 18:19:42 +02:00
|
|
|
test_client.post(url("/login"), data={"username": "user", "password": "pass"})
|
2026-07-16 07:00:42 +02:00
|
|
|
year = date.today().year
|
|
|
|
|
worked_date = f"{year}-01-10"
|
|
|
|
|
vacation_date = f"{year}-02-10"
|
2026-07-17 18:19:42 +02:00
|
|
|
test_client.post(url("/api/choices"), json={"date": worked_date, "amount": "55"})
|
|
|
|
|
test_client.post(url("/api/choices"), json={"date": vacation_date, "amount": "27.50"})
|
|
|
|
|
test_client.post(url("/api/statuses"), json={"date": vacation_date, "status": "vacation"})
|
2026-07-16 07:00:42 +02:00
|
|
|
|
2026-07-17 18:19:42 +02:00
|
|
|
response = test_client.get(url("/report"))
|
2026-07-16 07:00:42 +02:00
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert b"\xe2\x82\xac55.00" in response.data
|
|
|
|
|
assert b"\xe2\x82\xac27.50" in response.data
|
|
|
|
|
assert b"2 recorded days" in response.data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_report_can_navigate_to_another_year():
|
|
|
|
|
test_client = client()
|
2026-07-17 18:19:42 +02:00
|
|
|
test_client.post(url("/login"), data={"username": "user", "password": "pass"})
|
|
|
|
|
response = test_client.get(url("/report?year=2025"))
|
2026-07-16 07:00:42 +02:00
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert b"2025 report" in response.data
|
|
|
|
|
assert b"year=2024" in response.data
|
|
|
|
|
assert b"year=2026" in response.data
|