mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-18 11:44:18 +00:00
test: format test suite
This commit is contained in:
@@ -21,8 +21,8 @@ from langbot.pkg.storage.providers.localstorage import LocalStorageProvider
|
||||
@pytest.fixture
|
||||
def storage_provider(tmp_path):
|
||||
"""Create a LocalStorageProvider with a temporary storage path."""
|
||||
storage_path = str(tmp_path / "storage")
|
||||
with patch("langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH", storage_path):
|
||||
storage_path = str(tmp_path / 'storage')
|
||||
with patch('langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH', storage_path):
|
||||
mock_app = Mock()
|
||||
provider = LocalStorageProvider(mock_app)
|
||||
yield provider, storage_path
|
||||
@@ -35,15 +35,15 @@ class TestPathTraversalPrevention:
|
||||
async def test_absolute_path_save_rejected(self, storage_provider, tmp_path):
|
||||
"""Saving with an absolute path key must be blocked."""
|
||||
provider, storage_path = storage_provider
|
||||
target_file = str(tmp_path / "pwned.txt")
|
||||
target_file = str(tmp_path / 'pwned.txt')
|
||||
|
||||
with patch("langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH", storage_path):
|
||||
with patch('langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH', storage_path):
|
||||
with pytest.raises((ValueError, PermissionError)):
|
||||
await provider.save(target_file, b"malicious content")
|
||||
await provider.save(target_file, b'malicious content')
|
||||
|
||||
# The file must NOT exist outside the storage directory
|
||||
assert not os.path.exists(target_file), (
|
||||
f"Path traversal succeeded: file was written outside storage to {target_file}"
|
||||
f'Path traversal succeeded: file was written outside storage to {target_file}'
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -52,32 +52,28 @@ class TestPathTraversalPrevention:
|
||||
provider, storage_path = storage_provider
|
||||
|
||||
# Create a file outside the storage directory
|
||||
target_file = str(tmp_path / "secret.txt")
|
||||
with open(target_file, "wb") as f:
|
||||
f.write(b"secret data")
|
||||
target_file = str(tmp_path / 'secret.txt')
|
||||
with open(target_file, 'wb') as f:
|
||||
f.write(b'secret data')
|
||||
|
||||
with patch("langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH", storage_path):
|
||||
with patch('langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH', storage_path):
|
||||
with pytest.raises((ValueError, PermissionError, FileNotFoundError)):
|
||||
data = await provider.load(target_file)
|
||||
assert data != b"secret data", (
|
||||
"Path traversal succeeded: read file outside storage"
|
||||
)
|
||||
assert data != b'secret data', 'Path traversal succeeded: read file outside storage'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_absolute_path_exists_rejected(self, storage_provider, tmp_path):
|
||||
"""Exists check with an absolute path key must be blocked or return False."""
|
||||
provider, storage_path = storage_provider
|
||||
|
||||
target_file = str(tmp_path / "check_me.txt")
|
||||
with open(target_file, "wb") as f:
|
||||
f.write(b"data")
|
||||
target_file = str(tmp_path / 'check_me.txt')
|
||||
with open(target_file, 'wb') as f:
|
||||
f.write(b'data')
|
||||
|
||||
with patch("langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH", storage_path):
|
||||
with patch('langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH', storage_path):
|
||||
try:
|
||||
result = await provider.exists(target_file)
|
||||
assert result is False, (
|
||||
"Path traversal succeeded: exists() returned True for file outside storage"
|
||||
)
|
||||
assert result is False, 'Path traversal succeeded: exists() returned True for file outside storage'
|
||||
except (ValueError, PermissionError):
|
||||
pass # Expected
|
||||
|
||||
@@ -86,28 +82,26 @@ class TestPathTraversalPrevention:
|
||||
"""Deleting with an absolute path key must be blocked."""
|
||||
provider, storage_path = storage_provider
|
||||
|
||||
target_file = str(tmp_path / "do_not_delete.txt")
|
||||
with open(target_file, "wb") as f:
|
||||
f.write(b"important data")
|
||||
target_file = str(tmp_path / 'do_not_delete.txt')
|
||||
with open(target_file, 'wb') as f:
|
||||
f.write(b'important data')
|
||||
|
||||
with patch("langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH", storage_path):
|
||||
with patch('langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH', storage_path):
|
||||
with pytest.raises((ValueError, PermissionError, FileNotFoundError)):
|
||||
await provider.delete(target_file)
|
||||
|
||||
assert os.path.exists(target_file), (
|
||||
"Path traversal succeeded: file outside storage was deleted"
|
||||
)
|
||||
assert os.path.exists(target_file), 'Path traversal succeeded: file outside storage was deleted'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_absolute_path_size_rejected(self, storage_provider, tmp_path):
|
||||
"""Size check with an absolute path key must be blocked."""
|
||||
provider, storage_path = storage_provider
|
||||
|
||||
target_file = str(tmp_path / "measure_me.txt")
|
||||
with open(target_file, "wb") as f:
|
||||
f.write(b"some data")
|
||||
target_file = str(tmp_path / 'measure_me.txt')
|
||||
with open(target_file, 'wb') as f:
|
||||
f.write(b'some data')
|
||||
|
||||
with patch("langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH", storage_path):
|
||||
with patch('langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH', storage_path):
|
||||
with pytest.raises((ValueError, PermissionError, FileNotFoundError)):
|
||||
await provider.size(target_file)
|
||||
|
||||
@@ -116,41 +110,39 @@ class TestPathTraversalPrevention:
|
||||
"""Relative path traversal with '..' must be blocked."""
|
||||
provider, storage_path = storage_provider
|
||||
|
||||
target_file = str(tmp_path / "above_storage.txt")
|
||||
with open(target_file, "wb") as f:
|
||||
f.write(b"above storage secret")
|
||||
target_file = str(tmp_path / 'above_storage.txt')
|
||||
with open(target_file, 'wb') as f:
|
||||
f.write(b'above storage secret')
|
||||
|
||||
with patch("langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH", storage_path):
|
||||
relative_key = os.path.join("..", "above_storage.txt")
|
||||
with patch('langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH', storage_path):
|
||||
relative_key = os.path.join('..', 'above_storage.txt')
|
||||
with pytest.raises((ValueError, PermissionError, FileNotFoundError)):
|
||||
data = await provider.load(relative_key)
|
||||
assert data != b"above storage secret"
|
||||
assert data != b'above storage secret'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_dir_recursive_traversal_rejected(self, storage_provider, tmp_path):
|
||||
"""delete_dir_recursive with traversal path must be blocked."""
|
||||
provider, storage_path = storage_provider
|
||||
|
||||
outside_dir = tmp_path / "outside_dir"
|
||||
outside_dir = tmp_path / 'outside_dir'
|
||||
outside_dir.mkdir()
|
||||
(outside_dir / "file.txt").write_text("important")
|
||||
(outside_dir / 'file.txt').write_text('important')
|
||||
|
||||
with patch("langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH", storage_path):
|
||||
with patch('langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH', storage_path):
|
||||
with pytest.raises((ValueError, PermissionError)):
|
||||
await provider.delete_dir_recursive(str(outside_dir))
|
||||
|
||||
assert outside_dir.exists(), (
|
||||
"Path traversal succeeded: directory outside storage was deleted"
|
||||
)
|
||||
assert outside_dir.exists(), 'Path traversal succeeded: directory outside storage was deleted'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_legitimate_key_works(self, storage_provider):
|
||||
"""Normal keys without traversal must still work."""
|
||||
provider, storage_path = storage_provider
|
||||
|
||||
with patch("langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH", storage_path):
|
||||
key = "test_image_abc123.png"
|
||||
content = b"PNG image data"
|
||||
with patch('langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH', storage_path):
|
||||
key = 'test_image_abc123.png'
|
||||
content = b'PNG image data'
|
||||
|
||||
await provider.save(key, content)
|
||||
assert await provider.exists(key) is True
|
||||
@@ -166,9 +158,9 @@ class TestPathTraversalPrevention:
|
||||
"""Keys with legitimate subdirectories must still work."""
|
||||
provider, storage_path = storage_provider
|
||||
|
||||
with patch("langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH", storage_path):
|
||||
key = "bot_log_images/img_001.png"
|
||||
content = b"PNG image data"
|
||||
with patch('langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH', storage_path):
|
||||
key = 'bot_log_images/img_001.png'
|
||||
content = b'PNG image data'
|
||||
|
||||
await provider.save(key, content)
|
||||
assert await provider.exists(key) is True
|
||||
@@ -181,33 +173,33 @@ class TestPathTraversalPrevention:
|
||||
"""delete_dir_recursive should handle non-existing directories gracefully."""
|
||||
provider, storage_path = storage_provider
|
||||
|
||||
with patch("langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH", storage_path):
|
||||
with patch('langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH', storage_path):
|
||||
# Try to delete a non-existing directory - should not raise
|
||||
await provider.delete_dir_recursive("nonexistent_dir")
|
||||
await provider.delete_dir_recursive('nonexistent_dir')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_dir_recursive_with_files(self, storage_provider):
|
||||
"""delete_dir_recursive should delete directory with files inside."""
|
||||
provider, storage_path = storage_provider
|
||||
|
||||
with patch("langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH", storage_path):
|
||||
with patch('langbot.pkg.storage.providers.localstorage.LOCAL_STORAGE_PATH', storage_path):
|
||||
# Create a directory with files
|
||||
key1 = "test_dir/file1.txt"
|
||||
key2 = "test_dir/file2.txt"
|
||||
await provider.save(key1, b"content1")
|
||||
await provider.save(key2, b"content2")
|
||||
key1 = 'test_dir/file1.txt'
|
||||
key2 = 'test_dir/file2.txt'
|
||||
await provider.save(key1, b'content1')
|
||||
await provider.save(key2, b'content2')
|
||||
|
||||
# Verify files exist
|
||||
assert await provider.exists(key1)
|
||||
assert await provider.exists(key2)
|
||||
|
||||
# Delete directory recursively
|
||||
await provider.delete_dir_recursive("test_dir")
|
||||
await provider.delete_dir_recursive('test_dir')
|
||||
|
||||
# Verify files no longer exist
|
||||
assert not await provider.exists(key1)
|
||||
assert not await provider.exists(key2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v'])
|
||||
|
||||
@@ -8,6 +8,7 @@ Tests cover:
|
||||
|
||||
Uses moto library to mock AWS S3 service.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
@@ -44,8 +45,10 @@ def mock_app_with_s3_config():
|
||||
def s3_mock():
|
||||
"""Set up moto S3 mock context."""
|
||||
from moto import mock_aws
|
||||
|
||||
with mock_aws():
|
||||
import boto3
|
||||
|
||||
# Create bucket for tests that need pre-existing bucket
|
||||
s3 = boto3.client('s3', region_name='us-east-1')
|
||||
yield s3
|
||||
@@ -325,4 +328,4 @@ class TestS3StorageProviderErrorHandling:
|
||||
await provider.initialize()
|
||||
|
||||
with pytest.raises(Exception):
|
||||
await provider.size('nonexistent.txt')
|
||||
await provider.size('nonexistent.txt')
|
||||
|
||||
@@ -31,7 +31,7 @@ class TestStorageMgr:
|
||||
|
||||
storage_mgr = StorageMgr(mock_app)
|
||||
|
||||
with patch.object(LocalStorageProvider, "initialize", new_callable=AsyncMock):
|
||||
with patch.object(LocalStorageProvider, 'initialize', new_callable=AsyncMock):
|
||||
await storage_mgr.initialize()
|
||||
assert isinstance(storage_mgr.storage_provider, LocalStorageProvider)
|
||||
mock_app.logger.info.assert_called()
|
||||
@@ -41,12 +41,12 @@ class TestStorageMgr:
|
||||
"""Should use local storage when explicitly configured."""
|
||||
mock_app = Mock()
|
||||
mock_app.instance_config = Mock()
|
||||
mock_app.instance_config.data = {"storage": {"use": "local"}}
|
||||
mock_app.instance_config.data = {'storage': {'use': 'local'}}
|
||||
mock_app.logger = Mock()
|
||||
|
||||
storage_mgr = StorageMgr(mock_app)
|
||||
|
||||
with patch.object(LocalStorageProvider, "initialize", new_callable=AsyncMock):
|
||||
with patch.object(LocalStorageProvider, 'initialize', new_callable=AsyncMock):
|
||||
await storage_mgr.initialize()
|
||||
assert isinstance(storage_mgr.storage_provider, LocalStorageProvider)
|
||||
|
||||
@@ -55,14 +55,12 @@ class TestStorageMgr:
|
||||
"""Should use S3 storage when configured."""
|
||||
mock_app = Mock()
|
||||
mock_app.instance_config = Mock()
|
||||
mock_app.instance_config.data = {
|
||||
"storage": {"use": "s3", "s3": {"endpoint_url": "https://s3.amazonaws.com"}}
|
||||
}
|
||||
mock_app.instance_config.data = {'storage': {'use': 's3', 's3': {'endpoint_url': 'https://s3.amazonaws.com'}}}
|
||||
mock_app.logger = Mock()
|
||||
|
||||
storage_mgr = StorageMgr(mock_app)
|
||||
|
||||
with patch.object(S3StorageProvider, "initialize", new_callable=AsyncMock):
|
||||
with patch.object(S3StorageProvider, 'initialize', new_callable=AsyncMock):
|
||||
await storage_mgr.initialize()
|
||||
assert isinstance(storage_mgr.storage_provider, S3StorageProvider)
|
||||
|
||||
@@ -71,12 +69,12 @@ class TestStorageMgr:
|
||||
"""Should default to local storage for invalid storage type."""
|
||||
mock_app = Mock()
|
||||
mock_app.instance_config = Mock()
|
||||
mock_app.instance_config.data = {"storage": {"use": "invalid_type"}}
|
||||
mock_app.instance_config.data = {'storage': {'use': 'invalid_type'}}
|
||||
mock_app.logger = Mock()
|
||||
|
||||
storage_mgr = StorageMgr(mock_app)
|
||||
|
||||
with patch.object(LocalStorageProvider, "initialize", new_callable=AsyncMock):
|
||||
with patch.object(LocalStorageProvider, 'initialize', new_callable=AsyncMock):
|
||||
await storage_mgr.initialize()
|
||||
assert isinstance(storage_mgr.storage_provider, LocalStorageProvider)
|
||||
|
||||
@@ -90,9 +88,7 @@ class TestStorageMgr:
|
||||
|
||||
storage_mgr = StorageMgr(mock_app)
|
||||
|
||||
with patch.object(
|
||||
LocalStorageProvider, "initialize", new_callable=AsyncMock
|
||||
) as mock_init:
|
||||
with patch.object(LocalStorageProvider, 'initialize', new_callable=AsyncMock) as mock_init:
|
||||
await storage_mgr.initialize()
|
||||
mock_init.assert_called_once()
|
||||
|
||||
@@ -105,8 +101,8 @@ class TestStorageProviderBase:
|
||||
mock_app = Mock()
|
||||
|
||||
# Use LocalStorageProvider as concrete implementation
|
||||
with patch("os.path.exists", return_value=True):
|
||||
with patch("os.makedirs"):
|
||||
with patch('os.path.exists', return_value=True):
|
||||
with patch('os.makedirs'):
|
||||
provider = LocalStorageProvider(mock_app)
|
||||
assert provider.ap == mock_app
|
||||
|
||||
@@ -115,12 +111,12 @@ class TestStorageProviderBase:
|
||||
"""Provider base initialize should be callable and do nothing."""
|
||||
mock_app = Mock()
|
||||
|
||||
with patch("os.path.exists", return_value=True):
|
||||
with patch("os.makedirs"):
|
||||
with patch('os.path.exists', return_value=True):
|
||||
with patch('os.makedirs'):
|
||||
provider = LocalStorageProvider(mock_app)
|
||||
# Initialize should not raise
|
||||
await provider.initialize()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v'])
|
||||
|
||||
Reference in New Issue
Block a user