Skip to content

Commit 210ba65

Browse files
committed
tests
1 parent e789aff commit 210ba65

File tree

4 files changed

+175
-32
lines changed

4 files changed

+175
-32
lines changed

backend/tests/conftest.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,11 +273,27 @@ def mock_modal_dict(mocker):
273273
fake_dict = {}
274274
mock_dict = mocker.MagicMock()
275275

276-
mock_dict.__getitem__ = lambda self, key: fake_dict[key]
277-
mock_dict.__setitem__ = lambda self, key, val: fake_dict.__setitem__(key, val)
278-
mock_dict.__delitem__ = lambda self, key: fake_dict.__delitem__(key)
279-
mock_dict.__contains__ = lambda self, key: key in fake_dict
280-
mock_dict.get = lambda self, key, default=None: fake_dict.get(key, default)
276+
def getitem(_, key):
277+
return fake_dict[key]
278+
279+
def setitem(_, key, value):
280+
fake_dict[key] = value
281+
282+
def delitem(_, key):
283+
del fake_dict[key]
284+
285+
def contains(_, key):
286+
return key in fake_dict
287+
288+
def get(_, key, default=None):
289+
return fake_dict.get(key, default)
290+
291+
mock_dict.__getitem__ = getitem
292+
mock_dict.__setitem__ = setitem
293+
mock_dict.__delitem__ = delitem
294+
mock_dict.__contains__ = contains
295+
mock_dict.get = get
296+
mock_dict.keys.side_effect = fake_dict.keys
281297

282298
mocker.patch('modal.Dict.from_name', return_value=mock_dict)
283299
return fake_dict

backend/tests/unit/test_r2_connector.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import base64
22
from botocore.exceptions import ClientError
33

4+
from database.r2_connector import DEFAULT_PRESIGNED_URL_TTL
5+
46

57
class TestR2ConnectorInitialization:
68
"""Test connector initialization."""
@@ -142,6 +144,9 @@ def test_fetch_page_success(self, mock_r2_connector):
142144
Prefix="ns/",
143145
MaxKeys=3,
144146
)
147+
for call in mock_client.generate_presigned_url.call_args_list:
148+
kwargs = call.kwargs
149+
assert kwargs['ExpiresIn'] == DEFAULT_PRESIGNED_URL_TTL
145150

146151
def test_fetch_page_handles_error(self, mock_r2_connector):
147152
connector, mock_client, _ = mock_r2_connector
@@ -179,6 +184,9 @@ def test_fetch_page_cursor_fallback(self, mock_r2_connector):
179184
MaxKeys=3,
180185
StartAfter='ns/vid2.mp4',
181186
)
187+
for call in mock_client.generate_presigned_url.call_args_list:
188+
kwargs = call.kwargs
189+
assert kwargs['ExpiresIn'] == DEFAULT_PRESIGNED_URL_TTL
182190

183191

184192
class TestFetchAllVideoData:
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import pytest
2+
3+
from cache.video_cache import VideoCache, VIDEO_PAGE_TTL_SECONDS
4+
5+
6+
class TestVideoCachePagination:
7+
def test_get_page_respects_ttl(self, mock_modal_dict, mocker):
8+
time_mock = mocker.patch("cache.video_cache.time")
9+
time_mock.time.return_value = 1_000.0
10+
11+
cache = VideoCache(environment="dev")
12+
cache.set_page(
13+
namespace="ns",
14+
page_token=None,
15+
page_size=20,
16+
videos=[{"file_name": "vid.mp4"}],
17+
next_token="token",
18+
)
19+
20+
# Within TTL -> entry returned
21+
time_mock.time.return_value = 1_000.0 + VIDEO_PAGE_TTL_SECONDS - 10
22+
entry = cache.get_page("ns", None, 20)
23+
assert entry is not None
24+
assert entry["videos"][0]["file_name"] == "vid.mp4"
25+
26+
# Beyond TTL -> entry evicted and not returned
27+
time_mock.time.return_value = 1_000.0 + VIDEO_PAGE_TTL_SECONDS + 1
28+
expired_entry = cache.get_page("ns", None, 20)
29+
assert expired_entry is None
30+
assert not mock_modal_dict # key removed
31+
32+
def test_namespace_metadata_respects_ttl(self, mock_modal_dict, mocker):
33+
time_mock = mocker.patch("cache.video_cache.time")
34+
time_mock.time.return_value = 2_000.0
35+
36+
cache = VideoCache(environment="dev")
37+
cache.set_namespace_metadata("ns", {"total_videos": 5})
38+
39+
time_mock.time.return_value = 2_000.0 + VIDEO_PAGE_TTL_SECONDS - 10
40+
metadata = cache.get_namespace_metadata("ns")
41+
assert metadata is not None
42+
assert metadata["total_videos"] == 5
43+
44+
time_mock.time.return_value = 2_000.0 + VIDEO_PAGE_TTL_SECONDS + 1
45+
expired_metadata = cache.get_namespace_metadata("ns")
46+
assert expired_metadata is None
47+
assert not mock_modal_dict

