"""Test logic layer for fingerprint rules.""" import datetime from unittest.mock import MagicMock from unittest.mock import call from unittest.mock import patch import pytest from sound_recordings.api import app from sound_recordings.logic import fingerprint_rules def _rule(territory, policy='monetize', service='meta'): return { 'service': service, 'territory': territory, 'policy': policy, 'end': None, 'start': datetime.datetime.fromisoformat('2019-06-15T00:00:00+00:00') if policy != 'carveout' else None, # noqa:E501 'created_at': datetime.datetime.fromisoformat('2018-06-15T00:00:00+00:00'), 'created_by': 'ows-sound-recordings/post-vendors-123-rules/456/ContentProfile', # noqa:E501 'last_modified_at': datetime.datetime.fromisoformat('2018-06-15T00:00:00+00:00'), # noqa:E501 'last_modified_by': 'ows-sound-recordings/post-vendors-123-rules/456/ContentProfile' # noqa:E501 } @patch('sound_recordings.logic.fingerprint_rules.features.is_has_fingerprint_rule_in_snowflake_enabled', return_value=False) # noqa:E501 @patch('sound_recordings.models.fingerprint_rules.get_rules') @patch('sound_recordings.models.fingerprint_rules.create_rules') @patch('sound_recordings.models.fingerprint_rules.delete_rules') def test_upsert(mock_delete, mock_create, mock_get, mock_ff): """Test setting new list of rules on object.""" profile = ('ContentProfile', 123) new_rules = [ { 'policy': 'monetize', 'territory': 'US', 'service': 'meta', 'start': datetime.datetime.fromisoformat('2019-06-15T00:00:00+00:00'), 'end': datetime.datetime.fromisoformat('2020-06-15T00:00:00+00:00') }, # in current rules, no changes, do nothing { 'policy': 'block_access', 'territory': 'US', 'service': 'meta', 'start': datetime.datetime.fromisoformat('2021-06-15T00:00:00+00:00'), 'end': datetime.datetime.fromisoformat('2022-06-15T00:00:00+00:00') }, # in current rules, but dates changed, delete and create { 'policy': 'block_access', 'territory': 'US', 'service': 'meta', 'start': datetime.datetime.fromisoformat('2023-06-15T00:00:00+00:00'), 'end': datetime.datetime.fromisoformat('2024-06-15T00:00:00+00:00') } # not in current rules, create ] current_rules = [ { **new_rules[0], 'element_id': '123' }, { **new_rules[1], 'start': datetime.datetime.fromisoformat('2021-07-15T00:00:00+00:00'), 'end': datetime.datetime.fromisoformat('2022-07-15T00:00:00+00:00'), 'element_id': '456' } ] mock_get.return_value = current_rules with app.app_context(): (num_created, num_deleted) = fingerprint_rules.upsert( 'Track', 12345, new_rules, profile) assert num_created == 2 assert num_deleted == 1 assert mock_create.call_args_list == [ call('Track', 12345, [new_rules[1], new_rules[2]], profile) ] assert mock_delete.call_args_list == [call('Track', 12345, ['456'], profile)] @patch('sound_recordings.logic.fingerprint_rules.features.is_has_fingerprint_rule_in_snowflake_enabled', return_value=False) # noqa:E501 @patch('sound_recordings.models.fingerprint_rules.get_rules') @patch('sound_recordings.models.fingerprint_rules.create_rules') @patch('sound_recordings.models.fingerprint_rules.delete_rules') def test_upsert_affected_services_does_not_delete_other_services( mock_delete, mock_create, mock_get, mock_ff): """Test that rules for services outside affected_services are not deleted.""" profile = ('ContentProfile', 123) start = datetime.datetime.fromisoformat('2019-06-15T00:00:00+00:00') end = datetime.datetime.fromisoformat('2020-06-15T00:00:00+00:00') # new_rules only contains a rule for 'youtube' new_rules = [ { 'policy': 'monetize', 'territory': 'US', 'service': 'youtube', 'start': start, 'end': end } ] # current rules contain one 'youtube' rule (matching) and one 'meta' rule (outside filter) current_rules = [ { **new_rules[0], 'element_id': 'yt-rule-1' }, { 'policy': 'monetize', 'territory': 'US', 'service': 'meta', 'start': start, 'end': end, 'element_id': 'meta-rule-1' } ] mock_get.return_value = current_rules with app.app_context(): (num_created, num_deleted) = fingerprint_rules.upsert( 'Vendor', 99, new_rules, profile, affected_services=['youtube'] ) # the matching youtube rule already exists — nothing to create or delete assert num_created == 0 assert num_deleted == 0 mock_create.assert_called_once_with('Vendor', 99, [], profile) mock_delete.assert_called_once_with('Vendor', 99, [], profile) @patch('sound_recordings.logic.fingerprint_rules.features.is_has_fingerprint_rule_in_snowflake_enabled', return_value=False) # noqa:E501 @patch('sound_recordings.models.fingerprint_rules.get_rules') @patch('sound_recordings.models.fingerprint_rules.create_rules') @patch('sound_recordings.models.fingerprint_rules.delete_rules') def test_upsert_affected_services_deletes_only_within_filter(mock_delete, mock_create, mock_get, mock_ff): # noqa:E501 """Test that only rules for affected services are deleted when the new payload omits them.""" profile = ('ContentProfile', 123) start = datetime.datetime.fromisoformat('2019-06-15T00:00:00+00:00') end = datetime.datetime.fromisoformat('2020-06-15T00:00:00+00:00') # new_rules is intentionally empty — caller wants to wipe 'youtube' rules only new_rules = [] current_rules = [ { 'policy': 'monetize', 'territory': 'US', 'service': 'youtube', 'start': start, 'end': end, 'element_id': 'yt-rule-1' }, { 'policy': 'monetize', 'territory': 'US', 'service': 'meta', 'start': start, 'end': end, 'element_id': 'meta-rule-1' } ] mock_get.return_value = current_rules with app.app_context(): (num_created, num_deleted) = fingerprint_rules.upsert( 'Vendor', 99, new_rules, profile, affected_services=['youtube'] ) assert num_created == 0 assert num_deleted == 1 mock_create.assert_called_once_with('Vendor', 99, [], profile) # only the youtube rule should be deleted; the meta rule must be untouched mock_delete.assert_called_once_with('Vendor', 99, ['yt-rule-1'], profile) @patch('sound_recordings.logic.fingerprint_rules.features.is_has_fingerprint_rule_in_snowflake_enabled', return_value=False) # noqa:E501 @patch('sound_recordings.models.fingerprint_rules.get_rules') @patch('sound_recordings.models.fingerprint_rules.create_rules') @patch('sound_recordings.models.fingerprint_rules.delete_rules') def test_upsert_affected_services_adds_new_rule_while_ignoring_other_services( mock_delete, mock_create, mock_get, mock_ff): """Test that a new rule is created for a service while other services are untouched.""" profile = ('ContentProfile', 123) start = datetime.datetime.fromisoformat('2019-06-15T00:00:00+00:00') end = datetime.datetime.fromisoformat('2020-06-15T00:00:00+00:00') new_youtube_rule = { 'policy': 'monetize', 'territory': 'US', 'service': 'youtube', 'start': start, 'end': end } new_rules = [new_youtube_rule] # current rules only contain a meta rule; no youtube rule exists yet current_rules = [ { 'policy': 'monetize', 'territory': 'US', 'service': 'meta', 'start': start, 'end': end, 'element_id': 'meta-rule-1' } ] mock_get.return_value = current_rules with app.app_context(): (num_created, num_deleted) = fingerprint_rules.upsert( 'Vendor', 99, new_rules, profile, affected_services=['youtube'] ) assert num_created == 1 assert num_deleted == 0 # the new youtube rule must be created mock_create.assert_called_once_with('Vendor', 99, [new_youtube_rule], profile) # no youtube rules existed to delete; meta rule must be untouched mock_delete.assert_called_once_with('Vendor', 99, [], profile) @patch('sound_recordings.logic.fingerprint_rules.features.is_has_fingerprint_rule_in_snowflake_enabled', return_value=False) # noqa:E501 @patch('sound_recordings.models.fingerprint_rules.get_rules') @patch('sound_recordings.models.fingerprint_rules.create_rules') @patch('sound_recordings.models.fingerprint_rules.delete_rules') def test_upsert_affected_services_replaces_existing_rule_while_ignoring_other_services( mock_delete, mock_create, mock_get, mock_ff): """Test that an existing rule is replaced for a service while other services are untouched.""" profile = ('ContentProfile', 123) start = datetime.datetime.fromisoformat('2019-06-15T00:00:00+00:00') end = datetime.datetime.fromisoformat('2020-06-15T00:00:00+00:00') new_start = datetime.datetime.fromisoformat('2021-01-01T00:00:00+00:00') new_end = datetime.datetime.fromisoformat('2022-01-01T00:00:00+00:00') new_youtube_rule = { 'policy': 'monetize', 'territory': 'US', 'service': 'youtube', 'start': new_start, 'end': new_end } new_rules = [new_youtube_rule] current_rules = [ { 'policy': 'carveout', 'territory': 'US', 'service': 'youtube', 'start': start, 'end': end, 'element_id': 'yt-rule-1' }, { 'policy': 'monetize', 'territory': 'US', 'service': 'meta', 'start': start, 'end': end, 'element_id': 'meta-rule-1' } ] mock_get.return_value = current_rules with app.app_context(): (num_created, num_deleted) = fingerprint_rules.upsert( 'Vendor', 99, new_rules, profile, affected_services=['youtube'] ) assert num_created == 1 assert num_deleted == 1 # the new youtube rule (different dates) must be created mock_create.assert_called_once_with('Vendor', 99, [new_youtube_rule], profile) # the old youtube rule must be deleted; meta rule must be untouched mock_delete.assert_called_once_with('Vendor', 99, ['yt-rule-1'], profile) @patch('sound_recordings.models.fingerprint_rules.delete_rules_snowflake') @patch('sound_recordings.models.fingerprint_rules.create_rules_snowflake') @patch('sound_recordings.models.fingerprint_rules.get_rules_snowflake') @patch('sound_recordings.logic.fingerprint_rules.snowflake_util') @patch('sound_recordings.logic.fingerprint_rules.features.is_has_fingerprint_rule_in_snowflake_enabled', return_value=True) # noqa:E501 def test_upsert_routes_to_snowflake_when_ff_enabled( mock_ff, mock_sf_util, mock_get_sf, mock_create_sf, mock_delete_sf): """FF enabled -> upsert diffs and writes against Snowflake in one tx.""" profile = ('ContentProfile', 123) modified_at = datetime.datetime(2026, 6, 11, 0, 0, tzinfo=datetime.timezone.utc) # noqa:E501 tx = MagicMock() tx.fetchone.return_value = [modified_at] # SELECT CURRENT_TIMESTAMP() mock_sf_util.transaction.return_value.__enter__.return_value = tx new_rules = [_rule('US')] current_rules = [_rule('GB')] mock_get_sf.return_value = current_rules (num_created, num_deleted) = fingerprint_rules.upsert( 'Vendor', 99, new_rules, profile) # read + both writes use the same transaction handle and shared timestamp mock_get_sf.assert_called_once_with(tx, 'Vendor', 99) mock_create_sf.assert_called_once_with( tx, 'Vendor', 99, new_rules, profile, modified_at) mock_delete_sf.assert_called_once_with( tx, 'Vendor', 99, current_rules, profile, modified_at) assert (num_created, num_deleted) == (1, 1) def test_unique_rule_str(): """Test rule uniqueness logic.""" rule = { 'policy': 'block_access', 'territory': 'US', 'service': 'meta', 'start': datetime.datetime.fromisoformat('2023-06-15T00:00:00+00:00'), 'end': datetime.datetime.fromisoformat('2024-06-15T00:00:00+00:00') } assert fingerprint_rules._generate_rule_key(rule) == \ '2023-06-15|2024-06-15|US|block_access|meta' del rule['start'] assert fingerprint_rules._generate_rule_key(rule) == \ 'None|2024-06-15|US|block_access|meta' del rule['end'] assert fingerprint_rules._generate_rule_key(rule) == \ 'None|None|US|block_access|meta' assert ( fingerprint_rules._generate_rule_key({ 'policy': 'monetize', 'territory': 'US', 'service': 'tiktok', 'start': datetime.datetime.fromisoformat('2016-11-09T09:11:56+00:00'), 'end': None, }) == fingerprint_rules._generate_rule_key({ 'policy': 'monetize', 'territory': 'US', 'service': 'tiktok', 'start': datetime.datetime.fromisoformat('2016-11-09T00:00:00+00:00'), 'end': None, }) ) @patch('sound_recordings.models.fingerprint_rules.get_history') def test_fetch_history(mock_get_history): """Test fetch history rule.""" mock_get_history.return_value = { 'created': [ { 'territory': 'US', 'service': 'meta', 'policy': 'monetize', 'period_start': datetime.datetime(2019, 6, 15, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'period_end': datetime.datetime(2020, 6, 15, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'created_at': datetime.datetime(2021, 6, 15, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'created_by': 'ows-sound-recordings/post-vendors-33811-rules/123/ContentProfile', # noqa:E501 'last_modified_at': datetime.datetime(2022, 6, 15, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'last_modified_by': 'ows-sound-recordings/post-vendors-33811-rules/123/ContentProfile' # noqa:E501 } ], 'deleted': [ { 'territory': 'FR', 'service': 'meta', 'policy': 'monetize', 'period_start': datetime.datetime(2019, 6, 15, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'period_end': datetime.datetime(2020, 6, 15, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'created_at': datetime.datetime(2018, 6, 15, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'created_by': 'ows-sound-recordings/post-vendors-33811-rules/123/ContentProfile', # noqa:E501 'last_modified_at': datetime.datetime(2021, 6, 15, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'last_modified_by': 'ows-sound-recordings/post-vendors-33811-rules/123/ContentProfile' # noqa:E501 } ] } expected_result = [ { 'territory': 'FR', 'service': 'meta', 'policy': 'monetize', 'start': datetime.datetime(2019, 6, 15, 0, 0, tzinfo=datetime.timezone.utc).isoformat(), # noqa:E501 'end': datetime.datetime(2020, 6, 15, 0, 0, tzinfo=datetime.timezone.utc).isoformat(), # noqa:E501 'timestamp': datetime.datetime(2018, 6, 15, 0, 0, tzinfo=datetime.timezone.utc).isoformat(), # noqa:E501 'profile_type': 'ContentProfile', 'profile_id': '123', 'event_type': 'created' }, { 'territory': 'FR', 'service': 'meta', 'policy': 'monetize', 'start': datetime.datetime(2019, 6, 15, 0, 0, tzinfo=datetime.timezone.utc).isoformat(), # noqa:E501 'end': datetime.datetime(2020, 6, 15, 0, 0, tzinfo=datetime.timezone.utc).isoformat(), # noqa:E501 'timestamp': datetime.datetime(2021, 6, 15, 0, 0, tzinfo=datetime.timezone.utc).isoformat(), # noqa:E501 'profile_type': 'ContentProfile', 'profile_id': '123', 'event_type': 'deleted' }, { 'territory': 'US', 'service': 'meta', 'policy': 'monetize', 'start': datetime.datetime(2019, 6, 15, 0, 0, tzinfo=datetime.timezone.utc).isoformat(), # noqa:E501 'end': datetime.datetime(2020, 6, 15, 0, 0, tzinfo=datetime.timezone.utc).isoformat(), # noqa:E501 'timestamp': datetime.datetime(2022, 6, 15, 0, 0, tzinfo=datetime.timezone.utc).isoformat(), # noqa:E501 'profile_type': 'ContentProfile', 'profile_id': '123', 'event_type': 'created' } ] result = fingerprint_rules.fetch_history('Vendor', 123, None) assert result == expected_result def test_format_fingerprint_rules(): """Test format rule.""" rules = [ { 'active': True, 'territory': 'US', 'service': 'meta', 'policy': 'monetize', 'start': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'end': None, 'created_at': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'created_by': 'ows-sound-recordings/post-vendors-123-rules/456/ContentProfile', # noqa:E501 'last_modified_at': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'last_modified_by': 'ows-sound-recordings/post-vendors-123-rules/456/ContentProfile', # noqa:E501 'element_id': '123' }, # delete id, format profile, return empty dates { 'active': True, 'territory': 'US', 'service': 'meta', 'policy': 'monetize', 'start': datetime.datetime(2019, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'end': datetime.datetime(2020, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'created_at': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'created_by': 'ows-sound-recordings/post-vendors-123-rules/456/ContentProfile', # noqa:E501 'last_modified_at': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'last_modified_by': 'ows-sound-recordings/post-vendors-123-rules/456/ContentProfile', # noqa:E501 'element_id': '456' } # delete id format profile and format dates ] results = fingerprint_rules._format_base_rule(rules) assert results == [ { 'active': True, 'territory': 'US', 'service': 'meta', 'policy': 'monetize', 'start': '2021-06-15T00:00:00+00:00', 'end': None, 'created_at': '2021-06-15T00:00:00+00:00', 'created_by_profile_id': '456', 'created_by_profile_type': 'ContentProfile', 'last_modified_at': '2021-06-15T00:00:00+00:00', 'last_modified_by_profile_id': '456', 'last_modified_by_profile_type': 'ContentProfile', 'is_start_absolute': False, 'is_end_absolute': False, }, { 'active': True, 'territory': 'US', 'service': 'meta', 'policy': 'monetize', 'start': '2019-06-15T00:00:00+00:00', 'end': '2020-06-15T00:00:00+00:00', 'created_at': '2021-06-15T00:00:00+00:00', 'created_by_profile_id': '456', 'created_by_profile_type': 'ContentProfile', 'last_modified_at': '2021-06-15T00:00:00+00:00', 'last_modified_by_profile_id': '456', 'last_modified_by_profile_type': 'ContentProfile', 'is_start_absolute': False, 'is_end_absolute': False, } ] def test_format_fingerprint_rules_includes_mock_absolute_flags(): """Test _format_base_rule returns is_start_absolute=False and is_end_absolute=False.""" rules = [{ 'active': True, 'territory': 'US', 'service': 'meta', 'policy': 'monetize', 'start': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), 'end': None, 'created_at': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), 'created_by': 'ows-sound-recordings/post-vendors-123-rules/456/ContentProfile', 'last_modified_at': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'last_modified_by': 'ows-sound-recordings/post-vendors-123-rules/456/ContentProfile', # noqa:E501 }] results = fingerprint_rules._format_base_rule(rules) assert results[0]['is_start_absolute'] is False assert results[0]['is_end_absolute'] is False @patch('sound_recordings.models.ownership.get_bulk_track_owners') @patch('sound_recordings.models.fingerprint_rules.get_bulk_rules') def test_bulk_track_active_logic(mock_get, mock_owners): """Test rule active logic for tracks.""" track_rules = { 111: [_rule(x) for x in ['US', 'GB', 'FR', 'SE', 'CA', 'IR']] } subaccount_rules = { 333: [_rule(x) for x in ['US', 'GB']] } vendor_rules = { 222: [_rule(x) for x in ['US', 'SE']] } subaccount_rules[333].append(_rule('CA', policy='carveout')) vendor_rules[222].append(_rule('IR', policy='carveout')) mock_get.side_effect = [track_rules, vendor_rules, subaccount_rules] mock_owners.return_value = { 111: { 'vendor_id': 222, 'subaccount_id': 333 } } result = fingerprint_rules.match_bulk('Track', [111]) assert mock_owners.call_args_list == [call([111])] assert mock_get.call_args_list == [ call('Track', [111]), call('Vendor', [222]), call('Subaccount', [333]) ] assert len(result[0]['rules']) == len(track_rules[111]) assert { x['territory']: x['active'] for x in result[0]['rules'] } == { 'US': True, 'GB': True, 'FR': True, 'SE': True, 'CA': False, 'IR': False } @patch('sound_recordings.models.ownership.get_bulk_subaccount_owner') @patch('sound_recordings.models.fingerprint_rules.get_bulk_rules') def test_bulk_subaccount_active_logic(mock_get, mock_owner): """Test rule active logic for subaccounts.""" subaccount_rules = { 111: [_rule(x) for x in ['US', 'GB', 'FR']] } vendor_rules = { 222: [ _rule('US'), _rule('FR', policy='carveout') ] } mock_get.side_effect = [subaccount_rules, vendor_rules] mock_owner.return_value = { 111: { 'vendor_id': 222 } } result = fingerprint_rules.match_bulk('Subaccount', [111]) assert mock_owner.call_args_list == [call([111])] assert mock_get.call_args_list == [ call('Subaccount', [111]), call('Vendor', [222]) ] assert len(result[0]['rules']) == len(subaccount_rules[111]) assert { x['territory']: x['active'] for x in result[0]['rules'] } == { 'US': True, 'GB': True, 'FR': False } @patch('sound_recordings.models.fingerprint_rules.get_bulk_rules') def test_bulk_vendor_active_logic(mock_get): """Test rule active logic for vendors.""" vendor_rules = { 111: [_rule('US')] } mock_get.return_value = vendor_rules result = fingerprint_rules.match_bulk('Vendor', [111]) print('RESULT', result) assert mock_get.call_args_list == [call('Vendor', [111])] assert len(result[0]['rules']) == len(vendor_rules[111]) assert result[0]['rules'][0]['active'] is True @patch('sound_recordings.models.ownership.get_bulk_track_owners') @patch('sound_recordings.models.fingerprint_rules.get_bulk_rules') def test_bulk_vendor_double_wildcard_active_logic(mock_get, mock_owners): """Test wildcard rule for all.""" track_rules = { 111: [ _rule('US', service='meta'), _rule('GB', service='youtube') ] } vendor_rules = { 222: [ _rule('*', service='*') ] } subaccount_rules = { 333: [] } mock_get.side_effect = [track_rules, vendor_rules, subaccount_rules] mock_owners.return_value = { 111: { 'vendor_id': 222, 'subaccount_id': 333 } } result = fingerprint_rules.match_bulk('Track', [111]) assert len(result[0]['rules']) == len(track_rules[111]) assert { x['territory']: x['active'] for x in result[0]['rules'] } == { 'US': True, 'GB': True } @patch('sound_recordings.models.ownership.get_bulk_track_owners') @patch('sound_recordings.models.fingerprint_rules.get_bulk_rules') def test_bulk_vendor_service_wildcard_active_logic(mock_get, mock_owners): """Test wildcard rule for service.""" track_rules = { 111: [ _rule('US', service='meta'), _rule('GB', service='youtube') ] } vendor_rules = { 222: [ _rule('US', service='*') ] } subaccount_rules = { 333: [] } mock_get.side_effect = [track_rules, vendor_rules, subaccount_rules] mock_owners.return_value = { 111: { 'vendor_id': 222, 'subaccount_id': 333 } } result = fingerprint_rules.match_bulk('Track', [111]) assert len(result[0]['rules']) == len(track_rules[111]) assert { x['territory']: x['active'] for x in result[0]['rules'] } == { 'US': True, 'GB': True } @patch('sound_recordings.models.ownership.get_bulk_track_owners') @patch('sound_recordings.models.fingerprint_rules.get_bulk_rules') def test_bulk_vendor_territory_wildcard_active_logic(mock_get, mock_owners): """Test wildcard rule for all territory.""" track_rules = { 111: [ _rule('US', service='meta'), _rule('GB', service='youtube') ] } vendor_rules = { 222: [ _rule('*', service='meta') ] } subaccount_rules = { 333: [] } mock_get.side_effect = [track_rules, vendor_rules, subaccount_rules] mock_owners.return_value = { 111: { 'vendor_id': 222, 'subaccount_id': 333 } } result = fingerprint_rules.match_bulk('Track', [111]) assert len(result[0]['rules']) == len(track_rules[111]) assert { x['territory']: x['active'] for x in result[0]['rules'] } == { 'US': True, 'GB': True } @patch('sound_recordings.models.ownership.get_bulk_track_owners') @patch('sound_recordings.models.fingerprint_rules.get_bulk_rules') def test_bulk_track(mock_get, mock_owners): """Test bulk track rule.""" track_rules = { 111: [ _rule('US', service='meta'), _rule('GB', service='youtube') ], 888: [ _rule('FR', service='meta'), _rule('GB', service='youtube') ] } vendor_rules = { 222: [ _rule('US', service='*') ], 444: [ _rule('FR', service='*') ] } subaccount_rules = { 333: [], 555: [] } mock_get.side_effect = [track_rules, vendor_rules, subaccount_rules] mock_owners.return_value = { 111: { 'vendor_id': 222, 'subaccount_id': 333 }, 888: { 'vendor_id': 444, 'subaccount_id': 555 } } result = fingerprint_rules.match_bulk('Track', [111, 888]) assert len(result[0]['rules']) == len(track_rules[111]) assert len(result[1]['rules']) == len(track_rules[888]) assert { x['territory']: x['active'] for x in result[0]['rules'] } == { 'US': True, 'GB': True } assert { x['territory']: x['active'] for x in result[1]['rules'] } == { 'FR': True, 'GB': True } @patch('sound_recordings.models.fingerprint_rules.get_bulk_rules') def test_bulk_unexpected_resource(mock_get): """Test unexpected resource not used.""" with pytest.raises(fingerprint_rules.UnknownResourceType): fingerprint_rules.match_bulk('track', [111]) def test_created_by_profile_id_new_format(): """Test created_by profile id parsing with new format.""" rule = { 'active': True, 'territory': 'US', 'service': 'meta', 'policy': 'monetize', 'start': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'end': None, 'created_at': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'created_by': '456_7', # noqa:E501 'last_modified_at': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'last_modified_by': 'ows-sound-recordings/post-vendors-123-rules/456/ContentProfile', # noqa:E501 'element_id': '123' } result = fingerprint_rules._format_base_rule([rule]) assert result[0]['created_by_profile_id'] == '456' assert result[0]['created_by_profile_type'] == 'ContentProfile' assert result[0]['last_modified_by_profile_id'] == '456' assert result[0]['last_modified_by_profile_type'] == 'ContentProfile' def test_extract_events_profile_id_new_format(): """Test history extraction with new underscore-separated profile format.""" rule = { 'active': True, 'territory': 'US', 'service': 'meta', 'policy': 'monetize', 'period_start': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'period_end': None, 'created_at': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'created_by': '456_7', # noqa:E501 'last_modified_at': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'last_modified_by': '456_7', # noqa:E501 'element_id': '123' } events = fingerprint_rules._extract_events([rule], False) assert events assert events[0]['profile_id'] == '456' assert events[0]['profile_type'] == 'ContentProfile' def test_extract_deletion_events_profile_id_new_format(): """Test history extraction with new underscore-separated profile format.""" rule = { 'active': True, 'territory': 'US', 'service': 'meta', 'policy': 'monetize', 'period_start': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'period_end': None, 'created_at': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'created_by': '456_7', # noqa:E501 'last_modified_at': datetime.datetime(2021, 6, 15, 0, 0, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'last_modified_by': '456_7', # noqa:E501 'element_id': '123' } events = fingerprint_rules._extract_events([rule], True) assert events assert len(events) == 2 assert events[0]['profile_id'] == '456' assert events[0]['profile_type'] == 'ContentProfile' # The second event is the implied created event; it should also have the # correct profile information parsed from the new underscore-separated format. assert events[1]['profile_id'] == '456' assert events[1]['profile_type'] == 'ContentProfile'