ref #10: [fix] enforce UUID chat ids

Normalize chat ids to a single UUID form so locks, repository keys, and mount paths cannot diverge through path-like aliases.
This commit is contained in:
Azamat 2026-04-02 22:35:50 +03:00
parent 44f1549d80
commit e629e34c4d
7 changed files with 192 additions and 80 deletions

View file

@ -28,6 +28,9 @@ from repository.sandbox_session import InMemorySandboxSessionRepository
from usecase.interface import Attrs
from usecase.sandbox import CleanupExpiredSandboxes, CreateSandbox, CreateSandboxCommand
CHAT_ID = '123e4567-e89b-12d3-a456-426614174000'
NON_CANONICAL_CHAT_ID = '123E4567E89B12D3A456426614174000'
class FakeLogger:
def __init__(self) -> None:
@ -246,12 +249,12 @@ async def exercise_get_request(
await app.router.shutdown()
def test_post_create_returns_session(monkeypatch) -> None:
def test_post_create_returns_session_with_canonical_chat_id(monkeypatch) -> None:
config = build_config()
expires_at = datetime(2026, 4, 2, 12, 5, tzinfo=UTC)
session = SandboxSession(
session_id='session-123',
chat_id='chat-123',
chat_id=CHAT_ID,
container_id='container-123',
status=SandboxStatus.RUNNING,
created_at=expires_at - timedelta(minutes=5),
@ -276,19 +279,19 @@ def test_post_create_returns_session(monkeypatch) -> None:
app = app_module.create_app(config=config)
status_code, response = asyncio.run(
exercise_create_request(app, {'chat_id': 'chat-123'})
exercise_create_request(app, {'chat_id': NON_CANONICAL_CHAT_ID})
)
assert status_code == 200
assert response == {
'session_id': 'session-123',
'chat_id': 'chat-123',
'chat_id': CHAT_ID,
'container_id': 'container-123',
'status': 'running',
'expires_at': '2026-04-02T12:05:00Z',
}
assert len(create_usecase.commands) == 1
assert create_usecase.commands[0].chat_id == 'chat-123'
assert create_usecase.commands[0].chat_id == CHAT_ID
assert cleanup_usecase.calls >= 1
assert any(
message == 'http_request'
@ -299,10 +302,19 @@ def test_post_create_returns_session(monkeypatch) -> None:
assert docker_client.close_calls == 1
def test_post_create_maps_start_errors_to_service_unavailable(monkeypatch) -> None:
def test_post_create_rejects_non_uuid_chat_id(monkeypatch) -> None:
config = build_config()
expires_at = datetime(2026, 4, 2, 12, 5, tzinfo=UTC)
session = SandboxSession(
session_id='session-123',
chat_id=CHAT_ID,
container_id='container-123',
status=SandboxStatus.RUNNING,
created_at=expires_at - timedelta(minutes=5),
expires_at=expires_at,
)
logger = FakeLogger()
create_usecase = FakeCreateSandboxUsecase(error=SandboxStartError('chat-123'))
create_usecase = FakeCreateSandboxUsecase(session=session)
cleanup_usecase = FakeCleanupExpiredSandboxes()
docker_client = FakeDockerClient()
container = build_container(
@ -320,7 +332,37 @@ def test_post_create_maps_start_errors_to_service_unavailable(monkeypatch) -> No
app = app_module.create_app(config=config)
status_code, response = asyncio.run(
exercise_create_request(app, {'chat_id': 'chat-123'})
exercise_create_request(app, {'chat_id': 'x/../y'})
)
assert status_code == 422
assert 'detail' in response
assert create_usecase.commands == []
assert docker_client.close_calls == 1
def test_post_create_maps_start_errors_to_service_unavailable(monkeypatch) -> None:
config = build_config()
logger = FakeLogger()
create_usecase = FakeCreateSandboxUsecase(error=SandboxStartError(CHAT_ID))
cleanup_usecase = FakeCleanupExpiredSandboxes()
docker_client = FakeDockerClient()
container = build_container(
config,
create_usecase,
cleanup_usecase,
logger,
docker_client,
)
monkeypatch.setattr(app_module, 'build_container', lambda **kwargs: container)
monkeypatch.setattr(
app_module.FastAPIInstrumentor, 'instrument_app', lambda *args, **kwargs: None
)
app = app_module.create_app(config=config)
status_code, response = asyncio.run(
exercise_create_request(app, {'chat_id': CHAT_ID})
)
assert status_code == 503
@ -349,7 +391,7 @@ def test_post_create_maps_generic_sandbox_errors_to_internal_error(monkeypatch)
app = app_module.create_app(config=config)
status_code, response = asyncio.run(
exercise_create_request(app, {'chat_id': 'chat-123'})
exercise_create_request(app, {'chat_id': CHAT_ID})
)
assert status_code == 500
@ -362,7 +404,7 @@ def test_removed_user_endpoint_returns_not_found(monkeypatch) -> None:
expires_at = datetime(2026, 4, 2, 12, 5, tzinfo=UTC)
session = SandboxSession(
session_id='session-123',
chat_id='chat-123',
chat_id=CHAT_ID,
container_id='container-123',
status=SandboxStatus.RUNNING,
created_at=expires_at - timedelta(minutes=5),