"""End-to-end integration tests — full pipeline through mock GraphQL.""" import csv import pytest from integration.conftest import mock_graphql import app from config import RunOptions, load_config def _load(config_file): """Load config from file.""" return load_config(config_file) def _options(config, **overrides): """Build resolved RunOptions for tests.""" return RunOptions.resolve(config, **overrides) class TestDryRun: """Dry-run exercises the full pipeline without API calls.""" def test_produces_output_csv(self, api_server, config_file, csv_file, tmp_path): mock_graphql(api_server) output = str(tmp_path / 'output.csv') cfg = _load(config_file) results = app.run( config=cfg, input_file=csv_file, options=_options(cfg, dry_run=True, output=output), ) assert len(results.success) == 2 assert len(results.errors) == 0 with open(output, 'r') as f: rows = list(csv.DictReader(f)) assert len(rows) == 2 assert all(r['Contract ID'] == 'DRY_RUN' for r in rows) def test_skips_invalid_rows(self, api_server, config_file, tmp_path): mock_graphql(api_server) csv_content = ( 'Account ID,Contract Name,Contract Type,' 'Current Period Start Date\n' ',Missing ID,Distribution,01/01/2026\n' '100,Valid,Distribution,01/01/2026\n' ) csv_path = tmp_path / 'mixed.csv' csv_path.write_text(csv_content) cfg = _load(config_file) results = app.run( config=cfg, input_file=str(csv_path), options=_options(cfg, dry_run=True), ) assert len(results.success) == 1 assert len(results.skipped) == 1 class TestExecute: """Execute mode hits the mock GraphQL server.""" def test_successful_contract_creation(self, api_server, config_file, tmp_path): mock_graphql(api_server) csv_content = ( 'Account ID,Contract Name,Contract Type,' 'Current Period Start Date,Signing Entity\n' '100,Contract A,Distribution,01/01/2026,acme\n' '200,Contract B,Neighbouring Rights,02/01/2026,globex\n' ) csv_path = tmp_path / 'input.csv' csv_path.write_text(csv_content) output = str(tmp_path / 'output.csv') cfg = _load(config_file) results = app.run( config=cfg, input_file=str(csv_path), options=_options( cfg, output=output, throttle_capacity=10, throttle_rate=100.0, ), ) assert len(results.success) == 2 assert results.success[0].data.contract_id == 42 assert results.success[1].data.contract_id == 43 with open(output, 'r') as f: rows = list(csv.DictReader(f)) assert rows[0]['Contract ID'] == '42' assert rows[1]['Contract ID'] == '43' def test_api_error_records_failure(self, api_server, config_file, tmp_path): csv_content = ( 'Account ID,Contract Name,Contract Type,' 'Current Period Start Date\n' '100,Bad Contract,Distribution,01/01/2026\n' ) csv_path = tmp_path / 'bad.csv' csv_path.write_text(csv_content) # Override handler to return error for create mutation import json def _error_handler(request): body = request.get_json() query = body.get('query', '') if 'abacusReferenceSigningEntities' in query: return json.dumps( { 'data': { 'abacusReferenceSigningEntities': { 'items': [ { 'referenceSigningEntityId': '1', 'legalName': 'Acme', 'companyCode': '0001', 'address': None, 'companyRegistrationNumber': None, 'vatNumber': None, } ] } } } ) if 'abacusRunControllers' in query: return json.dumps({'data': {'abacusRunControllers': {'items': []}}}) if 'abacusCreateContractWithLifecycles' in query: return json.dumps({'errors': [{'message': 'Account not found'}]}) return json.dumps({'errors': [{'message': 'Unknown'}]}) api_server.expect_request('/graphql', method='POST').respond_with_handler( _error_handler ) cfg = _load(config_file) results = app.run( config=cfg, input_file=str(csv_path), options=_options( cfg, throttle_capacity=10, throttle_rate=100.0, ), ) assert len(results.success) == 0 assert len(results.errors) == 1 assert 'Account not found' in results.errors[0].result.error class TestOutputFiles: """Verify output file contents end-to-end.""" def test_failures_csv_written_on_mixed_results( self, api_server, config_file, tmp_path ): # Handler: first create succeeds, second fails call_count = {'create': 0} import json def _mixed_handler(request): body = request.get_json() query = body.get('query', '') if 'abacusReferenceSigningEntities' in query: return json.dumps( { 'data': { 'abacusReferenceSigningEntities': { 'items': [ { 'referenceSigningEntityId': '1', 'legalName': 'Acme', 'companyCode': '0001', 'address': None, 'companyRegistrationNumber': None, 'vatNumber': None, }, { 'referenceSigningEntityId': '2', 'legalName': 'Globex', 'companyCode': '0002', 'address': None, 'companyRegistrationNumber': None, 'vatNumber': None, }, ] } } } ) if 'abacusRunControllers' in query: return json.dumps({'data': {'abacusRunControllers': {'items': []}}}) if 'abacusCreateContractWithLifecycles' in query: call_count['create'] += 1 if call_count['create'] == 1: return json.dumps( { 'data': { 'abacusCreateContractWithLifecycles': { 'contractId': 42, 'contractName': 'A', 'contractType': 'distribution', } } } ) return json.dumps({'errors': [{'message': 'Bad request'}]}) return json.dumps({'errors': [{'message': 'Unknown'}]}) api_server.expect_request('/graphql', method='POST').respond_with_handler( _mixed_handler ) csv_content = ( 'Account ID,Contract Name,Contract Type,' 'Current Period Start Date,Signing Entity\n' '100,Contract A,Distribution,01/01/2026,acme\n' '200,Contract B,Neighbouring Rights,02/01/2026,globex\n' ) csv_path = tmp_path / 'input.csv' csv_path.write_text(csv_content) output = str(tmp_path / 'results.csv') cfg = _load(config_file) results = app.run( config=cfg, input_file=str(csv_path), options=_options( cfg, output=output, throttle_capacity=10, throttle_rate=100.0, ), ) assert len(results.success) == 1 assert len(results.errors) == 1 failures_csv = str(tmp_path / 'results_failures.csv') with open(failures_csv, 'r') as f: rows = list(csv.DictReader(f)) assert len(rows) == 1 assert rows[0]['Status'] == 'ERROR'