Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions src/backend/tests/unit/services/auth/test_auth_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Tests for langflow.services.auth.exceptions module."""

from langflow.services.auth.exceptions import (
AuthenticationError,
InactiveUserError,
InsufficientPermissionsError,
InvalidCredentialsError,
InvalidTokenError,
MissingCredentialsError,
TokenExpiredError,
)


class TestAuthenticationError:
def test_message(self):
exc = AuthenticationError("test error")
assert exc.message == "test error"
assert str(exc) == "test error"

def test_error_code(self):
exc = AuthenticationError("test", error_code="my_code")
assert exc.error_code == "my_code"

def test_no_error_code(self):
exc = AuthenticationError("test")
assert exc.error_code is None


class TestInvalidCredentialsError:
def test_default_message(self):
exc = InvalidCredentialsError()
assert exc.message == "Invalid credentials provided"
assert exc.error_code == "invalid_credentials"

def test_custom_message(self):
exc = InvalidCredentialsError("Wrong password")
assert exc.message == "Wrong password"
assert exc.error_code == "invalid_credentials"

def test_is_authentication_error(self):
assert issubclass(InvalidCredentialsError, AuthenticationError)


class TestMissingCredentialsError:
def test_default_message(self):
exc = MissingCredentialsError()
assert exc.message == "No credentials provided"
assert exc.error_code == "missing_credentials"

def test_custom_message(self):
exc = MissingCredentialsError("Token required")
assert exc.message == "Token required"


class TestInactiveUserError:
def test_default_message(self):
exc = InactiveUserError()
assert exc.message == "User account is inactive"
assert exc.error_code == "inactive_user"


class TestInsufficientPermissionsError:
def test_default_message(self):
exc = InsufficientPermissionsError()
assert exc.message == "Insufficient permissions"
assert exc.error_code == "insufficient_permissions"


class TestTokenExpiredError:
def test_default_message(self):
exc = TokenExpiredError()
assert exc.message == "Authentication token has expired"
assert exc.error_code == "token_expired"


class TestInvalidTokenError:
def test_default_message(self):
exc = InvalidTokenError()
assert exc.message == "Invalid authentication token"
assert exc.error_code == "invalid_token"


class TestExceptionHierarchy:
def test_all_subclass_authentication_error(self):
subclasses = [
InvalidCredentialsError,
MissingCredentialsError,
InactiveUserError,
InsufficientPermissionsError,
TokenExpiredError,
InvalidTokenError,
]
for cls in subclasses:
assert issubclass(cls, AuthenticationError), f"{cls.__name__} should be a subclass"
Empty file.
142 changes: 142 additions & 0 deletions src/backend/tests/unit/services/cache/test_async_disk_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Tests for langflow.services.cache.disk.AsyncDiskCache."""

import asyncio

import pytest
from langflow.services.cache.disk import AsyncDiskCache
from lfx.services.cache.utils import CACHE_MISS

pytestmark = pytest.mark.asyncio


@pytest.fixture
def cache(tmp_path):
"""Create a fresh AsyncDiskCache for each test."""
return AsyncDiskCache(str(tmp_path / "cache"), max_size=10, expiration_time=3600)


@pytest.fixture
def short_expiry_cache(tmp_path):
"""Cache with very short expiration for testing expiry."""
return AsyncDiskCache(str(tmp_path / "cache_exp"), max_size=10, expiration_time=0.1)


class TestAsyncDiskCacheSetGet:
async def test_set_and_get_string(self, cache):
await cache.set("key1", "value1")
result = await cache.get("key1")
assert result == "value1"

async def test_set_and_get_dict(self, cache):
data = {"name": "test", "count": 42}
await cache.set("key1", data)
result = await cache.get("key1")
assert result == data

async def test_set_and_get_list(self, cache):
data = [1, 2, 3, "four"]
await cache.set("key1", data)
result = await cache.get("key1")
assert result == data

