feat: adopt Python 3.15 and harden caching and share links
Build and Push Docker Container / build-and-push (push) Successful in 3m58s

- Switch local Python and the Docker base image to Python 3.15.
- Refresh locked dependencies with Python-version-specific resolution.
- Replace Pickle caching with native Redis strings, lists, and typed JSON.
- Validate cache TTLs and apply expiry atomically on Redis writes.
- Treat legacy binary cache values as misses and clear keys with SCAN.
- Ignore trailing prose after decoded spaces in share-link paths.
- Reject empty or slash-containing share IDs before database lookup.
- Add regression tests for plain and encoded spaces in share links.
- Update helper submodule for config lookup and tabular storage fixes.
- Update quart_common with JSON error response detection.
- Verify all 150 tests pass on CPython 3.15.0.
This commit is contained in:
2026-10-24 00:29:20 +02:00
parent 3194e534e0
commit 6514ba16f6
8 changed files with 886 additions and 475 deletions
+66
View File
@@ -0,0 +1,66 @@
import asyncio
import importlib.util
import sys
import types
from pathlib import Path
from unittest.mock import AsyncMock
import pytest
from quart import Quart
def load_share_module(monkeypatch):
stubs = {
'my_modules.decoratory.header': {'login_required': lambda fn: fn},
'my_modules.functions': {'get_ip': lambda: '192.0.2.1'},
'my_modules.app.setup': {
'LIMITER': types.SimpleNamespace(limit=lambda *args, **kwargs: lambda fn: fn),
},
'my_modules.app.logger': {'logger': None},
'my_modules.expiry': {'parse_expires': lambda value: value},
'quart_common.web.wide_event': {'add_wide_event_context': lambda **kwargs: None},
}
for name, values in stubs.items():
module = types.ModuleType(name)
module.__dict__.update(values)
monkeypatch.setitem(sys.modules, name, module)
path = Path(__file__).resolve().parents[1] / 'routes/side/main.py'
spec = importlib.util.spec_from_file_location('share_route_under_test', path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@pytest.mark.parametrize('suffix', [
'',
' in female shape like https:/example.com/-kzmIlsf7uUe',
'%20in%20female%20shape%20like%20https:/example.com/-kzmIlsf7uUe',
])
def test_share_link_looks_up_only_id(monkeypatch, suffix):
async def run_test():
module = load_share_module(monkeypatch)
app = Quart(__name__)
app.secret_key = 'test-secret'
app.convex = types.SimpleNamespace(get_file=AsyncMock(return_value=None))
app.register_blueprint(module.side_main_bp)
response = await app.test_client().get('/-eluTduc-npP' + suffix)
assert response.status_code == 404
app.convex.get_file.assert_awaited_once_with(file_id='eluTduc-npP')
asyncio.run(run_test())
@pytest.mark.parametrize('path', ['/-/other', '/-%20text', '/-file/other'])
def test_invalid_share_id_does_not_query_storage(monkeypatch, path):
async def run_test():
module = load_share_module(monkeypatch)
app = Quart(__name__)
app.convex = types.SimpleNamespace(get_file=AsyncMock())
app.register_blueprint(module.side_main_bp)
response = await app.test_client().get(path)
assert response.status_code == 404
app.convex.get_file.assert_not_awaited()
asyncio.run(run_test())