chore: add token revocation, caching, concurrency, and WebDAV lock modules
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import asyncio
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def temp_db_path():
|
||||
path = tempfile.mkdtemp()
|
||||
yield path
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def db_manager(temp_db_path):
|
||||
from mywebdav.database.manager import UserDatabaseManager
|
||||
manager = UserDatabaseManager(Path(temp_db_path), cache_size=10, flush_interval=1)
|
||||
await manager.start()
|
||||
yield manager
|
||||
await manager.stop()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def cache():
|
||||
from mywebdav.cache.layer import CacheLayer
|
||||
cache = CacheLayer(maxsize=100, flush_interval=60)
|
||||
await cache.start()
|
||||
yield cache
|
||||
await cache.stop()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def lock_manager():
|
||||
from mywebdav.concurrency.locks import LockManager
|
||||
manager = LockManager(default_timeout=5.0, cleanup_interval=60.0)
|
||||
await manager.start()
|
||||
yield manager
|
||||
await manager.stop()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def webdav_locks():
|
||||
from mywebdav.concurrency.webdav_locks import PersistentWebDAVLocks
|
||||
lock_manager = PersistentWebDAVLocks()
|
||||
await lock_manager.start()
|
||||
yield lock_manager
|
||||
await lock_manager.stop()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def token_manager():
|
||||
from mywebdav.auth_tokens import TokenManager
|
||||
manager = TokenManager()
|
||||
await manager.start()
|
||||
yield manager
|
||||
await manager.stop()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def rate_limiter():
|
||||
from mywebdav.middleware.rate_limit import RateLimiter
|
||||
limiter = RateLimiter()
|
||||
await limiter.start()
|
||||
yield limiter
|
||||
await limiter.stop()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def task_queue():
|
||||
from mywebdav.workers.queue import TaskQueue
|
||||
queue = TaskQueue(max_workers=2)
|
||||
await queue.start()
|
||||
yield queue
|
||||
await queue.stop()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def atomic_ops():
|
||||
from mywebdav.concurrency.locks import init_lock_manager, shutdown_lock_manager
|
||||
from mywebdav.concurrency.atomic import AtomicOperations, init_atomic_ops
|
||||
|
||||
await init_lock_manager(default_timeout=5.0)
|
||||
ops = init_atomic_ops()
|
||||
yield ops
|
||||
await shutdown_lock_manager()
|
||||
@@ -0,0 +1,185 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockUser:
|
||||
id: int
|
||||
used_storage_bytes: int
|
||||
storage_quota_bytes: int
|
||||
|
||||
|
||||
class TestAtomicOperations:
|
||||
@pytest.mark.asyncio
|
||||
async def test_quota_check_allowed(self, atomic_ops):
|
||||
user = MockUser(id=1, used_storage_bytes=1000, storage_quota_bytes=10000)
|
||||
save_called = False
|
||||
|
||||
async def save_callback(u):
|
||||
nonlocal save_called
|
||||
save_called = True
|
||||
|
||||
result = await atomic_ops.atomic_quota_check_and_update(user, 500, save_callback)
|
||||
|
||||
assert result.allowed is True
|
||||
assert result.current_usage == 1000
|
||||
assert result.quota == 10000
|
||||
assert result.requested == 500
|
||||
assert result.remaining == 9000
|
||||
assert save_called is True
|
||||
assert user.used_storage_bytes == 1500
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_quota_check_denied(self, atomic_ops):
|
||||
user = MockUser(id=1, used_storage_bytes=9500, storage_quota_bytes=10000)
|
||||
save_called = False
|
||||
|
||||
async def save_callback(u):
|
||||
nonlocal save_called
|
||||
save_called = True
|
||||
|
||||
result = await atomic_ops.atomic_quota_check_and_update(user, 1000, save_callback)
|
||||
|
||||
assert result.allowed is False
|
||||
assert result.remaining == 500
|
||||
assert save_called is False
|
||||
assert user.used_storage_bytes == 9500
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_quota_check_concurrent_requests(self, atomic_ops):
|
||||
user = MockUser(id=1, used_storage_bytes=0, storage_quota_bytes=1000)
|
||||
|
||||
async def save_callback(u):
|
||||
pass
|
||||
|
||||
async def request_quota(amount):
|
||||
return await atomic_ops.atomic_quota_check_and_update(user, amount, save_callback)
|
||||
|
||||
results = await asyncio.gather(*[request_quota(200) for _ in range(10)])
|
||||
|
||||
allowed_count = sum(1 for r in results if r.allowed)
|
||||
assert allowed_count == 5
|
||||
assert user.used_storage_bytes == 1000
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_create_success(self, atomic_ops):
|
||||
user = MockUser(id=1, used_storage_bytes=0, storage_quota_bytes=10000)
|
||||
|
||||
async def check_exists():
|
||||
return None
|
||||
|
||||
async def create_file():
|
||||
return {"id": 1, "name": "test.txt"}
|
||||
|
||||
result = await atomic_ops.atomic_file_create(
|
||||
user, None, "test.txt", check_exists, create_file
|
||||
)
|
||||
|
||||
assert result["name"] == "test.txt"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_create_exists(self, atomic_ops):
|
||||
user = MockUser(id=1, used_storage_bytes=0, storage_quota_bytes=10000)
|
||||
|
||||
async def check_exists():
|
||||
return {"id": 1, "name": "test.txt"}
|
||||
|
||||
async def create_file():
|
||||
return {"id": 2, "name": "test.txt"}
|
||||
|
||||
with pytest.raises(FileExistsError):
|
||||
await atomic_ops.atomic_file_create(
|
||||
user, None, "test.txt", check_exists, create_file
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_create_concurrent_same_name(self, atomic_ops):
|
||||
user = MockUser(id=1, used_storage_bytes=0, storage_quota_bytes=10000)
|
||||
created_files = []
|
||||
|
||||
async def check_exists():
|
||||
return len(created_files) > 0
|
||||
|
||||
async def create_file():
|
||||
file = {"id": len(created_files) + 1, "name": "test.txt"}
|
||||
created_files.append(file)
|
||||
return file
|
||||
|
||||
async def try_create():
|
||||
try:
|
||||
return await atomic_ops.atomic_file_create(
|
||||
user, 1, "test.txt", check_exists, create_file
|
||||
)
|
||||
except FileExistsError:
|
||||
return None
|
||||
|
||||
results = await asyncio.gather(*[try_create() for _ in range(5)])
|
||||
|
||||
successful = [r for r in results if r is not None]
|
||||
assert len(successful) == 1
|
||||
assert len(created_files) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_folder_create_success(self, atomic_ops):
|
||||
user = MockUser(id=1, used_storage_bytes=0, storage_quota_bytes=10000)
|
||||
|
||||
async def check_exists():
|
||||
return None
|
||||
|
||||
async def create_folder():
|
||||
return {"id": 1, "name": "Documents"}
|
||||
|
||||
result = await atomic_ops.atomic_folder_create(
|
||||
user, None, "Documents", check_exists, create_folder
|
||||
)
|
||||
|
||||
assert result["name"] == "Documents"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_folder_create_exists(self, atomic_ops):
|
||||
user = MockUser(id=1, used_storage_bytes=0, storage_quota_bytes=10000)
|
||||
|
||||
async def check_exists():
|
||||
return {"id": 1, "name": "Documents"}
|
||||
|
||||
async def create_folder():
|
||||
return {"id": 2, "name": "Documents"}
|
||||
|
||||
with pytest.raises(FileExistsError):
|
||||
await atomic_ops.atomic_folder_create(
|
||||
user, None, "Documents", check_exists, create_folder
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_update(self, atomic_ops):
|
||||
user = MockUser(id=1, used_storage_bytes=0, storage_quota_bytes=10000)
|
||||
update_called = False
|
||||
|
||||
async def update_callback():
|
||||
nonlocal update_called
|
||||
update_called = True
|
||||
return {"id": 1, "updated": True}
|
||||
|
||||
result = await atomic_ops.atomic_file_update(user, 1, update_callback)
|
||||
|
||||
assert update_called is True
|
||||
assert result["updated"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_batch_operation(self, atomic_ops):
|
||||
user = MockUser(id=1, used_storage_bytes=0, storage_quota_bytes=10000)
|
||||
|
||||
async def process_item(item):
|
||||
if item == "fail":
|
||||
raise ValueError("Failed item")
|
||||
return f"processed_{item}"
|
||||
|
||||
result = await atomic_ops.atomic_batch_operation(
|
||||
user, "test_batch", ["a", "b", "fail", "c"], process_item
|
||||
)
|
||||
|
||||
assert len(result["results"]) == 3
|
||||
assert len(result["errors"]) == 1
|
||||
assert "processed_a" in result["results"]
|
||||
assert result["errors"][0]["item"] == "fail"
|
||||
@@ -0,0 +1,167 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from mywebdav.cache.layer import LRUCache
|
||||
|
||||
|
||||
class TestLRUCache:
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_and_get(self):
|
||||
cache = LRUCache(maxsize=100)
|
||||
await cache.set("key1", "value1")
|
||||
entry = await cache.get("key1")
|
||||
assert entry is not None
|
||||
assert entry.value == "value1"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_missing_key(self):
|
||||
cache = LRUCache(maxsize=100)
|
||||
entry = await cache.get("nonexistent")
|
||||
assert entry is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ttl_expiration(self):
|
||||
cache = LRUCache(maxsize=100)
|
||||
await cache.set("key1", "value1", ttl=0.1)
|
||||
await asyncio.sleep(0.2)
|
||||
entry = await cache.get("key1")
|
||||
assert entry is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lru_eviction(self):
|
||||
cache = LRUCache(maxsize=3)
|
||||
await cache.set("key1", "value1")
|
||||
await cache.set("key2", "value2")
|
||||
await cache.set("key3", "value3")
|
||||
await cache.set("key4", "value4")
|
||||
|
||||
entry1 = await cache.get("key1")
|
||||
assert entry1 is None
|
||||
|
||||
entry4 = await cache.get("key4")
|
||||
assert entry4 is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete(self):
|
||||
cache = LRUCache(maxsize=100)
|
||||
await cache.set("key1", "value1")
|
||||
result = await cache.delete("key1")
|
||||
assert result is True
|
||||
entry = await cache.get("key1")
|
||||
assert entry is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dirty_tracking(self):
|
||||
cache = LRUCache(maxsize=100)
|
||||
await cache.set("key1", "value1", dirty=True)
|
||||
await cache.set("key2", "value2", dirty=False)
|
||||
|
||||
dirty_keys = await cache.get_dirty_keys()
|
||||
assert "key1" in dirty_keys
|
||||
assert "key2" not in dirty_keys
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mark_clean(self):
|
||||
cache = LRUCache(maxsize=100)
|
||||
await cache.set("key1", "value1", dirty=True)
|
||||
await cache.mark_clean("key1")
|
||||
|
||||
dirty_keys = await cache.get_dirty_keys()
|
||||
assert "key1" not in dirty_keys
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalidate_pattern(self):
|
||||
cache = LRUCache(maxsize=100)
|
||||
await cache.set("user:1:profile", "data1")
|
||||
await cache.set("user:1:files", "data2")
|
||||
await cache.set("user:2:profile", "data3")
|
||||
|
||||
count = await cache.invalidate_pattern("user:1:")
|
||||
assert count == 2
|
||||
|
||||
entry1 = await cache.get("user:1:profile")
|
||||
assert entry1 is None
|
||||
|
||||
entry2 = await cache.get("user:2:profile")
|
||||
assert entry2 is not None
|
||||
|
||||
|
||||
class TestCacheLayer:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_with_loader(self, cache):
|
||||
call_count = 0
|
||||
|
||||
async def loader():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return "loaded_value"
|
||||
|
||||
result1 = await cache.get("test_key", loader)
|
||||
assert result1 == "loaded_value"
|
||||
assert call_count == 1
|
||||
|
||||
result2 = await cache.get("test_key", loader)
|
||||
assert result2 == "loaded_value"
|
||||
assert call_count == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_and_get(self, cache):
|
||||
await cache.set("key1", {"data": "value"})
|
||||
result = await cache.get("key1")
|
||||
assert result == {"data": "value"}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete(self, cache):
|
||||
await cache.set("key1", "value1")
|
||||
await cache.delete("key1")
|
||||
result = await cache.get("key1")
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalidate_user_cache(self, cache):
|
||||
await cache.set("user:1:profile", "data1")
|
||||
await cache.set("user:1:files", "data2")
|
||||
await cache.set("user:2:profile", "data3")
|
||||
|
||||
await cache.invalidate_user_cache(1)
|
||||
|
||||
result1 = await cache.get("user:1:profile")
|
||||
assert result1 is None
|
||||
|
||||
result2 = await cache.get("user:2:profile")
|
||||
assert result2 == "data3"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stats(self, cache):
|
||||
await cache.get("miss1")
|
||||
await cache.set("hit1", "value")
|
||||
await cache.get("hit1")
|
||||
await cache.get("hit1")
|
||||
|
||||
stats = cache.get_stats()
|
||||
assert stats["hits"] == 2
|
||||
assert stats["misses"] == 1
|
||||
assert stats["sets"] == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_key(self, cache):
|
||||
key = cache.build_key("user_profile", user_id=123)
|
||||
assert key == "user:123:profile"
|
||||
|
||||
key = cache.build_key("folder_contents", user_id=1, folder_id=5)
|
||||
assert key == "user:1:folder:5:contents"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ttl_by_key_type(self, cache):
|
||||
assert cache._get_ttl_for_key("user:1:profile") == 300.0
|
||||
assert cache._get_ttl_for_key("folder_contents:1") == 30.0
|
||||
assert cache._get_ttl_for_key("unknown_key") == 300.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_persist_flag(self, cache):
|
||||
await cache.set("persistent_key", "value", persist=True)
|
||||
assert "persistent_key" in cache.dirty_keys
|
||||
|
||||
await cache.set("non_persistent_key", "value", persist=False)
|
||||
assert "non_persistent_key" not in cache.dirty_keys
|
||||
@@ -0,0 +1,131 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TestUserDatabaseManager:
|
||||
@pytest.mark.asyncio
|
||||
async def test_master_db_initialization(self, db_manager, temp_db_path):
|
||||
assert db_manager.master_db is not None
|
||||
assert db_manager.master_db.connection is not None
|
||||
master_path = Path(temp_db_path) / "master.db"
|
||||
assert master_path.exists()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_master_tables_created(self, db_manager):
|
||||
async with db_manager.get_master_connection() as conn:
|
||||
cursor = await conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||
)
|
||||
tables = [row[0] for row in await cursor.fetchall()]
|
||||
assert "users" in tables
|
||||
assert "revoked_tokens" in tables
|
||||
assert "rate_limits" in tables
|
||||
assert "webdav_locks" in tables
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_database_creation(self, db_manager, temp_db_path):
|
||||
user_id = 1
|
||||
async with db_manager.get_user_connection(user_id) as conn:
|
||||
assert conn is not None
|
||||
|
||||
user_db_path = Path(temp_db_path) / "users" / "1" / "database.db"
|
||||
assert user_db_path.exists()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_tables_created(self, db_manager):
|
||||
user_id = 1
|
||||
async with db_manager.get_user_connection(user_id) as conn:
|
||||
cursor = await conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||
)
|
||||
tables = [row[0] for row in await cursor.fetchall()]
|
||||
assert "files" in tables
|
||||
assert "folders" in tables
|
||||
assert "shares" in tables
|
||||
assert "activities" in tables
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connection_caching(self, db_manager):
|
||||
user_id = 1
|
||||
async with db_manager.get_user_connection(user_id):
|
||||
pass
|
||||
assert user_id in db_manager.databases
|
||||
|
||||
async with db_manager.get_user_connection(user_id):
|
||||
pass
|
||||
assert len(db_manager.databases) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connection_eviction(self, db_manager):
|
||||
for i in range(15):
|
||||
async with db_manager.get_user_connection(i):
|
||||
pass
|
||||
|
||||
assert len(db_manager.databases) <= db_manager.cache_size
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_buffered_write(self, db_manager):
|
||||
user_id = 1
|
||||
async with db_manager.get_user_connection(user_id):
|
||||
pass
|
||||
|
||||
await db_manager.execute_buffered(
|
||||
user_id,
|
||||
"INSERT INTO folders (name, owner_id) VALUES (?, ?)",
|
||||
("test_folder", user_id)
|
||||
)
|
||||
|
||||
user_db = db_manager.databases[user_id]
|
||||
assert user_db.dirty is True
|
||||
assert len(user_db.write_buffer) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flush_user(self, db_manager):
|
||||
user_id = 1
|
||||
async with db_manager.get_user_connection(user_id):
|
||||
pass
|
||||
|
||||
await db_manager.execute_buffered(
|
||||
user_id,
|
||||
"INSERT INTO folders (name, owner_id) VALUES (?, ?)",
|
||||
("test_folder", user_id)
|
||||
)
|
||||
|
||||
await db_manager.flush_user(user_id)
|
||||
|
||||
user_db = db_manager.databases[user_id]
|
||||
assert user_db.dirty is False
|
||||
assert len(user_db.write_buffer) == 0
|
||||
|
||||
async with db_manager.get_user_connection(user_id) as conn:
|
||||
cursor = await conn.execute("SELECT name FROM folders WHERE owner_id = ?", (user_id,))
|
||||
rows = await cursor.fetchall()
|
||||
assert len(rows) == 1
|
||||
assert rows[0][0] == "test_folder"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_master_buffered_write(self, db_manager):
|
||||
await db_manager.execute_master_buffered(
|
||||
"INSERT INTO users (username, email, hashed_password) VALUES (?, ?, ?)",
|
||||
("testuser", "test@test.com", "hash123")
|
||||
)
|
||||
|
||||
assert db_manager.master_db.dirty is True
|
||||
await db_manager.flush_master()
|
||||
assert db_manager.master_db.dirty is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_users_isolated(self, db_manager):
|
||||
for user_id in [1, 2, 3]:
|
||||
async with db_manager.get_user_connection(user_id) as conn:
|
||||
await conn.execute(
|
||||
"INSERT INTO folders (name, owner_id) VALUES (?, ?)",
|
||||
(f"folder_user_{user_id}", user_id)
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
for user_id in [1, 2, 3]:
|
||||
async with db_manager.get_user_connection(user_id) as conn:
|
||||
cursor = await conn.execute("SELECT COUNT(*) FROM folders")
|
||||
count = (await cursor.fetchone())[0]
|
||||
assert count == 1
|
||||
@@ -0,0 +1,158 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from unittest.mock import patch, AsyncMock, MagicMock
|
||||
|
||||
from mywebdav.monitoring.health import router, check_database, check_cache, check_locks, check_task_queue, check_storage
|
||||
|
||||
|
||||
class TestHealthChecks:
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_database_success(self):
|
||||
with patch('mywebdav.database.get_user_db_manager') as mock_db:
|
||||
mock_conn = AsyncMock()
|
||||
mock_cursor = AsyncMock()
|
||||
mock_cursor.fetchone = AsyncMock(return_value=(1,))
|
||||
mock_conn.execute = AsyncMock(return_value=mock_cursor)
|
||||
|
||||
mock_manager = MagicMock()
|
||||
mock_manager.get_master_connection.return_value.__aenter__ = AsyncMock(return_value=mock_conn)
|
||||
mock_manager.get_master_connection.return_value.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
mock_db.return_value = mock_manager
|
||||
|
||||
result = await check_database()
|
||||
assert result["ok"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_database_failure(self):
|
||||
with patch('mywebdav.database.get_user_db_manager') as mock_db:
|
||||
mock_db.side_effect = Exception("Connection failed")
|
||||
|
||||
result = await check_database()
|
||||
assert result["ok"] is False
|
||||
assert "Connection failed" in result["message"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_cache_success(self):
|
||||
with patch('mywebdav.cache.get_cache') as mock_cache:
|
||||
mock_cache_instance = MagicMock()
|
||||
mock_cache_instance.get_stats.return_value = {"hits": 100, "misses": 10}
|
||||
mock_cache.return_value = mock_cache_instance
|
||||
|
||||
result = await check_cache()
|
||||
assert result["ok"] is True
|
||||
assert "stats" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_cache_failure(self):
|
||||
with patch('mywebdav.cache.get_cache') as mock_cache:
|
||||
mock_cache.side_effect = RuntimeError("Cache not initialized")
|
||||
|
||||
result = await check_cache()
|
||||
assert result["ok"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_locks_success(self):
|
||||
with patch('mywebdav.concurrency.get_lock_manager') as mock_locks:
|
||||
mock_lock_manager = MagicMock()
|
||||
mock_lock_manager.get_stats = AsyncMock(return_value={"total_locks": 5, "active_locks": 2})
|
||||
mock_locks.return_value = mock_lock_manager
|
||||
|
||||
result = await check_locks()
|
||||
assert result["ok"] is True
|
||||
assert result["stats"]["total_locks"] == 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_task_queue_success(self):
|
||||
with patch('mywebdav.workers.get_task_queue') as mock_queue:
|
||||
mock_queue_instance = MagicMock()
|
||||
mock_queue_instance.get_stats = AsyncMock(return_value={"pending_tasks": 3})
|
||||
mock_queue.return_value = mock_queue_instance
|
||||
|
||||
result = await check_task_queue()
|
||||
assert result["ok"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_storage_success(self):
|
||||
with patch('mywebdav.settings.settings') as mock_settings:
|
||||
mock_settings.STORAGE_PATH = "/tmp"
|
||||
|
||||
with patch('os.path.exists', return_value=True):
|
||||
with patch('os.statvfs') as mock_statvfs:
|
||||
mock_stat = type('obj', (object,), {
|
||||
'f_bavail': 1000000,
|
||||
'f_blocks': 2000000,
|
||||
'f_frsize': 4096
|
||||
})()
|
||||
mock_statvfs.return_value = mock_stat
|
||||
|
||||
result = await check_storage()
|
||||
assert result["ok"] is True
|
||||
assert "free_gb" in result
|
||||
assert "used_percent" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_storage_path_not_exists(self):
|
||||
with patch('mywebdav.settings.settings') as mock_settings:
|
||||
mock_settings.STORAGE_PATH = "/nonexistent/path"
|
||||
|
||||
with patch('os.path.exists', return_value=False):
|
||||
result = await check_storage()
|
||||
assert result["ok"] is False
|
||||
|
||||
|
||||
class TestHealthEndpoints:
|
||||
@pytest.fixture
|
||||
def client(self):
|
||||
from fastapi import FastAPI
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
return TestClient(app)
|
||||
|
||||
def test_liveness_check(self, client):
|
||||
response = client.get("/health/live")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["alive"] is True
|
||||
|
||||
def test_readiness_check_success(self, client):
|
||||
with patch('mywebdav.monitoring.health.check_database') as mock_check:
|
||||
mock_check.return_value = {"ok": True}
|
||||
|
||||
response = client.get("/health/ready")
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_health_check_all_healthy(self, client):
|
||||
with patch('mywebdav.monitoring.health.check_database') as mock_db, \
|
||||
patch('mywebdav.monitoring.health.check_cache') as mock_cache, \
|
||||
patch('mywebdav.monitoring.health.check_locks') as mock_locks, \
|
||||
patch('mywebdav.monitoring.health.check_task_queue') as mock_queue, \
|
||||
patch('mywebdav.monitoring.health.check_storage') as mock_storage:
|
||||
|
||||
mock_db.return_value = {"ok": True}
|
||||
mock_cache.return_value = {"ok": True}
|
||||
mock_locks.return_value = {"ok": True}
|
||||
mock_queue.return_value = {"ok": True}
|
||||
mock_storage.return_value = {"ok": True}
|
||||
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "healthy"
|
||||
|
||||
def test_health_check_degraded(self, client):
|
||||
with patch('mywebdav.monitoring.health.check_database') as mock_db, \
|
||||
patch('mywebdav.monitoring.health.check_cache') as mock_cache, \
|
||||
patch('mywebdav.monitoring.health.check_locks') as mock_locks, \
|
||||
patch('mywebdav.monitoring.health.check_task_queue') as mock_queue, \
|
||||
patch('mywebdav.monitoring.health.check_storage') as mock_storage:
|
||||
|
||||
mock_db.return_value = {"ok": True}
|
||||
mock_cache.return_value = {"ok": False, "message": "Cache error"}
|
||||
mock_locks.return_value = {"ok": True}
|
||||
mock_queue.return_value = {"ok": True}
|
||||
mock_storage.return_value = {"ok": True}
|
||||
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "degraded"
|
||||
@@ -0,0 +1,140 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
|
||||
class TestLockManager:
|
||||
@pytest.mark.asyncio
|
||||
async def test_acquire_and_release(self, lock_manager):
|
||||
async with lock_manager.acquire("resource1", owner="test", user_id=1) as token:
|
||||
assert token is not None
|
||||
assert await lock_manager.is_locked("resource1")
|
||||
|
||||
assert not await lock_manager.is_locked("resource1")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lock_prevents_concurrent_access(self, lock_manager):
|
||||
results = []
|
||||
|
||||
async def task(task_id):
|
||||
async with lock_manager.acquire("shared_resource", timeout=10.0, owner=f"task{task_id}", user_id=task_id):
|
||||
results.append(f"start_{task_id}")
|
||||
await asyncio.sleep(0.1)
|
||||
results.append(f"end_{task_id}")
|
||||
|
||||
await asyncio.gather(task(1), task(2), task(3))
|
||||
|
||||
for i in range(3):
|
||||
start_idx = results.index(f"start_{i+1}")
|
||||
end_idx = results.index(f"end_{i+1}")
|
||||
assert end_idx == start_idx + 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lock_timeout(self, lock_manager):
|
||||
async with lock_manager.acquire("resource1", owner="holder", user_id=1):
|
||||
with pytest.raises(TimeoutError):
|
||||
async with lock_manager.acquire("resource1", timeout=0.1, owner="waiter", user_id=2):
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_try_acquire_success(self, lock_manager):
|
||||
token = await lock_manager.try_acquire("resource1", owner="test", user_id=1)
|
||||
assert token is not None
|
||||
|
||||
released = await lock_manager.release("resource1", token)
|
||||
assert released is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_try_acquire_fails_when_locked(self, lock_manager):
|
||||
token1 = await lock_manager.try_acquire("resource1", owner="holder", user_id=1)
|
||||
assert token1 is not None
|
||||
|
||||
token2 = await lock_manager.try_acquire("resource1", owner="waiter", user_id=2)
|
||||
assert token2 is None
|
||||
|
||||
await lock_manager.release("resource1", token1)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_extend_lock(self, lock_manager):
|
||||
token = await lock_manager.try_acquire("resource1", owner="test", user_id=1)
|
||||
assert token is not None
|
||||
|
||||
extended = await lock_manager.extend("resource1", token, extension=60.0)
|
||||
assert extended is True
|
||||
|
||||
info = await lock_manager.get_lock_info("resource1")
|
||||
assert info is not None
|
||||
assert info.extend_count == 1
|
||||
|
||||
await lock_manager.release("resource1", token)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_lock_info(self, lock_manager):
|
||||
async with lock_manager.acquire("resource1", owner="testowner", user_id=42):
|
||||
info = await lock_manager.get_lock_info("resource1")
|
||||
assert info is not None
|
||||
assert info.owner == "testowner"
|
||||
assert info.user_id == 42
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_release_with_wrong_token(self, lock_manager):
|
||||
token = await lock_manager.try_acquire("resource1", owner="test", user_id=1)
|
||||
assert token is not None
|
||||
|
||||
released = await lock_manager.release("resource1", "wrong_token")
|
||||
assert released is False
|
||||
|
||||
assert await lock_manager.is_locked("resource1")
|
||||
|
||||
await lock_manager.release("resource1", token)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_force_release(self, lock_manager):
|
||||
token = await lock_manager.try_acquire("resource1", owner="test", user_id=1)
|
||||
assert token is not None
|
||||
|
||||
released = await lock_manager.force_release("resource1", user_id=1)
|
||||
assert released is True
|
||||
assert not await lock_manager.is_locked("resource1")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_force_release_wrong_user(self, lock_manager):
|
||||
token = await lock_manager.try_acquire("resource1", owner="test", user_id=1)
|
||||
assert token is not None
|
||||
|
||||
released = await lock_manager.force_release("resource1", user_id=2)
|
||||
assert released is False
|
||||
assert await lock_manager.is_locked("resource1")
|
||||
|
||||
await lock_manager.release("resource1", token)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_lock_key(self, lock_manager):
|
||||
key = lock_manager.build_lock_key("quota_update", user_id=123)
|
||||
assert "123" in key
|
||||
assert "quota" in key
|
||||
|
||||
key = lock_manager.build_lock_key("file_create", user_id=1, parent_id=5, name_hash="abc")
|
||||
assert "1" in key
|
||||
assert "5" in key
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stats(self, lock_manager):
|
||||
token1 = await lock_manager.try_acquire("resource1", owner="test1", user_id=1)
|
||||
token2 = await lock_manager.try_acquire("resource2", owner="test2", user_id=2)
|
||||
|
||||
stats = await lock_manager.get_stats()
|
||||
assert stats["total_locks"] == 2
|
||||
assert stats["active_locks"] == 2
|
||||
|
||||
await lock_manager.release("resource1", token1)
|
||||
await lock_manager.release("resource2", token2)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_different_resources(self, lock_manager):
|
||||
async def acquire_resource(resource_id):
|
||||
async with lock_manager.acquire(f"resource_{resource_id}", owner=f"owner{resource_id}", user_id=resource_id):
|
||||
await asyncio.sleep(0.05)
|
||||
return resource_id
|
||||
|
||||
results = await asyncio.gather(*[acquire_resource(i) for i in range(10)])
|
||||
assert sorted(results) == list(range(10))
|
||||
@@ -0,0 +1,115 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
from mywebdav.middleware.rate_limit import RateLimitMiddleware
|
||||
|
||||
|
||||
class TestRateLimiter:
|
||||
@pytest.mark.asyncio
|
||||
async def test_first_request_allowed(self, rate_limiter):
|
||||
allowed, remaining, retry_after = await rate_limiter.check_rate_limit(
|
||||
"192.168.1.1", "api"
|
||||
)
|
||||
assert allowed is True
|
||||
assert remaining == 99
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rate_limit_exhausted(self, rate_limiter):
|
||||
for i in range(100):
|
||||
await rate_limiter.check_rate_limit("192.168.1.1", "api")
|
||||
|
||||
allowed, remaining, retry_after = await rate_limiter.check_rate_limit(
|
||||
"192.168.1.1", "api"
|
||||
)
|
||||
assert allowed is False
|
||||
assert remaining == 0
|
||||
assert retry_after > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_different_keys_independent(self, rate_limiter):
|
||||
for i in range(100):
|
||||
await rate_limiter.check_rate_limit("192.168.1.1", "api")
|
||||
|
||||
allowed, _, _ = await rate_limiter.check_rate_limit("192.168.1.1", "api")
|
||||
assert allowed is False
|
||||
|
||||
allowed, _, _ = await rate_limiter.check_rate_limit("192.168.1.2", "api")
|
||||
assert allowed is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_different_limit_types(self, rate_limiter):
|
||||
for i in range(5):
|
||||
await rate_limiter.check_rate_limit("192.168.1.1", "login")
|
||||
|
||||
allowed_login, _, _ = await rate_limiter.check_rate_limit("192.168.1.1", "login")
|
||||
assert allowed_login is False
|
||||
|
||||
allowed_api, _, _ = await rate_limiter.check_rate_limit("192.168.1.1", "api")
|
||||
assert allowed_api is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_rate_limit(self, rate_limiter):
|
||||
for i in range(100):
|
||||
await rate_limiter.check_rate_limit("192.168.1.1", "api")
|
||||
|
||||
allowed, _, _ = await rate_limiter.check_rate_limit("192.168.1.1", "api")
|
||||
assert allowed is False
|
||||
|
||||
await rate_limiter.reset_rate_limit("192.168.1.1", "api")
|
||||
|
||||
allowed, _, _ = await rate_limiter.check_rate_limit("192.168.1.1", "api")
|
||||
assert allowed is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_limit(self, rate_limiter):
|
||||
for i in range(5):
|
||||
allowed, _, _ = await rate_limiter.check_rate_limit("192.168.1.1", "login")
|
||||
assert allowed is True
|
||||
|
||||
allowed, _, _ = await rate_limiter.check_rate_limit("192.168.1.1", "login")
|
||||
assert allowed is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_limit(self, rate_limiter):
|
||||
for i in range(20):
|
||||
allowed, _, _ = await rate_limiter.check_rate_limit("192.168.1.1", "upload")
|
||||
assert allowed is True
|
||||
|
||||
allowed, _, _ = await rate_limiter.check_rate_limit("192.168.1.1", "upload")
|
||||
assert allowed is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_requests(self, rate_limiter):
|
||||
async def make_request():
|
||||
return await rate_limiter.check_rate_limit("192.168.1.1", "api")
|
||||
|
||||
results = await asyncio.gather(*[make_request() for _ in range(150)])
|
||||
|
||||
allowed_count = sum(1 for allowed, _, _ in results if allowed)
|
||||
assert allowed_count == 100
|
||||
|
||||
|
||||
class TestRateLimitMiddleware:
|
||||
def test_get_limit_type_login(self):
|
||||
middleware = RateLimitMiddleware(app=None)
|
||||
assert middleware._get_limit_type("/api/auth/login") == "login"
|
||||
|
||||
def test_get_limit_type_register(self):
|
||||
middleware = RateLimitMiddleware(app=None)
|
||||
assert middleware._get_limit_type("/api/auth/register") == "register"
|
||||
|
||||
def test_get_limit_type_upload(self):
|
||||
middleware = RateLimitMiddleware(app=None)
|
||||
assert middleware._get_limit_type("/files/upload") == "upload"
|
||||
|
||||
def test_get_limit_type_download(self):
|
||||
middleware = RateLimitMiddleware(app=None)
|
||||
assert middleware._get_limit_type("/files/download/123") == "download"
|
||||
|
||||
def test_get_limit_type_webdav(self):
|
||||
middleware = RateLimitMiddleware(app=None)
|
||||
assert middleware._get_limit_type("/webdav/folder/file.txt") == "webdav"
|
||||
|
||||
def test_get_limit_type_default(self):
|
||||
middleware = RateLimitMiddleware(app=None)
|
||||
assert middleware._get_limit_type("/api/users/me") == "api"
|
||||
@@ -0,0 +1,231 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
from mywebdav.workers.queue import TaskQueue, TaskStatus, TaskPriority, Task
|
||||
|
||||
|
||||
class TestTaskQueue:
|
||||
@pytest.mark.asyncio
|
||||
async def test_enqueue_task(self, task_queue):
|
||||
async def handler(**kwargs):
|
||||
return "success"
|
||||
|
||||
task_queue.register_handler("test_handler", handler)
|
||||
|
||||
task_id = await task_queue.enqueue(
|
||||
"default",
|
||||
"test_handler",
|
||||
{"key": "value"}
|
||||
)
|
||||
|
||||
assert task_id is not None
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
task = await task_queue.get_task_status(task_id)
|
||||
assert task.status == TaskStatus.COMPLETED
|
||||
assert task.result == "success"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_task_with_payload(self, task_queue):
|
||||
received_payload = {}
|
||||
|
||||
async def handler(**kwargs):
|
||||
received_payload.update(kwargs)
|
||||
return kwargs
|
||||
|
||||
task_queue.register_handler("payload_handler", handler)
|
||||
|
||||
task_id = await task_queue.enqueue(
|
||||
"default",
|
||||
"payload_handler",
|
||||
{"user_id": 1, "file_id": 123}
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
task = await task_queue.get_task_status(task_id)
|
||||
assert task.status == TaskStatus.COMPLETED
|
||||
assert received_payload["user_id"] == 1
|
||||
assert received_payload["file_id"] == 123
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_failed_task_retry(self, task_queue):
|
||||
attempt_count = 0
|
||||
|
||||
async def failing_handler(**kwargs):
|
||||
nonlocal attempt_count
|
||||
attempt_count += 1
|
||||
if attempt_count < 3:
|
||||
raise ValueError("Temporary failure")
|
||||
return "success after retries"
|
||||
|
||||
task_queue.register_handler("retry_handler", failing_handler)
|
||||
|
||||
task_id = await task_queue.enqueue(
|
||||
"default",
|
||||
"retry_handler",
|
||||
{},
|
||||
max_retries=3
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
task = await task_queue.get_task_status(task_id)
|
||||
assert task.status == TaskStatus.COMPLETED
|
||||
assert attempt_count == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permanently_failed_task(self, task_queue):
|
||||
async def always_failing_handler(**kwargs):
|
||||
raise ValueError("Permanent failure")
|
||||
|
||||
task_queue.register_handler("failing_handler", always_failing_handler)
|
||||
|
||||
task_id = await task_queue.enqueue(
|
||||
"default",
|
||||
"failing_handler",
|
||||
{},
|
||||
max_retries=2
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
task = await task_queue.get_task_status(task_id)
|
||||
assert task.status == TaskStatus.FAILED
|
||||
assert task.retry_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_task(self, task_queue):
|
||||
async def slow_handler(**kwargs):
|
||||
await asyncio.sleep(10)
|
||||
return "done"
|
||||
|
||||
task_queue.register_handler("slow_handler", slow_handler)
|
||||
|
||||
task_id = await task_queue.enqueue(
|
||||
"default",
|
||||
"slow_handler",
|
||||
{}
|
||||
)
|
||||
|
||||
cancelled = await task_queue.cancel_task(task_id)
|
||||
task = await task_queue.get_task_status(task_id)
|
||||
|
||||
if task.status == TaskStatus.PENDING:
|
||||
assert cancelled is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_priority_ordering(self, task_queue):
|
||||
execution_order = []
|
||||
|
||||
async def order_handler(**kwargs):
|
||||
execution_order.append(kwargs["priority"])
|
||||
return kwargs["priority"]
|
||||
|
||||
task_queue.register_handler("order_handler", order_handler)
|
||||
|
||||
await task_queue.stop()
|
||||
task_queue = TaskQueue(max_workers=1)
|
||||
task_queue.register_handler("order_handler", order_handler)
|
||||
|
||||
await task_queue.enqueue("default", "order_handler", {"priority": "low"}, priority=TaskPriority.LOW)
|
||||
await task_queue.enqueue("default", "order_handler", {"priority": "normal"}, priority=TaskPriority.NORMAL)
|
||||
await task_queue.enqueue("default", "order_handler", {"priority": "high"}, priority=TaskPriority.HIGH)
|
||||
await task_queue.enqueue("default", "order_handler", {"priority": "critical"}, priority=TaskPriority.CRITICAL)
|
||||
|
||||
await task_queue.start()
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert execution_order[0] == "critical"
|
||||
assert execution_order[1] == "high"
|
||||
|
||||
await task_queue.stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_different_queues(self, task_queue):
|
||||
results = {"thumbnails": False, "cleanup": False}
|
||||
|
||||
async def thumbnail_handler(**kwargs):
|
||||
results["thumbnails"] = True
|
||||
|
||||
async def cleanup_handler(**kwargs):
|
||||
results["cleanup"] = True
|
||||
|
||||
task_queue.register_handler("thumbnail_handler", thumbnail_handler)
|
||||
task_queue.register_handler("cleanup_handler", cleanup_handler)
|
||||
|
||||
await task_queue.enqueue("thumbnails", "thumbnail_handler", {})
|
||||
await task_queue.enqueue("cleanup", "cleanup_handler", {})
|
||||
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
assert results["thumbnails"] is True
|
||||
assert results["cleanup"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stats(self, task_queue):
|
||||
async def handler(**kwargs):
|
||||
return "done"
|
||||
|
||||
task_queue.register_handler("stats_handler", handler)
|
||||
|
||||
for _ in range(5):
|
||||
await task_queue.enqueue("default", "stats_handler", {})
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
stats = await task_queue.get_stats()
|
||||
assert stats["enqueued"] == 5
|
||||
assert stats["completed"] == 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_handler(self, task_queue):
|
||||
task_id = await task_queue.enqueue(
|
||||
"default",
|
||||
"unknown_handler",
|
||||
{}
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
task = await task_queue.get_task_status(task_id)
|
||||
assert task.status == TaskStatus.FAILED
|
||||
assert "Handler not found" in task.error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cleanup_completed_tasks(self, task_queue):
|
||||
async def handler(**kwargs):
|
||||
return "done"
|
||||
|
||||
task_queue.register_handler("cleanup_test", handler)
|
||||
|
||||
task_ids = []
|
||||
for _ in range(5):
|
||||
task_id = await task_queue.enqueue("default", "cleanup_test", {})
|
||||
task_ids.append(task_id)
|
||||
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
for task_id in task_ids:
|
||||
task = await task_queue.get_task_status(task_id)
|
||||
task.completed_at = 1.0
|
||||
|
||||
await task_queue.cleanup_completed_tasks(max_age=0)
|
||||
|
||||
for task_id in task_ids:
|
||||
task = await task_queue.get_task_status(task_id)
|
||||
assert task is None
|
||||
|
||||
|
||||
class TestTask:
|
||||
def test_task_creation(self):
|
||||
task = Task(
|
||||
id="task_123",
|
||||
queue_name="default",
|
||||
handler_name="test_handler",
|
||||
payload={"key": "value"}
|
||||
)
|
||||
assert task.id == "task_123"
|
||||
assert task.status == TaskStatus.PENDING
|
||||
assert task.retry_count == 0
|
||||
@@ -0,0 +1,138 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
import time
|
||||
from jose import jwt
|
||||
|
||||
from mywebdav.auth_tokens import TokenInfo
|
||||
from mywebdav.settings import settings
|
||||
|
||||
|
||||
class TestTokenManager:
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_access_token(self, token_manager):
|
||||
token, jti = token_manager.create_access_token(
|
||||
user_id=1,
|
||||
username="testuser",
|
||||
two_factor_verified=False
|
||||
)
|
||||
|
||||
assert token is not None
|
||||
assert jti is not None
|
||||
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
assert payload["sub"] == "testuser"
|
||||
assert payload["user_id"] == 1
|
||||
assert payload["jti"] == jti
|
||||
assert payload["type"] == "access"
|
||||
assert payload["2fa_verified"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_access_token_with_2fa(self, token_manager):
|
||||
token, jti = token_manager.create_access_token(
|
||||
user_id=1,
|
||||
username="testuser",
|
||||
two_factor_verified=True
|
||||
)
|
||||
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
assert payload["2fa_verified"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_refresh_token(self, token_manager):
|
||||
token, jti = token_manager.create_refresh_token(
|
||||
user_id=1,
|
||||
username="testuser"
|
||||
)
|
||||
|
||||
assert token is not None
|
||||
assert jti is not None
|
||||
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
assert payload["sub"] == "testuser"
|
||||
assert payload["type"] == "refresh"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_revoke_token(self, token_manager):
|
||||
token, jti = token_manager.create_access_token(
|
||||
user_id=1,
|
||||
username="testuser"
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
is_revoked_before = await token_manager.is_revoked(jti)
|
||||
assert is_revoked_before is False
|
||||
|
||||
await token_manager.revoke_token(jti, user_id=1)
|
||||
|
||||
is_revoked_after = await token_manager.is_revoked(jti)
|
||||
assert is_revoked_after is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_revoke_all_user_tokens(self, token_manager):
|
||||
jtis = []
|
||||
for i in range(5):
|
||||
_, jti = token_manager.create_access_token(
|
||||
user_id=1,
|
||||
username="testuser"
|
||||
)
|
||||
jtis.append(jti)
|
||||
|
||||
_, other_jti = token_manager.create_access_token(
|
||||
user_id=2,
|
||||
username="otheruser"
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
revoked_count = await token_manager.revoke_all_user_tokens(1)
|
||||
assert revoked_count == 5
|
||||
|
||||
for jti in jtis:
|
||||
assert await token_manager.is_revoked(jti) is True
|
||||
|
||||
assert await token_manager.is_revoked(other_jti) is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_revoked_unknown_token(self, token_manager):
|
||||
is_revoked = await token_manager.is_revoked("unknown_jti")
|
||||
assert is_revoked is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stats(self, token_manager):
|
||||
token_manager.create_access_token(user_id=1, username="user1")
|
||||
token_manager.create_access_token(user_id=2, username="user2")
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
stats = await token_manager.get_stats()
|
||||
assert stats["active_tokens"] >= 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_token_tracking(self, token_manager):
|
||||
_, jti = token_manager.create_access_token(
|
||||
user_id=1,
|
||||
username="testuser"
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
assert jti in token_manager.active_tokens
|
||||
|
||||
await token_manager.revoke_token(jti)
|
||||
|
||||
assert jti not in token_manager.active_tokens
|
||||
assert jti in token_manager.blacklist
|
||||
|
||||
|
||||
class TestTokenInfo:
|
||||
def test_token_info_creation(self):
|
||||
info = TokenInfo(
|
||||
jti="test_jti",
|
||||
user_id=1,
|
||||
token_type="access",
|
||||
expires_at=time.time() + 3600
|
||||
)
|
||||
assert info.jti == "test_jti"
|
||||
assert info.user_id == 1
|
||||
assert info.token_type == "access"
|
||||
@@ -0,0 +1,212 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from mywebdav.concurrency.webdav_locks import WebDAVLockInfo
|
||||
|
||||
|
||||
class TestWebDAVLockInfo:
|
||||
def test_is_expired_false(self):
|
||||
lock = WebDAVLockInfo(
|
||||
token="token123",
|
||||
path="/test/file.txt",
|
||||
path_hash="abc123",
|
||||
owner="user1",
|
||||
user_id=1,
|
||||
timeout=3600
|
||||
)
|
||||
assert lock.is_expired is False
|
||||
|
||||
def test_is_expired_true(self):
|
||||
lock = WebDAVLockInfo(
|
||||
token="token123",
|
||||
path="/test/file.txt",
|
||||
path_hash="abc123",
|
||||
owner="user1",
|
||||
user_id=1,
|
||||
timeout=0,
|
||||
created_at=time.time() - 1
|
||||
)
|
||||
assert lock.is_expired is True
|
||||
|
||||
def test_remaining_seconds(self):
|
||||
lock = WebDAVLockInfo(
|
||||
token="token123",
|
||||
path="/test/file.txt",
|
||||
path_hash="abc123",
|
||||
owner="user1",
|
||||
user_id=1,
|
||||
timeout=100
|
||||
)
|
||||
assert 99 <= lock.remaining_seconds <= 100
|
||||
|
||||
def test_to_dict(self):
|
||||
lock = WebDAVLockInfo(
|
||||
token="token123",
|
||||
path="/test/file.txt",
|
||||
path_hash="abc123",
|
||||
owner="user1",
|
||||
user_id=1
|
||||
)
|
||||
d = lock.to_dict()
|
||||
assert d["token"] == "token123"
|
||||
assert d["path"] == "/test/file.txt"
|
||||
assert d["owner"] == "user1"
|
||||
|
||||
def test_from_dict(self):
|
||||
data = {
|
||||
"token": "token123",
|
||||
"path": "/test/file.txt",
|
||||
"path_hash": "abc123",
|
||||
"owner": "user1",
|
||||
"user_id": 1,
|
||||
"scope": "exclusive",
|
||||
"depth": "0",
|
||||
"timeout": 3600,
|
||||
"created_at": time.time()
|
||||
}
|
||||
lock = WebDAVLockInfo.from_dict(data)
|
||||
assert lock.token == "token123"
|
||||
assert lock.user_id == 1
|
||||
|
||||
|
||||
class TestPersistentWebDAVLocks:
|
||||
@pytest.mark.asyncio
|
||||
async def test_acquire_lock(self, webdav_locks):
|
||||
token = await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1)
|
||||
assert token is not None
|
||||
assert token.startswith("opaquelocktoken:")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acquire_same_path_same_user(self, webdav_locks):
|
||||
token1 = await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1)
|
||||
token2 = await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1)
|
||||
assert token1 == token2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acquire_same_path_different_user(self, webdav_locks):
|
||||
token1 = await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1)
|
||||
token2 = await webdav_locks.acquire_lock("/test/file.txt", "user2", user_id=2)
|
||||
assert token1 is not None
|
||||
assert token2 is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_release_lock(self, webdav_locks):
|
||||
token = await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1)
|
||||
released = await webdav_locks.release_lock("/test/file.txt", token)
|
||||
assert released is True
|
||||
|
||||
lock_info = await webdav_locks.check_lock("/test/file.txt")
|
||||
assert lock_info is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_release_wrong_token(self, webdav_locks):
|
||||
await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1)
|
||||
released = await webdav_locks.release_lock("/test/file.txt", "wrong_token")
|
||||
assert released is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_lock(self, webdav_locks):
|
||||
await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1)
|
||||
lock_info = await webdav_locks.check_lock("/test/file.txt")
|
||||
|
||||
assert lock_info is not None
|
||||
assert lock_info.owner == "user1"
|
||||
assert lock_info.user_id == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_lock_nonexistent(self, webdav_locks):
|
||||
lock_info = await webdav_locks.check_lock("/nonexistent/file.txt")
|
||||
assert lock_info is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_locked(self, webdav_locks):
|
||||
await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1)
|
||||
|
||||
assert await webdav_locks.is_locked("/test/file.txt") is True
|
||||
assert await webdav_locks.is_locked("/test/file.txt", user_id=1) is False
|
||||
assert await webdav_locks.is_locked("/test/file.txt", user_id=2) is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_lock(self, webdav_locks):
|
||||
token = await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1, timeout=10)
|
||||
|
||||
lock_before = await webdav_locks.check_lock("/test/file.txt")
|
||||
created_at_before = lock_before.created_at
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
refreshed = await webdav_locks.refresh_lock("/test/file.txt", token, timeout=100)
|
||||
assert refreshed is True
|
||||
|
||||
lock_after = await webdav_locks.check_lock("/test/file.txt")
|
||||
assert lock_after.timeout == 100
|
||||
assert lock_after.created_at > created_at_before
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_wrong_token(self, webdav_locks):
|
||||
await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1)
|
||||
refreshed = await webdav_locks.refresh_lock("/test/file.txt", "wrong_token")
|
||||
assert refreshed is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_force_unlock(self, webdav_locks):
|
||||
await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1)
|
||||
|
||||
unlocked = await webdav_locks.force_unlock("/test/file.txt", user_id=1)
|
||||
assert unlocked is True
|
||||
|
||||
lock_info = await webdav_locks.check_lock("/test/file.txt")
|
||||
assert lock_info is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_force_unlock_wrong_user(self, webdav_locks):
|
||||
await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1)
|
||||
|
||||
unlocked = await webdav_locks.force_unlock("/test/file.txt", user_id=2)
|
||||
assert unlocked is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_locks(self, webdav_locks):
|
||||
await webdav_locks.acquire_lock("/file1.txt", "user1", user_id=1)
|
||||
await webdav_locks.acquire_lock("/file2.txt", "user1", user_id=1)
|
||||
await webdav_locks.acquire_lock("/file3.txt", "user2", user_id=2)
|
||||
|
||||
user1_locks = await webdav_locks.get_user_locks(1)
|
||||
assert len(user1_locks) == 2
|
||||
|
||||
user2_locks = await webdav_locks.get_user_locks(2)
|
||||
assert len(user2_locks) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_lock_by_token(self, webdav_locks):
|
||||
token = await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1)
|
||||
|
||||
lock_info = await webdav_locks.get_lock_by_token(token)
|
||||
assert lock_info is not None
|
||||
assert lock_info.path == "test/file.txt"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expired_lock_cleanup(self, webdav_locks):
|
||||
await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1, timeout=0)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
lock_info = await webdav_locks.check_lock("/test/file.txt")
|
||||
assert lock_info is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stats(self, webdav_locks):
|
||||
await webdav_locks.acquire_lock("/file1.txt", "user1", user_id=1)
|
||||
await webdav_locks.acquire_lock("/file2.txt", "user1", user_id=1)
|
||||
|
||||
stats = await webdav_locks.get_stats()
|
||||
assert stats["total_locks"] == 2
|
||||
assert stats["active_locks"] == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_path_normalization(self, webdav_locks):
|
||||
token1 = await webdav_locks.acquire_lock("/test/file.txt", "user1", user_id=1)
|
||||
token2 = await webdav_locks.acquire_lock("test/file.txt", "user1", user_id=1)
|
||||
token3 = await webdav_locks.acquire_lock("/test/file.txt/", "user1", user_id=1)
|
||||
|
||||
assert token1 == token2 == token3
|
||||
Reference in New Issue
Block a user