STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_session_isolation.py
Go to the documentation of this file.
1"""Per-user live-state isolation for the Engine Design backend.
2
3Two users connected at once must not share the live config, the optimizer job
4state, or the controller. Identity is the ``X-Auth-Email`` header (Caddy in prod,
5absent in dev -> ``local``). These tests drive the real dependency and endpoint
6coroutines directly -- no HTTP client needed -- so they exercise exactly what
7Caddy's forwarded header feeds into the app.
8
9See backend/session.py for the mechanism.
10"""
11
12from __future__ import annotations
13
14import asyncio
15import threading
16import time
17
18import pytest
19from fastapi import HTTPException
20from starlette.requests import Request
21
22from backend import session as session_mod
23from backend.session import SessionRegistry, get_session, JOB_SEMAPHORE, MAX_CONCURRENT_JOBS
24from backend.routers import config as config_router
25from backend.routers import optimizer as optimizer_router
26from backend.routers import control as control_router
27
28
29def _request(email: str | None) -> Request:
30 """A minimal ASGI request carrying (or omitting) the auth header."""
31 headers = [(b"x-auth-email", email.encode())] if email else []
32 return Request(
33 {
34 "type": "http",
35 "method": "GET",
36 "path": "/",
37 "query_string": b"",
38 "headers": headers,
39 }
40 )
41
42
43def _run(coro):
44 return asyncio.run(coro)
45
46
47# ── Identity / registry ──────────────────────────────────────────────────────
48
49
51 a = get_session(_request("alice@berkeley.edu"))
52 b = get_session(_request("bob@berkeley.edu"))
53 assert a is not b
54 assert a.user != b.user
55
56
58 a1 = get_session(_request("carol@berkeley.edu"))
59 a2 = get_session(_request("carol@berkeley.edu"))
60 assert a1 is a2
61
62
64 """No Caddy in dev -> no header -> everyone is `local`, one shared session,
65 with the default config loaded (the old single-user behaviour)."""
66 s1 = get_session(_request(None))
67 s2 = get_session(_request(None))
68 assert s1 is s2
69 assert s1.user == "local"
70 assert s1.app_state.has_config()
71
72
74 reg = SessionRegistry()
75 monkeypatch.setattr(session_mod, "SESSION_TTL_SECONDS", 100)
76 first = reg.get("dave@berkeley.edu")
77 # Backdate its last-used well past the TTL, then access again.
78 first.last_used = time.monotonic() - 1000
79 second = reg.get("dave@berkeley.edu")
80 assert second is not first # evicted and rebuilt
81
82
84 assert MAX_CONCURRENT_JOBS == 3
85 assert JOB_SEMAPHORE._value == 3
86
87
88# ── Live config isolation ────────────────────────────────────────────────────
89
90
92 a = get_session(_request("erin@berkeley.edu"))
93 b = get_session(_request("frank@berkeley.edu"))
94
95 base_b = _run(config_router.get_config(session=b))["config"]["design_requirements"]["target_thrust"]
96 target = base_b + 137.0 # a value clearly different from default
97
98 _run(config_router.update_config(
99 {"design_requirements": {"target_thrust": target}}, session=a
100 ))
101
102 a_thrust = _run(config_router.get_config(session=a))["config"]["design_requirements"]["target_thrust"]
103 b_thrust = _run(config_router.get_config(session=b))["config"]["design_requirements"]["target_thrust"]
104
105 assert a_thrust == pytest.approx(target)
106 assert b_thrust == pytest.approx(base_b) # B untouched by A's edit
107 assert a_thrust != pytest.approx(b_thrust)
108
109
110# ── Optimizer job-state isolation ────────────────────────────────────────────
111
112
114 a = get_session(_request("gina@berkeley.edu"))
115 b = get_session(_request("hank@berkeley.edu"))
116
117 # Simulate A mid-run with results ready; B has done nothing.
118 a.optimizer.optimization_status["running"] = True
119 a.optimizer.optimization_status["results"] = {"objective": 1.23}
120
121 assert _run(optimizer_router.get_layer1_status(session=a))["running"] is True
122 assert _run(optimizer_router.get_layer1_status(session=b))["running"] is False
123
124 # A sees its results; B is a clean 404, never A's data.
125 assert _run(optimizer_router.get_layer1_results(session=a))["results"] == {"objective": 1.23}
126 with pytest.raises(HTTPException) as exc:
127 _run(optimizer_router.get_layer1_results(session=b))
128 assert exc.value.status_code == 404
129
130
132 a = get_session(_request("iris@berkeley.edu"))
133 b = get_session(_request("jack@berkeley.edu"))
134 # Both mid-run: each has its own live stop event (as run_layer1 creates).
135 a.optimizer.optimization_status["running"] = True
136 b.optimizer.optimization_status["running"] = True
137 a.optimizer.stop_event = threading.Event()
138 b.optimizer.stop_event = threading.Event()
139
140 _run(optimizer_router.stop_layer1(session=a))
141
142 # A's stop event is set; B's is a different event and stays clear.
143 assert a.optimizer.stop_event.is_set()
144 assert b.optimizer.stop_event is not a.optimizer.stop_event
145 assert not b.optimizer.stop_event.is_set()
146 assert b.optimizer.optimization_status["running"] is True
147
148
149# ── Controller isolation ─────────────────────────────────────────────────────
150
151
153 a = get_session(_request("kim@berkeley.edu"))
154 b = get_session(_request("liam@berkeley.edu"))
155
156 _run(control_router.init_controller(control_router.ControllerInitRequest(), session=a))
157
158 assert _run(control_router.get_controller_status(session=a))["initialized"] is True
159 # B never initialized one -- must not inherit A's controller.
160 assert _run(control_router.get_controller_status(session=b))["initialized"] is False
161 assert b.controller.controller is None
Request _request(str|None email)
test_ttl_eviction_recreates_a_fresh_session(monkeypatch)