Exemple #1
0
    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
Exemple #4
0
 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
Exemple #7
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)
 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
Exemple #11
0
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