def test_already_attached(self, _m_getuid):
        """Check that an attached machine raises AlreadyAttachedError."""
        account_name = "test_account"
        cfg = FakeConfig.for_attached_machine(account_name=account_name)

        with pytest.raises(AlreadyAttachedError):
            action_auto_attach(mock.MagicMock(), cfg)
    def test_expires_handled_appropriately(self, m_getuid,
                                           _m_get_available_resources):
        token = {
            "machineTokenInfo": {
                "accountInfo": {
                    "id": "1",
                    "name": "accountname"
                },
                "contractInfo": {
                    "name": "contractname",
                    "id": "contract-1",
                    "effectiveTo": "2020-07-18T00:00:00Z",
                    "resourceEntitlements": [],
                },
            }
        }
        cfg = FakeConfig.for_attached_machine(account_name="accountname",
                                              machine_token=token)

        # Test that root's status works as expected (including the cache write)
        m_getuid.return_value = 0
        expected_dt = datetime.datetime(2020, 7, 18, 0, 0, 0)
        assert expected_dt == cfg.status()["expires"]

        # Test that the read from the status cache work properly for non-root
        # users
        m_getuid.return_value = 1000
        assert expected_dt == cfg.status()["expires"]
Example #3
0
    def test_already_attached(self, _m_getuid, capsys):
        """Check that an already-attached machine emits message and exits 0"""
        account_name = "test_account"
        cfg = FakeConfig.for_attached_machine(account_name=account_name)

        with pytest.raises(AlreadyAttachedError):
            action_attach(mock.MagicMock(), cfg)
    def test_non_root_users_are_rejected(self, getuid):
        """Check that a UID != 0 will receive a message and exit non-zero"""
        getuid.return_value = 1

        cfg = FakeConfig.for_attached_machine()
        with pytest.raises(exceptions.NonRootUserError):
            action_refresh(mock.MagicMock(), cfg)
Example #5
0
 def test_attached_reports_contract_and_service_status(
         self, m_repo_uf_status, m_livepatch_uf_status, _m_getuid,
         entitlements):
     """When attached, return contract and service user-facing status."""
     m_repo_uf_status.return_value = (
         status.UserFacingStatus.INAPPLICABLE,
         "repo details",
     )
     m_livepatch_uf_status.return_value = (
         status.UserFacingStatus.ACTIVE,
         "livepatch details",
     )
     token = {
         "machineTokenInfo": {
             "accountInfo": {
                 "id": "1",
                 "name": "accountname"
             },
             "contractInfo": {
                 "name": "contractname",
                 "resourceEntitlements": entitlements,
             },
         }
     }
     cfg = FakeConfig.for_attached_machine(account_name="accountname",
                                           machine_token=token)
     if not entitlements:
         support_level = status.UserFacingStatus.INAPPLICABLE.value
     else:
         support_level = entitlements[0]["affordances"]["supportLevel"]
     expected = {
         "attached": True,
         "account": "accountname",
         "expires": status.UserFacingStatus.INAPPLICABLE.value,
         "origin": None,
         "subscription": "contractname",
         "techSupportLevel": support_level,
         "services": [],
     }
     for cls in ENTITLEMENT_CLASSES:
         if cls.name == "livepatch":
             expected_status = status.UserFacingStatus.ACTIVE.value
             details = "livepatch details"
         else:
             expected_status = status.UserFacingStatus.INAPPLICABLE.value
             details = "repo details"
         expected["services"].append({
             "name":
             cls.name,
             "entitled":
             status.ContractStatus.UNENTITLED.value,
             "status":
             expected_status,
             "statusDetails":
             details,
         })
     assert expected == cfg.status()
     assert len(ENTITLEMENT_CLASSES) - 1 == m_repo_uf_status.call_count
     assert 1 == m_livepatch_uf_status.call_count
    def test_non_root_users_are_rejected(self, getuid, stdout):
        """Check that a UID != 0 will receive a message and exit non-zero"""
        getuid.return_value = 1

        cfg = FakeConfig.for_attached_machine()
        ret = action_refresh(mock.MagicMock(), cfg)

        assert 1 == ret
        assert (mock.call(status.MESSAGE_NONROOT_USER)
                in stdout.write.call_args_list)
