"""Tests for the HTTP layer and file listing in web.py.""" import io import web def test_listing_is_ordered_by_the_filename_clock_not_mtime(tmp_path, write_wav): # mtime is the last write (≈ the end of the recording) and drifts from the # name on any copy or round-trip. Here the older recording is touched last. import os early, late = tmp_path / '20260523_080000.wav', tmp_path / '20260523_090000.wav' write_wav(early) write_wav(late) os.utime(late, (1000, 1000)) # newest recording, oldest mtime names = [f['name'] for f in web.list_files(str(tmp_path))] assert names == ['20260523_090000.wav', '20260523_080000.wav'] # --------------------------------------------------------------------------- # /api/clip error handling # --------------------------------------------------------------------------- class _OfflineHandler(web._Handler): """A _Handler with no socket, for driving handler methods directly.""" def __init__(self, recordings_dir): self.recordings_dir = str(recordings_dir) self.analyses_dir = str(recordings_dir) self.command = 'GET' # _send skips the body on HEAD self.wfile = io.BytesIO() self.close_connection = False self.codes = [] def send_response(self, code): self.codes.append(code) def send_header(self, key, value): pass def end_headers(self): pass def test_clip_failure_after_headers_closes_connection_instead_of_writing_json(tmp_path, write_wav): # Once the audio headers are out, an error body would be a second response # on a keep-alive connection that already promised audio/wav plus an exact # Content-Length — desyncing every later request on that connection. write_wav(tmp_path / 'rec.wav') h = _OfflineHandler(tmp_path) def boom(path, start, end): h._send_clip_headers(b'HDR', 10, 1, 2) raise RuntimeError('decode exploded') h._clip_wav = boom h._api_clip({'file': ['rec.wav'], 'start': ['0'], 'end': ['1']}) assert h.close_connection is True assert h.codes == [200] # no second status line assert b'error' not in h.wfile.getvalue() # no JSON body def test_clip_failure_before_headers_still_reports_json(tmp_path, write_wav): write_wav(tmp_path / 'rec.wav') h = _OfflineHandler(tmp_path) def boom(path, start, end): raise RuntimeError('could not open') h._clip_wav = boom h._api_clip({'file': ['rec.wav'], 'start': ['0'], 'end': ['1']}) assert h.close_connection is False assert h.codes == [500] assert b'could not open' in h.wfile.getvalue() # --------------------------------------------------------------------------- # Browser UI static files (webui/) # --------------------------------------------------------------------------- def test_ui_serves_index_and_modules_with_their_types(tmp_path): h = _OfflineHandler(tmp_path) types = [] h.send_header = lambda key, value: types.append(value) if key == 'Content-Type' else None h._ui('index.html') h._ui('js/main.js') h._ui('style.css') assert h.codes == [200, 200, 200] assert types == ['text/html; charset=utf-8', 'text/javascript; charset=utf-8', 'text/css; charset=utf-8'] def test_ui_refuses_paths_outside_webui_and_unlisted_types(tmp_path): h = _OfflineHandler(tmp_path) h._ui('../web.py') # escapes webui/ h._ui('..\web.py') h._ui('js') # a directory h._ui('missing.js') assert h.codes == [404, 404, 404, 404] assert b'import' not in h.wfile.getvalue() def test_every_ui_module_import_resolves(): # No JS toolchain on the dev box: at least catch a renamed module or a # misspelt relative import before the browser does. import re js = web.WEBUI_DIR / 'js' for mod in js.glob('*.js'): for target in re.findall(r"from '\./([\w.]+)'", mod.read_text(encoding='utf-8')): assert (js / target).is_file(), f'{mod.name} imports missing {target}'