diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index e1943aa9a..d19d2473e 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3147,7 +3147,7 @@ - [ ] update_server - [ ] update_server_engine_attributes -## organizations - 28% implemented +## organizations - 30% implemented - [ ] accept_handshake - [ ] attach_policy - [ ] cancel_handshake @@ -3176,7 +3176,7 @@ - [X] list_accounts - [X] list_accounts_for_parent - [ ] list_aws_service_access_for_organization -- [ ] list_children +- [X] list_children - [ ] list_create_account_status - [ ] list_handshakes_for_account - [ ] list_handshakes_for_organization diff --git a/README.md b/README.md index e4fcb6508..189bf2c4f 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ It gets even better! Moto isn't just for Python code and it isn't just for S3. L |------------------------------------------------------------------------------| | KMS | @mock_kms | basic endpoints done | |------------------------------------------------------------------------------| -| Organizations | @mock_organizations | some endpoints done | +| Organizations | @mock_organizations | some core endpoints done | |------------------------------------------------------------------------------| | Polly | @mock_polly | all endpoints done | |------------------------------------------------------------------------------| diff --git a/moto/organizations/models.py b/moto/organizations/models.py index 0f3c67400..91896f537 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -4,6 +4,7 @@ import datetime import re from moto.core import BaseBackend, BaseModel +from moto.core.exceptions import RESTError from moto.core.utils import unix_time from moto.organizations import utils @@ -168,13 +169,31 @@ class OrganizationsBackend(BaseBackend): self.ou.append(new_ou) return new_ou.describe() + def get_organizational_unit_by_id(self, ou_id): + ou = next((ou for ou in self.ou if ou.id == ou_id), None) + if ou is None: + raise RESTError( + 'OrganizationalUnitNotFoundException', + "You specified an organizational unit that doesn't exist." + ) + return ou + + def validate_parent_id(self, parent_id): + try: + self.get_organizational_unit_by_id(parent_id) + except RESTError as e: + raise RESTError( + 'ParentNotFoundException', + "You specified parent that doesn't exist." + ) + return parent_id + def describe_organizational_unit(self, **kwargs): - ou = [ - ou for ou in self.ou if ou.id == kwargs['OrganizationalUnitId'] - ].pop(0) + ou = self.get_organizational_unit_by_id(kwargs['OrganizationalUnitId']) return ou.describe() def list_organizational_units_for_parent(self, **kwargs): + parent_id = self.validate_parent_id(kwargs['ParentId']) return dict( OrganizationalUnits=[ { @@ -183,7 +202,7 @@ class OrganizationsBackend(BaseBackend): 'Name': ou.name, } for ou in self.ou - if ou.parent_id == kwargs['ParentId'] + if ou.parent_id == parent_id ] ) @@ -192,11 +211,20 @@ class OrganizationsBackend(BaseBackend): self.accounts.append(new_account) return new_account.create_account_status - def describe_account(self, **kwargs): - account = [ + def get_account_by_id(self, account_id): + account = next(( account for account in self.accounts - if account.id == kwargs['AccountId'] - ].pop(0) + if account.id == account_id + ), None) + if account is None: + raise RESTError( + 'AccountNotFoundException', + "You specified an account that doesn't exist." + ) + return account + + def describe_account(self, **kwargs): + account = self.get_account_by_id(kwargs['AccountId']) return account.describe() def list_accounts(self): @@ -205,35 +233,27 @@ class OrganizationsBackend(BaseBackend): ) def list_accounts_for_parent(self, **kwargs): + parent_id = self.validate_parent_id(kwargs['ParentId']) return dict( Accounts=[ account.describe()['Account'] for account in self.accounts - if account.parent_id == kwargs['ParentId'] + if account.parent_id == parent_id ] ) def move_account(self, **kwargs): - new_parent_id = kwargs['DestinationParentId'] - all_parent_id = [parent.id for parent in self.ou] - account = [ - account for account in self.accounts - if account.id == kwargs['AccountId'] - ].pop(0) - assert new_parent_id in all_parent_id - assert account.parent_id == kwargs['SourceParentId'] + new_parent_id = self.validate_parent_id(kwargs['DestinationParentId']) + self.validate_parent_id(kwargs['SourceParentId']) + account = self.get_account_by_id(kwargs['AccountId']) index = self.accounts.index(account) self.accounts[index].parent_id = new_parent_id def list_parents(self, **kwargs): if re.compile(r'[0-9]{12}').match(kwargs['ChildId']): - obj_list = self.accounts + child_object = self.get_account_by_id(kwargs['ChildId']) else: - obj_list = self.ou - parent_id = [ - obj.parent_id for obj in obj_list - if obj.id == kwargs['ChildId'] - ].pop(0) + child_object = self.get_organizational_unit_by_id(kwargs['ChildId']) return dict( Parents=[ { @@ -241,17 +261,21 @@ class OrganizationsBackend(BaseBackend): 'Type': ou.type, } for ou in self.ou - if ou.id == parent_id + if ou.id == child_object.parent_id ] ) def list_children(self, **kwargs): + parent_id = self.validate_parent_id(kwargs['ParentId']) if kwargs['ChildType'] == 'ACCOUNT': obj_list = self.accounts elif kwargs['ChildType'] == 'ORGANIZATIONAL_UNIT': obj_list = self.ou else: - raise ValueError + raise RESTError( + 'InvalidInputException', + 'You specified an invalid value.' + ) return dict( Children=[ { @@ -259,7 +283,7 @@ class OrganizationsBackend(BaseBackend): 'Type': kwargs['ChildType'], } for obj in obj_list - if obj.parent_id == kwargs['ParentId'] + if obj.parent_id == parent_id ] ) diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index c2f06774a..ae9bacd82 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -2,9 +2,11 @@ from __future__ import unicode_literals import boto3 import sure # noqa -import yaml +from botocore.exceptions import ClientError +from nose.tools import assert_raises from moto import mock_organizations +from moto.organizations import utils from .organizations_test_utils import ( validate_organization, validate_roots, @@ -67,6 +69,20 @@ def test_describe_organizational_unit(): validate_organizational_unit(org, response) +@mock_organizations +def test_describe_organizational_unit_exception(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + with assert_raises(ClientError) as e: + response = client.describe_organizational_unit( + OrganizationalUnitId=utils.make_random_root_id() + ) + ex = e.exception + ex.operation_name.should.equal('DescribeOrganizationalUnit') + ex.response['Error']['Code'].should.equal('400') + ex.response['Error']['Message'].should.contain('OrganizationalUnitNotFoundException') + + @mock_organizations def test_list_organizational_units_for_parent(): client = boto3.client('organizations', region_name='us-east-1') @@ -81,6 +97,19 @@ def test_list_organizational_units_for_parent(): validate_organizational_unit(org, dict(OrganizationalUnit=ou)) +@mock_organizations +def test_list_organizational_units_for_parent_exception(): + client = boto3.client('organizations', region_name='us-east-1') + with assert_raises(ClientError) as e: + response = client.list_organizational_units_for_parent( + ParentId=utils.make_random_root_id() + ) + ex = e.exception + ex.operation_name.should.equal('ListOrganizationalUnitsForParent') + ex.response['Error']['Code'].should.equal('400') + ex.response['Error']['Message'].should.contain('ParentNotFoundException') + + # Accounts mockname = 'mock-account' mockdomain = 'moto-example.org' @@ -111,6 +140,17 @@ def test_describe_account(): response['Account']['Email'].should.equal(mockemail) +@mock_organizations +def test_describe_account_exception(): + client = boto3.client('organizations', region_name='us-east-1') + with assert_raises(ClientError) as e: + response = client.describe_account(AccountId=utils.make_random_account_id()) + ex = e.exception + ex.operation_name.should.equal('DescribeAccount') + ex.response['Error']['Code'].should.equal('400') + ex.response['Error']['Message'].should.contain('AccountNotFoundException') + + @mock_organizations def test_list_accounts(): client = boto3.client('organizations', region_name='us-east-1') @@ -211,7 +251,7 @@ def test_list_parents_for_accounts(): @mock_organizations -def test_list_chidlren(): +def test_list_children(): client = boto3.client('organizations', region_name='us-east-1') org = client.create_organization(FeatureSet='ALL')['Organization'] root_id = client.list_roots()['Roots'][0]['Id'] @@ -244,3 +284,28 @@ def test_list_chidlren(): response03['Children'][0]['Type'].should.equal('ACCOUNT') response04['Children'][0]['Id'].should.equal(ou02_id) response04['Children'][0]['Type'].should.equal('ORGANIZATIONAL_UNIT') + + +@mock_organizations +def test_list_children_exception(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + root_id = client.list_roots()['Roots'][0]['Id'] + with assert_raises(ClientError) as e: + response = client.list_children( + ParentId=utils.make_random_root_id(), + ChildType='ACCOUNT' + ) + ex = e.exception + ex.operation_name.should.equal('ListChildren') + ex.response['Error']['Code'].should.equal('400') + ex.response['Error']['Message'].should.contain('ParentNotFoundException') + with assert_raises(ClientError) as e: + response = client.list_children( + ParentId=root_id, + ChildType='BLEE' + ) + ex = e.exception + ex.operation_name.should.equal('ListChildren') + ex.response['Error']['Code'].should.equal('400') + ex.response['Error']['Message'].should.contain('InvalidInputException')