Example #7
0
    def test_when_attached(self, uid):
        @assert_not_attached
        def test_function(args, cfg):
            pass

        cfg = FakeConfig.for_attached_machine()

        with mock.patch("uaclient.cli.os.getuid", return_value=uid):
            with pytest.raises(AlreadyAttachedError):
                test_function(mock.Mock(), cfg)
    def test_refresh_contract_error_on_failure_to_update_contract(
            self, request_updated_contract, logging_error, getuid, stdout):
        """On failure in request_updates_contract emit an error."""
        request_updated_contract.return_value = False  # failure to refresh

        cfg = FakeConfig.for_attached_machine()
        ret = action_refresh(mock.MagicMock(), cfg)

        assert 1 == ret
        assert (mock.call(status.MESSAGE_REFRESH_FAILURE)
                in logging_error.call_args_list)
    def test_refresh_contract_happy_path(self, request_updated_contract,
                                         getuid, capsys):
        """On success from request_updates_contract root user can refresh."""
        request_updated_contract.return_value = True

        cfg = FakeConfig.for_attached_machine()
        ret = action_refresh(mock.MagicMock(), cfg)

        assert 0 == ret
        assert status.MESSAGE_REFRESH_SUCCESS in capsys.readouterr()[0]
        assert [mock.call(cfg)] == request_updated_contract.call_args_list
    def test_already_attached(self, _m_getuid, capsys):
        """Check that an already-attached machine emits message and exits 0"""
        account_name = "test_account"
        cfg = FakeConfig.for_attached_machine(account_name=account_name)

        ret = action_attach(mock.MagicMock(), cfg)

        assert 0 == ret
        expected_msg = "This machine is already attached to '{}'.".format(
            account_name)
        assert expected_msg in capsys.readouterr()[0]
Example #11
0
    def test_already_attached(self, stdout):
        """Check that an already-attached machine emits message and exits 0"""
        account_name = 'test_account'
        cfg = FakeConfig.for_attached_machine(account_name=account_name)

        ret = action_attach(mock.MagicMock(), cfg)

        assert 0 == ret
        expected_msg = "This machine is already attached to '{}'.".format(
            account_name)
        assert mock.call(expected_msg) in stdout.write.call_args_list
    def test_refresh_contract_error_on_failure_to_update_contract(
            self, request_updated_contract, logging_error, getuid):
        """On failure in request_updates_contract emit an error."""
        request_updated_contract.side_effect = exceptions.UserFacingError(
            "Failure to refresh")

        cfg = FakeConfig.for_attached_machine()

        with pytest.raises(exceptions.UserFacingError) as excinfo:
            action_refresh(mock.MagicMock(), cfg)

        assert "Failure to refresh" == excinfo.value.msg
    def test_invalid_service_error_message(self, m_getuid, uid,
                                           expected_error_template):
        """Check invalid service name results in custom error message."""
        m_getuid.return_value = uid

        cfg = FakeConfig.for_attached_machine()
        with pytest.raises(exceptions.UserFacingError) as err:
            args = mock.MagicMock()
            args.name = "bogus"
            action_disable(args, cfg)
        assert (expected_error_template.format(operation="disable",
                                               name="bogus") == err.value.msg)
