S1: shared value helpers (parse_dt/coerce_bool) + auto-Secure session cookie
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 37s

M9 hardening/DRY pass — section S1, commit 1 (the shared-toolkit foundation):

- Add src/thoughtsync/common.py with parse_dt() and coerce_bool(): one home for
  the ISO-date and truthy-flag coercions that were duplicated across modules.
  notes.py adopts them and deletes _parse_iso_dt, _iso_to_dt and _truthy
  (rule 22 — old copies removed; callers, incl. tests, updated).
- Security: the session cookie is now marked Secure automatically on any request
  that arrived over HTTPS (directly or via a proxy's X-Forwarded-Proto), via a
  SecureCookieSessionInterface override. Hardens HTTPS deployments without
  breaking plain-HTTP LAN installs — no config.

Behavior-preserving refactor + one security hardening. The backend serialization
layer, the json_error sweep, and the notes.py split follow as their own commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-23 19:24:36 -04:00
co-authored by Claude Opus 4.8
parent 3c78060051
commit 2abed7132c
4 changed files with 87 additions and 48 deletions
+16 -14
View File
@@ -3,15 +3,14 @@ from datetime import datetime, timezone
import pytest
from thoughtsync.app import create_app
from thoughtsync.common import coerce_bool, parse_dt
from thoughtsync.models.note import NOTE_COLORS, Note
from thoughtsync.notes import (
_attachment_ext,
_escape_like,
_header_filename,
_truthy,
_keep_spec,
_native_spec,
_parse_iso_dt,
_safe_filename,
_slugify,
_usec_to_dt,
@@ -170,18 +169,19 @@ def test_escape_like():
assert _escape_like("plain") == "plain"
def test_parse_iso_dt():
def test_parse_dt():
# A full ISO instant round-trips (used to validate the Timeline date range).
d = _parse_iso_dt("2026-07-19T12:30:00+00:00")
d = parse_dt("2026-07-19T12:30:00+00:00")
assert (d.year, d.month, d.day, d.hour, d.minute) == (2026, 7, 19, 12, 30)
assert d.tzinfo is not None
# a trailing Z is accepted as UTC
assert _parse_iso_dt("2026-07-19T00:00:00Z").tzinfo is not None
assert parse_dt("2026-07-19T00:00:00Z").tzinfo is not None
# a plain calendar date parses to midnight
assert _parse_iso_dt("2026-07-19").hour == 0
# garbage raises (the endpoint turns this into a 400)
with pytest.raises(ValueError):
_parse_iso_dt("not-a-date")
assert parse_dt("2026-07-19").hour == 0
# garbage / non-strings return None (the endpoint turns this into a 400)
assert parse_dt("not-a-date") is None
assert parse_dt("") is None
assert parse_dt(None) is None
async def test_titles_requires_auth(app):
@@ -264,11 +264,13 @@ def test_header_filename():
assert _header_filename("") == "file"
def test_truthy():
assert _truthy("true") and _truthy("1") and _truthy("yes") and _truthy("on")
assert not _truthy("false")
assert not _truthy(None)
assert not _truthy("")
def test_coerce_bool():
assert coerce_bool("true") and coerce_bool("1") and coerce_bool("yes") and coerce_bool("on")
assert coerce_bool(True)
assert not coerce_bool("false")
assert not coerce_bool(None)
assert not coerce_bool("")
assert not coerce_bool(False)
def test_normalize_recurrence():