1"""The share picker's roster (backend/directory.py).
3Sharing needs a list of people, and this app has never had one. The roster is
4the union of the auth service's login records and whoever already has data on
5the shared volume -- it needs both, because auth only learns about someone when
6they log in (30-day sessions, so the roster starts nearly empty) and dev runs
7with no auth service at all.
9The invariant that matters: auth being unset, down, slow, or lying must never
10break listing or sharing designs.
20sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
22pytest.importorskip(
"fastapi", reason=
"directory imports fastapi.Request")
24from stardesign
import directory
as shared_directory
26from backend
import directory, userdata
31 self.
headers = {
"cookie": cookie}
if cookie
else {}
34@pytest.fixture(autouse=True)
36 monkeypatch.setenv(
"USERDATA_DIR", str(tmp_path))
37 monkeypatch.delenv(
"AUTH_USERS_URL", raising=
False)
39 monkeypatch.setattr(shared_directory,
"_cache",
None)
43 def fake(req, timeout=None):
49 return json.dumps(rows).encode()
54 def __exit__(self, *a):
59 monkeypatch.setenv(
"AUTH_USERS_URL",
"https://auth.example.org")
60 monkeypatch.setattr(shared_directory.urllib.request,
"urlopen", fake)
64 """Dev, and the first weeks in prod: no auth roster, but the picker still
66 userdata.user_dir(
"alice@berkeley.edu")
67 userdata.user_dir(
"bob@berkeley.edu")
68 assert directory.roster(
_Req()) == [
69 {
"email":
"alice@berkeley.edu",
"name":
""},
70 {
"email":
"bob@berkeley.edu",
"name":
""},
75 """The volume only ever holds the slug; auth knows the real address and the
76 display name, and the same person must not appear twice."""
77 userdata.user_dir(
"alice@berkeley.edu")
79 {
"email":
"Alice@Berkeley.edu",
"name":
"Alice Adams"},
80 {
"email":
"carol@berkeley.edu",
"name":
"Carol Chen"},
82 assert directory.roster(
_Req()) == [
83 {
"email":
"Alice@Berkeley.edu",
"name":
"Alice Adams"},
84 {
"email":
"carol@berkeley.edu",
"name":
"Carol Chen"},
89 _fake_auth(monkeypatch, [{
"email":
"dana@berkeley.edu",
"name":
"Dana"}])
90 assert [u[
"email"]
for u
in directory.roster(
_Req())] == [
"dana@berkeley.edu"]
94 """This app becomes an authenticated-request forwarder. It must present the
95 caller's own cookie and nothing else, to a URL it took from the environment
96 rather than the request."""
99 directory.roster(
_Req(cookie=
"session=abc123"))
101 assert req.full_url ==
"https://auth.example.org/users"
102 assert req.get_header(
"Cookie") ==
"session=abc123"
106 assert set(req.headers) == {
"Cookie",
"User-agent"}
109@pytest.mark.parametrize(
"boom", [
110 urllib.error.URLError(
"down"),
112 TimeoutError(
"slow"),
113 ValueError(
"not json"),
116 userdata.user_dir(
"alice@berkeley.edu")
117 monkeypatch.setenv(
"AUTH_USERS_URL",
"https://auth.example.org")
119 def explode(req, timeout=None):
122 monkeypatch.setattr(shared_directory.urllib.request,
"urlopen", explode)
123 assert [u[
"email"]
for u
in directory.roster(
_Req())] == [
"alice@berkeley.edu"]
126@pytest.mark.parametrize("garbage", [{"not": "a list"}, ["a string"], [{"no": "email"}]])
128 userdata.user_dir(
"alice@berkeley.edu")
130 assert [u[
"email"]
for u
in directory.roster(
_Req())] == [
"alice@berkeley.edu"]
135 _fake_auth(monkeypatch, [{
"email":
"dana@berkeley.edu",
"name":
"Dana"}], calls=calls)
136 directory.roster(
_Req())
137 directory.roster(
_Req())
138 assert len(calls) == 1,
"listing designs must not be a network call per request"
142 _fake_auth(monkeypatch, [{
"email":
"Alice@Berkeley.edu",
"name":
"Alice Adams"}])
143 assert directory.display_names(
_Req())[
"alice@berkeley.edu"] ==
"Alice Adams"
147 """The cache must not mistake "never fetched" for "fetched just now".
149 `time.monotonic()` counts from boot, so a sentinel timestamp of 0.0 reads as
150 stale on a machine that has been up for hours and as *fresh* on one that has
151 not. That shipped: for the first TTL after every restart the roster came back
152 empty, from a cache that had never been filled, without auth being asked.
153 Local runs never caught it -- a fresh CI container did.
156 _fake_auth(monkeypatch, [{
"email":
"dana@berkeley.edu",
"name":
"Dana"}], calls=calls)
157 monkeypatch.setattr(shared_directory,
"_cache",
None)
158 monkeypatch.setattr(shared_directory.time,
"monotonic",
lambda: 3.0)
160 assert [u[
"email"]
for u
in directory.roster(
_Req())] == [
"dana@berkeley.edu"]
161 assert len(calls) == 1,
"a cold cache must hit auth, whatever the clock reads"
__init__(self, cookie="")
test_auth_users_who_never_opened_this_app_are_still_shareable(monkeypatch)
test_malformed_auth_response_is_ignored(monkeypatch, garbage)
test_forwards_only_the_session_cookie(monkeypatch)
test_volume_users_alone_when_auth_is_unset()
test_a_fresh_process_asks_auth_even_on_a_just_booted_machine(monkeypatch)
test_display_names_maps_slugs(monkeypatch)
_isolate(tmp_path, monkeypatch)
test_union_prefers_auths_spelling_and_name(monkeypatch)
_fake_auth(monkeypatch, rows, *, calls=None)
test_auth_failure_degrades_to_the_volume(monkeypatch, boom)
test_roster_is_cached(monkeypatch)
static double read(int32_t code)