69 lines
3.1 KiB
Python
69 lines
3.1 KiB
Python
"""Tests for #1118 — api() retries on network errors (stale keep-alive after idle)."""
|
|
import re
|
|
|
|
|
|
def _src() -> str:
|
|
with open("static/workspace.js") as f:
|
|
return f.read()
|
|
|
|
|
|
class TestApiRetryOnNetworkError:
|
|
"""The api() function in workspace.js should retry on fetch TypeError (network failure)."""
|
|
|
|
def test_api_function_has_retry_loop(self):
|
|
"""api() must contain a retry loop (for/while with attempt counter)."""
|
|
src = _src()
|
|
assert re.search(r'for\s*\(\s*let\s+\w+\s*=\s*0\s*;', src), \
|
|
"api() must have a for loop with attempt counter"
|
|
|
|
def test_api_retries_on_typeerror(self):
|
|
"""api() must retry when fetch throws TypeError (network failure)."""
|
|
src = _src()
|
|
# Find the api function body
|
|
m = re.search(r'async function api\(path,opts.*?\n\}(?!\n[\s]*function)', src, re.DOTALL)
|
|
assert m, "api() function must exist"
|
|
body = m.group(0)
|
|
assert 'TypeError' in body, \
|
|
"api() must check for TypeError to detect network failures"
|
|
assert 'attempt' in body, \
|
|
"api() must track attempt count for retry logic"
|
|
|
|
def test_api_does_not_retry_http_errors(self):
|
|
"""api() must NOT retry on HTTP error responses (4xx/5xx) — only network failures."""
|
|
src = _src()
|
|
m = re.search(r'async function api\(path,opts.*?\n\}(?!\n[\s]*function)', src, re.DOTALL)
|
|
assert m, "api() function must exist"
|
|
body = m.group(0)
|
|
# The retry should be in catch block (after res.ok check), not in the ok path
|
|
# HTTP errors throw Error (not TypeError), so only TypeError triggers retry
|
|
assert 'e instanceof TypeError' in body, \
|
|
"api() retry must be limited to TypeError (network errors), not all errors"
|
|
|
|
def test_api_max_3_attempts(self):
|
|
"""api() must limit retries (max 3 attempts total = 1 initial + 2 retries)."""
|
|
src = _src()
|
|
m = re.search(r'async function api\(path,opts.*?\n\}(?!\n[\s]*function)', src, re.DOTALL)
|
|
assert m, "api() function must exist"
|
|
body = m.group(0)
|
|
# Should have attempt < 2 (0, 1, 2 = 3 attempts max)
|
|
assert re.search(r'attempt\s*<\s*2', body), \
|
|
"api() must limit to 3 attempts max (attempt < 2)"
|
|
|
|
def test_api_preserves_401_redirect(self):
|
|
"""api() must still redirect to login on 401 without escaping subpath mounts."""
|
|
src = _src()
|
|
assert "res.status===401" in src, \
|
|
"api() must still check for 401 status"
|
|
assert "login?next=" in src, \
|
|
"api() must still redirect to login on 401"
|
|
assert "/login?next=" not in src, \
|
|
"api() must not escape subpath mounts by redirecting to root /login"
|
|
|
|
def test_api_preserves_error_parsing(self):
|
|
"""api() must still parse JSON error bodies for non-200 responses."""
|
|
src = _src()
|
|
assert "JSON.parse(text)" in src, \
|
|
"api() must still parse JSON error responses"
|
|
assert "res.json()" in src, \
|
|
"api() must still parse JSON success responses"
|