"""Unit tests for the Feature model.""" from unittest.mock import patch import pytest from account.connectors import mysql from account.constants import error from account.constants.features import FEATURES from account.models import feature from account.models.types import Feature as FeatureType from tests.unit import db_operations @pytest.fixture def db_fixture(): """Set up the features tables.""" db_operations.create_tables() db_operations.seed_vendor_table() db_operations.seed_vendor_restricted_features_table() db_operations.seed_features_table() @pytest.fixture() def features_fixture() -> list[FeatureType]: return [ {'feature_id': 1, 'feature_name': 'Audio Release Builder'}, {'feature_id': 2, 'feature_name': 'Music Video Release Builder'}, {'feature_id': 999, 'feature_name': 'Nonexistent Feature'}, ] def test_get_enabled_features_for_vendor(db_fixture): """Test getting the enabled features for a vendor.""" vendor_id = 1 result = feature.get_enabled_features_for_vendor(vendor_id) assert result.message['items'] == [ {'feature_id': 1, 'feature_name': 'One'}, {'feature_id': 2, 'feature_name': 'Two'}, ] def test_get_enabled_features_for_vendor_no_restrictions(db_fixture): """Test getting the enabled features for a vendor with no restrictions.""" vendor_id = 2 result = feature.get_enabled_features_for_vendor(vendor_id) assert result.message['items'] == [ {'feature_id': 1, 'feature_name': 'One'}, {'feature_id': 2, 'feature_name': 'Two'}, {'feature_id': 3, 'feature_name': 'Three'}, ] def test_get_vendors_with_feature(db_fixture): """Test getting the vendors with a specific feature.""" feature_id = 3 result = feature.get_vendors_with_feature(feature_id, page_limit=0, page_offset=0) assert result['items'] == [2, 3, 4] def test_get_vendors_with_feature_invalid(db_fixture): """Test getting the vendors with a feature that doesn't exist.""" feature_id = 999 result = feature.get_vendors_with_feature(feature_id, page_limit=0, page_offset=0) assert result['items'] == [] def test_get_restricted_features_for_vendor(db_fixture): """Test getting the restricted features for a vendor.""" vendor_id = 1 result = feature.get_restricted_features_for_vendor(vendor_id) assert result.message['items'] == [{'feature_id': 3, 'feature_name': 'Three'}] def test_get_restricted_features_for_vendor_no_restrictions(db_fixture): """Test getting the restricted features for a vendor with no restrictions.""" vendor_id = 2 result = feature.get_restricted_features_for_vendor(vendor_id) assert result.message['items'] == [] def test_get_feature(db_fixture): """Test getting a feature given a feature_id.""" feature_id = 1 result = feature.get_feature(feature_id) assert result.message == {'feature_id': 1, 'feature_name': 'One'} def test_get_feature_not_found(db_fixture): """Test getting a feature with an invalid feature_id.""" feature_id = 5 result = feature.get_feature(feature_id) assert not result def test_is_valid_features(db_fixture): """Test checking feature_ids to be existing Features.""" feature_ids = [1, 2, 3] result = feature.is_valid_features(feature_ids) assert result.status == 200 assert result.message is True assert bool(result) is True def test_is_valid_features_invalid_id(db_fixture): """Test checking feature_ids with id that doesn't exist.""" feature_ids = [1, 2, 3, 4] result = feature.is_valid_features(feature_ids) assert result.status == 404 assert result.errors == { 'code': 'not_found_error', 'message': error.ERROR_MESSAGE_FEATURE_NOT_FOUND.format(feature_id=str([4])), } def test_is_valid_features_invalid_type(db_fixture): """Test checking feature_ids with id that have invalid type.""" feature_ids = [1, 2, 3, 4, 'hello'] result = feature.is_valid_features(feature_ids) assert result.status == 404 assert result.errors == { 'code': 'not_found_error', 'message': error.ERROR_MESSAGE_FEATURE_NOT_FOUND.format(feature_id=str([4, 'hello'])), } def test_bulk_add_restricted_features_for_vendor(db_fixture): """Test bulk adding a restricted features for vendor. Context: Vendor has no restricted features yet. Action: Add few restricted features. Expected result: Created restricted features from the request are returned. `get_restricted_features_for_vendor` response has all restricted features from the request. `get_enabled_features_for_vendor` response has all features that haven't been restricted. """ vendor_id = 123 feature_ids = [1, 3] result = feature.bulk_add_restricted_features_for_vendor(vendor_id, feature_ids) assert result.message == [ {'vendor_id': 123, 'feature_id': 1}, {'vendor_id': 123, 'feature_id': 3}, ] restricted_features = feature.get_restricted_features_for_vendor(vendor_id) assert restricted_features.message['items'] == [ {'feature_id': 1, 'feature_name': 'One'}, {'feature_id': 3, 'feature_name': 'Three'}, ] enabled_features = feature.get_enabled_features_for_vendor(vendor_id) assert enabled_features.message['items'] == [{'feature_id': 2, 'feature_name': 'Two'}] def test_bulk_add_restricted_features_for_vendor_idempotent(db_fixture): """Test idempotent bulk adding a restricted features for vendor. Context: Vendor already has restricted features. Action: Add more restricted features, including the existing ones. Expected result: All restricted features from the request are returned. No duplicated entries. No errors of existing entries. `get_restricted_features_for_vendor` response has all restricted features from the request. """ vendor_id = 1 # Vendor already has restricted feature id = 3 feature_ids = [1, 2, 3] result = feature.bulk_add_restricted_features_for_vendor(vendor_id, feature_ids) assert result.message == [ {'vendor_id': 1, 'feature_id': 3}, {'vendor_id': 1, 'feature_id': 1}, {'vendor_id': 1, 'feature_id': 2}, ] restricted_features = feature.get_restricted_features_for_vendor(vendor_id) assert restricted_features.message['items'] == [ {'feature_id': 1, 'feature_name': 'One'}, {'feature_id': 2, 'feature_name': 'Two'}, {'feature_id': 3, 'feature_name': 'Three'}, ] def test_bulk_remove_restricted_features_for_vendor(db_fixture): """Test bulk removing a restricted features for vendor. Context: Vendor has one restricted feature. Action: Remove few restricted features, including the existing one. Expected result: Only feature_id of the previously existing vendor restricted feature is returned. No errors of non-existing feature_id. """ vendor_id = 1 # Vendor already has restricted feature id = 3 feature_ids = [1, 2, 3] restricted_features = feature.get_restricted_features_for_vendor(vendor_id) assert restricted_features.message['items'] == [{'feature_id': 3, 'feature_name': 'Three'}] result = feature.bulk_remove_restricted_features_for_vendor(vendor_id, feature_ids) assert result.message == [3] restricted_features = feature.get_restricted_features_for_vendor(vendor_id) assert restricted_features.message['items'] == [] VENDOR_1_UUID = '87682992-bff2-40ff-aa75-18eb6214679e' # feature 3 restricted -> enabled [1, 2] VENDOR_2_UUID = '6097ad5a-2186-4dbe-8286-82ce933d3538' # no restrictions -> enabled [1, 2, 3] UNKNOWN_UUID = '00000000-0000-0000-0000-000000000000' @pytest.mark.parametrize( 'input_uuids, expected', [ pytest.param([VENDOR_1_UUID], {VENDOR_1_UUID: [1, 2]}, id='with_restrictions'), pytest.param([VENDOR_2_UUID], {VENDOR_2_UUID: [1, 2, 3]}, id='no_restrictions'), pytest.param([UNKNOWN_UUID], {}, id='unknown_omitted'), pytest.param( [VENDOR_1_UUID, VENDOR_2_UUID, UNKNOWN_UUID], {VENDOR_1_UUID: [1, 2], VENDOR_2_UUID: [1, 2, 3]}, id='mixed', ), ], ) def test_get_enabled_feature_ids_for_vendor_uuids(db_fixture, input_uuids, expected): """Test getting enabled feature IDs for vendor UUIDs.""" with mysql.session_scope(read_only=True) as session: result = feature.get_enabled_feature_ids_for_vendor_uuids(input_uuids, session) assert result == expected SUBACCOUNT_1_UUID = ( '11387154-0267-11ef-82c8-4a2888760684' # vendor_id=1, feature 3 restricted -> enabled [1, 2] ) SUBACCOUNT_NO_RESTRICTIONS_UUID = ( 'b1e2f3a4-0000-4000-8000-000000000002' # vendor_id=2, no restrictions -> enabled [1, 2, 3] ) @pytest.fixture def db_fixture_with_subaccounts(db_fixture): """Extend db_fixture with subaccount data.""" db_operations.seed_subaccount_table() @pytest.mark.parametrize( 'input_uuids, expected', [ pytest.param( [SUBACCOUNT_1_UUID], {SUBACCOUNT_1_UUID: [1, 2]}, id='with_restrictions', ), pytest.param( [SUBACCOUNT_NO_RESTRICTIONS_UUID], {SUBACCOUNT_NO_RESTRICTIONS_UUID: [1, 2, 3]}, id='no_restrictions', ), pytest.param([UNKNOWN_UUID], {}, id='unknown_omitted'), pytest.param( [SUBACCOUNT_1_UUID, SUBACCOUNT_NO_RESTRICTIONS_UUID, UNKNOWN_UUID], {SUBACCOUNT_1_UUID: [1, 2], SUBACCOUNT_NO_RESTRICTIONS_UUID: [1, 2, 3]}, id='mixed', ), ], ) def test_get_enabled_feature_ids_for_subaccount_uuids( db_fixture_with_subaccounts, input_uuids, expected ): """Test getting enabled feature IDs for subaccount UUIDs.""" with mysql.session_scope(read_only=True) as session: result = feature.get_enabled_feature_ids_for_subaccount_uuids(input_uuids, session) assert result == expected def test_get_enabled_features_for_vendor_with_session(db_fixture): """Test getting the enabled features for a vendor and with a session.""" vendor_id = 1 with mysql.session_scope(read_only=True) as session: result = feature.get_enabled_features_for_vendor_with_session(vendor_id, session) assert result == [ {'feature_id': 1, 'feature_name': 'One'}, {'feature_id': 2, 'feature_name': 'Two'}, ] def test_get_enabled_features_for_vendors_with_session(): """Test getting the enabled features for multiple vendors with a session.""" # Create a real session but mock the execute method to # avoid ROW() which is not supported in SQLite. with mysql.session_scope(read_only=True) as session: with patch.object(session, 'execute') as mock_execute: # Mock SQL result rows: (vendor_id, feature_id, feature_name) mock_rows = [ (1, 1, 'One'), (1, 2, 'Two'), (2, 1, 'One'), # vendor 999 doesn't exist, so no rows for it ] mock_execute.return_value.fetchall.return_value = mock_rows vendor_ids = [1, 2, 999] result = feature.get_enabled_features_for_vendors_with_session(vendor_ids, session) # Vendor 1 should have features assert result[1] == [ {'feature_id': 1, 'feature_name': 'One'}, {'feature_id': 2, 'feature_name': 'Two'}, ] # Vendor 2 should have features assert result[2] == [ {'feature_id': 1, 'feature_name': 'One'}, ] # Vendor 999 should be present. assert 999 not in result def test_get_enabled_features_for_vendors_with_session_empty(): """Test getting enabled features for an empty vendor list.""" with mysql.session_scope(read_only=True) as session: empty_result = feature.get_enabled_features_for_vendors_with_session([], session) assert empty_result == {} def test_get_enabled_features_for_vendors_with_session_sql_called(): """Test that get_enabled_features_for_vendors_with_session calls the correct SQL query.""" with mysql.session_scope(read_only=True) as session: with patch.object(session, 'execute') as mock_execute: mock_execute.return_value.fetchall.return_value = [] vendor_ids = [123, 456, 789] feature.get_enabled_features_for_vendors_with_session(vendor_ids, session) # Assert that session.execute was called once mock_execute.assert_called_once() # Get the SQL query and parameters that were executed call_args = mock_execute.call_args executed_query = str(call_args[0][0]) executed_params = call_args[0][1] # Assert the query contains the expected SQL structure with bound parameters expected_sql = """SELECT v.column_0 as vendor_id, f.feature_id, f.feature_name FROM ( VALUES ROW(:vendor_id_0), ROW(:vendor_id_1), ROW(:vendor_id_2) ) AS v INNER JOIN vendor vnd ON vnd.vendor_id = v.column_0 CROSS JOIN features f WHERE NOT EXISTS ( SELECT 1 FROM vendor_restricted_features vrf WHERE vrf.vendor_id = v.column_0 AND vrf.feature_id = f.feature_id ) ORDER BY v.column_0, f.feature_id""" assert executed_query == expected_sql # Assert the parameters are correctly bound expected_params = {'vendor_id_0': 123, 'vendor_id_1': 456, 'vendor_id_2': 789} assert executed_params == expected_params def test_convert_features_to_list(features_fixture): """ Test the convert_features_to_list converts a list of Feature objects to a list of FEATURES enum values, excluding any feature IDs that are not present in the FEATURES enum. """ features_list = feature.convert_features_to_list(features_fixture) expected_features_list = [FEATURES.AUDIO_RELEASE_BUILDER, FEATURES.MUSIC_VIDEO_RELEASE_BUILDER] assert features_list == expected_features_list