1"""Per-user data helper (backend/userdata.py).
3Identity comes from X-Auth-Email (Caddy in prod; a `local` fallback in dev), and
4one user's saved configs must never be visible to another. Tests the module
5directly so it needs neither the full app nor its heavy deps.
13sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15pytest.importorskip(
"fastapi", reason=
"userdata imports fastapi.Request")
17from backend
import userdata
25@pytest.fixture(autouse=True)
27 monkeypatch.setenv(
"USERDATA_DIR", str(tmp_path))
31 assert userdata.current_user(
_Req({})) ==
"local"
35 assert userdata.current_user(
_Req({
"X-Auth-Email":
"Alice@Berkeley.EDU"})) ==
"alice@berkeley.edu"
39 who = userdata.current_user(
_Req({
"X-Auth-Email":
"../../etc/passwd"}))
40 assert "/" not in who
and ".." not in who
44 userdata.write_config(
"u@x.edu",
"My Baseline", {
"input": {
"a": 1}})
45 userdata.write_config(
"u@x.edu",
"Higher Pc!!", {
"input": {
"a": 2}})
46 assert [c[
"slug"]
for c
in userdata.list_configs(
"u@x.edu")] == [
"higher-pc",
"my-baseline"]
47 blob = userdata.read_config(
"u@x.edu",
"my-baseline")
48 assert blob[
"config"] == {
"input": {
"a": 1}}
and blob[
"name"] ==
"My Baseline"
49 assert userdata.delete_config(
"u@x.edu",
"my-baseline")
is True
50 assert userdata.delete_config(
"u@x.edu",
"my-baseline")
is False
54 userdata.write_config(
"alice@x.edu",
"one", {
"input": {}})
55 assert userdata.list_configs(
"bob@x.edu") == []
59 with pytest.raises(ValueError):
60 userdata.write_config(
"u@x.edu",
"***", {})
64 assert userdata.read_config(
"u@x.edu",
"../../../etc/passwd")
is None
71 """An ?owner= param and the injected header must normalize identically, or
72 an ownership check hinges on two slightly different transforms."""
73 for raw
in [
"Alice@Berkeley.EDU",
"alice@berkeley.edu",
" Alice@Berkeley.EDU "]:
74 assert userdata.slug_user(raw) == userdata.current_user(
_Req({
"X-Auth-Email": raw}))
78 for raw
in [
"../..",
"a/b",
".",
"..",
""]:
79 slug = userdata.slug_user(raw)
80 assert "/" not in slug
and slug
not in (
"",
".",
"..")
84 """The cross-user browse inspects every sibling folder; if inspection
85 created them, one listing would invent a directory per user it looked at."""
86 userdata.user_dir(
"ghost@x.edu", create=
False)
87 assert list(tmp_path.iterdir()) == []
89 userdata.user_dir(
"real@x.edu")
90 assert (tmp_path /
"real@x.edu" / userdata.APP).is_dir()
94 assert userdata.all_users() == []
96 userdata.user_dir(
"alice@x.edu")
97 userdata.user_dir(
"bob@x.edu")
98 userdata.user_dir(
"carol@x.edu", app=
"someotherapp")
99 (tmp_path /
".hidden").mkdir()
100 (tmp_path /
"stray-file").write_text(
"")
102 assert userdata.all_users() == [
"alice@x.edu",
"bob@x.edu"]
103 assert userdata.all_users(app=
"someotherapp") == [
"carol@x.edu"]
107 monkeypatch.setenv(
"USERDATA_DIR", str(tmp_path /
"not-created-yet"))
108 assert userdata.all_users() == []
test_all_users_lists_only_people_with_data_for_this_app(tmp_path)
test_slug_user_matches_current_user_exactly()
test_current_user_falls_back_to_local_without_header()
_isolate(tmp_path, monkeypatch)
test_current_user_lowercases_the_email()
test_user_dir_create_false_does_not_conjure_a_tree(tmp_path)
test_all_users_survives_a_missing_root(tmp_path, monkeypatch)
test_slug_user_cannot_escape_the_root()
test_traversal_slug_reads_nothing()
test_invalid_name_rejected()
test_users_are_isolated()
test_write_read_list_delete_roundtrip()
test_identity_cannot_escape_via_traversal()