diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5858036..7078ed1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.9] + python-version: ["3.9", "3.11", "3.12"] steps: - uses: actions/checkout@v2 diff --git a/eva_sub_cli/call_home.py b/eva_sub_cli/call_home.py new file mode 100644 index 0000000..aab046c --- /dev/null +++ b/eva_sub_cli/call_home.py @@ -0,0 +1,115 @@ +import logging +import os +import traceback +import uuid +from datetime import datetime, timezone + +import requests +from ebi_eva_common_pyutils.config import WritableConfig + +from eva_sub_cli import __version__, SUB_CLI_CONFIG_FILE +from eva_sub_cli.submission_ws import _submission_ws_base_url + +logger = logging.getLogger(__name__) + +CALL_HOME_PATH = 'call-home' +DEPLOYMENT_ID_DIR = os.path.join(os.path.expanduser('~'), '.eva-sub-cli') +DEPLOYMENT_ID_FILE = os.path.join(DEPLOYMENT_ID_DIR, 'deployment_id') + +EVENT_START = 'START' +EVENT_VALIDATION_COMPLETED = 'VALIDATION_COMPLETED' +EVENT_END = 'END' +EVENT_FAILURE = 'FAILURE' + + +def _get_call_home_url(): + return os.path.join(_submission_ws_base_url(), CALL_HOME_PATH) + + +def _get_or_create_deployment_id(): + try: + if os.path.isfile(DEPLOYMENT_ID_FILE): + with open(DEPLOYMENT_ID_FILE, 'r') as f: + deployment_id = f.read().strip() + if deployment_id: + return deployment_id + deployment_id = str(uuid.uuid4()) + os.makedirs(DEPLOYMENT_ID_DIR, exist_ok=True) + with open(DEPLOYMENT_ID_FILE, 'w') as f: + f.write(deployment_id) + return deployment_id + except Exception: + logger.debug('Failed to read or create deployment ID file, using transient ID') + return str(uuid.uuid4()) + + +def _get_or_create_run_id(submission_dir): + try: + config_path = os.path.join(submission_dir, SUB_CLI_CONFIG_FILE) + config = WritableConfig(config_path) + run_id = config.get('run_id') + if run_id: + return str(run_id) + run_id = str(uuid.uuid4()) + config.set('run_id', value=run_id) + config.write() + return run_id + except Exception: + logger.debug('Failed to read or create run ID in config, using transient ID') + return str(uuid.uuid4()) + + +class CallHomeClient: + + def __init__(self, submission_dir, executor, tasks): + self.start_time = datetime.now(timezone.utc) + self.deployment_id = _get_or_create_deployment_id() + self.run_id = _get_or_create_run_id(submission_dir) + self.executor = executor + self.tasks = tasks + + def _build_payload(self, event_type, **kwargs): + now = datetime.now(timezone.utc) + if event_type == EVENT_START: + runtime_seconds = 0 + else: + elapsed = now - self.start_time + runtime_seconds = int(elapsed.total_seconds()) + payload = { + 'deploymentId': self.deployment_id, + 'runId': self.run_id, + 'eventType': event_type, + 'cliVersion': __version__, + 'createdAt': now.isoformat(), + 'runtimeSeconds': runtime_seconds, + 'executor': self.executor, + 'tasks': self.tasks, + } + if kwargs: + payload.update(kwargs) + return payload + + def _send_event(self, event_type, **kwargs): + try: + payload = self._build_payload(event_type, **kwargs) + requests.post(_get_call_home_url(), json=payload, timeout=5) + except Exception: + logger.debug('Failed to send %s call-home event', event_type) + + def send_start(self): + self._send_event(EVENT_START) + + def send_validation_completed(self, validation_result): + self._send_event(EVENT_VALIDATION_COMPLETED, validation_result=validation_result) + + def send_end(self): + self._send_event(EVENT_END) + + def send_failure(self, exception=None): + kwargs = {} + if exception is not None: + kwargs['exceptionMessage'] = str(exception) + kwargs['exceptionStacktrace'] = ''.join( + traceback.format_exception(type(exception), exception, exception.__traceback__) + ) + self._send_event(EVENT_FAILURE, **kwargs) diff --git a/eva_sub_cli/etc/call_home_payload_schema.json b/eva_sub_cli/etc/call_home_payload_schema.json new file mode 100644 index 0000000..251a673 --- /dev/null +++ b/eva_sub_cli/etc/call_home_payload_schema.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Call-Home Event Payload", + "description": "Payload sent by the eva-sub-cli to the submission WS /call-home endpoint for usage telemetry.", + "type": "object", + "properties": { + "deploymentId": { + "type": "string", + "format": "uuid", + "description": "Persistent UUID identifying a unique CLI installation. Stored at ~/.eva-sub-cli/deployment_id." + }, + "runId": { + "type": "string", + "format": "uuid", + "description": "Persistent UUID identifying a submission directory run. Stored in the .eva_sub_cli_config.yml file within the submission directory." + }, + "eventType": { + "type": "string", + "enum": ["START", "VALIDATION_COMPLETED", "END", "FAILURE"], + "description": "Type of lifecycle event. START is sent when main() begins, VALIDATION_COMPLETED after validation finishes, END when the program completes, FAILURE on any error." + }, + "cliVersion": { + "type": "string", + "description": "Version of the eva-sub-cli package (e.g. '0.5.3')." + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 UTC timestamp of when the event was created." + }, + "runtimeSeconds": { + "type": "integer", + "minimum": 0, + "description": "Elapsed wall-clock seconds since the CLI started. Always 0 for START events." + }, + "executor": { + "type": "string", + "enum": ["native", "docker"], + "description": "Execution backend selected for validation." + }, + "tasks": { + "type": "array", + "items": { + "type": "string", + "enum": ["validate", "submit"] + }, + "description": "List of tasks requested by the user." + }, + "validation_result": { + "type": "string", + "description": "Outcome of the validation step. Only present in VALIDATION_COMPLETED events." + }, + "exceptionMessage": { + "type": "string", + "description": "The exception message string. Only present in FAILURE events when an exception was caught." + }, + "exceptionStacktrace": { + "type": "string", + "description": "Full Python stacktrace of the exception. Only present in FAILURE events when an exception was caught." + } + }, + "required": [ + "deploymentId", + "runId", + "eventType", + "cliVersion", + "createdAt", + "runtimeSeconds", + "executor", + "tasks" + ], + "additionalProperties": true +} diff --git a/eva_sub_cli/executables/cli.py b/eva_sub_cli/executables/cli.py index 88e43ee..6a2c8b5 100755 --- a/eva_sub_cli/executables/cli.py +++ b/eva_sub_cli/executables/cli.py @@ -19,6 +19,7 @@ MetadataTemplateVersionNotFoundException from eva_sub_cli.exceptions.submission_status_exception import SubmissionStatusException from eva_sub_cli.exceptions.submission_not_found_exception import SubmissionNotFoundException +from eva_sub_cli.call_home import CallHomeClient from eva_sub_cli.file_utils import is_submission_dir_writable, DirLockError, DirLock from eva_sub_cli.orchestrator import VALIDATE, SUBMIT, DOCKER, NATIVE from eva_sub_cli.validators.validator import ALL_VALIDATION_TASKS @@ -107,42 +108,71 @@ def main(): else: logging_config.add_stdout_handler(logging.INFO) + # Initialize call-home + call_home = None + try: + call_home = CallHomeClient( + submission_dir=args.submission_dir, + executor=args.executor, + tasks=args.tasks + ) + call_home.send_start() + except Exception: + pass + + caught_exception = None try: # lock the submission directory with DirLock(os.path.join(args.submission_dir)) as lock: # Create the log file logging_config.add_file_handler(os.path.join(args.submission_dir, 'eva_submission.log'), logging.DEBUG) # Pass on all the arguments to the orchestrator - orchestrator.orchestrate_process(**args.__dict__) - except DirLockError: + orchestrator.orchestrate_process(call_home=call_home, **args.__dict__) + except DirLockError as dle: print(f'Could not acquire the lock file for {args.submission_dir} because another process is using this ' f'directory or a previous process did not terminate correctly. ' f'If the problem persists, remove the lock file manually.') + caught_exception = dle exit_status = 65 except FileNotFoundError as fne: print(fne) + caught_exception = fne exit_status = 66 except SubmissionNotFoundException as snfe: print(f'{snfe}. Please contact EVA Helpdesk') + caught_exception = snfe exit_status = 67 except SubmissionStatusException as sse: print(f'{sse}. Please try again later. If the problem persists, please contact EVA Helpdesk') + caught_exception = sse exit_status = 68 except MetadataTemplateVersionException as mte: print(mte) + caught_exception = mte exit_status = 69 except MetadataTemplateVersionNotFoundException as mte: print(mte) + caught_exception = mte exit_status = 70 except SubmissionUploadException as sue: print(sue) + caught_exception = sue exit_status = 71 except HTTPError as http_err: print(http_err) if http_err.response is not None and http_err.response.text: print(http_err.response.text) + caught_exception = http_err exit_status = 72 except Exception as ex: print(ex) + caught_exception = ex exit_status = 73 + + if call_home is not None: + if exit_status == 0: + call_home.send_end() + else: + call_home.send_failure(caught_exception) + return exit_status diff --git a/eva_sub_cli/orchestrator.py b/eva_sub_cli/orchestrator.py index e5f2526..52d8d0c 100755 --- a/eva_sub_cli/orchestrator.py +++ b/eva_sub_cli/orchestrator.py @@ -299,7 +299,7 @@ def check_validation_required(tasks, sub_config, username=None, password=None): def orchestrate_process(submission_dir, metadata_json, metadata_xlsx, tasks, executor, validation_tasks=ALL_VALIDATION_TASKS, username=None, password=None, - shallow_validation=False, nextflow_config=None, **kwargs): + shallow_validation=False, nextflow_config=None, call_home=None, **kwargs): # load config config_file_path = os.path.join(submission_dir, SUB_CLI_CONFIG_FILE) sub_config = WritableConfig(config_file_path, version=__version__) @@ -341,6 +341,8 @@ def orchestrate_process(submission_dir, metadata_json, metadata_xlsx, nextflow_config=nextflow_config) with validator: validator.validate_and_report() + if call_home: + call_home.send_validation_completed(validator.results) if not metadata_json: metadata_json = os.path.join(validator.output_dir, 'metadata.json') sub_config.set('metadata_json', value=metadata_json) diff --git a/eva_sub_cli/submission_ws.py b/eva_sub_cli/submission_ws.py index eb94a84..4243e23 100644 --- a/eva_sub_cli/submission_ws.py +++ b/eva_sub_cli/submission_ws.py @@ -9,6 +9,16 @@ from eva_sub_cli.auth import get_auth from eva_sub_cli.exceptions.submission_upload_exception import SubmissionUploadException +DEFAULT_SUBMISSION_WS_URL = 'https://www.ebi.ac.uk/eva/webservices/submission-ws/v1/' + + +def _submission_ws_base_url(): + """Retrieve the base URL for the submission web services. + In order of preference from the environment variable or the hardcoded value.""" + if os.environ.get(SUBMISSION_WS_VAR): + return os.environ.get(SUBMISSION_WS_VAR) + else: + return DEFAULT_SUBMISSION_WS_URL class SubmissionWSClient(AppLogger): """ @@ -19,19 +29,13 @@ def __init__(self, username=None, password=None): self.auth = get_auth(username, password) self.base_url = self._submission_ws_url - SUBMISSION_WS_URL = 'https://www.ebi.ac.uk/eva/webservices/submission-ws/v1/' SUBMISSION_INITIATE_PATH = 'submission/initiate' SUBMISSION_UPLOADED_PATH = 'submission/{submissionId}/uploaded' SUBMISSION_STATUS_PATH = 'submission/{submissionId}/status' @property def _submission_ws_url(self): - """Retrieve the base URL for the submission web services. - In order of preference from the environment variable or the hardcoded value.""" - if os.environ.get(SUBMISSION_WS_VAR): - return os.environ.get(SUBMISSION_WS_VAR) - else: - return self.SUBMISSION_WS_URL + return _submission_ws_base_url() def _submission_initiate_url(self): return os.path.join(self.base_url, self.SUBMISSION_INITIATE_PATH) diff --git a/tests/test_call_home.py b/tests/test_call_home.py new file mode 100644 index 0000000..10e09aa --- /dev/null +++ b/tests/test_call_home.py @@ -0,0 +1,192 @@ +import os +import shutil +import time +import uuid +from unittest import TestCase +from unittest.mock import patch + +from ebi_eva_common_pyutils.config import Configuration, WritableConfig +from requests.exceptions import ConnectionError, Timeout + +from eva_sub_cli import SUB_CLI_CONFIG_FILE +from eva_sub_cli.call_home import _get_or_create_deployment_id, _get_or_create_run_id, \ + CallHomeClient, EVENT_START, EVENT_FAILURE, EVENT_END + + +class TestDeploymentId(TestCase): + + resources_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources') + + def setUp(self): + self.config_dir = os.path.join(self.resources_dir, '.eva-sub-cli') + self.deployment_file = os.path.join(self.config_dir, 'deployment_id') + + def tearDown(self): + if os.path.exists(self.config_dir):( + shutil.rmtree(self.config_dir)) + + def test_creates_new_id_when_file_missing(self): + with patch('eva_sub_cli.call_home.DEPLOYMENT_ID_DIR', self.config_dir), \ + patch('eva_sub_cli.call_home.DEPLOYMENT_ID_FILE', self.deployment_file): + deployment_id = _get_or_create_deployment_id() + # Should be a valid UUID + uuid.UUID(deployment_id) + # File should have been created + self.assertTrue(os.path.isfile(self.deployment_file)) + with open(self.deployment_file) as f: + self.assertEqual(f.read().strip(), deployment_id) + + def test_reads_existing_id(self): + os.makedirs(self.config_dir, exist_ok=True) + existing_id = str(uuid.uuid4()) + with open(self.deployment_file, 'w') as f: + f.write(existing_id) + + with patch('eva_sub_cli.call_home.DEPLOYMENT_ID_DIR', self.config_dir), \ + patch('eva_sub_cli.call_home.DEPLOYMENT_ID_FILE', self.deployment_file): + deployment_id = _get_or_create_deployment_id() + self.assertEqual(deployment_id, existing_id) + + def test_handles_permission_error(self): + with patch('eva_sub_cli.call_home.DEPLOYMENT_ID_DIR', '/nonexistent/path'), \ + patch('eva_sub_cli.call_home.DEPLOYMENT_ID_FILE', '/nonexistent/path/deployment_id'): + deployment_id = _get_or_create_deployment_id() + # Should return a transient UUID without raising + uuid.UUID(deployment_id) + + def test_regenerates_on_empty_file(self): + os.makedirs(self.config_dir, exist_ok=True) + with open(self.deployment_file, 'w') as f: + f.write('') + + with patch('eva_sub_cli.call_home.DEPLOYMENT_ID_DIR', self.config_dir), \ + patch('eva_sub_cli.call_home.DEPLOYMENT_ID_FILE', self.deployment_file): + deployment_id = _get_or_create_deployment_id() + uuid.UUID(deployment_id) + with open(self.deployment_file) as f: + self.assertEqual(f.read().strip(), deployment_id) + + +class TestRunId(TestCase): + + resources_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources') + + def setUp(self): + self.submission_dir = os.path.join(self.resources_dir, 'submission_dir') + os.makedirs(self.submission_dir, exist_ok=True) + + def tearDown(self): + if os.path.exists(self.submission_dir): + shutil.rmtree(self.submission_dir) + + def test_creates_new_id_when_config_has_no_run_id(self): + run_id = _get_or_create_run_id(self.submission_dir) + uuid.UUID(run_id) + # Verify it was written to config + config_path = os.path.join(self.submission_dir, SUB_CLI_CONFIG_FILE) + self.assertTrue(os.path.isfile(config_path)) + config = Configuration(config_path) + self.assertEqual(config.query('run_id'), run_id) + + def test_reads_existing_id_from_config(self): + existing_id = str(uuid.uuid4()) + # Create config with an existing run_id + config_path = os.path.join(self.submission_dir, SUB_CLI_CONFIG_FILE) + config = WritableConfig(config_path) + config.set('run_id', value=existing_id) + config.write() + + run_id = _get_or_create_run_id(self.submission_dir) + self.assertEqual(run_id, existing_id) + + def test_handles_unreadable_config(self): + with patch('eva_sub_cli.call_home.WritableConfig', side_effect=Exception('Cannot read config')): + run_id = _get_or_create_run_id(self.submission_dir) + uuid.UUID(run_id) + + +class TestCallHomeClient(TestCase): + + resources_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources') + + def setUp(self): + self.submission_dir = os.path.join(self.resources_dir, 'submission_dir') + os.makedirs(self.submission_dir, exist_ok=True) + + def tearDown(self): + if os.path.exists(self.submission_dir): + shutil.rmtree(self.submission_dir) + + @patch('eva_sub_cli.call_home._get_or_create_run_id', return_value='test-run-id') + @patch('eva_sub_cli.call_home._get_or_create_deployment_id', return_value='test-deployment-id') + def _create_client(self, mock_dep_id, mock_run_id): + return CallHomeClient( + submission_dir=self.submission_dir, + executor='native', + tasks=['validate', 'submit'] + ) + + def test_build_payload(self): + client = self._create_client() + payload = client._build_payload(EVENT_START) + self.assertEqual(payload['runtimeSeconds'], 0) + self.assertEqual(payload['eventType'], EVENT_START) + self.assertEqual(payload['deploymentId'], 'test-deployment-id') + self.assertEqual(payload['runId'], 'test-run-id') + self.assertEqual(payload['executor'], 'native') + self.assertEqual(payload['tasks'], ['validate', 'submit']) + self.assertIn('cliVersion', payload) + self.assertIn('createdAt', payload) + + def test_non_zero_runtime_on_non_start(self): + client = self._create_client() + time.sleep(1) + payload = client._build_payload(EVENT_END) + self.assertNotEqual(payload['runtimeSeconds'], 0) + self.assertEqual(payload['eventType'], EVENT_END) + + @patch('eva_sub_cli.call_home.requests.post', side_effect=ConnectionError('connection refused')) + def test_connection_error_swallowed(self, mock_post): + client = self._create_client() + # Should not raise + client.send_start() + + @patch('eva_sub_cli.call_home.requests.post', side_effect=Timeout('timed out')) + def test_timeout_swallowed(self, mock_post): + client = self._create_client() + client.send_end() + + @patch('eva_sub_cli.call_home.requests.post', side_effect=Exception('unexpected error')) + def test_generic_exception_swallowed(self, mock_post): + client = self._create_client() + client.send_failure() + + @patch('eva_sub_cli.call_home.requests.post') + def test_send_all_event_types(self, mock_post): + client = self._create_client() + client.send_start() + client.send_validation_completed('PASS') + client.send_end() + self.assertEqual(mock_post.call_count, 3) + + @patch('eva_sub_cli.call_home.requests.post') + def test_send_failure_event(self, mock_post): + client = self._create_client() + client.send_failure() + payload = mock_post.call_args.kwargs['json'] + self.assertEqual(payload['eventType'], EVENT_FAILURE) + self.assertNotIn('exceptionMessage', payload) + self.assertNotIn('exceptionStacktrace', payload) + + @patch('eva_sub_cli.call_home.requests.post') + def test_send_failure_with_exception(self, mock_post): + client = self._create_client() + try: + raise ValueError('something went wrong') + except ValueError as e: + client.send_failure(exception=e) + payload = mock_post.call_args.kwargs['json'] + self.assertEqual(payload['eventType'], EVENT_FAILURE) + self.assertEqual(payload['exceptionMessage'], 'something went wrong') + self.assertIn('ValueError', payload['exceptionStacktrace']) + self.assertIn('something went wrong', payload['exceptionStacktrace']) diff --git a/tests/test_cli.py b/tests/test_cli.py index 674a7db..ee9322d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -36,7 +36,8 @@ def test_main(self): vcf_files=[], reference_fasta='', metadata_json=None, metadata_xlsx='', tasks='validate', executor='native', debug=False) with patch('eva_sub_cli.executables.cli.parse_args', return_value=args), \ - patch('eva_sub_cli.orchestrator.orchestrate_process'): + patch('eva_sub_cli.orchestrator.orchestrate_process'), \ + patch('eva_sub_cli.executables.cli.CallHomeClient'): exit_status = cli.main() # Check that the debug message is shown logger = orchestrator.logger @@ -61,8 +62,9 @@ def test_validate_args(self): args = cli.parse_args(cmd_args) assert args.submission_dir == self.submission_dir - with patch('sys.exit') as m_exit: - cli.parse_args(cmd_args[:2]+cmd_args[4:]) + with patch('sys.exit', side_effect=SystemExit) as m_exit: + with self.assertRaises(SystemExit): + cli.parse_args(cmd_args[:2]+cmd_args[4:]) m_exit.assert_called_once_with(2) def test_main_exception_handling(self): @@ -94,6 +96,7 @@ def test_main_exception_handling(self): with patch('eva_sub_cli.executables.cli.parse_args', return_value=args), \ patch('eva_sub_cli.executables.cli.orchestrator.orchestrate_process', side_effect=exception), \ + patch('eva_sub_cli.executables.cli.CallHomeClient'), \ patch('builtins.print') as mock_print: exit_status = cli.main() @@ -107,3 +110,48 @@ def test_main_exception_handling(self): if (isinstance(exception, HTTPError)): self.assertIn(exception.response.text, printed_texts) + + def test_main_sends_start_and_end_on_success(self): + args = Mock(submission_dir=self.submission_dir, + vcf_files=[], reference_fasta='', metadata_json=None, metadata_xlsx='', + tasks=['validate'], executor='native', debug=False) + with patch('eva_sub_cli.executables.cli.parse_args', return_value=args), \ + patch('eva_sub_cli.orchestrator.orchestrate_process'), \ + patch('eva_sub_cli.executables.cli.CallHomeClient') as MockCallHome: + mock_client = MockCallHome.return_value + exit_status = cli.main() + self.assertEqual(exit_status, 0) + mock_client.send_start.assert_called_once() + mock_client.send_end.assert_called_once() + mock_client.send_failure.assert_not_called() + + def test_main_sends_start_and_failure_on_exception(self): + args = Mock(submission_dir=self.submission_dir, + vcf_files=[], reference_fasta='', metadata_json=None, metadata_xlsx='', + tasks=['submit'], executor='native', debug=False) + exception = Exception('boom') + with patch('eva_sub_cli.executables.cli.parse_args', return_value=args), \ + patch('eva_sub_cli.executables.cli.orchestrator.orchestrate_process', + side_effect=exception), \ + patch('eva_sub_cli.executables.cli.CallHomeClient') as MockCallHome, \ + patch('builtins.print'): + mock_client = MockCallHome.return_value + exit_status = cli.main() + self.assertNotEqual(exit_status, 0) + mock_client.send_start.assert_called_once() + mock_client.send_failure.assert_called_once() + failure_arg = mock_client.send_failure.call_args[0][0] + self.assertIsInstance(failure_arg, Exception) + self.assertEqual(str(failure_arg), 'boom') + mock_client.send_end.assert_not_called() + + def test_main_unaffected_by_call_home_init_failure(self): + args = Mock(submission_dir=self.submission_dir, + vcf_files=[], reference_fasta='', metadata_json=None, metadata_xlsx='', + tasks=['validate'], executor='native', debug=False) + with patch('eva_sub_cli.executables.cli.parse_args', return_value=args), \ + patch('eva_sub_cli.orchestrator.orchestrate_process'), \ + patch('eva_sub_cli.executables.cli.CallHomeClient', + side_effect=Exception('call home init failed')): + exit_status = cli.main() + self.assertEqual(exit_status, 0) diff --git a/tests/test_submit.py b/tests/test_submit.py index 46c20d9..808b9a4 100644 --- a/tests/test_submit.py +++ b/tests/test_submit.py @@ -9,7 +9,7 @@ from eva_sub_cli import SUB_CLI_CONFIG_FILE from eva_sub_cli.file_utils import is_submission_dir_writable -from eva_sub_cli.submission_ws import SubmissionWSClient +from eva_sub_cli.submission_ws import SubmissionWSClient, DEFAULT_SUBMISSION_WS_URL from eva_sub_cli.validators.validator import READY_FOR_SUBMISSION_TO_EVA from eva_sub_cli.submit import StudySubmitter, SUB_CLI_CONFIG_KEY_SUBMISSION_ID, \ SUB_CLI_CONFIG_KEY_SUBMISSION_UPLOAD_URL @@ -58,11 +58,11 @@ def test_submit(self): self.submitter.submit() mock_post.assert_called_once_with( - os.path.join(test_submission_ws_client.SUBMISSION_WS_URL, 'submission/initiate'), + os.path.join(DEFAULT_SUBMISSION_WS_URL, 'submission/initiate'), headers={'Accept': 'application/json', 'Authorization': 'Bearer a token'}) mock_put.assert_called_once_with( - os.path.join(test_submission_ws_client.SUBMISSION_WS_URL, 'submission/mock_submission_id/uploaded'), + os.path.join(DEFAULT_SUBMISSION_WS_URL, 'submission/mock_submission_id/uploaded'), headers={'Accept': 'application/json', 'Authorization': 'Bearer a token'}, json=self.metadata_json)