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"]
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)
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)
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]
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)
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)
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)
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)
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
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
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