Example #14
0
    def test_attached_config_refresh_machine_token_and_services(
            self, client, get_machine_id, process_entitlement_delta):
        """When attached, refresh machine token and entitled services.

        Processing service deltas are processed in a sorted order based on
        name to ensure operations occur the same regardless of dict ordering.
        """

        refresh_route = API_V1_TMPL_CONTEXT_MACHINE_TOKEN_REFRESH.format(
            contract='cid', machine='mid')
        access_route_ent1 = API_V1_TMPL_RESOURCE_MACHINE_ACCESS.format(
            resource='ent1', machine='mid')

        # resourceEntitlements specifically ordered reverse alphabetically
        # to ensure proper sorting for process_contract_delta calls below
        machine_token = {
            'machineToken': 'mToken',
            'machineTokenInfo': {'contractInfo': {
                'id': 'cid', 'resourceEntitlements': [
                    {'entitled': False, 'type': 'ent2'},
                    {'entitled': True, 'type': 'ent1'}]}}}

        def fake_contract_client(cfg):
            client = FakeContractClient(cfg)
            # Note ent2 access route is not called
            client._responses = {
                refresh_route: machine_token,
                access_route_ent1: {
                    'entitlement': {
                        'entitled': True, 'type': 'ent1', 'new': 'newval'}}}
            return client

        client.side_effect = fake_contract_client
        cfg = FakeConfig.for_attached_machine(machine_token=machine_token)
        assert True is request_updated_contract(cfg)
        assert machine_token == cfg.read_cache('machine-token')
        # Redact public content
        assert (
            '<REDACTED>' == cfg.read_cache(
                'public-machine-token')['machineToken'])

        # Deltas are processed in a sorted fashion so that if enableByDefault
        # is true, the order of enablement operations is the same regardless
        # of dict key ordering.
        process_calls = [
            mock.call({'entitlement': {'entitled': True, 'type': 'ent1'}},
                      {'entitlement': {'entitled': True, 'type': 'ent1',
                                       'new': 'newval'}},
                      allow_enable=False),
            mock.call({'entitlement': {'entitled': False, 'type': 'ent2'}},
                      {'entitlement': {'entitled': False, 'type': 'ent2'}},
                      allow_enable=False)]
        assert process_calls == process_entitlement_delta.call_args_list
    def test_attached_config_and_contract_token_runtime_error(self, client):
        """When attached, error if called with a contract_token."""
        def fake_contract_client(cfg):
            return FakeContractClient(cfg)

        client.side_effect = fake_contract_client
        cfg = FakeConfig.for_attached_machine()
        with pytest.raises(RuntimeError) as exc:
            request_updated_contract(cfg, contract_token="something")

        expected_msg = (
            "Got unexpected contract_token on an already attached machine")
        assert expected_msg == str(exc.value)
Example #16
0
    def test_assert_attached_root_exceptions(self, attached, uid,
                                             expected_exception):
        @assert_attached_root
        def test_function(args, cfg):
            return mock.sentinel.success

        if attached:
            cfg = FakeConfig.for_attached_machine()
        else:
            cfg = FakeConfig()

        with pytest.raises(expected_exception):
            with mock.patch("uaclient.cli.os.getuid", return_value=uid):
                test_function(mock.Mock(), cfg)
