diff --git a/tests/unit/authorization/test_middleware.py b/tests/unit/authorization/test_middleware.py index 8cd2ea97e..da80f1145 100644 --- a/tests/unit/authorization/test_middleware.py +++ b/tests/unit/authorization/test_middleware.py @@ -26,7 +26,14 @@ @pytest.fixture(name="dummy_auth_tuple") def fixture_dummy_auth_tuple() -> AuthTuple: - """Standard auth tuple for testing.""" + """ + Provide a standard AuthTuple used by tests. + + Returns: + AuthTuple: A tuple of (user_id, username, is_admin_flag, token) where + `user_id` and `username` are strings, `is_admin_flag` is a boolean, and + `token` is a mock token string. + """ return ("user_id", "username", False, "mock_token") @@ -35,7 +42,17 @@ class TestGetAuthorizationResolvers: @pytest.fixture def mock_configuration(self, mocker: MockerFixture) -> MockType: - """Mock configuration object.""" + """ + Create a mock configuration with empty authorization access rules and empty JWT role rules. + + Parameters: + mocker (pytest_mock.MockerFixture): Fixture used to create the MagicMock. + + Returns: + Mock: A MagicMock whose `authorization_configuration.access_rules` and + `authentication_configuration.jwk_configuration.jwt_configuration.role_rules` + are set to empty lists. + """ config = mocker.MagicMock() config.authorization_configuration.access_rules = [] config.authentication_configuration.jwk_configuration.jwt_configuration.role_rules = ( @@ -45,12 +62,24 @@ def mock_configuration(self, mocker: MockerFixture) -> MockType: @pytest.fixture def sample_access_rule(self) -> AccessRule: - """Sample access rule for testing.""" + """ + Create a sample AccessRule with role "test" and the Query action for use in tests. + + Returns: + AccessRule: An AccessRule configured with role "test" and actions + containing `Action.QUERY`. + """ return AccessRule(role="test", actions=[Action.QUERY]) @pytest.fixture def sample_role_rule(self) -> JwtRoleRule: - """Sample role rule for testing.""" + """ + Create a sample role rule of type JwtRoleRule. + + Returns: + JwtRoleRule: Configured with jsonpath "$.test", operator + `JsonPathOperator.EQUALS`, value "test", and roles ["test"]. + """ return JwtRoleRule( jsonpath="$.test", operator=JsonPathOperator.EQUALS, @@ -170,7 +199,18 @@ class TestPerformAuthorizationCheck: @pytest.fixture def mock_resolvers(self, mocker: MockerFixture) -> tuple[MockType, MockType]: - """Mock role and access resolvers.""" + """ + Create paired mock role and access resolvers for tests. + + Parameters: + mocker (MockerFixture): Pytest mocker fixture used to create mock objects. + + Returns: + tuple: (role_resolver, access_resolver) + - role_resolver: an AsyncMock whose `resolve_roles` returns `{"employee"}`. + - access_resolver: a MagicMock with `check_access` returning + `True` and `get_actions` returning `{Action.QUERY}`. + """ role_resolver = mocker.AsyncMock() access_resolver = mocker.MagicMock() role_resolver.resolve_roles.return_value = {"employee"} @@ -191,7 +231,15 @@ async def test_access_denied( dummy_auth_tuple: AuthTuple, mock_resolvers: tuple[MockType, MockType], ) -> None: - """Test HTTPException when access is denied.""" + """ + Verify that auth.check raises an HTTP 403 with expected details when access is denied. + + Patches the authorization resolvers to deny access, invokes + _perform_authorization_check for Action.ADMIN, and asserts the raised + HTTPException has status 403 and a detail payload containing a + `response` message about lack of permission and a `cause` that mentions + the user is not authorized to access the endpoint. + """ role_resolver, access_resolver = mock_resolvers access_resolver.check_access.return_value = False # Override to deny access diff --git a/tests/unit/authorization/test_resolvers.py b/tests/unit/authorization/test_resolvers.py index 2df1cc27d..d274e0780 100644 --- a/tests/unit/authorization/test_resolvers.py +++ b/tests/unit/authorization/test_resolvers.py @@ -15,7 +15,20 @@ def claims_to_token(claims: dict) -> str: - """Convert JWT claims dictionary to a JSON string token.""" + """Convert JWT claims dictionary to a JSON string token. + + Create a JWT-like token by encoding the provided claims into a + base64url JSON payload and wrapping it with placeholder header + and signature. + + Parameters: + claims (dict): JWT claims to serialize and encode. + + Returns: + token (str): A token string in the form "foo_header..foo_signature", where the payload is base64url-encoded without + padding. + """ string_claims = json.dumps(claims) b64_encoded_claims = ( @@ -26,7 +39,18 @@ def claims_to_token(claims: dict) -> str: def claims_to_auth_tuple(claims: dict) -> AuthTuple: - """Convert JWT claims dictionary to an auth tuple.""" + """ + Builds an AuthTuple from JWT claims for use in tests. + + Parameters: + claims (dict): JWT claims to encode into the returned token. + + Returns: + AuthTuple: A 4-tuple (username, token_id, expired, jwt_token) where + `username` is the fixed string "user", `token_id` is the fixed string + "token", `expired` is False, and `jwt_token` is the token produced from + `claims`. + """ return ("user", "token", False, claims_to_token(claims)) @@ -35,7 +59,16 @@ class TestJwtRolesResolver: @pytest.fixture async def employee_role_rule(self) -> JwtRoleRule: - """Role rule for RedHat employees.""" + """Role rule for RedHat employees. + + JwtRoleRule that grants the "employee" role when + `realm_access.roles` contains "redhat:employees". + + Returns: + JwtRoleRule: Configured to match `$.realm_access.roles[*]` with a + CONTAINS operator for the value `"redhat:employees"` and map + matches to the `["employee"]` role. + """ return JwtRoleRule( jsonpath="$.realm_access.roles[*]", operator=JsonPathOperator.CONTAINS, @@ -47,7 +80,17 @@ async def employee_role_rule(self) -> JwtRoleRule: async def employee_resolver( self, employee_role_rule: JwtRoleRule ) -> JwtRolesResolver: - """JwtRolesResolver with a rule for RedHat employees.""" + """JwtRolesResolver with a rule for RedHat employees. + + Create a JwtRolesResolver configured with the provided + employee role rule. + + Parameters: + employee_role_rule (JwtRoleRule): Rule used to map JWT claims to the employee role. + + Returns: + JwtRolesResolver: Resolver initialized with the given rule. + """ return JwtRolesResolver([employee_role_rule]) @pytest.fixture @@ -70,7 +113,14 @@ async def employee_claims(self) -> dict[str, Any]: @pytest.fixture async def non_employee_claims(self) -> dict[str, Any]: - """JWT claims for a non-RedHat employee.""" + """JWT claims for a non-RedHat employee. + + Provide JWT claims representing a non-Red Hat employee. + + Returns: + dict: JWT claims where `realm_access.roles` does not include the Red Hat employee role + (e.g., contains `"uma_authorization"` and `"default-roles-example"`). + """ return { "exp": 1754489339, "iat": 1754488439, @@ -114,7 +164,13 @@ async def test_negate_operator( @pytest.fixture async def email_rule_resolver(self) -> JwtRolesResolver: - """JwtRolesResolver with a rule for email domain.""" + """JwtRolesResolver with a rule for email domain. + + Returns: + JwtRolesResolver: Resolver configured with a single MATCH rule on + `$.email` using the regex with RedHat domain that yields the + `redhat_employee` role. + """ return JwtRolesResolver( [ JwtRoleRule( @@ -128,7 +184,13 @@ async def email_rule_resolver(self) -> JwtRolesResolver: @pytest.fixture async def equals_rule_resolver(self) -> JwtRolesResolver: - """JwtRolesResolver with a rule for exact email match.""" + """JwtRolesResolver with a rule for exact email match. + + Returns: + JwtRolesResolver: Resolver configured with one JwtRoleRule + (jsonpath="$.foo", operator=EQUALS, value=["bar"], + roles=["foobar"]). + """ return JwtRolesResolver( [ JwtRoleRule( @@ -150,7 +212,12 @@ async def test_resolve_roles_equals_operator( @pytest.fixture async def in_rule_resolver(self) -> JwtRolesResolver: - """JwtRolesResolver with a rule for IN operator.""" + """JwtRolesResolver with a rule for IN operator. + + Returns: + JwtRolesResolver: Resolver that maps the JSONPath value ['bar'] or + ['baz'] at "$.foo" to the role "in_role". + """ return JwtRolesResolver( [ JwtRoleRule( @@ -281,12 +348,23 @@ class TestGenericAccessResolver: @pytest.fixture def admin_access_rules(self) -> list[AccessRule]: - """Access rules with admin role for testing.""" + """Access rules with admin role for testing. + + Returns: + list[AccessRule]: A list with one AccessRule for role "superuser" + whose actions include Action.ADMIN. + """ return [AccessRule(role="superuser", actions=[Action.ADMIN])] @pytest.fixture def multi_role_access_rules(self) -> list[AccessRule]: - """Access rules with multiple roles for testing.""" + """Access rules with multiple roles for testing. + + Returns: + list[AccessRule]: A list containing two AccessRule instances — the + "user" role allowing `Action.QUERY` and `Action.GET_MODELS`, and + the "moderator" role allowing `Action.FEEDBACK`. + """ return [ AccessRule(role="user", actions=[Action.QUERY, Action.GET_MODELS]), AccessRule(role="moderator", actions=[Action.FEEDBACK]), diff --git a/tests/unit/cache/test_cache_factory.py b/tests/unit/cache/test_cache_factory.py index 23ce2d7ea..06aeb772f 100644 --- a/tests/unit/cache/test_cache_factory.py +++ b/tests/unit/cache/test_cache_factory.py @@ -29,13 +29,28 @@ @pytest.fixture(scope="module", name="noop_cache_config_fixture") def noop_cache_config() -> ConversationHistoryConfiguration: - """Fixture containing initialized instance of ConversationHistoryConfiguration.""" + """Fixture containing initialized instance of ConversationHistoryConfiguration. + + Provide a ConversationHistoryConfiguration configured for the + NOOP cache type. + + Returns: + ConversationHistoryConfiguration: configuration instance with `type` set to CACHE_TYPE_NOOP + """ return ConversationHistoryConfiguration(type=CACHE_TYPE_NOOP) @pytest.fixture(scope="module", name="memory_cache_config_fixture") def memory_cache_config() -> ConversationHistoryConfiguration: - """Fixture containing initialized instance of InMemory cache.""" + """Fixture containing initialized instance of InMemory cache. + + Provide a ConversationHistoryConfiguration configured for an + in-memory conversation cache. + + Returns: + ConversationHistoryConfiguration: Configuration with type set to + in-memory and an InMemoryCacheConfig(max_entries=10). + """ return ConversationHistoryConfiguration( type=CACHE_TYPE_MEMORY, memory=InMemoryCacheConfig(max_entries=10) ) @@ -43,7 +58,16 @@ def memory_cache_config() -> ConversationHistoryConfiguration: @pytest.fixture(scope="module", name="postgres_cache_config_fixture") def postgres_cache_config() -> ConversationHistoryConfiguration: - """Fixture containing initialized instance of PostgreSQL cache.""" + """Fixture containing initialized instance of PostgreSQL cache. + + Create a ConversationHistoryConfiguration configured for a + PostgreSQL cache. + + Returns: + ConversationHistoryConfiguration: Configuration with type set to + POSTGRES and `postgres` populated with db="database", user="user", and + password=SecretStr("password"). + """ return ConversationHistoryConfiguration( type=CACHE_TYPE_POSTGRES, postgres=PostgreSQLDatabaseConfiguration( @@ -54,7 +78,20 @@ def postgres_cache_config() -> ConversationHistoryConfiguration: @pytest.fixture(name="sqlite_cache_config_fixture") def sqlite_cache_config(tmpdir: Path) -> ConversationHistoryConfiguration: - """Fixture containing initialized instance of SQLite cache.""" + """Fixture containing initialized instance of SQLite cache. + + Create a ConversationHistoryConfiguration for an SQLite cache + using a temporary directory. + + Parameters: + tmpdir (Path): Temporary directory path; the SQLite file will be + created at `tmpdir / "test.sqlite"`. + + Returns: + ConversationHistoryConfiguration: Configuration with `type` set to the + SQLite cache constant and `sqlite` set to a SQLiteDatabaseConfiguration + pointing to the test database path. + """ db_path = str(tmpdir / "test.sqlite") return ConversationHistoryConfiguration( type=CACHE_TYPE_SQLITE, sqlite=SQLiteDatabaseConfiguration(db_path=db_path) @@ -63,7 +100,14 @@ def sqlite_cache_config(tmpdir: Path) -> ConversationHistoryConfiguration: @pytest.fixture(scope="module", name="invalid_cache_type_config_fixture") def invalid_cache_type_config() -> ConversationHistoryConfiguration: - """Fixture containing instance of ConversationHistoryConfiguration with improper settings.""" + """Fixture containing instance of ConversationHistoryConfiguration with improper settings. + + Create a ConversationHistoryConfiguration whose type is set to + an invalid string to test factory validation. + + Returns: + ConversationHistoryConfiguration: configuration with `type` set to "foo bar baz". + """ c = ConversationHistoryConfiguration() # the conversation cache type name is incorrect in purpose c.type = "foo bar baz" # pyright: ignore @@ -112,7 +156,15 @@ def test_conversation_cache_sqlite( def test_conversation_cache_sqlite_improper_config(tmpdir: Path) -> None: - """Check if memory cache configuration is checked in cache factory.""" + """Check if memory cache configuration is checked in cache factory. + + Verifies that a nil SQLite configuration causes + CacheFactory.conversation_cache to raise a ValueError. + + Expects a ValueError with message containing "Expecting configuration for + SQLite cache" when the ConversationHistoryConfiguration has type SQLITE but + its sqlite field is None. + """ db_path = str(tmpdir / "test.sqlite") cc = ConversationHistoryConfiguration( type=CACHE_TYPE_SQLITE, sqlite=SQLiteDatabaseConfiguration(db_path=db_path) @@ -136,7 +188,15 @@ def test_conversation_cache_postgres( def test_conversation_cache_postgres_improper_config() -> None: - """Check if PostgreSQL cache configuration is checked in cache factory.""" + """Check if PostgreSQL cache configuration is checked in cache factory. + + Verify that the cache factory raises a ValueError when the PostgreSQL configuration is missing. + + This test simulates an absent `postgres` config on a + ConversationHistoryConfiguration with type `POSTGRES` and asserts that + CacheFactory.conversation_cache raises ValueError with message containing + "Expecting configuration for PostgreSQL cache". + """ cc = ConversationHistoryConfiguration( type=CACHE_TYPE_POSTGRES, postgres=PostgreSQLDatabaseConfiguration( @@ -152,7 +212,14 @@ def test_conversation_cache_postgres_improper_config() -> None: def test_conversation_cache_no_type() -> None: - """Check if wrong cache configuration is detected properly.""" + """Check if wrong cache configuration is detected properly. + + Verify that a ConversationHistoryConfiguration with no type causes the factory to reject it. + + Asserts that calling CacheFactory.conversation_cache with a configuration + whose `type` is None raises a ValueError with message "Cache type must be + set". + """ cc = ConversationHistoryConfiguration(type=CACHE_TYPE_NOOP) # simulate improper configuration (can not be done directly as model checks this) cc.type = None @@ -163,6 +230,13 @@ def test_conversation_cache_no_type() -> None: def test_conversation_cache_wrong_cache( invalid_cache_type_config_fixture: ConversationHistoryConfiguration, ) -> None: - """Check if wrong cache configuration is detected properly.""" + """Check if wrong cache configuration is detected properly. + + Verify that an unsupported cache type in the configuration raises a ValueError. + + This test calls CacheFactory.conversation_cache with a configuration whose + type is invalid and asserts a ValueError is raised with a message + containing "Invalid cache type". + """ with pytest.raises(ValueError, match="Invalid cache type"): CacheFactory.conversation_cache(invalid_cache_type_config_fixture) diff --git a/tests/unit/cache/test_noop_cache.py b/tests/unit/cache/test_noop_cache.py index 0ad3ac9d2..f05ba78c5 100644 --- a/tests/unit/cache/test_noop_cache.py +++ b/tests/unit/cache/test_noop_cache.py @@ -29,7 +29,13 @@ @pytest.fixture(name="cache_fixture") def cache() -> NoopCache: - """Fixture with constucted and initialized in memory cache object.""" + """Fixture with constructed and initialized in memory cache object. + + Create and initialize an in-memory NoopCache for use as a test fixture. + + Returns: + NoopCache: An initialized NoopCache instance ready for tests. + """ c = NoopCache() c.initialize_cache() return c @@ -50,7 +56,15 @@ def test_insert_or_append(cache_fixture: NoopCache) -> None: def test_insert_or_append_skip_user_id_check(cache_fixture: NoopCache) -> None: - """Test the behavior of insert_or_append method.""" + """Test the behavior of insert_or_append method. + + Verify that insert_or_append accepts a non-UUID user identifier when + skip_user_id_check is True. + + This test calls insert_or_append with a user-provided (non-UUID) user id, a + conversation id, and a cache entry while passing skip_user_id_check=True; + the operation is expected to complete without raising an exception. + """ skip_user_id_check = True cache_fixture.insert_or_append( USER_PROVIDED_USER_ID, CONVERSATION_ID, cache_entry_1, skip_user_id_check @@ -95,7 +109,14 @@ def test_delete_nonexistent_conversation(cache_fixture: NoopCache) -> None: def test_delete_improper_conversation_id(cache_fixture: NoopCache) -> None: - """Test delete with invalid conversation ID.""" + """Test delete with invalid conversation ID. + + Verify that deleting with an invalid conversation ID raises a ValueError. + + Raises: + ValueError: If the conversation ID is not a valid UUID (message + contains "Invalid conversation ID"). + """ with pytest.raises(ValueError, match="Invalid conversation ID"): cache_fixture.delete(USER_ID, "invalid-id") @@ -154,7 +175,12 @@ def test_list_no_conversations(cache_fixture: NoopCache) -> None: def test_ready(cache_fixture: NoopCache) -> None: - """Test if in memory cache always report ready.""" + """Test if in memory cache always report ready. + + Verify that the in-memory NoopCache reports readiness. + + Asserts that calling ready() on the provided cache_fixture returns True. + """ assert cache_fixture.ready() @@ -193,6 +219,12 @@ def test_get_improper_user_id(cache_fixture: NoopCache, uuid: str | None) -> Non def test_get_improper_conversation_id(cache_fixture: NoopCache) -> None: - """Test how improper conversation ID is handled.""" + """Test how improper conversation ID is handled. + + Validate that get() raises a ValueError when the conversation ID is not a valid UUID. + + Calls get() with an invalid conversation ID and expects a ValueError + containing "Invalid conversation ID". + """ with pytest.raises(ValueError, match="Invalid conversation ID"): cache_fixture.get(USER_ID, "this-is-not-valid-uuid") diff --git a/tests/unit/cache/test_postgres_cache.py b/tests/unit/cache/test_postgres_cache.py index 0c161223a..e55c5a542 100644 --- a/tests/unit/cache/test_postgres_cache.py +++ b/tests/unit/cache/test_postgres_cache.py @@ -47,10 +47,24 @@ class CursorMock: """Mock class for simulating DB cursor exceptions.""" def __init__(self) -> None: - """Construct the mock cursor class.""" + """Construct the mock cursor class. + + Initialize the mock database cursor used in tests. + + Sets up internal state so the cursor simulates a DatabaseError when `execute` is called. + """ def execute(self, command: Any) -> None: - """Execute any SQL command.""" + """Execute any SQL command. + + Execute the given SQL command using the database cursor. + + Parameters: + command (Any): SQL statement or command object to execute. + + Raises: + psycopg2.DatabaseError: Always raised with the message "can not INSERT". + """ raise psycopg2.DatabaseError("can not INSERT") @@ -62,13 +76,28 @@ def __init__(self) -> None: """Construct the connection mock class.""" def cursor(self) -> None: - """Getter for mock cursor.""" + """Getter for mock cursor. + + Simulate obtaining a database cursor and raise an OperationalError to + represent a failed connection. + + Raises: + psycopg2.OperationalError: Always raised to simulate inability to acquire a cursor. + """ raise psycopg2.OperationalError("can not SELECT") @pytest.fixture(scope="module", name="postgres_cache_config_fixture") def postgres_cache_config() -> PostgreSQLDatabaseConfiguration: - """Fixture containing initialized instance of PostgreSQL cache.""" + """Fixture containing initialized instance of PostgreSQL cache. + + Create a PostgreSQLDatabaseConfiguration with placeholder connection values for use in tests. + + Returns: + PostgreSQLDatabaseConfiguration: A configuration object with host, + port, db, user, and a SecretStr password. Values are placeholders and + not intended for real database connections. + """ # can be any configuration, becuase tests won't really try to # connect to database return PostgreSQLDatabaseConfiguration( @@ -84,7 +113,13 @@ def test_cache_initialization( postgres_cache_config_fixture: PostgreSQLDatabaseConfiguration, mocker: MockerFixture, ) -> None: - """Test the get operation when DB is connected.""" + """Test the get operation when DB is connected. + + Verifies that PostgresCache constructs successfully and exposes a non-None + connection when psycopg2.connect is available. + + Asserts the created cache object is not None and that its `connection` attribute is set. + """ # prevent real connection to PG instance mocker.patch("psycopg2.connect") cache = PostgresCache(postgres_cache_config_fixture) @@ -111,7 +146,14 @@ def test_cache_initialization_connect_finalizer( postgres_cache_config_fixture: PostgreSQLDatabaseConfiguration, mocker: MockerFixture, ) -> None: - """Test the get operation when DB is not connected.""" + """Test the get operation when DB is not connected. + + Ensure PostgresCache propagates exceptions raised during its initialization. + + Patches psycopg2.connect to avoid real DB access and makes + PostgresCache.initialize_cache raise an exception; constructing + PostgresCache must raise that exception. + """ # prevent real connection to PG instance mocker.patch("psycopg2.connect") @@ -213,7 +255,11 @@ def test_get_operation_when_disconnected( postgres_cache_config_fixture: PostgreSQLDatabaseConfiguration, mocker: MockerFixture, ) -> None: - """Test the get() method.""" + """Test the get() method. + + Verify that get() raises a CacheError with message "cache is disconnected" + when the cache has no active database connection. + """ # prevent real connection to PG instance mocker.patch("psycopg2.connect") cache = PostgresCache(postgres_cache_config_fixture) @@ -335,7 +381,15 @@ def test_delete_operation_operation_error( postgres_cache_config_fixture: PostgreSQLDatabaseConfiguration, mocker: MockerFixture, ) -> None: - """Test the delete() method.""" + """Test the delete() method. + + Verifies that PostgresCache.delete raises a CacheError when the database + connection fails during a delete operation. + + The test patches psycopg2.connect, injects a ConnectionMock that simulates + a connection error, and asserts that calling delete(...) raises a + CacheError containing the substring "delete". + """ # prevent real connection to PG instance mocker.patch("psycopg2.connect") cache = PostgresCache(postgres_cache_config_fixture) @@ -417,7 +471,10 @@ def test_topic_summary_after_conversation_delete( postgres_cache_config_fixture: PostgreSQLDatabaseConfiguration, mocker: MockerFixture, ) -> None: - """Test that topic summary is deleted when conversation is deleted.""" + """Test that topic summary is deleted when conversation is deleted. + + Verify that deleting a conversation also removes its topic summary from the cache. + """ # prevent real connection to PG instance mock_connect = mocker.patch("psycopg2.connect") cache = PostgresCache(postgres_cache_config_fixture) diff --git a/tests/unit/cache/test_sqlite_cache.py b/tests/unit/cache/test_sqlite_cache.py index 48617f7ea..9e8f5626f 100644 --- a/tests/unit/cache/test_sqlite_cache.py +++ b/tests/unit/cache/test_sqlite_cache.py @@ -47,7 +47,13 @@ def __init__(self) -> None: """Construct the mock cursor class.""" def execute(self, command: Any) -> None: - """Execute any SQL command.""" + """Execute any SQL command. + + Execute the provided SQL command on this cursor. + + Raises: + sqlite3.Error: Always raised with message "can not SELECT". + """ raise sqlite3.Error("can not SELECT") @@ -56,15 +62,39 @@ class ConnectionMock: """Mock class for connection.""" def __init__(self) -> None: - """Construct the connection mock class.""" + """Construct the connection mock class. + + Create a mock database connection whose cursor simulates execution errors. + + The mock's cursor() method returns a CursorMock whose execute() raises + sqlite3.Error, used to simulate a faulty connection in tests. + """ def cursor(self) -> Any: - """Getter for mock cursor.""" + """Getter for mock cursor. + + Provide a mock database cursor for testing. + + Returns: + CursorMock: A mock cursor instance that simulates a DB cursor; its + `execute` raises `sqlite3.Error` to emulate select-related errors. + """ return CursorMock() def create_cache(path: Path) -> SQLiteCache: - """Create the cache instance.""" + """Create the cache instance. + + Create a SQLiteCache configured to use a test.sqlite file + inside the given directory. + + Parameters: + path (Path): Directory in which the `test.sqlite` database file will be created. + + Returns: + SQLiteCache: Cache instance configured to use the `test.sqlite` + database at the provided path. + """ db_path = str(path / "test.sqlite") cc = SQLiteDatabaseConfiguration(db_path=db_path) return SQLiteCache(cc) @@ -78,7 +108,14 @@ def test_cache_initialization(tmpdir: Path) -> None: def test_cache_initialization_wrong_connection() -> None: - """Test the get operation when DB can not be connected.""" + """Test the get operation when DB can not be connected. + + Verify that creating a cache with an invalid database path fails to open the database. + + Asserts that attempting to create a SQLiteCache with a non-existent or + inaccessible path raises an exception containing the text "unable to open + database file". + """ with pytest.raises(Exception, match="unable to open database file"): _ = create_cache(Path("/foo/bar/baz")) @@ -116,7 +153,13 @@ def test_initialize_cache_when_connected(tmpdir: Path) -> None: def test_initialize_cache_when_disconnected(tmpdir: Path) -> None: - """Test the initialize_cache().""" + """Test the initialize_cache(). + + Verify that initialize_cache raises a CacheError when the cache is disconnected. + + Raises: + CacheError: If the cache connection is None with message "cache is disconnected". + """ cache = create_cache(tmpdir) cache.connection = None @@ -125,7 +168,14 @@ def test_initialize_cache_when_disconnected(tmpdir: Path) -> None: def test_get_operation_when_disconnected(tmpdir: Path) -> None: - """Test the get() method.""" + """Test the get() method. + + Verify that retrieving entries raises CacheError when the cache is disconnected. + + Sets the cache connection to None and asserts that calling `get` for a user + and conversation raises a `CacheError` with the message "cache is + disconnected". + """ cache = create_cache(tmpdir) cache.connection = None # no operation for @connection decorator @@ -197,7 +247,13 @@ def test_list_operation_when_disconnected(tmpdir: Path) -> None: def test_list_operation_when_connected(tmpdir: Path) -> None: - """Test the list() method.""" + """Test the list() method. + + Verify that listing conversations on a newly created, connected cache returns an empty list. + + Asserts that cache.list(USER_ID_1, False) produces a falsy result and that + the returned value is a list. + """ cache = create_cache(tmpdir) # should not fail diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index ed75c1f6d..6a5afcac3 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -29,7 +29,8 @@ def prepare_agent_mocks_fixture( with proper agent_id setup to avoid initialization errors. Yields: - tuple: (mock_client, mock_agent) + tuple: (mock_client, mock_agent) — two AsyncMock objects + representing the client and the agent. """ mock_client = mocker.AsyncMock() mock_agent = mocker.AsyncMock() diff --git a/tests/unit/metrics/test_utis.py b/tests/unit/metrics/test_utis.py index be1338f59..62d3a0f2f 100644 --- a/tests/unit/metrics/test_utis.py +++ b/tests/unit/metrics/test_utis.py @@ -78,7 +78,17 @@ async def test_setup_model_metrics(mocker: MockerFixture) -> None: def test_update_llm_token_count_from_turn(mocker: MockerFixture) -> None: - """Test the update_llm_token_count_from_turn function.""" + """Test the update_llm_token_count_from_turn function. + + Verifies that update_llm_token_count_from_turn increments LLM token metrics + for received and sent tokens using the token counts produced by the + formatter. + + Sets up a mock formatter that returns 3 tokens for the output and 2 tokens + for the input, then asserts that: + - llm_token_received_total is labeled with the provider and model and incremented by 3. + - llm_token_sent_total is labeled with the provider and model and incremented by 2. + """ mocker.patch("metrics.utils.Tokenizer.get_instance") mock_formatter_class = mocker.patch("metrics.utils.ChatFormat") mock_formatter = mocker.Mock()