frontend/streamlit/pages/search_demo.py

Lines changed: 99 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@
2323
st.session_state.repo_page_tokens = []
2424
if 'repo_current_page_idx' not in st.session_state:
2525
st.session_state.repo_current_page_idx = 0
26+
if 'repo_total_videos' not in st.session_state:
27+
st.session_state.repo_total_videos = 0
28+
if 'repo_total_pages' not in st.session_state:
29+
st.session_state.repo_total_pages = 0
30+
if 'repo_loading' not in st.session_state:
31+
st.session_state.repo_loading = False
32+
if 'repo_error' not in st.session_state:
33+
st.session_state.repo_error = None
34+
if 'repo_action' not in st.session_state:
35+
st.session_state.repo_action = None
2636

2737
# Configs
2838
SEARCH_API_URL = Config.SEARCH_API_URL
@@ -35,6 +45,11 @@
3545
IS_INTERNAL_ENV = Config.IS_INTERNAL_ENV
3646

3747

48+
def set_repo_action(action: str) -> None:
49+
"""Schedule a repository navigation action for the next render."""
50+
st.session_state.repo_action = action
51+
52+
3853
def search_videos(query: str):
3954
"""Send search query to backend."""
4055
try:
@@ -62,6 +77,8 @@ def fetch_videos_page(page_token: str | None = None, page_size: int = REPO_PAGE_
6277
return {
6378
"videos": data.get("videos", []),
6479
"next_page_token": data.get("next_page_token"),
80+
"total_videos": data.get("total_videos", 0),
81+
"total_pages": data.get("total_pages", 0),
6582
}
6683
return {"error": f"Fetch failed with status {resp.status_code}"}
6784
except requests.RequestException as e:
@@ -78,6 +95,8 @@ def load_repository_page(page_token: str | None = None, append: bool = False) ->
7895

7996
videos = result.get("videos", [])
8097
next_token = result.get("next_page_token")
98+
total_videos = result.get("total_videos", 0) or 0
99+
total_pages = result.get("total_pages", 0) or 0
81100

82101
if append:
83102
st.session_state.repo_pages.append(videos)
@@ -90,6 +109,8 @@ def load_repository_page(page_token: str | None = None, append: bool = False) ->
90109

91110
st.session_state.repo_videos = st.session_state.repo_pages[st.session_state.repo_current_page_idx]
92111
st.session_state.repo_next_token = next_token
112+
st.session_state.repo_total_videos = total_videos
113+
st.session_state.repo_total_pages = total_pages
93114
st.session_state.repo_initialized = True
94115
return True, None
95116

@@ -102,6 +123,11 @@ def reset_repository_state() -> None:
102123
st.session_state.repo_pages = []
103124
st.session_state.repo_page_tokens = []
104125
st.session_state.repo_current_page_idx = 0
126+
st.session_state.repo_total_videos = 0
127+
st.session_state.repo_total_pages = 0
128+
st.session_state.repo_loading = False
129+
st.session_state.repo_error = None
130+
st.session_state.repo_action = None
105131

106132

107133
def upload_files_to_backend(files_data: list[tuple[bytes, str, str]]):
@@ -423,42 +449,88 @@ def delete_confirmation_dialog(hashed_identifier: str, filename: str):
423449

424450
current_page_idx = st.session_state.repo_current_page_idx
425451
total_loaded_pages = len(st.session_state.repo_pages)
426-
has_more_pages = st.session_state.repo_next_token is not None
452+
repo_action = st.session_state.repo_action
453+
st.session_state.repo_action = None
454+
455+
if repo_action == "prev" and not st.session_state.repo_loading:
456+
if current_page_idx > 0:
457+
st.session_state.repo_current_page_idx -= 1
458+
st.session_state.repo_videos = st.session_state.repo_pages[st.session_state.repo_current_page_idx]
459+
st.session_state.repo_error = None
460+
st.rerun()
461+
462+
if repo_action == "next" and not st.session_state.repo_loading:
463+
if current_page_idx + 1 < total_loaded_pages:
464+
st.session_state.repo_current_page_idx += 1
465+
st.session_state.repo_videos = st.session_state.repo_pages[st.session_state.repo_current_page_idx]
466+
st.session_state.repo_error = None
467+
st.rerun()
468+
elif st.session_state.repo_next_token:
469+
st.session_state.repo_loading = True
470+
try:
471+
with st.spinner("Loading next page..."):
472+
success, error = load_repository_page(
473+
page_token=st.session_state.repo_next_token,
474+
append=True
475+
)
476+
finally:
477+
st.session_state.repo_loading = False
478+
if not success and error:
479+
st.session_state.repo_error = error
480+
else:
481+
st.session_state.repo_error = None
482+
st.rerun()
427483

428-
prev_disabled = current_page_idx <= 0
429-
next_disabled = (current_page_idx >= total_loaded_pages - 1) and not has_more_pages
484+
current_page_idx = st.session_state.repo_current_page_idx
485+
total_loaded_pages = len(st.session_state.repo_pages)
486+
total_pages = st.session_state.repo_total_pages or 0
487+
total_videos = st.session_state.repo_total_videos or 0
488+
489+
prev_disabled = st.session_state.repo_loading or current_page_idx <= 0
490+
if st.session_state.repo_loading:
491+
next_disabled = True
492+
elif total_pages > 0:
493+
next_disabled = (current_page_idx + 1) >= total_pages
494+
else:
495+
next_disabled = True
430496

431497
nav_info_col, nav_prev_col, nav_next_col = st.columns([6, 0.3, 0.3])
432498

433499
with nav_info_col:
434-
page_label = f"Page {current_page_idx + 1} of {max(total_loaded_pages, 1)}"
435-
if has_more_pages:
436-
page_label += " (more available)"
437-
st.markdown(f"<div style='text-align:left;font-weight:600;'>{page_label}</div>", unsafe_allow_html=True)
500+
if total_pages > 0:
501+
current_display_page = current_page_idx + 1
502+
else:
503+
current_display_page = 0
504+
page_label = f"Page {current_display_page} of {total_pages}"
505+
video_suffix = "video" if total_videos == 1 else "videos"
506+
page_label += f" • {total_videos} {video_suffix}"
507+
st.markdown(
508+
f"<div style='text-align:left;font-weight:600;'>{page_label}</div>",
509+
unsafe_allow_html=True
510+
)
438511

439512
with nav_prev_col:
440-
if st.button("◄", disabled=prev_disabled, use_container_width=True, key="repo_prev_btn"):
441-
if current_page_idx > 0:
442-
st.session_state.repo_current_page_idx -= 1
443-
st.session_state.repo_videos = st.session_state.repo_pages[st.session_state.repo_current_page_idx]
444-
st.rerun()
513+
st.button(
514+
"◄",
515+
disabled=prev_disabled,
516+
use_container_width=True,
517+
key="repo_prev_btn",
518+
on_click=set_repo_action,
519+
args=("prev",),
520+
)
445521

446522
with nav_next_col:
447-
if st.button("►", disabled=next_disabled, use_container_width=True, key="repo_next_btn"):
448-
if current_page_idx + 1 < total_loaded_pages:
449-
st.session_state.repo_current_page_idx += 1
450-
st.session_state.repo_videos = st.session_state.repo_pages[st.session_state.repo_current_page_idx]
451-
st.rerun()
452-
elif st.session_state.repo_next_token:
453-
with st.spinner("Loading next page..."):
454-
success, error = load_repository_page(
455-
page_token=st.session_state.repo_next_token,
456-
append=True
457-
)
458-
if not success and error:
459-
st.error(f"Failed to load additional videos: {error}")
460-
else:
461-
st.rerun()
523+
st.button(
524+
"►",
525+
disabled=next_disabled,
526+
use_container_width=True,
527+
key="repo_next_btn",
528+
on_click=set_repo_action,
529+
args=("next",),
530+
)
531+
532+
if st.session_state.repo_error:
533+
st.error(f"Failed to load additional videos: {st.session_state.repo_error}")
462534

463535
if videos:
464536
# Create a grid of videos

0 commit comments

Comments
 (0)