def _get_fake_responses( self, url: str ) -> Tuple[Optional[Dict[str, Any]], Optional[Dict[str, str]]]: """Return response and headers if faked for this URL in uaclient.conf. :return: A tuple of response and header dicts if the URL has an overlay response defined. Return (None, {}) otherwise. :raises exceptions.URLError: When faked response "code" is != 200. URLError reason will be "response" value and any optional "headers" provided. """ responses = self._get_response_overlay(url) if not responses: return None, {} if len(responses) == 1: # When only one respose is defined, repeat it for all calls response = responses[0] else: # When multiple responses defined pop the first one off the list. response = responses.pop(0) if response["code"] == 200: return response["response"], response.get("headers", {}) # Must be an error e = error.URLError(response["response"]) raise exceptions.UrlError( e, code=response["code"], headers=response.get("headers", {}), url=url, )
def test_url_error_handled_gracefully( self, m_get_parser, _m_setup_logging, error_url, expected_log, capsys, logging_sandbox, caplog_text, ): m_args = m_get_parser.return_value.parse_args.return_value m_args.action.side_effect = exceptions.UrlError(socket.gaierror( -2, "Name or service not known"), url=error_url) with pytest.raises(SystemExit) as excinfo: main(["some", "args"]) exc = excinfo.value assert 1 == exc.code out, err = capsys.readouterr() assert "" == out assert "{}\n".format(messages.CONNECTIVITY_ERROR.msg) == err error_log = caplog_text() assert expected_log in error_log assert "Traceback (most recent call last):" in error_log
def test_request_resources_error_on_network_disconnected( self, m_request_resources, FakeConfig): """Raise error get_available_resources can't contact backend""" cfg = FakeConfig() urlerror = exceptions.UrlError( socket.gaierror(-2, "Name or service not known")) m_request_resources.side_effect = urlerror with pytest.raises(exceptions.UrlError) as exc: get_available_resources(cfg) assert urlerror == exc.value
def request_url( self, path, data=None, headers=None, method=None, query_params=None, potentially_sensitive: bool = True, ): path = path.lstrip("/") if not headers: headers = self.headers() if headers.get("content-type") == "application/json" and data: data = json.dumps(data).encode("utf-8") url = urljoin(getattr(self.cfg, self.cfg_url_base_attr), path) fake_response, fake_headers = self._get_fake_responses(url) if fake_response: return fake_response, fake_headers # URL faked by uaclient.conf if query_params: # filter out None values filtered_params = { k: v for k, v in sorted(query_params.items()) if v is not None } url += "?" + urlencode(filtered_params) try: response, headers = util.readurl( url=url, data=data, headers=headers, method=method, timeout=self.url_timeout, potentially_sensitive=potentially_sensitive, ) except error.URLError as e: body = None if hasattr(e, "body"): body = e.body # type: ignore elif hasattr(e, "read"): body = e.read().decode("utf-8") # type: ignore if body: try: error_details = json.loads( body, cls=util.DatetimeAwareJSONDecoder) except ValueError: error_details = None if error_details: raise self.api_error_cls(e, error_details) raise exceptions.UrlError(e, code=getattr(e, "code", None), headers=headers, url=url) return response, headers
class TestAttachWithToken: @pytest.mark.parametrize( "request_updated_contract_side_effect, expected_error_class," " expect_status_call", [ (None, None, False), (exceptions.UrlError("cause"), exceptions.UrlError, True), ( exceptions.UserFacingError("test"), exceptions.UserFacingError, True, ), ], ) @mock.patch(M_PATH + "identity.get_instance_id", return_value="my-iid") @mock.patch("uaclient.jobs.update_messaging.update_apt_and_motd_messages") @mock.patch("uaclient.status.status") @mock.patch(M_PATH + "contract.request_updated_contract") @mock.patch(M_PATH + "config.UAConfig.write_cache") def test_attach_with_token( self, m_write_cache, m_request_updated_contract, m_status, m_update_apt_and_motd_msgs, _m_get_instance_id, request_updated_contract_side_effect, expected_error_class, expect_status_call, FakeConfig, ): cfg = FakeConfig() m_request_updated_contract.side_effect = ( request_updated_contract_side_effect ) if expected_error_class: with pytest.raises(expected_error_class): attach_with_token(cfg, "token", False) else: attach_with_token(cfg, "token", False) if expect_status_call: assert [mock.call(cfg=cfg)] == m_status.call_args_list if not expect_status_call: assert [ mock.call("instance-id", "my-iid") ] == m_write_cache.call_args_list assert [mock.call(cfg)] == m_update_apt_and_motd_msgs.call_args_list
def fake_contract_client(cfg): fake_client = FakeContractClient(cfg) fake_client._responses = { API_V1_CONTEXT_MACHINE_TOKEN: exceptions.ContractAPIError( exceptions.UrlError( "Server error", code=error_code, url="http://me", headers={}, ), error_response=json.loads( error_response, cls=util.DatetimeAwareJSONDecoder), ) } return fake_client
def test_error_on_connectivity_errors( self, _m_getuid, _m_get_contract_information, m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, FakeConfig, ): """Raise UrlError on connectivity issues""" m_get_avail_resources.side_effect = exceptions.UrlError( socket.gaierror(-2, "Name or service not known")) cfg = FakeConfig() with pytest.raises(exceptions.UrlError): action_status(mock.MagicMock(all=False, simulate_with_token=None), cfg=cfg)
def test_handles_4XX_contract_errors( self, _m_get_instance_id, m_request_auto_attach_contract_token, http_msg, http_code, http_response, FakeConfig, ): """VMs running on non-auto-attach images do not return a token.""" cfg = FakeConfig() m_request_auto_attach_contract_token.side_effect = ContractAPIError( exceptions.UrlError( http_msg, code=http_code, url="http://me", headers={} ), error_response=http_response, ) with pytest.raises(NonAutoAttachImageError) as excinfo: auto_attach(cfg, fake_instance_factory()) assert messages.UNSUPPORTED_AUTO_ATTACH == str(excinfo.value)
def test_raise_unexpected_errors( self, _m_get_instance_id, m_request_auto_attach_contract_token, FakeConfig, ): """Any unexpected errors will be raised.""" cfg = FakeConfig() unexpected_error = ContractAPIError( exceptions.UrlError( "Server error", code=500, url="http://me", headers={} ), error_response={"message": "something unexpected"}, ) m_request_auto_attach_contract_token.side_effect = unexpected_error with pytest.raises(ContractAPIError) as excinfo: auto_attach(cfg, fake_instance_factory()) assert unexpected_error == excinfo.value
def test_refresh_contract_error_on_failure_to_update_contract( self, m_remove_notice, request_updated_contract, logging_error, getuid, FakeConfig, ): """On failure in request_updates_contract emit an error.""" request_updated_contract.side_effect = exceptions.UrlError( mock.MagicMock() ) cfg = FakeConfig.for_attached_machine() with pytest.raises(exceptions.UserFacingError) as excinfo: action_refresh(mock.MagicMock(target="contract"), cfg=cfg) assert messages.REFRESH_CONTRACT_FAILURE == excinfo.value.msg assert [ mock.call("", messages.NOTICE_REFRESH_CONTRACT_WARNING) ] != m_remove_notice.call_args_list
class TestActionStatus: def test_status_help( self, _m_getuid, _m_get_contract_information, _m_get_available_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, capsys, ): with pytest.raises(SystemExit): with mock.patch("sys.argv", ["/usr/bin/ua", "status", "--help"]): main() out, _err = capsys.readouterr() assert HELP_OUTPUT == out @pytest.mark.parametrize("use_all", (True, False)) @pytest.mark.parametrize( "notices,notice_status", ( ([], ""), ( [["a", "adesc"], ["b2", "bdesc"]], "\nNOTICES\n a: adesc\nb2: bdesc\n", ), ), ) def test_attached( self, _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, notices, notice_status, use_all, capsys, FakeConfig, ): """Check that root and non-root will emit attached status""" cfg = FakeConfig.for_attached_machine() cfg.write_cache("notices", notices) assert 0 == action_status(mock.MagicMock(all=use_all, simulate_with_token=None), cfg=cfg) # capsys already converts colorized non-printable chars to space # Strip non-printables from output printable_stdout = capsys.readouterr()[0].replace(" " * 17, " " * 8) # On older versions of pytest, capsys doesn't set sys.stdout.encoding # to something that Python parses as UTF-8 compatible, so we get the # ASCII dash; testing for the "wrong" dash here is OK, because we have # a specific test that the correct one is used in # test_unicode_dash_replacement_when_unprintable expected_dash = "-" status_tmpl = ATTACHED_STATUS if use_all else ATTACHED_STATUS_NOBETA if sys.stdout.encoding and "UTF-8" in sys.stdout.encoding.upper(): expected_dash = "\u2014" assert (status_tmpl.format(dash=expected_dash, notices=notice_status) == printable_stdout) def test_unattached( self, _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, capsys, FakeConfig, ): """Check that unattached status is emitted to console""" cfg = FakeConfig() assert 0 == action_status(mock.MagicMock(all=False, simulate_with_token=None), cfg=cfg) assert UNATTACHED_STATUS == capsys.readouterr()[0] def test_simulated( self, _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, capsys, FakeConfig, ): """Check that a simulated status is emitted to console""" cfg = FakeConfig() assert 0 == action_status( mock.MagicMock(all=False, simulate_with_token="some_token"), cfg=cfg, ) assert SIMULATED_STATUS == capsys.readouterr()[0] @mock.patch("uaclient.version.get_version", return_value="test_version") @mock.patch("uaclient.util.subp") @mock.patch(M_PATH + "time.sleep") def test_wait_blocks_until_lock_released( self, m_sleep, _m_subp, _m_get_version, _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, capsys, FakeConfig, ): """Check that --wait will will block and poll until lock released.""" cfg = FakeConfig() lock_file = cfg.data_path("lock") cfg.write_cache("lock", "123:ua auto-attach") def fake_sleep(seconds): if m_sleep.call_count == 3: os.unlink(lock_file) m_sleep.side_effect = fake_sleep assert 0 == action_status(mock.MagicMock(all=False, simulate_with_token=None), cfg=cfg) assert [mock.call(1)] * 3 == m_sleep.call_args_list assert "...\n" + UNATTACHED_STATUS == capsys.readouterr()[0] @pytest.mark.parametrize( "format_type,event_logger_mode", (("json", EventLoggerMode.JSON), ("yaml", EventLoggerMode.YAML)), ) @pytest.mark.parametrize( "environ", ( {}, { "UA_DATA_DIR": "data_dir", "UA_TEST": "test", "UA_FEATURES_ALLOW_BETA": "true", "UA_CONFIG_FILE": "config_file", }, ), ) @pytest.mark.parametrize("use_all", (True, False)) def test_unattached_formats( self, _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, use_all, environ, format_type, event_logger_mode, capsys, FakeConfig, event, ): """Check that unattached status json output is emitted to console""" cfg = FakeConfig() args = mock.MagicMock(format=format_type, all=use_all, simulate_with_token=None) with mock.patch.object(os, "environ", environ): with mock.patch.object(event, "_event_logger_mode", event_logger_mode), mock.patch.object( event, "_command", "status"): assert 0 == action_status(args, cfg=cfg) expected_environment = [] if environ: expected_environment = [ { "name": "UA_CONFIG_FILE", "value": "config_file" }, { "name": "UA_DATA_DIR", "value": "data_dir" }, { "name": "UA_FEATURES_ALLOW_BETA", "value": "true" }, ] expected_services = [ { "name": "esm-apps", "description": "UA Apps: Extended Security Maintenance (ESM)", "available": "yes", }, { "name": "livepatch", "description": "Canonical Livepatch service", "available": "yes", }, ] if not use_all: expected_services.pop(0) expected = { "_doc": ("Content provided in json response is currently " "considered Experimental and may change"), "_schema_version": "0.1", "version": version.get_version(features=cfg.features), "execution_status": status.UserFacingConfigStatus.INACTIVE.value, "execution_details": messages.NO_ACTIVE_OPERATIONS, "attached": False, "machine_id": None, "effective": None, "expires": None, "notices": [], "services": expected_services, "environment_vars": expected_environment, "contract": { "id": "", "name": "", "created_at": "", "products": [], "tech_support_level": "n/a", }, "account": { "name": "", "id": "", "created_at": "", "external_account_ids": [], }, "config_path": None, "config": { "data_dir": mock.ANY }, "simulated": False, "errors": [], "warnings": [], "result": "success", } if format_type == "json": assert expected == json.loads(capsys.readouterr()[0]) else: assert expected == yaml.safe_load(capsys.readouterr()[0]) @pytest.mark.parametrize( "format_type,event_logger_mode", (("json", EventLoggerMode.JSON), ("yaml", EventLoggerMode.YAML)), ) @pytest.mark.parametrize( "environ", ( {}, { "UA_DATA_DIR": "data_dir", "UA_TEST": "test", "UA_FEATURES_ALLOW_BETA": "true", "UA_CONFIG_FILE": "config_file", }, ), ) @pytest.mark.parametrize("use_all", (True, False)) def test_attached_formats( self, _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, use_all, environ, format_type, event_logger_mode, capsys, FakeConfig, event, ): """Check that unattached status json output is emitted to console""" cfg = FakeConfig.for_attached_machine() args = mock.MagicMock(format=format_type, all=use_all, simulate_with_token=None) with mock.patch.object(os, "environ", environ): with mock.patch.object(event, "_event_logger_mode", event_logger_mode), mock.patch.object( event, "_command", "status"): assert 0 == action_status(args, cfg=cfg) expected_environment = [] if environ: expected_environment = [ { "name": "UA_CONFIG_FILE", "value": "config_file" }, { "name": "UA_DATA_DIR", "value": "data_dir" }, { "name": "UA_FEATURES_ALLOW_BETA", "value": "true" }, ] if use_all: services = SERVICES_JSON_ALL else: services = [ svc for svc in SERVICES_JSON_ALL if svc["name"] not in BETA_SVC_NAMES ] inapplicable_services = [ service["name"] for service in RESPONSE_AVAILABLE_SERVICES if not service["available"] ] filtered_services = [ service for service in services if service["name"] not in inapplicable_services ] if format_type == "json": contract_created_at = "2020-05-08T19:02:26+00:00" account_created_at = "2019-06-14T06:45:50+00:00" expires = "2040-05-08T19:02:26+00:00" effective = "2000-05-08T19:02:26+00:00" else: contract_created_at = datetime.datetime( 2020, 5, 8, 19, 2, 26, tzinfo=datetime.timezone.utc) account_created_at = datetime.datetime( 2019, 6, 14, 6, 45, 50, tzinfo=datetime.timezone.utc) expires = datetime.datetime(2040, 5, 8, 19, 2, 26, tzinfo=datetime.timezone.utc) effective = datetime.datetime(2000, 5, 8, 19, 2, 26, tzinfo=datetime.timezone.utc) tech_support_level = status.UserFacingStatus.INAPPLICABLE.value expected = { "_doc": ("Content provided in json response is currently " "considered Experimental and may change"), "_schema_version": "0.1", "version": version.get_version(features=cfg.features), "execution_status": status.UserFacingConfigStatus.INACTIVE.value, "execution_details": messages.NO_ACTIVE_OPERATIONS, "attached": True, "machine_id": "test_machine_id", "effective": effective, "expires": expires, "notices": [], "services": filtered_services, "environment_vars": expected_environment, "contract": { "id": "cid", "name": "test_contract", "created_at": contract_created_at, "products": ["free"], "tech_support_level": tech_support_level, }, "account": { "id": "acct-1", "name": "test_account", "created_at": account_created_at, "external_account_ids": [{ "IDs": ["id1"], "Origin": "AWS" }], }, "config_path": None, "config": { "data_dir": mock.ANY }, "simulated": False, "errors": [], "warnings": [], "result": "success", } if format_type == "json": assert expected == json.loads(capsys.readouterr()[0]) else: yaml_output = yaml.safe_load(capsys.readouterr()[0]) # On earlier versions of pyyaml, we don't add the timezone # info when converting a date string into a datetime object. # Since we only want to test if we are producing a valid # yaml file in the status output, we can manually add # the timezone info to make the test work as expected for key, value in yaml_output.items(): if isinstance(value, datetime.datetime): yaml_output[key] = value.replace( tzinfo=datetime.timezone.utc) elif isinstance(value, dict): for inner_key, inner_value in value.items(): if isinstance(inner_value, datetime.datetime): yaml_output[key][inner_key] = inner_value.replace( tzinfo=datetime.timezone.utc) assert expected == yaml_output @pytest.mark.parametrize( "format_type,event_logger_mode", (("json", EventLoggerMode.JSON), ("yaml", EventLoggerMode.YAML)), ) @pytest.mark.parametrize("use_all", (True, False)) def test_simulated_formats( self, _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, use_all, format_type, event_logger_mode, capsys, FakeConfig, event, ): """Check that simulated status json output is emitted to console""" cfg = FakeConfig() args = mock.MagicMock(format=format_type, all=use_all, simulate_with_token="some_token") with mock.patch.object(event, "_event_logger_mode", event_logger_mode), mock.patch.object( event, "_command", "status"): assert 0 == action_status(args, cfg=cfg) expected_services = [ { "auto_enabled": "yes", "available": "yes", "description": "UA Apps: Extended Security Maintenance (ESM)", "entitled": "no", "name": "esm-apps", }, { "auto_enabled": "yes", "available": "no", "description": "UA Infra: Extended Security Maintenance (ESM)", "entitled": "yes", "name": "esm-infra", }, { "auto_enabled": "no", "available": "no", "description": "NIST-certified core packages", "entitled": "no", "name": "fips", }, { "auto_enabled": "no", "available": "no", "description": "NIST-certified core packages with priority" " security updates", "entitled": "no", "name": "fips-updates", }, { "auto_enabled": "no", "available": "yes", "description": "Canonical Livepatch service", "entitled": "yes", "name": "livepatch", }, { "auto_enabled": "no", "available": "no", "description": "Beta-version Ubuntu Kernel with PREEMPT_RT" " patches", "entitled": "no", "name": "realtime-kernel", }, { "auto_enabled": "no", "available": "no", "description": "Security Updates for the Robot Operating" " System", "entitled": "no", "name": "ros", }, { "auto_enabled": "no", "available": "no", "description": "All Updates for the Robot Operating System", "entitled": "no", "name": "ros-updates", }, ] if not use_all: expected_services = expected_services[1:-3] expected = { "_doc": "Content provided in json response is currently considered" " Experimental and may change", "_schema_version": "0.1", "attached": False, "machine_id": None, "notices": [], "account": { "created_at": "2019-06-14T06:45:50Z", "external_account_ids": [], "id": "some_id", "name": "Name", }, "contract": { "created_at": "2021-05-21T20:00:53Z", "id": "some_id", "name": "Name", "products": ["uai-essential-virtual"], "tech_support_level": "essential", }, "environment_vars": [], "execution_status": "inactive", "execution_details": "No Ubuntu Advantage operations are running", "expires": "9999-12-31T00:00:00Z", "effective": None, "services": expected_services, "simulated": True, "version": version.get_version(features=cfg.features), "config_path": None, "config": { "data_dir": mock.ANY }, "errors": [], "warnings": [], "result": "success", } if format_type == "json": assert expected == json.loads(capsys.readouterr()[0]) else: assert expected == yaml.safe_load(capsys.readouterr()[0]) def test_error_on_connectivity_errors( self, _m_getuid, _m_get_contract_information, m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, FakeConfig, ): """Raise UrlError on connectivity issues""" m_get_avail_resources.side_effect = exceptions.UrlError( socket.gaierror(-2, "Name or service not known")) cfg = FakeConfig() with pytest.raises(exceptions.UrlError): action_status(mock.MagicMock(all=False, simulate_with_token=None), cfg=cfg) @pytest.mark.parametrize( "encoding,expected_dash", (("utf-8", "\u2014"), ("UTF-8", "\u2014"), ("ascii", "-")), ) def test_unicode_dash_replacement_when_unprintable( self, _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, encoding, expected_dash, FakeConfig, ): # This test can't use capsys because it doesn't emulate sys.stdout # encoding accurately in older versions of pytest underlying_stdout = io.BytesIO() fake_stdout = io.TextIOWrapper(underlying_stdout, encoding=encoding) with mock.patch("sys.stdout", fake_stdout): action_status( mock.MagicMock(all=True, simulate_with_token=None), cfg=FakeConfig.for_attached_machine(), ) fake_stdout.flush() # Make sure all output is in underlying_stdout out = underlying_stdout.getvalue().decode(encoding) # Colour codes are converted to spaces, so strip them out for # comparison out = out.replace(" " * 17, " " * 8) expected_out = ATTACHED_STATUS.format(dash=expected_dash, notices="") assert expected_out == out @pytest.mark.parametrize( "exception_to_throw,exception_type,exception_message", ( ( exceptions.UrlError("Not found", 404), exceptions.UrlError, "Not found", ), ( exceptions.ContractAPIError( exceptions.UrlError("Unauthorized", 401), {"message": "unauthorized"}, ), exceptions.UserFacingError, "Invalid token. See https://ubuntu.com/advantage", ), ), ) def test_errors_are_raised_appropriately( self, _m_getuid, m_get_contract_information, _m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, exception_to_throw, exception_type, exception_message, capsys, FakeConfig, ): """Check that simulated status json/yaml output raises errors.""" m_get_contract_information.side_effect = exception_to_throw cfg = FakeConfig() args = mock.MagicMock(format="json", all=False, simulate_with_token="some_token") with pytest.raises(exception_type) as exc: action_status(args, cfg=cfg) assert exc.type == exception_type assert exception_message in getattr(exc.value, "msg", exc.value.args) @pytest.mark.parametrize( "token_to_use,warning_message,contract_field,date_value", ( ( "expired_token", 'Contract "some_id" expired on December 31, 2019', "effectiveTo", "2019-12-31T00:00:00Z", ), ( "token_not_valid_yet", 'Contract "some_id" is not effective until December 31, 9999', "effectiveFrom", "9999-12-31T00:00:00Z", ), ), ) @pytest.mark.parametrize( "format_type,event_logger_mode", (("json", EventLoggerMode.JSON), ("yaml", EventLoggerMode.YAML)), ) def test_errors_for_token_dates( self, _m_getuid, m_get_contract_information, _m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, format_type, event_logger_mode, token_to_use, warning_message, contract_field, date_value, capsys, FakeConfig, event, ): """Check errors for expired tokens, and not valid yet tokens.""" def contract_info_side_effect(cfg, token): response = copy.deepcopy(RESPONSE_CONTRACT_INFO) response["contractInfo"][contract_field] = date_value return response m_get_contract_information.side_effect = contract_info_side_effect cfg = FakeConfig() args = mock.MagicMock(format=format_type, all=False, simulate_with_token=token_to_use) with mock.patch.object(event, "_event_logger_mode", event_logger_mode), mock.patch.object( event, "_command", "status"): assert 1 == action_status(args, cfg=cfg) if format_type == "json": output = json.loads(capsys.readouterr()[0]) else: output = yaml.safe_load(capsys.readouterr()[0]) assert output["errors"][0]["message"] == warning_message @pytest.mark.parametrize( "contract_changed,is_attached", ( (False, True), (True, False), (True, True), (False, False), ), ) @mock.patch(M_PATH_UACONFIG + "add_notice") def test_is_contract_changed( self, m_add_notice, _m_getuid, _m_get_contract_information, _m_get_available_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, contract_changed, is_attached, capsys, FakeConfig, ): _m_contract_changed.return_value = contract_changed if is_attached: cfg = FakeConfig().for_attached_machine() else: cfg = FakeConfig() action_status(mock.MagicMock(all=False, simulate_with_token=None), cfg=cfg) if is_attached: if contract_changed: assert [ mock.call("", messages.NOTICE_REFRESH_CONTRACT_WARNING) ] == m_add_notice.call_args_list else: assert [ mock.call("", messages.NOTICE_REFRESH_CONTRACT_WARNING) ] not in m_add_notice.call_args_list else: assert _m_contract_changed.call_count == 0