Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import time
import uuid
from collections.abc import Generator

import pytest
Expand All @@ -9,6 +10,20 @@
]


@pytest.fixture(autouse=True)
def fake_browser_session_id(monkeypatch: pytest.MonkeyPatch) -> None:
"""Bypass the browser-backed local storage component during tests.

``streamlit.testing.v1.AppTest`` cannot run the local storage frontend, so
the real component would block forever. Each simulated session instead gets
a fresh id, matching how distinct browsers behave in production.
"""
monkeypatch.setattr(
"open_cups.session_state.load_or_create_session_id",
lambda: str(uuid.uuid4()),
)


class MockTime:
def __init__(self, initial_time: float | None = None) -> None:
self._current_time = initial_time if initial_time is not None else time.time()
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies = [
"qrcode>=8.1",
"streamlit>=1.49.1",
"streamlit-autorefresh>=1.0.1",
"streamlit-local-storage>=0.0.25",
]

[dependency-groups]
Expand Down Expand Up @@ -75,6 +76,7 @@ mypy_path = "src"
module = [
"streamlit.*",
"streamlit_autorefresh.*",
"streamlit_local_storage.*",
"plotly.*",
]
ignore_missing_imports = true
30 changes: 26 additions & 4 deletions src/open_cups/session_state.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,43 @@
import uuid

import streamlit as st
from streamlit_local_storage import LocalStorage

SESSION_ID_STORAGE_KEY = "session_id"


class SessionState:
"""Per-user session state wrapper.

The session identity is persisted in the browser's local storage so it
survives Streamlit session loss (websocket drops while a phone is locked,
page reloads). Local storage is scoped to the browser and never travels in
the URL, so sharing the room link cannot leak a user's identity.

See https://docs.streamlit.io/develop/api-reference/caching-and-state/st.session_state
for more details.
"""

def __init__(self) -> None:
if "session_id" not in st.session_state:
existing = st.query_params.get("session_id")
st.session_state.session_id = existing or str(uuid.uuid4())
st.query_params["session_id"] = st.session_state.session_id
if SESSION_ID_STORAGE_KEY not in st.session_state:
st.session_state.session_id = load_or_create_session_id()
Comment on lines +22 to +23

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Use the shared key constant consistently for st.session_state to avoid divergence.

You’re checking for SESSION_ID_STORAGE_KEY in st.session_state but assigning via the session_id attribute. If the constant changes, the check and assignment will diverge. Please write to st.session_state[SESSION_ID_STORAGE_KEY] = load_or_create_session_id() and have the session_id property read from that key so there’s a single source of truth.

Suggested implementation:

    def __init__(self) -> None:
        if SESSION_ID_STORAGE_KEY not in st.session_state:
            st.session_state[SESSION_ID_STORAGE_KEY] = load_or_create_session_id()

    @property
    def session_id(self) -> str:
        return str(st.session_state[SESSION_ID_STORAGE_KEY])

You should also search the rest of the codebase for any direct uses of st.session_state.session_id and update them to use st.session_state[SESSION_ID_STORAGE_KEY] or this session_id property, to ensure everything consistently uses the shared key constant.


@property
def session_id(self) -> str:
return str(st.session_state.session_id)


def load_or_create_session_id() -> str:
"""Return the browser's persisted session id, creating one if absent.

This is the single seam touching the browser-backed component, which lets
tests patch it without driving a real frontend.
"""
local_storage = LocalStorage()
existing = local_storage.getItem(SESSION_ID_STORAGE_KEY)
if existing:
return str(existing)

new_session_id = str(uuid.uuid4())
local_storage.setItem(SESSION_ID_STORAGE_KEY, new_session_id)
return new_session_id
14 changes: 14 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading