mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 18:07:20 +01:00
The Dart half of the credential sync: a `KuchniaCredentialService` that owns the value locally, a `sync_merge_kuchnia.dart` adapter mirroring the Python one, and a `_syncKuchniaCredential` tick alongside the banks and budget. The phone can now receive the panel password, which is the prerequisite for it fetching anything itself. One source of truth, per the answered question: the value lives in the synced document store and nowhere else. Deliberately *not* mirrored into `flutter_secure_storage` -- two homes for one value is how a stale password survives a change made on the other device, and the credential has to be readable synchronously at merge time anyway. Gated by a second shared fixture, `tests/fixtures/kuchnia_credential.json`, asserted by both suites. Two cases carry the weight: - an **unparsable** edit time must fall back to the same clock on both sides (Python catches `ValueError` from `fromisoformat`, Dart's `tryParse` returns null). Verified they agree on `wall_time_ms == 0`; mutating that expectation fails both suites. - a **blank half** contributes nothing, so a half-filled settings form cannot push an empty-string password that wins LWW against a real one. `KuchniaCredentialService.init()` is called in the WorkManager isolate as well as `main()`. A fresh isolate has its own static state and `_syncKuchniaCredential` no-ops when the singleton is missing, so without it a background tick would neither publish this device's credential nor receive a peer's -- and on the phone the background tick is most of the syncing there is. Same class of trap as the `initSyncDeviceId()` one already documented there. `.toDouble()` came off the bank record: the fields are already `double`, so it was a no-op the analyzer flags. The field *types* are what keep the encoding byte-identical to Python's, and `kuchnia_parity_test.dart` asserts that. 654 Dart tests, 1040 Python tests, 100% Python branch coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QQpCiPaSbyRpwsLkH3dgsX
120 lines
3.9 KiB
Python
Executable File
120 lines
3.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Regenerate the shared catering-credential parity fixture.
|
|
|
|
The fixture (``tests/fixtures/kuchnia_credential.json``) is read by *both*
|
|
``diet_guard/tests/test_kuchnia_credential_sync.py`` and
|
|
``app/test/sync_merge_kuchnia_test.dart``, for the same reason the menu
|
|
fixture is: two suites written independently from the same prose is not a
|
|
gate.
|
|
|
|
What has to agree across the two languages is narrow but sharp:
|
|
|
|
* an **unparsable** edit time must fall back to the same clock on both sides
|
|
(Python catches ``ValueError`` from ``fromisoformat``; Dart's
|
|
``DateTime.tryParse`` returns null), because a junk stamp that resolved
|
|
differently per device would hand the race to whichever side was more
|
|
forgiving, and
|
|
* a **blank half** must contribute nothing, or a half-filled settings form
|
|
pushes an empty-string password that wins LWW against a real one.
|
|
|
|
The node id is pinned because it differs per install and is not part of what
|
|
parity guarantees; the wall time and the field values are.
|
|
|
|
Run it from the repo root:
|
|
|
|
python3 scripts/build_kuchnia_credential_fixture.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from diet_guard.sync_merge._kuchnia import credential_to_log
|
|
|
|
FIXTURE = (
|
|
Path(__file__).resolve().parent.parent / "tests/fixtures/kuchnia_credential.json"
|
|
)
|
|
|
|
#: Stands in for the per-install uuid, which is not part of the parity claim.
|
|
_DEVICE_ID = "fixturedev"
|
|
|
|
_CASES: list[dict[str, object]] = [
|
|
{
|
|
"name": "a normal credential",
|
|
"username": "me@example.com",
|
|
"password": "hunter2",
|
|
"edited_at": "2026-08-23T10:00:00+02:00",
|
|
},
|
|
{
|
|
"name": "an unparsable edit time falls back to the epoch",
|
|
"username": "me@example.com",
|
|
"password": "hunter2",
|
|
"edited_at": "not-a-timestamp",
|
|
},
|
|
{
|
|
"name": "a blank password contributes nothing",
|
|
"username": "me@example.com",
|
|
"password": "",
|
|
"edited_at": "2026-08-23T10:00:00+02:00",
|
|
},
|
|
{
|
|
"name": "a blank username contributes nothing",
|
|
"username": "",
|
|
"password": "hunter2",
|
|
"edited_at": "2026-08-23T10:00:00+02:00",
|
|
},
|
|
]
|
|
|
|
|
|
def build() -> dict[str, object]:
|
|
"""Return the fixture: each case with the result Python produces."""
|
|
cases = []
|
|
with patch(
|
|
"diet_guard.sync_merge._kuchnia.device_id",
|
|
return_value=_DEVICE_ID,
|
|
):
|
|
for case in _CASES:
|
|
entry = dict(case)
|
|
log = credential_to_log(
|
|
str(case["username"]),
|
|
str(case["password"]),
|
|
str(case["edited_at"]),
|
|
)
|
|
entry["expected_empty"] = not log
|
|
if log:
|
|
record = log["kuchnia"]
|
|
entry["expected_wall_time_ms"] = record.fields["password"][
|
|
1
|
|
].wall_time_ms
|
|
entry["expected_username"] = record.fields["username"][0]
|
|
entry["expected_password"] = record.fields["password"][0]
|
|
cases.append(entry)
|
|
return {
|
|
"_comment": (
|
|
"Shared parity fixture for the catering credential's sync adapter. "
|
|
"Read by BOTH diet_guard/tests/test_kuchnia_credential_sync.py and "
|
|
"app/test/sync_merge_kuchnia_test.dart. Regenerate with "
|
|
"scripts/build_kuchnia_credential_fixture.py."
|
|
),
|
|
"device_id": _DEVICE_ID,
|
|
"cases": cases,
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
"""Write the fixture to disk.
|
|
|
|
Deliberately silent: ``T201`` bans ``print`` here, and the script's output
|
|
is the file itself -- ``git diff`` is how you check what changed.
|
|
"""
|
|
FIXTURE.parent.mkdir(parents=True, exist_ok=True)
|
|
with FIXTURE.open("w", encoding="utf-8") as handle:
|
|
json.dump(build(), handle, ensure_ascii=False, indent=2, sort_keys=True)
|
|
handle.write("\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|