Example #17
0
    def test_assert_attached_root_happy_path(self, capsys):
        @assert_attached_root
        def test_function(args, cfg):
            return mock.sentinel.success

        cfg = FakeConfig.for_attached_machine()

        with mock.patch("uaclient.cli.os.getuid", return_value=0):
            ret = test_function(mock.Mock(), cfg)

        assert mock.sentinel.success == ret

        out, _err = capsys.readouterr()
        assert "" == out.strip()
    def test_attached_config_refresh_errors_on_expired_contract(
            self, client, get_machine_id, process_entitlement_delta):
        """Error when refreshing contract parses an expired contract token."""

        machine_token = {
            "machineToken": "mToken",
            "machineTokenInfo": {
                "contractInfo": {
                    "effectiveTo":
                    "2018-07-18T00:00:00Z",  # Expired date
                    "id":
                    "cid",
                    "resourceEntitlements": [
                        {
                            "entitled": False,
                            "type": "ent2"
                        },
                        {
                            "entitled": True,
                            "type": "ent1"
                        },
                    ],
                }
            },
        }

        def fake_contract_client(cfg):
            client = FakeContractClient(cfg)
            # Note ent2 access route is not called
            client._responses = {
                self.refresh_route: machine_token,
                self.access_route_ent1: {
                    "entitlement": {
                        "entitled": True,
                        "type": "ent1",
                        "new": "newval",
                    }
                },
            }
            return client

        client.side_effect = fake_contract_client
        cfg = FakeConfig.for_attached_machine(machine_token=machine_token)
        with pytest.raises(exceptions.UserFacingError) as exc:
            request_updated_contract(cfg)
        assert MESSAGE_CONTRACT_EXPIRED_ERROR == str(exc.value)
        assert machine_token == cfg.read_cache("machine-token")

        # No deltas are processed when contract is expired
        assert 0 == process_entitlement_delta.call_count
    def test_attached(self, m_getuid, m_get_avail_resources, capsys):
        """Check that root and non-root will emit attached status"""
        cfg = FakeConfig.for_attached_machine()
        assert 0 == action_status(mock.MagicMock(), 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 = "-"
        if sys.stdout.encoding and "UTF-8" in sys.stdout.encoding.upper():
            expected_dash = "\u2014"
        assert ATTACHED_STATUS.format(dash=expected_dash) == printable_stdout
    def test_user_facing_error_on_machine_token_refresh_failure(
            self, client, get_machine_id):
        """When attaching, error on failure to refresh the machine token."""
        def fake_contract_client(cfg):
            fake_client = FakeContractClient(cfg)
            fake_client._responses = {
                self.refresh_route:
                exceptions.UserFacingError("Machine token refresh fail"),
                self.access_route_ent1:
                exceptions.UserFacingError("Broken ent1 route"),
            }
            return fake_client

        client.side_effect = fake_contract_client
        cfg = FakeConfig.for_attached_machine()
        with pytest.raises(exceptions.UserFacingError) as exc:
            request_updated_contract(cfg)

        assert "Machine token refresh fail" == str(exc.value)
    def test_user_facing_error_on_service_token_refresh_failure(
            self, client, get_machine_id):
        """When attaching, error on any failed specific service refresh."""

        machine_token = {
            "machineToken": "mToken",
            "machineTokenInfo": {
                "contractInfo": {
                    "id":
                    "cid",
                    "resourceEntitlements": [
                        {
                            "entitled": True,
                            "type": "ent2"
                        },
                        {
                            "entitled": True,
                            "type": "ent1"
                        },
                    ],
                }
            },
        }

        def fake_contract_client(cfg):
            fake_client = FakeContractClient(cfg)
            fake_client._responses = {
                self.refresh_route:
                machine_token,
                self.access_route_ent1:
                exceptions.UserFacingError("Broken ent1 route"),
                self.access_route_ent2:
                exceptions.UserFacingError("Broken ent2 route"),
            }
            return fake_client

        client.side_effect = fake_contract_client
        cfg = FakeConfig.for_attached_machine(machine_token=machine_token)
        with pytest.raises(exceptions.UserFacingError) as exc:
            request_updated_contract(cfg)

        assert "Broken ent1 route" == str(exc.value)
Example #22
0
 def test_root_attached(self, _m_getuid):
     """Test we get the correct status dict when attached with basic conf"""
     cfg = FakeConfig.for_attached_machine()
     expected_services = [{
         "entitled": status.ContractStatus.UNENTITLED.value,
         "name": cls.name,
         "status": status.UserFacingStatus.INAPPLICABLE.value,
         "statusDetails": mock.ANY,
     } for cls in entitlements.ENTITLEMENT_CLASSES]
     expected = {
         "account": "test_account",
         "attached": True,
         "expires": status.UserFacingStatus.INAPPLICABLE.value,
         "origin": None,
         "services": expected_services,
         "subscription": "test_contract",
         "techSupportLevel": status.UserFacingStatus.INAPPLICABLE.value,
     }
     assert expected == cfg.status()
     # cfg.status() idempotent
     assert expected == cfg.status()
    def test_unicode_dash_replacement_when_unprintable(self, _m_getuid,
                                                       _m_get_avail_resources,
                                                       encoding,
                                                       expected_dash):
        # 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(), 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)
        assert expected_out == out
Example #24
0
    def test_root_followed_by_nonroot(self, m_getuid, tmpdir):
        """Ensure that non-root run after root returns data"""
        cfg = UAConfig({"data_dir": tmpdir.strpath})

        # Run as root
        m_getuid.return_value = 0
        before = copy.deepcopy(cfg.status())

        # Replicate an attach by modifying the underlying config and confirm
        # that we see different status
        other_cfg = FakeConfig.for_attached_machine()
        cfg.write_cache("accounts", {"accounts": other_cfg.accounts})
        cfg.write_cache("machine-token", other_cfg.machine_token)
        assert cfg._status() != before

        # Run as regular user and confirm that we see the result from
        # last time we called .status()
        m_getuid.return_value = 1000
        after = cfg.status()

        assert before == after
    def test_entitlements_disabled_if_can_disable_and_prompt_true(
        self, m_entitlements, m_getuid, m_prompt, prompt_response
    ):
        m_getuid.return_value = 0
        m_prompt.return_value = prompt_response

        m_entitlements.ENTITLEMENT_CLASSES = [
            entitlement_cls_mock_factory(False),
            entitlement_cls_mock_factory(True),
            entitlement_cls_mock_factory(False),
        ]

        return_code = action_detach(
            mock.MagicMock(), FakeConfig.for_attached_machine()
        )

        # Check that can_disable is called correctly
        for ent_cls in m_entitlements.ENTITLEMENT_CLASSES:
            assert [
                mock.call(silent=True)
            ] == ent_cls.return_value.can_disable.call_args_list

        # Check that disable is only called when can_disable is true
        for undisabled_cls in [
            m_entitlements.ENTITLEMENT_CLASSES[0],
            m_entitlements.ENTITLEMENT_CLASSES[2],
        ]:
            assert 0 == undisabled_cls.return_value.disable.call_count
        disabled_cls = m_entitlements.ENTITLEMENT_CLASSES[1]
        if prompt_response:
            assert [
                mock.call(silent=True)
            ] == disabled_cls.return_value.disable.call_args_list
            assert 0 == return_code
        else:
            assert 0 == disabled_cls.return_value.disable.call_count
            assert 1 == return_code
Example #26
0
    def test_attached_config_refresh_machine_token_and_services(
        self, client, get_machine_id, process_entitlement_delta
    ):
        """When attached, refresh machine token and entitled services.

        Processing service deltas are processed in a sorted order based on
        name to ensure operations occur the same regardless of dict ordering.
        """

        # resourceEntitlements specifically ordered reverse alphabetically
        # to ensure proper sorting for process_contract_delta calls below
        machine_token = {
            "machineToken": "mToken",
            "machineTokenInfo": {
                "contractInfo": {
                    "id": "cid",
                    "resourceEntitlements": [
                        {"entitled": False, "type": "ent2"},
                        {"entitled": True, "type": "ent1"},
                    ],
                }
            },
        }

        def fake_contract_client(cfg):
            client = FakeContractClient(cfg)
            # Note ent2 access route is not called
            client._responses = {
                self.refresh_route: machine_token,
                self.access_route_ent1: {
                    "entitlement": {
                        "entitled": True,
                        "type": "ent1",
                        "new": "newval",
                    }
                },
            }
            return client

        client.side_effect = fake_contract_client
        cfg = FakeConfig.for_attached_machine(machine_token=machine_token)
        assert None is request_updated_contract(cfg)
        assert machine_token == cfg.read_cache("machine-token")

        # Deltas are processed in a sorted fashion so that if enableByDefault
        # is true, the order of enablement operations is the same regardless
        # of dict key ordering.
        process_calls = [
            mock.call(
                {"entitlement": {"entitled": True, "type": "ent1"}},
                {
                    "entitlement": {
                        "entitled": True,
                        "type": "ent1",
                        "new": "newval",
                    }
                },
                allow_enable=False,
            ),
            mock.call(
                {"entitlement": {"entitled": False, "type": "ent2"}},
                {"entitlement": {"entitled": False, "type": "ent2"}},
                allow_enable=False,
            ),
        ]
        assert process_calls == process_entitlement_delta.call_args_list
    def test_root_attached(
        self,
        _m_getuid,
        m_get_avail_resources,
        avail_res,
        entitled_res,
        uf_entitled,
        uf_status,
    ):
        """Test we get the correct status dict when attached with basic conf"""
        resource_names = [resource["name"] for resource in avail_res]
        default_entitled = status.ContractStatus.UNENTITLED.value
        default_status = status.UserFacingStatus.UNAVAILABLE.value
        token = {
            "machineTokenInfo": {
                "accountInfo": {
                    "id": "acct-1",
                    "name": "test_account"
                },
                "contractInfo": {
                    "id": "cid",
                    "name": "test_contract",
                    "resourceEntitlements": entitled_res,
                },
            }
        }
        available_resource_response = [{
            "name":
            cls.name,
            "available":
            bool({
                "name": cls.name,
                "available": True
            } in avail_res),
        } for cls in entitlements.ENTITLEMENT_CLASSES]
        m_get_avail_resources.return_value = available_resource_response

        cfg = FakeConfig.for_attached_machine(machine_token=token)
        expected_services = [{
            "description":
            cls.description,
            "entitled":
            uf_entitled if cls.name in resource_names else default_entitled,
            "name":
            cls.name,
            "status":
            uf_status if cls.name in resource_names else default_status,
            "statusDetails":
            mock.ANY,
            "description_override":
            None,
        } for cls in entitlements.ENTITLEMENT_CLASSES]
        expected = copy.deepcopy(DEFAULT_STATUS)
        expected.update({
            "account-id": "acct-1",
            "account": "test_account",
            "attached": True,
            "services": expected_services,
            "subscription": "test_contract",
            "subscription-id": "cid",
        })
        assert expected == cfg.status()
        assert m_get_avail_resources.call_count == 1
        # cfg.status() idempotent
        assert expected == cfg.status()
 def test_attached_reports_contract_and_service_status(
     self,
     m_repo_contract_status,
     m_repo_uf_status,
     m_livepatch_contract_status,
     m_livepatch_uf_status,
     _m_getuid,
     entitlements,
 ):
     """When attached, return contract and service user-facing status."""
     m_repo_contract_status.return_value = status.ContractStatus.ENTITLED
     m_repo_uf_status.return_value = (
         status.UserFacingStatus.INAPPLICABLE,
         "repo details",
     )
     m_livepatch_contract_status.return_value = (
         status.ContractStatus.ENTITLED)
     m_livepatch_uf_status.return_value = (
         status.UserFacingStatus.ACTIVE,
         "livepatch details",
     )
     token = {
         "availableResources": ALL_RESOURCES_AVAILABLE,
         "machineTokenInfo": {
             "accountInfo": {
                 "id": "1",
                 "name": "accountname"
             },
             "contractInfo": {
                 "id": "contract-1",
                 "name": "contractname",
                 "resourceEntitlements": entitlements,
             },
         },
     }
     cfg = FakeConfig.for_attached_machine(account_name="accountname",
                                           machine_token=token)
     if not entitlements:
         support_level = status.UserFacingStatus.INAPPLICABLE.value
     else:
         support_level = entitlements[0]["affordances"]["supportLevel"]
     expected = copy.deepcopy(DEFAULT_STATUS)
     expected.update({
         "attached": True,
         "account": "accountname",
         "account-id": "1",
         "subscription": "contractname",
         "subscription-id": "contract-1",
         "techSupportLevel": support_level,
     })
     for cls in ENTITLEMENT_CLASSES:
         if cls.name == "livepatch":
             expected_status = status.UserFacingStatus.ACTIVE.value
             details = "livepatch details"
         else:
             expected_status = status.UserFacingStatus.INAPPLICABLE.value
             details = "repo details"
         expected["services"].append({
             "name":
             cls.name,
             "description":
             cls.description,
             "entitled":
             status.ContractStatus.ENTITLED.value,
             "status":
             expected_status,
             "statusDetails":
             details,
             "description_override":
             None,
         })
     assert expected == cfg.status()
     assert len(ENTITLEMENT_CLASSES) - 1 == m_repo_uf_status.call_count
     assert 1 == m_livepatch_uf_status.call_count