# flake8: noqa F811,F401 """Integration tests for the database module. The tests are performed using a test database that is created and dropped for each test run. The test database is a copy of the public database schema, with the following nuances: * No data is copied from the public database (test database starts empty). * Auto-increment values are reset to 1 for all tables. """ import asyncio from datetime import datetime, timedelta from functools import partial from uuid import uuid4 import pytest import pytest_asyncio from conftest import new_user from conftest import use_test_db as utdb from src.backend.connectors.db import db from src.backend.constants import ( AuditStatus, AuditTypes, DBAuditLogActions, DBColumns, RowReportCols, RowTypes, SnowFlakeColumns, Tables, User, ) from src.backend.models import NewAuditFlag from src.backend.typings import AuditGroupID, AuditID @pytest.fixture(autouse=True) def use_test_db(utdb): # noqa yield utdb class TestDatabase: _class = db.Database @pytest.fixture(scope="class") def instance(self): yield self._class() @pytest_asyncio.fixture async def connection(self, instance): conn = await instance.get_connection() yield conn @pytest.mark.asyncio async def test_many_simultaneous_connections(self, instance): conn_count = 20 async def open_and_yield_connection(): conn = await instance.get_connection() return conn tasks = [open_and_yield_connection() for _ in range(conn_count)] try: conns = await asyncio.gather(*tasks) finally: assert all( conn.open for conn in conns ), "Expected all connections to be open" for conn in conns: conn.close() assert all( not conn.open for conn in conns ), "Expected all connections to be closed" @pytest.mark.asyncio async def test_execute_query(self, instance, connection): temp_table_name = f"temp_table_{uuid4().hex}" temp_table_sql = f""" CREATE TEMPORARY TABLE {temp_table_name} (id INT AUTO_INCREMENT PRIMARY KEY, value INT DEFAULT 0) """ await instance.execute_query_nofetch(temp_table_sql, connection=connection) # Insert 5 rows for i in range(5): last_row_id = await instance.execute_query_nofetch( f"INSERT INTO {temp_table_name} (value) VALUES ({i})", lastrowid=True, connection=connection, ) expected_last_row_id = i + 1 assert last_row_id == expected_last_row_id rows = await instance.execute_query_fetchall( f"SELECT * FROM {temp_table_name} ORDER BY id", connection=connection ) assert len(rows) == 5 class TestClient: @pytest.fixture def instance(self, use_test_db): """Create a new test instance of the database client.""" yield db.Client() @pytest.fixture def add_audit_rows(self, instance): async def _(rows: list[tuple]): await asyncio.gather(*[instance.AuditRows.add(*row) for row in rows]) await instance.AuditRows.save() return _ @pytest_asyncio.fixture async def new_audit(self, instance, new_user) -> AuditGroupID: audit_number = await instance.AuditGroups.new( user=new_user, label_id=1, label_name="mock_label", include_audio=True, include_video=True, include_art_track=True, scheduled_for=datetime.now(), ) return audit_number @pytest_asyncio.fixture async def audits(self, instance) -> list[dict]: """Return the rows of the audits table.""" return await instance.db.execute_query_fetchall( f"SELECT * FROM {Tables.AUDITS}" ) @pytest_asyncio.fixture async def flags(self, instance) -> list[dict]: """Return the rows of the flags table.""" return await instance.db.execute_query_fetchall( f"SELECT * FROM {Tables.AUDITS_FLAGS}" ) @pytest_asyncio.fixture async def audits_in_group(self, instance, new_audit) -> list[dict]: """Return the audits in the group.""" return await instance.AuditGroups.list_children(new_audit) @pytest_asyncio.fixture async def sr_audit_id(self, instance, audits_in_group) -> AuditID: """Return the SR audit ID from the audits in the group.""" return next( audit[DBColumns.ID] for audit in audits_in_group if audit[DBColumns.TYPE] == AuditTypes.SR ) class TestAuditRows: @pytest.mark.asyncio async def test_add(self, instance, new_audit): """Test AuditRows with a batch of rows.""" test_row_count = 3 for i in range(test_row_count): await instance.AuditRows.add( audit_id=new_audit, row_idx=i, row_type=RowTypes.ANALYZED, row_data={"index": i}, ) await instance.AuditRows.save() rows = await instance.AuditRows.get_by_audit_group_id(new_audit) assert len(rows) == test_row_count for i in range(test_row_count): row = rows[i] assert row["audit"] == new_audit assert row["row_idx"] == i assert row["type"] == RowTypes.ANALYZED assert row["data"] == {"index": i} @pytest.fixture def user_create(self, instance): async def _(test_defaults=False) -> dict: """Create a new mock user and return the fields.""" mock_fields = { "subject": "mock_subject", "nickname": "mock_nickname", "name": "mock_name", "email": "mock_email@mock.com", "locale": None if test_defaults else "XXX", "timezone": None if test_defaults else "XXX", "scopes": {}, } await instance.Users.create(mock_fields["subject"], mock_fields) return mock_fields return _ @pytest.mark.asyncio @pytest.mark.parametrize("test_defaults", [False, True]) async def test_users_create_get_update(self, instance, test_defaults, user_create): """Test user_create, user_get, and user_update methods.""" # Make sure user table is empty # Create user mock_fields = await user_create(test_defaults) mock_subject = mock_fields[User.SUBJECT] # Update user partial_update = {User.NICKNAME: mock_fields[User.NICKNAME] + "_new"} await instance.Users.update(mock_subject, partial_update) # Get user user = await instance.Users.get(mock_subject) if not test_defaults: # No check if defaults are used, some values in the # database will be different from those initially set user.pop(User.ID) # Remove id from check assert user == (mock_fields | partial_update) # Create user with duplicate nickname await instance.Users.create( "subject2", mock_fields | {User.NICKNAME: user[User.NICKNAME]} ) users = await instance.db.execute_query_fetchall( f"SELECT * FROM {Tables.USERS}" ) assert len(users) == 2 assert ( users[0][User.NICKNAME] != users[1][User.NICKNAME] ), "Nicknames should be unique." @pytest.mark.asyncio async def test_users_create_get_update_by_id(self, instance, user_create): """Test user_create, user_get, and user_update methods.""" # Make sure user table is empty # Create user mock_fields = await user_create() users = await instance.db.execute_query_fetchall( f"SELECT * FROM {Tables.USERS} LIMIT 1" ) user_id = users[0][User.ID] # Update user partial_update = {User.EMAIL: mock_fields[User.EMAIL] + "_new"} await instance.Users.update_by_id(user_id, partial_update) # Get user user = await instance.Users.get_by_id(user_id) expected_updated = mock_fields | partial_update user.pop(User.ID) # Remove id from check assert user == expected_updated @pytest.mark.asyncio @pytest.mark.parametrize("limit", [None, 0, 1]) @pytest.mark.parametrize("offset", [None, 0, 1]) async def test_users_list(self, instance, user_create, limit, offset): """Test user_list method.""" await user_create() # Create exactly 1 user users_in_db = await instance.db.execute_query_fetchall( f"SELECT * FROM {Tables.USERS}" ) total_count, users = await instance.Users.list(limit=limit, offset=offset) assert total_count == 1, "Expected 1 user in the database" if limit: if offset: assert len(users) == 0, "Expected 0 users" else: assert len(users) == 1, "Expected 1 user" else: assert len(users) == len(users_in_db) == 1, "Expected 1 user" if users: assert all(users[User.ID] for users in users), ( "Expected all users to be " "dict" ) class TestUserPermissionsUpsert: @pytest_asyncio.fixture async def new_user_permissions(self, instance, new_user): sample_scopes = ["mock_scope_1", "mock_scope_2", "mock_scope_3"] sample_levels = ["ALL", "READ", "NONE"] await instance.db.execute_query_nofetch( f""" INSERT INTO {Tables.USERS_PERMISSIONS_LEVELS} (level) VALUES {','.join("(%s)" for _ in sample_levels)} """, sample_levels, ) await instance.db.execute_query_nofetch( f""" INSERT INTO {Tables.USERS_PERMISSIONS_SCOPES} (scope) VALUES {','.join("(%s)" for _ in sample_scopes)} """, sample_scopes, ) await instance.Permissions.upsert( new_user, {sample_scopes[0]: sample_levels[0]} ) yield sample_scopes, sample_levels @pytest.mark.asyncio async def test_get_meta(self, instance, new_user, new_user_permissions): meta = await instance.Permissions.get_meta() assert set(meta["levels"].values()) == set(new_user_permissions[1]) assert set(v["scope"] for v in meta["scopes"].values()) == set( new_user_permissions[0] ) @pytest.mark.asyncio async def test_permissions_upsert_insert_delete( self, instance, new_user, new_user_permissions ): async def _get_permissions(): return await instance.db.execute_query_fetchall(f""" SELECT S.scope, L.level FROM {Tables.USERS_PERMISSIONS} P LEFT JOIN {Tables.USERS_PERMISSIONS_SCOPES} S ON S.id = P.scope LEFT JOIN {Tables.USERS_PERMISSIONS_LEVELS} L ON L.id = P.level """) sample_scopes, sample_levels = new_user_permissions # Verify the initial permission was inserted permissions = await _get_permissions() assert len(permissions) == 1, "Expected 1 permission" assert permissions[0]["scope"] == sample_scopes[0], "Scope mismatch" assert permissions[0]["level"] == sample_levels[0], "Level mismatch" # Delete the permission and verify it was removed await instance.Permissions.upsert(new_user, {sample_scopes[0]: None}) permissions = await _get_permissions() assert len(permissions) == 0, "Expected 0 permissions" @pytest.mark.asyncio async def test_permissions_upsert_update( self, instance, new_user, new_user_permissions ): async def _get_permissions(): return await instance.db.execute_query_fetchall(f""" SELECT S.scope, L.level FROM {Tables.USERS_PERMISSIONS} P LEFT JOIN {Tables.USERS_PERMISSIONS_SCOPES} S ON S.id = P.scope LEFT JOIN {Tables.USERS_PERMISSIONS_LEVELS} L ON L.id = P.level """) sample_scopes, sample_levels = new_user_permissions # Update the existing permission and insert a new one await instance.Permissions.upsert( new_user, { sample_scopes[0]: sample_levels[1], sample_scopes[1]: sample_levels[2], }, ) permissions_updated = await _get_permissions() assert len(permissions_updated) == 2, "Expected 2 permissions" assert ( next(p for p in permissions_updated if p["scope"] == sample_scopes[0])[ "level" ] == sample_levels[1] ), "New level mismatch for the updated permission" assert ( next(p for p in permissions_updated if p["scope"] == sample_scopes[1])[ "level" ] == sample_levels[2] ), "Level mismatch for the new permission" @pytest.mark.asyncio async def test_audit_list(self, instance, new_audit): expected_cols = [ "audit", "date_completed_utc", "date_created_utc", "group", "type", "user", "user_nickname", ] audits = await instance.Audits.list(limit=10) assert len(audits) == 3 assert all(col in audits[0] for col in expected_cols) @pytest.mark.asyncio async def test_audit_group_list_children( self, instance, new_audit, audits_in_group ): expected_cols = [ "id", "type", ] assert len(audits_in_group) == 3 assert all(col in audits_in_group[0] for col in expected_cols) class TestAuditGroupList: @pytest_asyncio.fixture(autouse=True) async def do_setup(self, instance, new_audit, new_user): audit_ids = [ row["id"] for row in await instance.db.execute_query_fetchall( f"SELECT id FROM {Tables.AUDITS} WHERE `group` = {new_audit}" ) ] for action in ( DBAuditLogActions.STARTED, DBAuditLogActions.COMPLETED, DBAuditLogActions.COMPLETED_FETCH, ): for audit_id in audit_ids: await instance.Audits.get_log( audit_id, action=action, user=new_user ) @pytest.mark.asyncio async def test_audit_group_list(self, instance): audits = await instance.AuditGroups.list() assert audits[1] assert len(audits[0]) == audits[1] expected_keys = [ "group", "label_id", "label_name", "error", "types", "date_created_utc", "created_by", "date_scheduled_utc", "date_started_utc", "date_last_resolved_utc", "date_completed_utc", "row_count", "flags_total", "flags_resolved", "rows_flagged", "rows_resolved", "_created_by", "flags_pending", "flags_resolved_pct", "rows_pending", "date_scheduled_or_created_utc", "score", ] first_item = audits[0][0] assert all(key in first_item for key in expected_keys) @pytest.mark.asyncio @pytest.mark.parametrize( "kwargs,result_expected", [ ({"limit": 1}, True), ({"created_by": 1}, True), ({"created_by": 2}, False), # Test user is 1 ({"status": None}, True), ({"status": AuditStatus.COMPLETED}, True), ({"status": "not whitelisted status"}, False), *[ ({"audit_types": [audit_type]}, True) for audit_type in [ AuditTypes.SR, AuditTypes.MV, AuditTypes.AT, ] ], ({"audit_types": [AuditTypes.SR, AuditTypes.MV]}, True), ({"audit_types": []}, True), ({"audit_types": None}, True), ( { "audit_types": [ "MOCK_TYPE", ] }, False, ), ({"scheduled_date_start": datetime.today().strftime("%Y-%m-%d")}, True), ({"scheduled_date_start": datetime.today() - timedelta(days=2)}, True), ({"scheduled_date_start": datetime.today() + timedelta(days=2)}, False), ({"scheduled_date_end": datetime.today().strftime("%Y-%m-%d")}, True), ({"scheduled_date_end": datetime.today() - timedelta(days=2)}, False), ({"scheduled_date_end": datetime.today() + timedelta(days=2)}, True), ({"score_max": 100}, True), ({"score_max": 0}, True), ({"score_min": 0}, False), ({"score_min": 100}, False), ({"text": "label"}, True), ({"text": "not existing text"}, False), ], ) async def test_audit_group_list_kwargs(self, instance, kwargs, result_expected): audits = await instance.AuditGroups.list(**kwargs) rows, count = audits assert bool(rows) == result_expected assert bool(count) == result_expected @pytest.mark.asyncio @pytest.mark.parametrize( "order_by,exception", [("date_created_utc", False), ("not_whitelisted_column", True)], ) async def test_audit_group_list_whitelisted_order_by_columns_only( self, instance, order_by, exception ): func = partial(instance.AuditGroups.list, order_by=order_by) if exception: with pytest.raises(ValueError): await func() else: await func() @pytest.mark.asyncio async def test_audit_new(self, instance, new_audit, audits): new_audit_id = new_audit assert len(audits) == 3 assert all(audit["group"] == new_audit_id for audit in audits) assert [audit["type"] for audit in audits] == ["SR", "MV", "AT"] assert [audit["id"] for audit in audits] == [1, 2, 3] audit_groups = await instance.db.execute_query_fetchall( f"SELECT * FROM {Tables.AUDITS_GROUPS}" ) assert audit_groups[0] == { "date_archived_utc": None, "error": None, "id": 1, "label_id": 1, "label_name": "mock_label", "user": 1, } audits_log = await instance.db.execute_query_fetchall( f"SELECT * FROM {Tables.AUDITS_LOG}" ) assert len(audits_log) == 3 @pytest.mark.asyncio async def test_audit_group_archive(self, instance, new_audit): audit_group_id = new_audit # Get audit_group = await instance.AuditGroups.get(new_audit) assert audit_group["id"] == audit_group_id assert audit_group[DBColumns.DATE_ARCHIVED] is None # Delete await instance.AuditGroups.archive([audit_group_id] * 3) # Test deduplication audits = await instance.db.execute_query_fetchall( f"SELECT * FROM {Tables.AUDITS_GROUPS} WHERE id = {audit_group_id}" ) assert len(audits) == 1 assert audits[0][DBColumns.DATE_ARCHIVED].date() == datetime.now().date() @pytest.mark.asyncio async def test_audit_group_delete( self, instance, new_audit, audits_in_group, audits ): audit_group_id = new_audit # Get audit_group = await instance.AuditGroups.get(new_audit) assert audit_group["id"] == audit_group_id # List children assert len(audits_in_group) == len(audits) # Delete await instance.AuditGroups.delete([audit_group_id] * 3) # Test deduplication audits = await instance.db.execute_query_fetchall( f"SELECT * FROM {Tables.AUDITS_GROUPS} WHERE id = {audit_group_id}" ) assert len(audits) == 0 @pytest.mark.asyncio @pytest.mark.parametrize( "archived_days_ago,purge_days_ago,purge_expected", [(3, -1, True), (3, 4, False), (3, 3, True), (3, 2, True), (3, None, True)], ) async def test_audit_group_purge( self, instance, new_audit, audits_in_group, audits, archived_days_ago, purge_days_ago, purge_expected, ): audit_group_id = new_audit # Get audit_group = await instance.AuditGroups.get(new_audit) assert audit_group["id"] == audit_group_id # List children assert len(audits_in_group) == len(audits) # Set test archived date await instance.db.execute_query_nofetch( f"UPDATE {Tables.AUDITS_GROUPS} " f"SET date_archived_utc = DATE_SUB(UTC_TIMESTAMP(), INTERVAL {archived_days_ago} DAY)" f" WHERE id = {audit_group_id}" ) # Delete await instance.AuditGroups.purge(purge_days_ago) audits = await instance.db.execute_query_fetchall( f"SELECT * FROM {Tables.AUDITS_GROUPS} WHERE id = {audit_group_id}" ) assert bool(len(audits)) != purge_expected @pytest.mark.asyncio async def test_audit_log_add_get(self, instance, new_audit, audits): first_audit_id = audits[0]["id"] mock_action = "MOCK_ACTION" # Add await instance.Audits.get_log( first_audit_id, action=mock_action, details="mock_details", row_count=1, user=1, ) result = await instance.db.execute_query_fetchall( f"SELECT * FROM {Tables.AUDITS_LOG} WHERE action = '{mock_action}'" ) assert len(result) == 1 assert result[0]["action"] == mock_action # Get (for audit group) audit_group_id = audits[0]["group"] result = await instance.AuditGroups.get_logs(audit_group_id) assert len(result) > 1 @pytest.mark.asyncio @pytest.mark.parametrize("expect_match", [True, False]) async def test_audit_get_id(self, instance, new_audit, expect_match, audits): first_audit = audits[0] _id, _type = first_audit["id"], first_audit["type"] if not expect_match: _id += 1 result = await instance.Audits.get_id(_id, _type) expected = _id if expect_match else None assert result == expected @pytest_asyncio.fixture async def new_flags(self, instance, new_audit, new_user): flags = [ NewAuditFlag( text="mock_text", row_idx=1, isrc="mock_isrc", ) ] * 3 await instance.Flags.add(new_audit, new_user, flags) yield flags @pytest.mark.asyncio async def test_audit_add_flags(self, instance, new_flags, flags): # Created assert len(flags) == len(new_flags) @pytest.mark.asyncio async def test_flags_resolve(self, instance, new_audit, new_user, new_flags): flags = await instance.AuditGroups.get_flags(new_audit) flag_ids = [flag["id"] for flag in flags] resolution_type = "MOCK_RESOLUTION" resolution_subtype = "MOCK_SUBTYPE" flags_resolved = await instance.Flags.resolve( flag_ids, new_user, resolution_type, resolution_subtype ) assert flags_resolved assert len(flags_resolved) == len(flags) assert {flag["id"] for flag in flags_resolved} == {flag["id"] for flag in flags} assert all(flag["resolution"] == resolution_type for flag in flags_resolved) assert all( flag["resolution_subtype"] == resolution_subtype for flag in flags_resolved ) @pytest.mark.asyncio async def test_flags_unique_rows(self, instance, new_audit, new_user, new_flags): await instance.Audits.get_log( new_audit, action=DBAuditLogActions.COMPLETED_FETCH, user=new_user ) await instance.AuditRows.add( new_audit, 1, RowTypes.ANALYZED, {"upc": "123456789012"} ) result = await instance.Flags.unique_rows([new_audit]) assert len(result) == 1 assert result[0] == { "audit": 1, "group": 1, "rows_analyzed": 0, "rows_flagged": 1, "rows_pending": 1, "rows_total": None, } @pytest.mark.asyncio async def test_audit_group_mark_run_as_failed(self, instance, new_audit, audits): audit = audits[0] audit_group_id = audit["group"] mock_error_message = "This is a mock error message." await instance.AuditGroups.mark_run_as_failed( audit_group_id, mock_error_message ) audit_group = await instance.db.execute_query_fetchone( f"SELECT * FROM {Tables.AUDITS_GROUPS} WHERE id = {audit_group_id}" ) assert audit_group["error"] == mock_error_message @pytest.mark.asyncio async def test_audit_meta_set_get(self, instance, sr_audit_id, new_audit): audit_group_id = new_audit await instance.Meta.set( sr_audit_id, { "key": "value", "key2": "value2", }, ) result = await instance.Meta.get(sr_audit_id, ["key", "key2"]) assert result["key"] == "value" assert result["key2"] == "value2" result_group = await instance.Meta.get_audit_group(audit_group_id) assert result_group == [ {"id": 1, "key": "key2", "type": "SR", "value": "value2"}, {"id": 1, "key": "key", "type": "SR", "value": "value"}, {"id": 2, "key": None, "type": "MV", "value": None}, {"id": 3, "key": None, "type": "AT", "value": None}, ] class TestExportRowsTable: @pytest.mark.asyncio async def test_rows_table_no_rows_expected(self, instance, new_audit): rows = await instance.Export.rows_table(new_audit) assert len(rows) == 0 @pytest.mark.asyncio async def test_rows_table_rows_expecting_list_values( self, instance, new_audit, new_user, add_audit_rows, audits_in_group ): """Test rows_table export with rows that expect list values.""" keys_expecting_list_values = [ SnowFlakeColumns.CONFLICTING_TERRITORIES, SnowFlakeColumns.LIST_CONFLICTING_TERRITORIES, ] await add_audit_rows( [ ( audit["id"], 1, RowTypes.ANALYZED, {key: f"mock_{key}" for key in keys_expecting_list_values}, ) for audit in audits_in_group ] ) rows = await instance.Export.rows_table(new_audit) assert rows assert len(rows) == len(audits_in_group) for key in keys_expecting_list_values: assert all(isinstance(row[key], list) for row in rows), "Expected list" @pytest.mark.asyncio async def test_get_actionable_conflict_count( self, instance, new_audit, add_audit_rows, audits_in_group, sr_audit_id ): number_of_conflicting_owner_rows_to_add: int = 3 conflicting_owner_values = ( ["mock_conflicting_owner"] * number_of_conflicting_owner_rows_to_add ) + [ None ] * 5 # Mix some rows with None values mock_audit_rows = [ ( sr_audit_id, i, RowTypes.ANALYZED, {SnowFlakeColumns.CONFLICTING_OWNERS: value}, ) for i, value in enumerate(conflicting_owner_values) ] await add_audit_rows(mock_audit_rows) result = await instance.Export.get_actionable_conflict_count(new_audit) assert result == number_of_conflicting_owner_rows_to_add @pytest.mark.asyncio async def test_get_actionable_attached_conflict_count( self, instance, new_audit, add_audit_rows, sr_audit_id ): number_of_locked_rows_to_add: int = 3 trues = [True] * number_of_locked_rows_to_add falses = [False, None] * 5 # Mix some rows with False and None values values = trues + falses mock_audit_rows = [ ( sr_audit_id, i, RowTypes.ANALYZED, {SnowFlakeColumns.IS_LOCKED: value}, ) for i, value in enumerate(values) ] await add_audit_rows(mock_audit_rows) result = await instance.Export.get_actionable_attached_conflict_count(new_audit) assert result == number_of_locked_rows_to_add