Settings was every registry group, then invites, accounts, groups and activity, in one scroll. It is now six tabs held in ?tab=: - General: General, Notes, Attachments, Links. - People: Access, Invites, Accounts, Groups. - Email, with a line saying whether email is on and what it still needs. - Security. - Activity. A registry group that no tab names lands on General, so it can't go missing. Save saves only the current tab. Edits waiting on other tabs keep their values and mark their tab with a dot. Leaving the page with unsaved edits asks first. Review fixes: - On a phone, a field stacks under its label instead of overflowing beside it. - Descriptions are easier to read. - The SMTP encryption choice shows readable names (choice_labels). - The test-email button uses .btn-sm and is disabled while email is off. - The accounts list is headed Accounts, not People, now that People is a tab. - The docs point at the new paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
3.0 KiB
Python
79 lines
3.0 KiB
Python
from inkwell.settings import MAX_BODY_MB, REGISTRY, validate_updates
|
|
|
|
|
|
def test_registry_has_expected_keys():
|
|
keys = {d.key for d in REGISTRY}
|
|
assert {"site_name", "allow_registration", "session_ttl_days"} <= keys
|
|
|
|
|
|
def test_validate_rejects_unknown_key():
|
|
clean, error = validate_updates({"nope": 1})
|
|
assert error is not None
|
|
assert clean == {}
|
|
|
|
|
|
def test_validate_coerces_bool_and_int():
|
|
clean, error = validate_updates({"allow_registration": "true", "session_ttl_days": "45"})
|
|
assert error is None
|
|
assert clean["allow_registration"] is True
|
|
assert clean["session_ttl_days"] == 45
|
|
|
|
|
|
def test_each_integer_with_a_dangerous_range_is_bounded():
|
|
"""Session length 0 expired every session at once; an attachment limit above the
|
|
body ceiling allowed files no request could carry; a negative one refused all (#5384)."""
|
|
for key, bad, good in (
|
|
("session_ttl_days", 0, 1),
|
|
("trash_retention_days", -1, 0),
|
|
("max_attachment_mb", 0, 1),
|
|
("max_attachment_mb", MAX_BODY_MB, MAX_BODY_MB - 1),
|
|
):
|
|
clean, error = validate_updates({key: str(bad)})
|
|
assert error is not None and clean == {}, f"{key}={bad} refused"
|
|
clean, error = validate_updates({key: str(good)})
|
|
assert error is None and clean == {key: good}, f"{key}={good} kept"
|
|
|
|
|
|
def test_a_stored_value_from_before_the_bounds_reads_as_the_nearest_bound():
|
|
from inkwell.settings import _BY_KEY, _coerce
|
|
|
|
assert _coerce(_BY_KEY["session_ttl_days"], 0) == 1
|
|
assert _coerce(_BY_KEY["max_attachment_mb"], 500) == MAX_BODY_MB - 1
|
|
assert _coerce(_BY_KEY["trash_retention_days"], 30) == 30
|
|
|
|
|
|
def test_validate_rejects_bad_int():
|
|
clean, error = validate_updates({"session_ttl_days": "not-a-number"})
|
|
assert error is not None
|
|
assert clean == {}
|
|
|
|
|
|
def test_an_empty_secret_keeps_what_is_saved():
|
|
clean, error = validate_updates({"smtp_password": "", "smtp_host": "smtp.example.test"})
|
|
assert error is None
|
|
assert clean == {"smtp_host": "smtp.example.test"}
|
|
|
|
|
|
def test_a_choice_must_be_one_of_its_values():
|
|
assert validate_updates({"smtp_security": "tls"}) == ({"smtp_security": "tls"}, None)
|
|
clean, error = validate_updates({"smtp_security": "ssl"})
|
|
assert clean == {} and error
|
|
|
|
|
|
def test_the_public_address_is_an_http_url_without_a_trailing_slash():
|
|
assert validate_updates({"public_url": "https://notes.example.test/"}) == (
|
|
{"public_url": "https://notes.example.test"},
|
|
None,
|
|
)
|
|
assert validate_updates({"public_url": ""}) == ({"public_url": ""}, None)
|
|
clean, error = validate_updates({"public_url": "javascript:alert(1)"})
|
|
assert clean == {} and error
|
|
|
|
|
|
def test_choice_labels_name_every_choice():
|
|
"""The UI pairs labels with choices by position, so a label list of another length
|
|
would put one choice's name on another."""
|
|
for d in REGISTRY:
|
|
if d.choice_labels is not None:
|
|
assert d.choices is not None and len(d.choice_labels) == len(d.choices), d.key
|