def test_root_unattached(self, _m_getuid, m_get_available_resources): """Test we get the correct status dict when unattached""" cfg = FakeConfig({}) m_get_available_resources.return_value = [ { "name": "esm-infra", "available": True }, { "name": "fips", "available": False }, ] esm_desc = ENTITLEMENT_CLASS_BY_NAME["esm-infra"].description fips_desc = ENTITLEMENT_CLASS_BY_NAME["fips"].description expected = copy.deepcopy(DEFAULT_STATUS) expected["services"] = [ { "available": "yes", "name": "esm-infra", "description": esm_desc }, { "available": "no", "name": "fips", "description": fips_desc }, ] assert expected == cfg.status()
def test_nonroot_without_cache_is_same_as_unattached_root(self, m_getuid): m_getuid.return_value = 1000 cfg = FakeConfig() nonroot_status = cfg.status() m_getuid.return_value = 0 root_unattached_status = cfg.status() assert root_unattached_status == nonroot_status
def test_root_unattached(self, _m_getuid): """Test we get the correct status dict when unattached""" cfg = FakeConfig({}) expected = { "attached": False, "expires": status.UserFacingStatus.INAPPLICABLE.value, "origin": None, "services": [], "techSupportLevel": status.UserFacingStatus.INAPPLICABLE.value, } assert expected == cfg.status()
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_nonroot_unattached_is_same_as_unattached_root( self, m_getuid, m_get_available_resources): m_get_available_resources.return_value = [{ "name": "esm-infra", "available": True }] m_getuid.return_value = 1000 cfg = FakeConfig() nonroot_status = cfg.status() m_getuid.return_value = 0 root_unattached_status = cfg.status() assert root_unattached_status == nonroot_status
def test_happy_path_with_token_arg(self, action_status, contract_machine_attach, discharge_root_macaroon): """A mock-heavy test for the happy path with the contract token arg""" # TODO: Improve this test with less general mocking and more # post-conditions token = 'contract-token' args = mock.MagicMock(token=token) cfg = FakeConfig.with_account() machine_token = { 'machineTokenInfo': { 'contractInfo': { 'name': 'mycontract', 'resourceEntitlements': [] } } } def fake_contract_attach(contract_token): cfg.write_cache('machine-token', machine_token) return machine_token contract_machine_attach.side_effect = fake_contract_attach ret = action_attach(args, cfg) assert 0 == ret assert 1 == action_status.call_count expected_calls = [mock.call(contract_token=token)] assert expected_calls == contract_machine_attach.call_args_list assert 0 == discharge_root_macaroon.call_count
def test_happy_path_without_token_arg(self, action_status, contract_client, discharge_root_macaroon, request_updated_contract): """A mock-heavy test for the happy path without an argument""" # TODO: Improve this test with less general mocking and more # post-conditions bound_macaroon = b'bound_bytes_macaroon' discharge_root_macaroon.return_value = bound_macaroon args = mock.MagicMock(token=None) cfg = FakeConfig.with_account() machine_token = { 'machineTokenInfo': { 'contractInfo': { 'name': 'mycontract', 'resourceEntitlements': [] } } } def fake_contract_updates(cfg, contract_token, allow_enable): cfg.write_cache('machine-token', machine_token) return True request_updated_contract.side_effect = fake_contract_updates ret = action_attach(args, cfg) assert 0 == ret assert 1 == action_status.call_count expected_macaroon = bound_macaroon.decode('utf-8') assert expected_macaroon == cfg._cache_contents['bound-macaroon']
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_invalid_token_user_facing_error_on_invalid_token_refresh_failure( self, client, get_machine_id ): """When attaching, invalid token errors result in proper user error.""" def fake_contract_client(cfg): fake_client = FakeContractClient(cfg) fake_client._responses = { API_V1_CONTEXT_MACHINE_TOKEN: ContractAPIError( util.UrlError( "Server error", code=500, url="http://me", headers={} ), error_response={ "message": "invalid token: checksum error" }, ) } return fake_client client.side_effect = fake_contract_client cfg = FakeConfig() with pytest.raises(exceptions.UserFacingError) as exc: request_updated_contract(cfg, contract_token="yep") assert MESSAGE_ATTACH_INVALID_TOKEN == str(exc.value)
def test_token_is_a_required_argument(self, _m_getuid): """When missing the required token argument, raise a UserFacingError""" args = mock.MagicMock() args.token = None with pytest.raises(UserFacingError) as e: action_attach(args, FakeConfig()) assert status.MESSAGE_ATTACH_REQUIRES_TOKEN == str(e.value)
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_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_non_root_users_are_rejected(getuid): """Check that a UID != 0 will receive a message and exit non-zero""" getuid.return_value = 1 cfg = FakeConfig() with pytest.raises(NonRootUserError): action_auto_attach(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_unattached_error_message(self, m_getuid, _m_prompt): """Check that root user gets unattached message.""" m_getuid.return_value = 0 cfg = FakeConfig() with pytest.raises(exceptions.UnattachedError) as err: action_detach(mock.MagicMock(), cfg) assert status.MESSAGE_UNATTACHED == err.value.msg
def test_non_aws_cloud_type_raises_error(self, m_get_cloud_type, cloud_type): """Non-aws clouds will error.""" m_get_cloud_type.return_value = cloud_type with pytest.raises(NonAutoAttachImageError) as excinfo: _get_contract_token_from_cloud_identity(FakeConfig()) assert status.MESSAGE_UNSUPPORTED_AUTO_ATTACH_CLOUD_TYPE.format( cloud_type=cloud_type) == str(excinfo.value)
def test_not_attached_errors(self, getuid, stdout): """Check that an unattached machine emits message and exits 1""" cfg = FakeConfig() ret = action_refresh(mock.MagicMock(), cfg) assert 1 == ret expected_msg = status.MESSAGE_UNATTACHED assert mock.call(expected_msg) in stdout.write.call_args_list
def test_assert_attached_when_unattached(self, uid): @assert_attached() def test_function(args, cfg): pass cfg = FakeConfig() with mock.patch("uaclient.cli.os.getuid", return_value=uid): with pytest.raises(UnattachedError): test_function(mock.Mock(), cfg)
def test_error_on_connectivity_errors(self, m_getuid, m_get_avail_resources, capsys): """Raise UrlError on connectivity issues""" m_get_avail_resources.side_effect = util.UrlError( socket.gaierror(-2, "Name or service not known")) cfg = FakeConfig() with pytest.raises(util.UrlError): action_status(mock.MagicMock(), cfg)
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_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_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_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_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_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_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_request_resources_error_on_network_disconnected( self, m_request_resources): """Raise error get_available_resources can't contact backend""" cfg = FakeConfig() urlerror = util.UrlError( socket.gaierror(-2, "Name or service not known")) m_request_resources.side_effect = urlerror with pytest.raises(util.UrlError) as exc: get_available_resources(cfg) assert urlerror == exc.value
def test_unattached_error_message(self, m_getuid, uid, expected_error_template): """Check that root user gets unattached message.""" m_getuid.return_value = uid cfg = FakeConfig() with pytest.raises(exceptions.UserFacingError) as err: args = mock.MagicMock() args.name = "esm-infra" action_disable(args, cfg) assert (expected_error_template.format( name="esm-infra") == err.value.msg)