Portal sign-in with the Ecare office login

Ecare staff sign in with their office username and password, checked by
the Ecare sign-in service from the backend (AUTH_TOKEN_API_URL); a good
Ecare login still needs a Portal User. Service down = "try again
shortly", never a wrong password. /api/auth/login, me, logout; sign-in
tries limited per address. Users page: Ecare office login or Portal
password per user. Context.md: 27/10 runs, the login retry bug.
71 test files, 1239 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ManikandanandClaude Opus 5.5 committed 2026-10-27 17:55:50 +05:30
1 parent 83a0cf65ae
commit 64efb68ea1
10 files changed
+556 -48

No files matched your search

+165
View File
@@ -273,6 +273,171 @@ def the_sql_table_allows_the_same_roles_and_moves_old_ones():
assert first < sql.index("SET role = 'Admin' WHERE role = 'Operator'"), "Admins move up before Operators take their place"
# ── Ecare office sign-in (2026-10-27) ────────────────────────────────────────
class FakeEcare:
"""EcareSignIn.check, canned: "ok", "bad" or "down"."""
def __init__(self, verdict="ok"):
self.verdict = verdict
self.calls = []
def check(self, username, password):
self.calls.append(username)
return self.verdict
def _ecare_accounts(verdict="ok"):
store, clock, ecare = FakeUserStore(), Clock(), FakeEcare(verdict)
acc = users.Accounts(store, now=clock, ecare=ecare)
acc.create_user("Asha", "Asha R", "Super Admin", "first-password-1")
acc.create_user("ravi", "Ravi", "Viewer", None, by="asha", ecare=True)
return acc, store, clock, ecare
class _Reply:
def __init__(self, body: bytes, status=200):
self._body, self.status = body, status
def read(self):
return self._body
def __enter__(self):
return self
def __exit__(self, *a):
return False
def _service(answer):
"""(EcareSignIn, the requests it sent) with urlopen giving `answer`:
bytes for a 200 body, an int for an HTTP error, or an exception."""
import urllib.error
sent = []
def urlopen(request, timeout=None):
sent.append((request, timeout))
if isinstance(answer, BaseException):
raise answer
if isinstance(answer, int):
raise urllib.error.HTTPError(request.full_url, answer, "x", {},
None)
return _Reply(answer)
return users.EcareSignIn("https://auth.example/api/login/token",
urlopen=urlopen), sent
@case
def the_ecare_service_is_asked_from_the_backend_with_a_15_second_limit():
import json
svc, sent = _service(b'{"success": true, "token": "abc"}')
assert svc.check(" Ravi ", "pw-123") == "ok"
request, timeout = sent[0]
assert timeout == 15 and request.get_method() == "POST"
assert request.full_url == "https://auth.example/api/login/token"
assert request.get_header("Content-type") == "application/json"
assert json.loads(request.data) == {"username": "Ravi",
"password": "pw-123"}
@case
def the_ecare_answer_reads_as_ok_bad_or_down():
import socket
import urllib.error
for answer, want in ((b'{"token": "abc"}', "ok"),
(b'{"token": ""}', "bad"),
(b'{"success": false, "token": "abc"}', "bad"),
(b'{"success": true}', "bad"),
(b"not json", "down"),
(401, "bad"), (403, "bad"), (400, "bad"),
# A wrong AUTH_TOKEN_API_URL, not a wrong password:
# else every user would lock themselves out.
(404, "down"), (405, "down"),
(500, "down"), (503, "down"),
(socket.timeout("slow"), "down"),
(urllib.error.URLError("no route"), "down"),
(ConnectionResetError(), "down")):
svc, _ = _service(answer)
got = svc.check("ravi", "pw-123")
assert got == want, (answer, got, want)
@case
def an_ecare_user_signs_in_with_the_office_password_only():
acc, store, _, ecare = _ecare_accounts("ok")
u = store.users["ravi"]
assert u["pw_iterations"] == 0 and u["must_change"] is False, u
user, why = acc.sign_in(" RAVI ", "office-pass")
assert why == "ok" and user["username"] == "ravi", why
assert ecare.calls == ["RAVI"], "the name as typed, trimmed"
assert ("ravi", "sign_in", "") in store.events
assert users.is_ecare(user) and not users.is_ecare(store.get("asha"))
@case
def a_local_user_never_reaches_the_ecare_service():
acc, _, _, ecare = _ecare_accounts("ok")
assert acc.sign_in("asha", "first-password-1")[1] == "ok"
assert acc.sign_in("asha", "office-pass")[1] == "bad"
assert ecare.calls == []
@case
def a_good_ecare_login_without_a_portal_user_is_no_access():
acc, store, _, _ = _ecare_accounts("ok")
assert acc.sign_in("stranger", "office-pass") == (None, "no_access")
assert ("stranger", "no_access", "") in store.events
assert "stranger" not in store.users, "never added by itself"
@case
def a_wrong_ecare_password_counts_toward_the_lock():
acc, store, clock, ecare = _ecare_accounts("bad")
for _ in range(4):
assert acc.sign_in("ravi", "nope")[1] == "bad"
assert acc.sign_in("ravi", "nope")[1] == "locked"
ecare.verdict = "ok"
n = len(ecare.calls)
assert acc.sign_in("ravi", "office-pass")[1] == "locked"
assert len(ecare.calls) == n, "a locked user is not sent to Ecare"
clock.t += timedelta(minutes=15, seconds=1)
assert acc.sign_in("ravi", "office-pass")[1] == "ok"
@case
def ecare_down_is_never_a_wrong_password():
acc, store, _, _ = _ecare_accounts("down")
assert acc.sign_in("ravi", "office-pass") == (None, "down")
assert store.users["ravi"]["failed_count"] == 0
assert ("ravi", "service_down", "") in store.events
acc.ecare = None # AUTH_TOKEN_API_URL not set
assert acc.sign_in("ravi", "office-pass") == (None, "down")
@case
def a_disabled_ecare_user_stays_out_and_empty_input_never_leaves():
acc, _, _, ecare = _ecare_accounts("ok")
acc.set_disabled("ravi", True, by="asha")
assert acc.sign_in("ravi", "office-pass") == (None, "disabled")
n = len(ecare.calls)
assert acc.sign_in("ravi", "")[1] == "bad"
assert acc.sign_in("", "pw")[1] == "bad"
assert len(ecare.calls) == n, "nothing empty is sent to Ecare"
@case
def an_ecare_users_password_is_changed_in_ecare_not_here():
acc, _, _, _ = _ecare_accounts()
for fn in (lambda: acc.reset_password("ravi", "temp-password-9",
by="asha"),
lambda: acc.change_password("ravi", "a", "new-password-1")):
try:
fn()
raise AssertionError("allowed")
except users.UserError as exc:
assert "Ecare" in str(exc), exc
@case
def sessions_time_out_after_thirty_idle_minutes():
clock = Clock()