async def test_set_and_get_int(self, cache):
await cache.set("key1", 42)
result = await cache.get("key1")
assert result == 42

async def test_get_missing_key(self, cache):
result = await cache.get("nonexistent")
assert result is CACHE_MISS


class TestAsyncDiskCacheDelete:
async def test_delete_existing(self, cache):
from lfx.services.cache.utils import CACHE_MISS

await cache.set("key1", "value1")
await cache.delete("key1")
result = await cache.get("key1")
assert result is CACHE_MISS

async def test_delete_nonexistent(self, cache):
# Should not raise
await cache.delete("nonexistent")


class TestAsyncDiskCacheClear:
async def test_clear(self, cache):
from lfx.services.cache.utils import CACHE_MISS

await cache.set("k1", "v1")
await cache.set("k2", "v2")
await cache.clear()
assert await cache.get("k1") is CACHE_MISS
assert await cache.get("k2") is CACHE_MISS


class TestAsyncDiskCacheUpsert:
# Note: We pass an explicit lock to avoid a deadlock in AsyncDiskCache._upsert
# which calls self.set() without passing the lock, causing it to re-acquire self.lock.

async def test_upsert_new_key(self, cache):
lock = asyncio.Lock()
await cache.upsert("key1", "value1", lock=lock)
result = await cache.get("key1")
assert result == "value1"

async def test_upsert_merge_dicts(self, cache):
lock = asyncio.Lock()
await cache.set("key1", {"a": 1, "b": 2})
await cache.upsert("key1", {"b": 3, "c": 4}, lock=lock)
result = await cache.get("key1")
assert result == {"a": 1, "b": 3, "c": 4}

async def test_upsert_replace_non_dict(self, cache):
lock = asyncio.Lock()
await cache.set("key1", "old_value")
await cache.upsert("key1", "new_value", lock=lock)
result = await cache.get("key1")
assert result == "new_value"


class TestAsyncDiskCacheContains:
async def test_contains_existing(self, cache):
await cache.set("key1", "v")
assert await cache.contains("key1") is True

async def test_contains_missing(self, cache):
assert await cache.contains("nonexistent") is False


class TestAsyncDiskCacheExpiration:
async def test_expired_item_returns_cache_miss(self, short_expiry_cache):
from lfx.services.cache.utils import CACHE_MISS

await short_expiry_cache.set("key1", "value1")
await asyncio.sleep(0.5) # Wait for expiration (margin for slow CI)
result = await short_expiry_cache.get("key1")
assert result is CACHE_MISS


class TestAsyncDiskCacheTeardown:
async def test_teardown(self, cache):
await cache.set("key1", "value1")
await cache.teardown()
# After teardown, cache should be cleared
assert len(cache.cache) == 0


class TestAsyncDiskCacheLocking:
async def test_get_with_external_lock(self, cache):
lock = asyncio.Lock()
await cache.set("key1", "value1")
async with lock:
result = await cache.get("key1", lock=lock)
assert result == "value1"

