"""Tests for file processing module.""" import tempfile from unittest.mock import patch import copy import os import pytest from constants import sales_file_structure import file_processing import lambda_exceptions @pytest.fixture def phonofile_format_sales_data(): """Phonofile formatted sales data.""" return { 'pYear': '2015', 'releaseDate': '20130416', 'store': 'Google', 'adminFee': '0.000000', 'UsageTypeID': '5', 'releaseArtist': 'Freak Kitchen', 'PlayingTimeMinutes': '3', 'trkArtist': 'Freak Kitchen', 'PlayingTimeSeconds': '53', 'upc': '7071245083038', 'Publishers': 'Thunder Songs / Extra Publisher', 'grossRevenue': '0.000597144840', 'Songwriters': 'Mattias IA Eklundh', 'netRevenue': '0.00047771587200', 'label': 'TSP-Thunderstruck Productions', 'isrc': 'DKK3A9800104', 'unitsSold': '2', 'mechRate': '0.091', 'copyrightWithholding': '0.0000', 'PublicDomain': 'False', 'transactionType': 'Track', 'trkTitle': 'Entertain me', 'trkUID': '865942', 'pMonth': '7', 'distFee': '0.00011942896800', 'releaseTitle': 'Freak Kitchen'} @pytest.fixture def db_format_sales_data(): """DB formatted sales data.""" return { 'label': 'TSP-Thunderstruck Productions', 'upc': '7071245083038', 'release_date': '20130416', 'release_title': 'Freak Kitchen', 'release_artist': 'Freak Kitchen', 'isrc': 'DKK3A9800104', 'original_track_id': '865942', 'track_name': 'Entertain me', 'track_artist': 'Freak Kitchen', 'length_minute': '3', 'length_seconds': '53', 'writer': 'Mattias IA Eklundh', 'original_publishers': 'Thunder Songs / Extra Publisher', 'transaction_type': 'Track', 'store': 'Google', 'qty': '2', 'gross_revenue': '0.000597144840', 'net_revenue': '0.00047771587200', 'dist_fee': '0.00011942896800', 'royalty_rate': '0.091', 'royalty': '0.0000', 'admin_fee': '0.000000', 'public_domain': 'False', 'usage_type': '5', 'period_month': '7', 'period_year': '2015'} @pytest.fixture def calculated_sales_data(): """Lambda calculated sales data.""" return { 'period_id': '1', 'royalty_calculated': 0.182, 'release_date_calculated': 'Apr 16, 2013', 'royalty_rate_calculated': 0.091, 'track_id': 'PHF865942', 'publisher': 'Thunder Songs', 'company': 'phonofile', 'sales_file_name': 'sales_file.txt'} def test_identify_company_by_file_headers_phonofile(): """Test identify_company_by_file_headers can identify Phonofile company.""" headers = { 'pMonth', 'pYear', 'label', 'upc', 'releaseDate', 'releaseTitle', 'releaseArtist', 'isrc', 'trkUID', 'trkTitle', 'trkArtist', 'PlayingTimeMinutes', 'PlayingTimeSeconds', 'Songwriters', 'Publishers', 'transactionType', 'store', 'unitsSold', 'grossRevenue', 'netRevenue', 'distFee', 'mechRate', 'copyrightWithholding', 'adminFee', 'PublicDomain', 'UsageTypeID'} result = file_processing.identify_company_by_file_headers(headers) assert result == sales_file_structure.PHONOFILE def test_identify_company_by_file_headers_finetunes(): """Test identify_company_by_file_headers can identify Finetunes company.""" headers = { 'Sales Period Month', 'Sales Period Year', 'Label Name', 'UPC', 'Album/Release Date', 'Album/Release Name', 'Album/Release Artist', 'ISRC Code', 'Internal Unique Track ID (if applicable)', 'Track Name', 'Track Artist', 'Total Playing Time - Minutes', 'Total Playing Time - Seconds', 'Songwriter(s)/Composer(s)', 'Publisher(s)', 'Transaction Type /Configuration Code', 'Store', 'Units Sold', 'Gross Revenue (From Source)', 'Net Revenue (Amount Paid to Label)', 'Label\'s Distribution Fee', 'Mechanical Royalty Rate', ('Total Mechanical Royalties Deducted' ' from Label’s Receipts (if applicable)')} result = file_processing.identify_company_by_file_headers(headers) assert result == sales_file_structure.FINETUNES def test_identify_company_by_file_headers_fail(): """Test the function raises an exception if headers don't match.""" headers = {'Headers', 'Don\t', 'Match', '!'} with pytest.raises(lambda_exceptions.UnexpectedFileHeaders): file_processing.identify_company_by_file_headers(headers) @pytest.mark.parametrize('value, expected_result', [ ('DEPRECATED-NOAPC0840100', 'NOAPC0840100'), ('-NOAPC0840111', 'NOAPC0840111'), ('_NOAPC0840111', 'NOAPC0840111'), ('__NOAPC0840111', 'NOAPC0840111'), (115, 115) ]) def test_remove_leading_dash_underscore(value, expected_result): """Test remove_leading_dash_underscore function.""" result = file_processing.remove_leading_dash_underscore(value) assert result == expected_result @pytest.mark.parametrize('value, expected_result', [ ('5553555000308-D', '5553555000308'), ('5553555000308', '5553555000308'), (115, 115) ]) def test_remove_non_digits(value, expected_result): """Test remove_non_digits function.""" result = file_processing.remove_non_digits(value) assert result == expected_result @pytest.mark.parametrize('value, expected_result', [ ('publisher||invalid publisher||invalid publisher2', 'publisher'), ('publisher / invalid publisher', 'publisher'), ('publisher publusher poblosher', 'publisher publusher poblosher'), ('before_first_sep||before_second / afterall', 'before_first_sep'), ('before_first_another / before_second||afterall', 'before_first_another'), ('||||||DATAHERE||||', 'DATAHERE'), ('||||||', None), ('', '') ]) def test_extract_publisher(value, expected_result): """Test extract_publisher function.""" result = file_processing.extract_publisher(value) assert result == expected_result @pytest.mark.parametrize('original_track_id, expected_result', ( ['123', 'PHF123'], [123, 'PHF123'])) def test_format_track_id_for_phf(original_track_id, expected_result): """Test format_track_id_for_phf function.""" result = file_processing.format_track_id_for_phf(original_track_id) assert result == expected_result def test_format_company_sales_data_record( phonofile_format_sales_data, db_format_sales_data): """Test format_company_sales_data_record correctly changes sales data.""" company = 'phonofile' result = file_processing.format_company_sales_data_record( phonofile_format_sales_data, company) assert result == db_format_sales_data def test_format_company_sales_data_invalid_record(phonofile_format_sales_data): """Test format_company_sales_data_record raises for invalid content.""" company = 'phonofile' sales_data = phonofile_format_sales_data sales_data.pop('releaseTitle') with pytest.raises(lambda_exceptions.ContentDoesNotMatchHeaders): file_processing.format_company_sales_data_record( phonofile_format_sales_data, company) def test_format_company_sales_data_extra_field(phonofile_format_sales_data): """Test format_company_sales_data_record raises for invalid content.""" company = 'phonofile' sales_data = phonofile_format_sales_data sales_data[None] = 'this trash really exists in the sales data, lol' with pytest.raises(lambda_exceptions.ContentDoesNotMatchHeaders): file_processing.format_company_sales_data_record( phonofile_format_sales_data, company) def test_format_company_sales_data_not_enough_fields( phonofile_format_sales_data): """Test format_company_sales_data_record raises for invalid content.""" company = 'phonofile' sales_data = phonofile_format_sales_data sales_data['releaseTitle'] = None with pytest.raises(lambda_exceptions.ContentDoesNotMatchHeaders): file_processing.format_company_sales_data_record( phonofile_format_sales_data, company) @pytest.mark.parametrize('value, expected_result', ( ['20051011', '2005-10-11'], ['00010101', '2017-09-29'], ['2005101', '2017-09-29'], ['200510111', '2017-09-29'], ['01/03/2007', '2007-03-01'], ['2007-06-21', '2007-06-21'], ['', '2017-09-29'], [None, '2017-09-29'])) def test_calculate_release_date(value, expected_result): """Test format_track_id_for_phf function.""" result = file_processing.calculate_release_date(value) assert result == expected_result @pytest.mark.parametrize( 'year, month, expected_result', ( ['2000', '1', 1], ['2222', '1', None])) def test_calculate_release_period(year, month, periods_data, expected_result): """Test format_track_id_for_phf function.""" result = file_processing.calculate_release_period( year, month, periods_data) assert result == expected_result @pytest.mark.parametrize( 'test_upc, test_isrc', ( ['6586862404435', 'NOHBH0702009'], ['_6586862404435-D', 'DEPRECATED-NOHBH0702009'])) def test_calculate_additional_sales_data_fields( db_format_sales_data, periods_data, test_upc, test_isrc): """Test calculate_additional_sales_data_fields can calculate new fields.""" company = 'phonofile' sales_file_name = 'sales_data.txt' db_format_sales_data['upc'] = test_upc db_format_sales_data['isrc'] = test_isrc expected_formatted_upc = '6586862404435' expected_formatted_isrc = 'NOHBH0702009' expected_result = { **db_format_sales_data, 'royalty_calculated': 0.182, 'release_date_calculated': '2013-04-16', 'royalty_rate_calculated': 0.091, 'track_id': 'PHF865942', 'publisher': 'Thunder Songs', 'company': company, 'period_id': 3, 'sales_file_name': sales_file_name, 'qty': 2, 'upc': expected_formatted_upc, 'isrc': expected_formatted_isrc} result = file_processing.calculate_additional_sales_data_fields( db_format_sales_data, company, sales_file_name, periods_data) assert result == expected_result def test_extract_mechadmin_track_data_from_sales_data( db_format_sales_data, calculated_sales_data): """Test extract_mechadmin_track_data_from_sales_data works as expected. The function should return only fields related to model PhfMechadminTrack. """ sales_data = {**db_format_sales_data, **calculated_sales_data} expected_result = { 'company': 'phonofile', 'isrc': 'DKK3A9800104', 'label': 'TSP-Thunderstruck Productions', 'length_minute': '3', 'length_seconds': '53', 'original_publishers': 'Thunder Songs / Extra Publisher', 'original_track_id': '865942', 'public_domain': 'False', 'publisher': 'Thunder Songs', 'release_artist': 'Freak Kitchen', 'release_date': '20130416', 'release_date_calculated': 'Apr 16, 2013', 'release_title': 'Freak Kitchen', 'track_artist': 'Freak Kitchen', 'track_id': 'PHF865942', 'track_name': 'Entertain me', 'upc': '7071245083038', 'writer': 'Mattias IA Eklundh'} result = file_processing.extract_mechadmin_track_data_from_sales_data( sales_data) assert result == expected_result def test_extract_publishing_escrow_data_from_sales_data( db_format_sales_data, calculated_sales_data): """Test extract_publishing_escrow_data_from_sales_data works as expected. The function should return fields related to model PhfPublishingEscrow. """ sales_data = {**db_format_sales_data, **calculated_sales_data} expected_result = { 'admin_fee': '0.000000', 'dist_fee': '0.00011942896800', 'gross_revenue': '0.000597144840', 'net_revenue': '0.00047771587200', 'period_id': '1', 'qty': '2', 'royalty': '0.0000', 'royalty_calculated': 0.182, 'royalty_rate': '0.091', 'royalty_rate_calculated': 0.091, 'sales_file_name': 'sales_file.txt', 'store': 'Google', 'track_id': 'PHF865942', 'transaction_type': 'Track', 'usage_type': '5'} result = file_processing.extract_publishing_escrow_data_from_sales_data( sales_data) assert result == expected_result @patch('file_processing.calculate_additional_sales_data_fields') def test_process_sales_data_record_validation_failed( calculate_additional_sales_data_fields_mock, phonofile_format_sales_data, db_format_sales_data, calculated_sales_data, periods_data): """Test process_sales_data raises error on invalid data.""" company = 'phonofile' sales_file_name = 'sales_file.txt' sales_data = phonofile_format_sales_data sales_data['PlayingTimeSeconds'] = 'blahblah' returned_calculated_sales_data = copy.deepcopy(db_format_sales_data) returned_calculated_sales_data.update(calculated_sales_data) calculate_additional_sales_data_fields_mock.assert_not_called() line_number = 1 with pytest.raises(lambda_exceptions.DataValidationFailed): file_processing.process_sales_data_record( phonofile_format_sales_data, company, sales_file_name, periods_data, line_number) @patch('file_processing.calculate_additional_sales_data_fields') def test_process_sales_data_record_track_id_exists_in_db( calculate_additional_sales_data_fields_mock, phonofile_format_sales_data, db_format_sales_data, calculated_sales_data, periods_data): """Test process_sales_data makes necessary calculations and saves data.""" company = 'phonofile' sales_file_name = 'sales_file.txt' returned_calculated_sales_data = copy.deepcopy(db_format_sales_data) returned_calculated_sales_data.update(calculated_sales_data) calculate_additional_sales_data_fields_mock.return_value = ( returned_calculated_sales_data) line_number = 1 result = file_processing.process_sales_data_record( phonofile_format_sales_data, company, sales_file_name, periods_data, line_number) calculate_additional_sales_data_fields_mock.assert_called_once_with( db_format_sales_data, company, sales_file_name, periods_data) tracks_data = { 'company': 'phonofile', 'isrc': 'DKK3A9800104', 'label': 'TSP-Thunderstruck Productions', 'length_minute': '3', 'length_seconds': '53', 'original_publishers': 'Thunder Songs / Extra Publisher', 'original_track_id': '865942', 'public_domain': 'False', 'publisher': 'Thunder Songs', 'release_artist': 'Freak Kitchen', 'release_date': '20130416', 'release_date_calculated': 'Apr 16, 2013', 'release_title': 'Freak Kitchen', 'track_artist': 'Freak Kitchen', 'track_id': 'PHF865942', 'track_name': 'Entertain me', 'upc': '7071245083038', 'writer': 'Mattias IA Eklundh'} publishing_escrow_data = { 'net_revenue': '0.00047771587200', 'sales_file_name': 'sales_file.txt', 'gross_revenue': '0.000597144840', 'track_id': 'PHF865942', 'qty': '2', 'dist_fee': '0.00011942896800', 'royalty_rate': '0.091', 'royalty_rate_calculated': 0.091, 'period_id': '1', 'royalty_calculated': 0.182, 'usage_type': '5', 'store': 'Google', 'royalty': '0.0000', 'transaction_type': 'Track', 'admin_fee': '0.000000'} assert result == (tracks_data, publishing_escrow_data) @patch('models.phf_mechadmin_track.get_tracks_by_list_of_track_ids') @patch('models.phf_mechadmin_track.bulk_insert_phf_mechadmin_track') @patch('models.phf_publishing_escrow.bulk_insert_phf_publishing_escrow') def test_bulk_insert_new_phf_metadata_success( bulk_insert_phf_publishing_escrow_mock, bulk_insert_phf_mechadmin_track_mock, get_tracks_by_list_of_track_ids_mock): """Test it's possible to perform bulk insert of phf data.""" existing_track = {'track_id': 'PHF1'} new_track = {'track_id': 'PHF2'} tracks_data = {'PHF1': existing_track, 'PHF2': new_track} escrows_data = [{'some': 'data'}] get_tracks_by_list_of_track_ids_mock.return_value = [existing_track] # remove existing tracks from to_create list. result = file_processing.bulk_insert_new_phf_metadata( tracks_data, escrows_data) assert result == 200 get_tracks_by_list_of_track_ids_mock.assert_called_once_with( ['PHF1', 'PHF2']) bulk_insert_phf_mechadmin_track_mock.assert_called_once_with([new_track]) bulk_insert_phf_publishing_escrow_mock.assert_called_once_with( escrows_data) def test_validate_mandatory_fields_success(db_format_sales_data): """Test validate_mandatory_fields function.""" result = file_processing.validate_mandatory_fields(db_format_sales_data) assert result == 200 @pytest.mark.parametrize( 'missing_fields', ( ['length_minute'], ['track_name', 'track_artist'], ['qty', 'store'] ) ) def test_validate_mandatory_fields_fail( db_format_sales_data, missing_fields): """Test validate_mandatory_fields function.""" sales_data = db_format_sales_data expected_msg = ( '(MandatoryFieldsMissing(...), \'Required fields are missing:' ) for f in missing_fields: sales_data.pop(f) with pytest.raises(lambda_exceptions.MandatoryFieldsMissing) as e: file_processing.validate_mandatory_fields(sales_data) err_msg = str(e.value) assert err_msg.startswith(expected_msg) for field in missing_fields: missing_field_found = err_msg.find(field) assert missing_field_found > 0 @pytest.mark.parametrize('encoding, special_symbol', ( ['windows-1252', b'\x9c'], ['utf-8', b'2'])) def test_guess_file_segment_encoding(encoding, special_symbol): """Test guess_file_segment_encoding.""" tmp = tempfile.NamedTemporaryFile( mode='wb', delete=False) file_path = tmp.name with open(file_path, mode='wb') as f: f.write(special_symbol) res = file_processing.guess_file_segment_encoding(file_path, 0, 2) os.unlink(file_path) assert res == encoding def test_extract_file_headers(): """Test extract_file_headers.""" tmp = tempfile.NamedTemporaryFile( mode='w', delete=False) file_path = tmp.name headers_data = '1,2,3' with open(file_path, mode='w') as f: f.write(headers_data) res = file_processing.extract_file_headers(file_path) assert res == '1,2,3'