async def test_set_with_external_lock(self, cache):
lock = asyncio.Lock()
async with lock:
await cache.set("key1", "value1", lock=lock)
result = await cache.get("key1")
assert result == "value1"
145 changes: 145 additions & 0 deletions src/backend/tests/unit/services/cache/test_async_in_memory_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""Tests for AsyncInMemoryCache."""

import asyncio

import pytest
from langflow.services.cache.service import AsyncInMemoryCache
from lfx.services.cache.utils import CACHE_MISS

pytestmark = pytest.mark.asyncio


class TestAsyncInMemoryCacheBasic:
"""Basic async get/set/delete/clear operations."""

async def test_set_and_get(self):
cache = AsyncInMemoryCache()
await cache.set("key1", "value1")
result = await cache.get("key1")
assert result == "value1"

async def test_get_missing_key_returns_cache_miss(self):
cache = AsyncInMemoryCache()
result = await cache.get("nonexistent")
assert result is CACHE_MISS

async def test_set_overwrites_existing(self):
cache = AsyncInMemoryCache()
await cache.set("key1", "value1")
await cache.set("key1", "value2")
result = await cache.get("key1")
assert result == "value2"

async def test_delete(self):
cache = AsyncInMemoryCache()
await cache.set("key1", "value1")
await cache.delete("key1")
result = await cache.get("key1")
assert result is CACHE_MISS

async def test_delete_nonexistent_key(self):
cache = AsyncInMemoryCache()
await cache.delete("nonexistent") # Should not raise

async def test_clear(self):
cache = AsyncInMemoryCache()
await cache.set("key1", "value1")
await cache.set("key2", "value2")
await cache.clear()
assert await cache.get("key1") is CACHE_MISS
assert await cache.get("key2") is CACHE_MISS

async def test_contains(self):
cache = AsyncInMemoryCache()
await cache.set("key1", "value1")
assert await cache.contains("key1") is True
assert await cache.contains("nonexistent") is False


class TestAsyncInMemoryCacheLRU:
"""Tests for LRU eviction in async cache."""

async def test_max_size_eviction(self):
cache = AsyncInMemoryCache(max_size=3)
await cache.set("key1", "value1")
await cache.set("key2", "value2")
await cache.set("key3", "value3")
await cache.set("key4", "value4")
# key1 should be evicted (least recently used)
assert await cache.get("key1") is CACHE_MISS
assert await cache.get("key4") == "value4"

async def test_access_refreshes_order(self):
cache = AsyncInMemoryCache(max_size=3)
await cache.set("key1", "value1")
await cache.set("key2", "value2")
await cache.set("key3", "value3")
# Access key1 to make it recently used
await cache.get("key1")
# Add key4 should evict key2
await cache.set("key4", "value4")
assert await cache.get("key1") == "value1"
assert await cache.get("key2") is CACHE_MISS


class TestAsyncInMemoryCacheExpiration:
"""Tests for expiration in async cache."""

async def test_expired_item_returns_cache_miss(self):
cache = AsyncInMemoryCache(expiration_time=0.1)
await cache.set("key1", "value1")
await asyncio.sleep(0.5)
result = await cache.get("key1")
assert result is CACHE_MISS


class TestAsyncInMemoryCacheUpsert:
"""Tests for upsert in async cache."""

async def test_upsert_new_key(self):
cache = AsyncInMemoryCache()
await cache.upsert("key1", {"a": 1})
result = await cache.get("key1")
assert result == {"a": 1}

async def test_upsert_merges_dicts(self):
cache = AsyncInMemoryCache()
await cache.set("key1", {"a": 1, "b": 2})
await cache.upsert("key1", {"b": 3, "c": 4})
result = await cache.get("key1")
assert result == {"a": 1, "b": 3, "c": 4}

async def test_upsert_non_dict_replaces(self):
cache = AsyncInMemoryCache()
await cache.set("key1", "old")
await cache.upsert("key1", "new")
result = await cache.get("key1")
assert result == "new"


class TestAsyncInMemoryCacheDataTypes:
"""Tests for various data types in async cache."""

async def test_store_dict(self):
cache = AsyncInMemoryCache()
await cache.set("key1", {"nested": {"data": True}})
result = await cache.get("key1")
assert result == {"nested": {"data": True}}

async def test_store_list(self):
cache = AsyncInMemoryCache()
await cache.set("key1", [1, 2, 3])
result = await cache.get("key1")
assert result == [1, 2, 3]

async def test_store_numeric(self):
cache = AsyncInMemoryCache()
await cache.set("key1", 42)
result = await cache.get("key1")
assert result == 42

async def test_store_string(self):
cache = AsyncInMemoryCache()
await cache.set("key1", "hello world")
result = await cache.get("key1")
assert result == "hello world"
Loading
Loading