diff --git a/moto/managedblockchain/exceptions.py b/moto/managedblockchain/exceptions.py index 456eabc05..4735389ae 100644 --- a/moto/managedblockchain/exceptions.py +++ b/moto/managedblockchain/exceptions.py @@ -31,7 +31,18 @@ class ResourceNotFoundException(ManagedBlockchainClientError): self.code = 404 super(ResourceNotFoundException, self).__init__( "ResourceNotFoundException", - "An error occurred (BadRequestException) when calling the {0} operation: {1}".format( + "An error occurred (ResourceNotFoundException) when calling the {0} operation: {1}".format( + pretty_called_method, operation_error + ), + ) + + +class ResourceAlreadyExistsException(ManagedBlockchainClientError): + def __init__(self, pretty_called_method, operation_error): + self.code = 409 + super(ResourceAlreadyExistsException, self).__init__( + "ResourceAlreadyExistsException", + "An error occurred (ResourceAlreadyExistsException) when calling the {0} operation: {1}".format( pretty_called_method, operation_error ), ) diff --git a/moto/managedblockchain/models.py b/moto/managedblockchain/models.py index 034e45d35..233e875c3 100644 --- a/moto/managedblockchain/models.py +++ b/moto/managedblockchain/models.py @@ -12,6 +12,7 @@ from .exceptions import ( ResourceNotFoundException, InvalidRequestException, ResourceLimitExceededException, + ResourceAlreadyExistsException, ) from .utils import ( @@ -22,6 +23,9 @@ from .utils import ( member_name_exist_in_network, number_of_members_in_network, admin_password_ok, + get_node_id, + number_of_nodes_in_member, + nodes_in_member, ) FRAMEWORKS = [ @@ -212,6 +216,10 @@ class ManagedBlockchainProposal(BaseModel): return self.actions["Removals"] return default_return + def check_to_expire_proposal(self): + if datetime.datetime.utcnow() > self.expirtationdate: + self.status = "EXPIRED" + def to_dict(self): # Format for list_proposals d = { @@ -244,10 +252,6 @@ class ManagedBlockchainProposal(BaseModel): return d def set_vote(self, votermemberid, votermembername, vote): - if datetime.datetime.utcnow() > self.expirtationdate: - self.status = "EXPIRED" - return False - if vote.upper() == "YES": self.yes_vote_count += 1 else: @@ -273,7 +277,14 @@ class ManagedBlockchainProposal(BaseModel): elif perct_no > self.network_threshold: self.status = "REJECTED" - return True + # It is a tie - reject + if ( + self.status == "IN_PROGRESS" + and self.network_threshold_comp == "GREATER_THAN" + and self.outstanding_vote_count == 0 + and perct_yes == perct_no + ): + self.status = "REJECTED" class ManagedBlockchainInvitation(BaseModel): @@ -413,12 +424,92 @@ class ManagedBlockchainMember(BaseModel): ] = logpublishingconfiguration +class ManagedBlockchainNode(BaseModel): + def __init__( + self, + id, + networkid, + memberid, + availabilityzone, + instancetype, + logpublishingconfiguration, + region, + ): + self.creationdate = datetime.datetime.utcnow() + self.id = id + self.instancetype = instancetype + self.networkid = networkid + self.memberid = memberid + self.logpublishingconfiguration = logpublishingconfiguration + self.region = region + self.status = "AVAILABLE" + self.availabilityzone = availabilityzone + + @property + def member_id(self): + return self.memberid + + @property + def node_status(self): + return self.status + + def to_dict(self): + # Format for list_nodes + d = { + "Id": self.id, + "Status": self.status, + "CreationDate": self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"), + "AvailabilityZone": self.availabilityzone, + "InstanceType": self.instancetype, + } + return d + + def get_format(self): + # Format for get_node + frameworkattributes = { + "Fabric": { + "PeerEndpoint": "{0}.{1}.{2}.managedblockchain.{3}.amazonaws.com:30003".format( + self.id.lower(), + self.networkid.lower(), + self.memberid.lower(), + self.region, + ), + "PeerEventEndpoint": "{0}.{1}.{2}.managedblockchain.{3}.amazonaws.com:30004".format( + self.id.lower(), + self.networkid.lower(), + self.memberid.lower(), + self.region, + ), + } + } + + d = { + "NetworkId": self.networkid, + "MemberId": self.memberid, + "Id": self.id, + "InstanceType": self.instancetype, + "AvailabilityZone": self.availabilityzone, + "FrameworkAttributes": frameworkattributes, + "LogPublishingConfiguration": self.logpublishingconfiguration, + "Status": self.status, + "CreationDate": self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"), + } + return d + + def delete(self): + self.status = "DELETED" + + def update(self, logpublishingconfiguration): + self.logpublishingconfiguration = logpublishingconfiguration + + class ManagedBlockchainBackend(BaseBackend): def __init__(self, region_name): self.networks = {} self.members = {} self.proposals = {} self.invitations = {} + self.nodes = {} self.region_name = region_name def reset(self): @@ -453,10 +544,10 @@ class ManagedBlockchainBackend(BaseBackend): if frameworkconfiguration["Fabric"]["Edition"] not in EDITIONS: raise BadRequestException("CreateNetwork", "Invalid request body") - ## Generate network ID + # Generate network ID network_id = get_network_id() - ## Generate memberid ID and initial member + # Generate memberid ID and initial member member_id = get_member_id() self.members[member_id] = ManagedBlockchainMember( id=member_id, @@ -524,7 +615,7 @@ class ManagedBlockchainBackend(BaseBackend): "Member ID format specified in proposal is not valid.", ) - ## Generate proposal ID + # Generate proposal ID proposal_id = get_proposal_id() self.proposals[proposal_id] = ManagedBlockchainProposal( @@ -558,6 +649,8 @@ class ManagedBlockchainBackend(BaseBackend): proposalsfornetwork = [] for proposal_id in self.proposals: if self.proposals.get(proposal_id).network_id == networkid: + # See if any are expired + self.proposals.get(proposal_id).check_to_expire_proposal() proposalsfornetwork.append(self.proposals[proposal_id]) return proposalsfornetwork @@ -572,6 +665,9 @@ class ManagedBlockchainBackend(BaseBackend): raise ResourceNotFoundException( "GetProposal", "Proposal {0} not found.".format(proposalid) ) + + # See if it needs to be set to expipred + self.proposals.get(proposalid).check_to_expire_proposal() return self.proposals.get(proposalid) def vote_on_proposal(self, networkid, proposalid, votermemberid, vote): @@ -594,43 +690,65 @@ class ManagedBlockchainBackend(BaseBackend): if vote.upper() not in VOTEVALUES: raise BadRequestException("VoteOnProposal", "Invalid request body") + # See if it needs to be set to expipred + self.proposals.get(proposalid).check_to_expire_proposal() + + # Exception if EXPIRED + if self.proposals.get(proposalid).proposal_status == "EXPIRED": + raise InvalidRequestException( + "VoteOnProposal", + "Proposal {0} is expired and you cannot vote on it.".format(proposalid), + ) + + # Check if IN_PROGRESS + if self.proposals.get(proposalid).proposal_status != "IN_PROGRESS": + raise InvalidRequestException( + "VoteOnProposal", + "Proposal {0} has status {1} and you cannot vote on it.".format( + proposalid, self.proposals.get(proposalid).proposal_status + ), + ) + # Check to see if this member already voted - # TODO Verify exception if votermemberid in self.proposals.get(proposalid).proposal_votes: - raise BadRequestException("VoteOnProposal", "Invalid request body") + raise ResourceAlreadyExistsException( + "VoteOnProposal", + "Member {0} has already voted on proposal {1}.".format( + votermemberid, proposalid + ), + ) - # Will return false if vote was not cast (e.g., status wrong) - if self.proposals.get(proposalid).set_vote( + # Cast vote + self.proposals.get(proposalid).set_vote( votermemberid, self.members.get(votermemberid).name, vote.upper() - ): - if self.proposals.get(proposalid).proposal_status == "APPROVED": - ## Generate invitations - for propinvitation in self.proposals.get(proposalid).proposal_actions( - "Invitations" - ): - invitation_id = get_invitation_id() - self.invitations[invitation_id] = ManagedBlockchainInvitation( - id=invitation_id, - networkid=networkid, - networkname=self.networks.get(networkid).network_name, - networkframework=self.networks.get(networkid).network_framework, - networkframeworkversion=self.networks.get( - networkid - ).network_framework_version, - networkcreationdate=self.networks.get( - networkid - ).network_creationdate, - region=self.region_name, - networkdescription=self.networks.get( - networkid - ).network_description, - ) + ) - ## Delete members - for propmember in self.proposals.get(proposalid).proposal_actions( - "Removals" - ): - self.delete_member(networkid, propmember["MemberId"]) + if self.proposals.get(proposalid).proposal_status == "APPROVED": + # Generate invitations + for propinvitation in self.proposals.get(proposalid).proposal_actions( + "Invitations" + ): + invitation_id = get_invitation_id() + self.invitations[invitation_id] = ManagedBlockchainInvitation( + id=invitation_id, + networkid=networkid, + networkname=self.networks.get(networkid).network_name, + networkframework=self.networks.get(networkid).network_framework, + networkframeworkversion=self.networks.get( + networkid + ).network_framework_version, + networkcreationdate=self.networks.get( + networkid + ).network_creationdate, + region=self.region_name, + networkdescription=self.networks.get(networkid).network_description, + ) + + # Delete members + for propmember in self.proposals.get(proposalid).proposal_actions( + "Removals" + ): + self.delete_member(networkid, propmember["MemberId"]) def list_proposal_votes(self, networkid, proposalid): # Check if network exists @@ -754,7 +872,7 @@ class ManagedBlockchainBackend(BaseBackend): "GetMember", "Member {0} not found.".format(memberid) ) - ## Cannot get a member than has been delted (it does show up in the list) + # Cannot get a member than has been deleted (it does show up in the list) if self.members.get(memberid).member_status == "DELETED": raise ResourceNotFoundException( "GetMember", "Member {0} not found.".format(memberid) @@ -791,6 +909,10 @@ class ManagedBlockchainBackend(BaseBackend): # Remove network del self.networks[networkid] + # Remove any nodes associated + for nodeid in nodes_in_member(self.nodes, memberid): + del self.nodes[nodeid] + def update_member(self, networkid, memberid, logpublishingconfiguration): # Check if network exists if networkid not in self.networks: @@ -805,6 +927,173 @@ class ManagedBlockchainBackend(BaseBackend): self.members.get(memberid).update(logpublishingconfiguration) + def create_node( + self, + networkid, + memberid, + availabilityzone, + instancetype, + logpublishingconfiguration, + ): + # Check if network exists + if networkid not in self.networks: + raise ResourceNotFoundException( + "CreateNode", "Network {0} not found.".format(networkid) + ) + + if memberid not in self.members: + raise ResourceNotFoundException( + "CreateNode", "Member {0} not found.".format(memberid) + ) + + networkedition = self.networks.get(networkid).network_edition + if ( + number_of_nodes_in_member(self.nodes, memberid) + >= EDITIONS[networkedition]["MaxNodesPerMember"] + ): + raise ResourceLimitExceededException( + "CreateNode", + "Maximum number of nodes exceeded in member {0}. The maximum number of nodes you can have in a member in a {1} Edition network is {2}".format( + memberid, + networkedition, + EDITIONS[networkedition]["MaxNodesPerMember"], + ), + ) + + # See if the instance family is correct + correctinstancefamily = False + for chkinsttypepre in EDITIONS["STANDARD"]["AllowedNodeInstanceTypes"]: + chkinsttypepreregex = chkinsttypepre + ".*" + if re.match(chkinsttypepreregex, instancetype, re.IGNORECASE): + correctinstancefamily = True + break + + if correctinstancefamily is False: + raise InvalidRequestException( + "CreateNode", + "Requested instance {0} isn't supported.".format(instancetype), + ) + + # Check for specific types for starter + if networkedition == "STARTER": + if instancetype not in EDITIONS["STARTER"]["AllowedNodeInstanceTypes"]: + raise InvalidRequestException( + "CreateNode", + "Instance type {0} is not supported with STARTER Edition networks.".format( + instancetype + ), + ) + + # Simple availability zone check + chkregionpreregex = self.region_name + "[a-z]" + if re.match(chkregionpreregex, availabilityzone, re.IGNORECASE) is None: + raise InvalidRequestException( + "CreateNode", "Availability Zone is not valid", + ) + + node_id = get_node_id() + self.nodes[node_id] = ManagedBlockchainNode( + id=node_id, + networkid=networkid, + memberid=memberid, + availabilityzone=availabilityzone, + instancetype=instancetype, + logpublishingconfiguration=logpublishingconfiguration, + region=self.region_name, + ) + + # Return the node ID + d = {"NodeId": node_id} + return d + + def list_nodes(self, networkid, memberid, status=None): + if networkid not in self.networks: + raise ResourceNotFoundException( + "ListNodes", "Network {0} not found.".format(networkid) + ) + + if memberid not in self.members: + raise ResourceNotFoundException( + "ListNodes", "Member {0} not found.".format(memberid) + ) + + # If member is deleted, cannot list nodes + if self.members.get(memberid).member_status == "DELETED": + raise ResourceNotFoundException( + "ListNodes", "Member {0} not found.".format(memberid) + ) + + nodesformember = [] + for node_id in self.nodes: + if self.nodes.get(node_id).member_id == memberid and ( + status is None or self.nodes.get(node_id).node_status == status + ): + nodesformember.append(self.nodes[node_id]) + return nodesformember + + def get_node(self, networkid, memberid, nodeid): + # Check if network exists + if networkid not in self.networks: + raise ResourceNotFoundException( + "GetNode", "Network {0} not found.".format(networkid) + ) + + if memberid not in self.members: + raise ResourceNotFoundException( + "GetNode", "Member {0} not found.".format(memberid) + ) + + if nodeid not in self.nodes: + raise ResourceNotFoundException( + "GetNode", "Node {0} not found.".format(nodeid) + ) + + # Cannot get a node than has been deleted (it does show up in the list) + if self.nodes.get(nodeid).node_status == "DELETED": + raise ResourceNotFoundException( + "GetNode", "Node {0} not found.".format(nodeid) + ) + + return self.nodes.get(nodeid) + + def delete_node(self, networkid, memberid, nodeid): + # Check if network exists + if networkid not in self.networks: + raise ResourceNotFoundException( + "DeleteNode", "Network {0} not found.".format(networkid) + ) + + if memberid not in self.members: + raise ResourceNotFoundException( + "DeleteNode", "Member {0} not found.".format(memberid) + ) + + if nodeid not in self.nodes: + raise ResourceNotFoundException( + "DeleteNode", "Node {0} not found.".format(nodeid) + ) + + self.nodes.get(nodeid).delete() + + def update_node(self, networkid, memberid, nodeid, logpublishingconfiguration): + # Check if network exists + if networkid not in self.networks: + raise ResourceNotFoundException( + "UpdateNode", "Network {0} not found.".format(networkid) + ) + + if memberid not in self.members: + raise ResourceNotFoundException( + "UpdateNode", "Member {0} not found.".format(memberid) + ) + + if nodeid not in self.nodes: + raise ResourceNotFoundException( + "UpdateNode", "Node {0} not found.".format(nodeid) + ) + + self.nodes.get(nodeid).update(logpublishingconfiguration) + managedblockchain_backends = {} for region in Session().get_available_regions("managedblockchain"): diff --git a/moto/managedblockchain/responses.py b/moto/managedblockchain/responses.py index 34206b3c4..7dd628eba 100644 --- a/moto/managedblockchain/responses.py +++ b/moto/managedblockchain/responses.py @@ -11,6 +11,7 @@ from .utils import ( proposalid_from_managedblockchain_url, invitationid_from_managedblockchain_url, memberid_from_managedblockchain_url, + nodeid_from_managedblockchain_url, ) @@ -324,3 +325,103 @@ class ManagedBlockchainResponse(BaseResponse): self.backend.delete_member(network_id, member_id) headers["content-type"] = "application/json" return 200, headers, "" + + @classmethod + def node_response(clazz, request, full_url, headers): + region_name = region_from_managedblckchain_url(full_url) + response_instance = ManagedBlockchainResponse( + managedblockchain_backends[region_name] + ) + return response_instance._node_response(request, full_url, headers) + + def _node_response(self, request, full_url, headers): + method = request.method + if hasattr(request, "body"): + body = request.body + else: + body = request.data + parsed_url = urlparse(full_url) + querystring = parse_qs(parsed_url.query, keep_blank_values=True) + network_id = networkid_from_managedblockchain_url(full_url) + member_id = memberid_from_managedblockchain_url(full_url) + if method == "GET": + status = None + if "status" in querystring: + status = querystring["status"][0] + return self._all_nodes_response(network_id, member_id, status, headers) + elif method == "POST": + json_body = json.loads(body.decode("utf-8")) + return self._node_response_post( + network_id, member_id, json_body, querystring, headers + ) + + def _all_nodes_response(self, network_id, member_id, status, headers): + nodes = self.backend.list_nodes(network_id, member_id, status) + response = json.dumps({"Nodes": [node.to_dict() for node in nodes]}) + headers["content-type"] = "application/json" + return 200, headers, response + + def _node_response_post( + self, network_id, member_id, json_body, querystring, headers + ): + instancetype = json_body["NodeConfiguration"]["InstanceType"] + availabilityzone = json_body["NodeConfiguration"]["AvailabilityZone"] + logpublishingconfiguration = json_body["NodeConfiguration"][ + "LogPublishingConfiguration" + ] + + response = self.backend.create_node( + network_id, + member_id, + availabilityzone, + instancetype, + logpublishingconfiguration, + ) + return 200, headers, json.dumps(response) + + @classmethod + def nodeid_response(clazz, request, full_url, headers): + region_name = region_from_managedblckchain_url(full_url) + response_instance = ManagedBlockchainResponse( + managedblockchain_backends[region_name] + ) + return response_instance._nodeid_response(request, full_url, headers) + + def _nodeid_response(self, request, full_url, headers): + method = request.method + if hasattr(request, "body"): + body = request.body + else: + body = request.data + network_id = networkid_from_managedblockchain_url(full_url) + member_id = memberid_from_managedblockchain_url(full_url) + node_id = nodeid_from_managedblockchain_url(full_url) + if method == "GET": + return self._nodeid_response_get(network_id, member_id, node_id, headers) + elif method == "PATCH": + json_body = json.loads(body.decode("utf-8")) + return self._nodeid_response_patch( + network_id, member_id, node_id, json_body, headers + ) + elif method == "DELETE": + return self._nodeid_response_delete(network_id, member_id, node_id, headers) + + def _nodeid_response_get(self, network_id, member_id, node_id, headers): + node = self.backend.get_node(network_id, member_id, node_id) + response = json.dumps({"Node": node.get_format()}) + headers["content-type"] = "application/json" + return 200, headers, response + + def _nodeid_response_patch( + self, network_id, member_id, node_id, json_body, headers + ): + logpublishingconfiguration = json_body + self.backend.update_node( + network_id, member_id, node_id, logpublishingconfiguration, + ) + return 200, headers, "" + + def _nodeid_response_delete(self, network_id, member_id, node_id, headers): + self.backend.delete_node(network_id, member_id, node_id) + headers["content-type"] = "application/json" + return 200, headers, "" diff --git a/moto/managedblockchain/urls.py b/moto/managedblockchain/urls.py index c7d191aab..442a73233 100644 --- a/moto/managedblockchain/urls.py +++ b/moto/managedblockchain/urls.py @@ -13,4 +13,7 @@ url_paths = { "{0}/invitations/(?P[^/.]+)$": ManagedBlockchainResponse.invitationid_response, "{0}/networks/(?P[^/.]+)/members$": ManagedBlockchainResponse.member_response, "{0}/networks/(?P[^/.]+)/members/(?P[^/.]+)$": ManagedBlockchainResponse.memberid_response, + "{0}/networks/(?P[^/.]+)/members/(?P[^/.]+)/nodes$": ManagedBlockchainResponse.node_response, + "{0}/networks/(?P[^/.]+)/members/(?P[^/.]+)/nodes?(?P[^/.]+)$": ManagedBlockchainResponse.node_response, + "{0}/networks/(?P[^/.]+)/members/(?P[^/.]+)/nodes/(?P[^/.]+)$": ManagedBlockchainResponse.nodeid_response, } diff --git a/moto/managedblockchain/utils.py b/moto/managedblockchain/utils.py index ea8f50513..c8118619e 100644 --- a/moto/managedblockchain/utils.py +++ b/moto/managedblockchain/utils.py @@ -104,3 +104,32 @@ def admin_password_ok(password): return False else: return True + + +def nodeid_from_managedblockchain_url(full_url): + id_search = re.search("\/nd-[A-Z0-9]{26}", full_url, re.IGNORECASE) + return_id = None + if id_search: + return_id = id_search.group(0).replace("/", "") + return return_id + + +def get_node_id(): + return "nd-" + "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(26) + ) + + +def number_of_nodes_in_member(nodes, memberid, node_status=None): + return len( + [ + nodid + for nodid in nodes + if nodes.get(nodid).member_id == memberid + and (node_status is None or nodes.get(nodid).node_status == node_status) + ] + ) + + +def nodes_in_member(nodes, memberid): + return [nodid for nodid in nodes if nodes.get(nodid).member_id == memberid] diff --git a/tests/test_managedblockchain/helpers.py b/tests/test_managedblockchain/helpers.py index 38c13b512..f8c6d29b9 100644 --- a/tests/test_managedblockchain/helpers.py +++ b/tests/test_managedblockchain/helpers.py @@ -28,6 +28,17 @@ multiple_policy_actions = { "Invitations": [{"Principal": "123456789012"}, {"Principal": "123456789013"}] } +default_nodeconfiguration = { + "InstanceType": "bc.t3.small", + "AvailabilityZone": "us-east-1a", + "LogPublishingConfiguration": { + "Fabric": { + "ChaincodeLogs": {"Cloudwatch": {"Enabled": False}}, + "PeerLogs": {"Cloudwatch": {"Enabled": False}}, + } + }, +} + def member_id_exist_in_list(members, memberid): memberidxists = False @@ -65,3 +76,12 @@ def select_invitation_id_for_network(invitations, networkid, status=None): if status is None or invitation["Status"] == status: invitationsfornetwork.append(invitation["InvitationId"]) return invitationsfornetwork + + +def node_id_exist_in_list(nodes, nodeid): + nodeidxists = False + for node in nodes: + if node["Id"] == nodeid: + nodeidxists = True + break + return nodeidxists diff --git a/tests/test_managedblockchain/test_managedblockchain_invitations.py b/tests/test_managedblockchain/test_managedblockchain_invitations.py index 81b20a9ba..0f70d7f88 100644 --- a/tests/test_managedblockchain/test_managedblockchain_invitations.py +++ b/tests/test_managedblockchain/test_managedblockchain_invitations.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import boto3 import sure # noqa -from moto.managedblockchain.exceptions import BadRequestException from moto import mock_managedblockchain from . import helpers diff --git a/tests/test_managedblockchain/test_managedblockchain_members.py b/tests/test_managedblockchain/test_managedblockchain_members.py index 76d29dd55..9120e4aee 100644 --- a/tests/test_managedblockchain/test_managedblockchain_members.py +++ b/tests/test_managedblockchain/test_managedblockchain_members.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import boto3 import sure # noqa -from moto.managedblockchain.exceptions import BadRequestException from moto import mock_managedblockchain from . import helpers @@ -204,7 +203,7 @@ def test_create_another_member_withopts(): @mock_managedblockchain -def test_create_and_delete_member(): +def test_invite_and_remove_member(): conn = boto3.client("managedblockchain", region_name="us-east-1") # Create network @@ -362,17 +361,14 @@ def test_create_too_many_members(): response["Invitations"], network_id, "PENDING" )[0] - # Try to create member with already used invitation + # Try to create one too many members response = conn.create_member.when.called_with( InvitationId=invitation_id, NetworkId=network_id, MemberConfiguration=helpers.create_member_configuration( "testmember6", "admin", "Admin12345", False, "Test Member 6" ), - ).should.throw( - Exception, - "5 is the maximum number of members allowed in a STARTER Edition network", - ) + ).should.throw(Exception, "is the maximum number of members allowed in a",) @mock_managedblockchain diff --git a/tests/test_managedblockchain/test_managedblockchain_networks.py b/tests/test_managedblockchain/test_managedblockchain_networks.py index 4e1579017..c2a332983 100644 --- a/tests/test_managedblockchain/test_managedblockchain_networks.py +++ b/tests/test_managedblockchain/test_managedblockchain_networks.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import boto3 import sure # noqa -from moto.managedblockchain.exceptions import BadRequestException from moto import mock_managedblockchain from . import helpers diff --git a/tests/test_managedblockchain/test_managedblockchain_nodes.py b/tests/test_managedblockchain/test_managedblockchain_nodes.py new file mode 100644 index 000000000..32a5bc62c --- /dev/null +++ b/tests/test_managedblockchain/test_managedblockchain_nodes.py @@ -0,0 +1,477 @@ +from __future__ import unicode_literals + +import boto3 +import sure # noqa + +from moto import mock_managedblockchain +from . import helpers + + +@mock_managedblockchain +def test_create_node(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + # Create network + response = conn.create_network( + Name="testnetwork1", + Description="Test Network 1", + Framework="HYPERLEDGER_FABRIC", + FrameworkVersion="1.2", + FrameworkConfiguration=helpers.default_frameworkconfiguration, + VotingPolicy=helpers.default_votingpolicy, + MemberConfiguration=helpers.default_memberconfiguration, + ) + network_id = response["NetworkId"] + member_id = response["MemberId"] + + # Create a node + response = conn.create_node( + NetworkId=network_id, + MemberId=member_id, + NodeConfiguration=helpers.default_nodeconfiguration, + ) + node_id = response["NodeId"] + + # Find node in full list + response = conn.list_nodes(NetworkId=network_id, MemberId=member_id) + nodes = response["Nodes"] + nodes.should.have.length_of(1) + helpers.node_id_exist_in_list(nodes, node_id).should.equal(True) + + # Get node details + response = conn.get_node(NetworkId=network_id, MemberId=member_id, NodeId=node_id) + response["Node"]["AvailabilityZone"].should.equal("us-east-1a") + + # Update node + logconfignewenabled = not helpers.default_nodeconfiguration[ + "LogPublishingConfiguration" + ]["Fabric"]["ChaincodeLogs"]["Cloudwatch"]["Enabled"] + logconfignew = { + "Fabric": {"ChaincodeLogs": {"Cloudwatch": {"Enabled": logconfignewenabled}}} + } + conn.update_node( + NetworkId=network_id, + MemberId=member_id, + NodeId=node_id, + LogPublishingConfiguration=logconfignew, + ) + + # Delete node + conn.delete_node( + NetworkId=network_id, MemberId=member_id, NodeId=node_id, + ) + + # Find node in full list + response = conn.list_nodes(NetworkId=network_id, MemberId=member_id) + nodes = response["Nodes"] + nodes.should.have.length_of(1) + helpers.node_id_exist_in_list(nodes, node_id).should.equal(True) + + # Find node in full list - only DELETED + response = conn.list_nodes( + NetworkId=network_id, MemberId=member_id, Status="DELETED" + ) + nodes = response["Nodes"] + nodes.should.have.length_of(1) + helpers.node_id_exist_in_list(nodes, node_id).should.equal(True) + + # But cannot get + response = conn.get_node.when.called_with( + NetworkId=network_id, MemberId=member_id, NodeId=node_id, + ).should.throw(Exception, "Node {0} not found".format(node_id)) + + +@mock_managedblockchain +def test_create_node_standard_edition(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + frameworkconfiguration = {"Fabric": {"Edition": "STANDARD"}} + + response = conn.create_network( + Name="testnetwork1", + Description="Test Network 1", + Framework="HYPERLEDGER_FABRIC", + FrameworkVersion="1.2", + FrameworkConfiguration=frameworkconfiguration, + VotingPolicy=helpers.default_votingpolicy, + MemberConfiguration=helpers.default_memberconfiguration, + ) + network_id = response["NetworkId"] + member_id = response["MemberId"] + + # Instance type only allowed with standard edition + logconfigbad = dict(helpers.default_nodeconfiguration) + logconfigbad["InstanceType"] = "bc.t3.large" + response = conn.create_node( + NetworkId=network_id, MemberId=member_id, NodeConfiguration=logconfigbad, + ) + node_id = response["NodeId"] + + # Get node details + response = conn.get_node(NetworkId=network_id, MemberId=member_id, NodeId=node_id) + response["Node"]["InstanceType"].should.equal("bc.t3.large") + + # Need another member so the network does not get deleted + # Create proposal + response = conn.create_proposal( + NetworkId=network_id, + MemberId=member_id, + Actions=helpers.default_policy_actions, + ) + proposal_id = response["ProposalId"] + + # Vote yes + response = conn.vote_on_proposal( + NetworkId=network_id, + ProposalId=proposal_id, + VoterMemberId=member_id, + Vote="YES", + ) + + # Get the invitation + response = conn.list_invitations() + invitation_id = response["Invitations"][0]["InvitationId"] + + # Create the member + response = conn.create_member( + InvitationId=invitation_id, + NetworkId=network_id, + MemberConfiguration=helpers.create_member_configuration( + "testmember2", "admin", "Admin12345", False, "Test Member 2" + ), + ) + + # Remove member 1 - should remove nodes + conn.delete_member(NetworkId=network_id, MemberId=member_id) + + # Should now be an exception + response = conn.list_nodes.when.called_with( + NetworkId=network_id, MemberId=member_id, + ).should.throw(Exception, "Member {0} not found".format(member_id)) + + +@mock_managedblockchain +def test_create_too_many_nodes(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + # Create network + response = conn.create_network( + Name="testnetwork1", + Description="Test Network 1", + Framework="HYPERLEDGER_FABRIC", + FrameworkVersion="1.2", + FrameworkConfiguration=helpers.default_frameworkconfiguration, + VotingPolicy=helpers.default_votingpolicy, + MemberConfiguration=helpers.default_memberconfiguration, + ) + network_id = response["NetworkId"] + member_id = response["MemberId"] + + # Create a node + response = conn.create_node( + NetworkId=network_id, + MemberId=member_id, + NodeConfiguration=helpers.default_nodeconfiguration, + ) + + # Create another node + response = conn.create_node( + NetworkId=network_id, + MemberId=member_id, + NodeConfiguration=helpers.default_nodeconfiguration, + ) + + # Find node in full list + response = conn.list_nodes(NetworkId=network_id, MemberId=member_id) + nodes = response["Nodes"] + nodes.should.have.length_of(2) + + # Try to create one too many nodes + response = conn.create_node.when.called_with( + NetworkId=network_id, + MemberId=member_id, + NodeConfiguration=helpers.default_nodeconfiguration, + ).should.throw( + Exception, "Maximum number of nodes exceeded in member {0}".format(member_id), + ) + + +@mock_managedblockchain +def test_create_node_badnetwork(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.create_node.when.called_with( + NetworkId="n-ABCDEFGHIJKLMNOP0123456789", + MemberId="m-ABCDEFGHIJKLMNOP0123456789", + NodeConfiguration=helpers.default_nodeconfiguration, + ).should.throw(Exception, "Network n-ABCDEFGHIJKLMNOP0123456789 not found") + + +@mock_managedblockchain +def test_create_node_badmember(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.create_network( + Name="testnetwork1", + Description="Test Network 1", + Framework="HYPERLEDGER_FABRIC", + FrameworkVersion="1.2", + FrameworkConfiguration=helpers.default_frameworkconfiguration, + VotingPolicy=helpers.default_votingpolicy, + MemberConfiguration=helpers.default_memberconfiguration, + ) + network_id = response["NetworkId"] + + response = conn.create_node.when.called_with( + NetworkId=network_id, + MemberId="m-ABCDEFGHIJKLMNOP0123456789", + NodeConfiguration=helpers.default_nodeconfiguration, + ).should.throw(Exception, "Member m-ABCDEFGHIJKLMNOP0123456789 not found") + + +@mock_managedblockchain +def test_create_node_badnodeconfig(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.create_network( + Name="testnetwork1", + Description="Test Network 1", + Framework="HYPERLEDGER_FABRIC", + FrameworkVersion="1.2", + FrameworkConfiguration=helpers.default_frameworkconfiguration, + VotingPolicy=helpers.default_votingpolicy, + MemberConfiguration=helpers.default_memberconfiguration, + ) + network_id = response["NetworkId"] + member_id = response["MemberId"] + + # Incorrect instance type + logconfigbad = dict(helpers.default_nodeconfiguration) + logconfigbad["InstanceType"] = "foo" + response = conn.create_node.when.called_with( + NetworkId=network_id, MemberId=member_id, NodeConfiguration=logconfigbad, + ).should.throw(Exception, "Requested instance foo isn't supported.") + + # Incorrect instance type for edition + logconfigbad = dict(helpers.default_nodeconfiguration) + logconfigbad["InstanceType"] = "bc.t3.large" + response = conn.create_node.when.called_with( + NetworkId=network_id, MemberId=member_id, NodeConfiguration=logconfigbad, + ).should.throw( + Exception, + "Instance type bc.t3.large is not supported with STARTER Edition networks", + ) + + # Incorrect availability zone + logconfigbad = dict(helpers.default_nodeconfiguration) + logconfigbad["AvailabilityZone"] = "us-east-11" + response = conn.create_node.when.called_with( + NetworkId=network_id, MemberId=member_id, NodeConfiguration=logconfigbad, + ).should.throw(Exception, "Availability Zone is not valid") + + +@mock_managedblockchain +def test_list_nodes_badnetwork(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.list_nodes.when.called_with( + NetworkId="n-ABCDEFGHIJKLMNOP0123456789", + MemberId="m-ABCDEFGHIJKLMNOP0123456789", + ).should.throw(Exception, "Network n-ABCDEFGHIJKLMNOP0123456789 not found") + + +@mock_managedblockchain +def test_list_nodes_badmember(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.create_network( + Name="testnetwork1", + Description="Test Network 1", + Framework="HYPERLEDGER_FABRIC", + FrameworkVersion="1.2", + FrameworkConfiguration=helpers.default_frameworkconfiguration, + VotingPolicy=helpers.default_votingpolicy, + MemberConfiguration=helpers.default_memberconfiguration, + ) + network_id = response["NetworkId"] + + response = conn.list_nodes.when.called_with( + NetworkId=network_id, MemberId="m-ABCDEFGHIJKLMNOP0123456789", + ).should.throw(Exception, "Member m-ABCDEFGHIJKLMNOP0123456789 not found") + + +@mock_managedblockchain +def test_get_node_badnetwork(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.get_node.when.called_with( + NetworkId="n-ABCDEFGHIJKLMNOP0123456789", + MemberId="m-ABCDEFGHIJKLMNOP0123456789", + NodeId="nd-ABCDEFGHIJKLMNOP0123456789", + ).should.throw(Exception, "Network n-ABCDEFGHIJKLMNOP0123456789 not found") + + +@mock_managedblockchain +def test_get_node_badmember(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.create_network( + Name="testnetwork1", + Description="Test Network 1", + Framework="HYPERLEDGER_FABRIC", + FrameworkVersion="1.2", + FrameworkConfiguration=helpers.default_frameworkconfiguration, + VotingPolicy=helpers.default_votingpolicy, + MemberConfiguration=helpers.default_memberconfiguration, + ) + network_id = response["NetworkId"] + + response = conn.get_node.when.called_with( + NetworkId=network_id, + MemberId="m-ABCDEFGHIJKLMNOP0123456789", + NodeId="nd-ABCDEFGHIJKLMNOP0123456789", + ).should.throw(Exception, "Member m-ABCDEFGHIJKLMNOP0123456789 not found") + + +@mock_managedblockchain +def test_get_node_badnode(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.create_network( + Name="testnetwork1", + Description="Test Network 1", + Framework="HYPERLEDGER_FABRIC", + FrameworkVersion="1.2", + FrameworkConfiguration=helpers.default_frameworkconfiguration, + VotingPolicy=helpers.default_votingpolicy, + MemberConfiguration=helpers.default_memberconfiguration, + ) + network_id = response["NetworkId"] + member_id = response["MemberId"] + + response = conn.get_node.when.called_with( + NetworkId=network_id, + MemberId=member_id, + NodeId="nd-ABCDEFGHIJKLMNOP0123456789", + ).should.throw(Exception, "Node nd-ABCDEFGHIJKLMNOP0123456789 not found") + + +@mock_managedblockchain +def test_delete_node_badnetwork(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.delete_node.when.called_with( + NetworkId="n-ABCDEFGHIJKLMNOP0123456789", + MemberId="m-ABCDEFGHIJKLMNOP0123456789", + NodeId="nd-ABCDEFGHIJKLMNOP0123456789", + ).should.throw(Exception, "Network n-ABCDEFGHIJKLMNOP0123456789 not found") + + +@mock_managedblockchain +def test_delete_node_badmember(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.create_network( + Name="testnetwork1", + Description="Test Network 1", + Framework="HYPERLEDGER_FABRIC", + FrameworkVersion="1.2", + FrameworkConfiguration=helpers.default_frameworkconfiguration, + VotingPolicy=helpers.default_votingpolicy, + MemberConfiguration=helpers.default_memberconfiguration, + ) + network_id = response["NetworkId"] + + response = conn.delete_node.when.called_with( + NetworkId=network_id, + MemberId="m-ABCDEFGHIJKLMNOP0123456789", + NodeId="nd-ABCDEFGHIJKLMNOP0123456789", + ).should.throw(Exception, "Member m-ABCDEFGHIJKLMNOP0123456789 not found") + + +@mock_managedblockchain +def test_delete_node_badnode(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.create_network( + Name="testnetwork1", + Description="Test Network 1", + Framework="HYPERLEDGER_FABRIC", + FrameworkVersion="1.2", + FrameworkConfiguration=helpers.default_frameworkconfiguration, + VotingPolicy=helpers.default_votingpolicy, + MemberConfiguration=helpers.default_memberconfiguration, + ) + network_id = response["NetworkId"] + member_id = response["MemberId"] + + response = conn.delete_node.when.called_with( + NetworkId=network_id, + MemberId=member_id, + NodeId="nd-ABCDEFGHIJKLMNOP0123456789", + ).should.throw(Exception, "Node nd-ABCDEFGHIJKLMNOP0123456789 not found") + + +@mock_managedblockchain +def test_update_node_badnetwork(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.update_node.when.called_with( + NetworkId="n-ABCDEFGHIJKLMNOP0123456789", + MemberId="m-ABCDEFGHIJKLMNOP0123456789", + NodeId="nd-ABCDEFGHIJKLMNOP0123456789", + LogPublishingConfiguration=helpers.default_nodeconfiguration[ + "LogPublishingConfiguration" + ], + ).should.throw(Exception, "Network n-ABCDEFGHIJKLMNOP0123456789 not found") + + +@mock_managedblockchain +def test_update_node_badmember(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.create_network( + Name="testnetwork1", + Description="Test Network 1", + Framework="HYPERLEDGER_FABRIC", + FrameworkVersion="1.2", + FrameworkConfiguration=helpers.default_frameworkconfiguration, + VotingPolicy=helpers.default_votingpolicy, + MemberConfiguration=helpers.default_memberconfiguration, + ) + network_id = response["NetworkId"] + + response = conn.update_node.when.called_with( + NetworkId=network_id, + MemberId="m-ABCDEFGHIJKLMNOP0123456789", + NodeId="nd-ABCDEFGHIJKLMNOP0123456789", + LogPublishingConfiguration=helpers.default_nodeconfiguration[ + "LogPublishingConfiguration" + ], + ).should.throw(Exception, "Member m-ABCDEFGHIJKLMNOP0123456789 not found") + + +@mock_managedblockchain +def test_update_node_badnode(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + response = conn.create_network( + Name="testnetwork1", + Description="Test Network 1", + Framework="HYPERLEDGER_FABRIC", + FrameworkVersion="1.2", + FrameworkConfiguration=helpers.default_frameworkconfiguration, + VotingPolicy=helpers.default_votingpolicy, + MemberConfiguration=helpers.default_memberconfiguration, + ) + network_id = response["NetworkId"] + member_id = response["MemberId"] + + response = conn.update_node.when.called_with( + NetworkId=network_id, + MemberId=member_id, + NodeId="nd-ABCDEFGHIJKLMNOP0123456789", + LogPublishingConfiguration=helpers.default_nodeconfiguration[ + "LogPublishingConfiguration" + ], + ).should.throw(Exception, "Node nd-ABCDEFGHIJKLMNOP0123456789 not found") diff --git a/tests/test_managedblockchain/test_managedblockchain_proposals.py b/tests/test_managedblockchain/test_managedblockchain_proposals.py index 407d26246..aa899e3a1 100644 --- a/tests/test_managedblockchain/test_managedblockchain_proposals.py +++ b/tests/test_managedblockchain/test_managedblockchain_proposals.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import boto3 import sure # noqa -from moto.managedblockchain.exceptions import BadRequestException from moto import mock_managedblockchain from . import helpers diff --git a/tests/test_managedblockchain/test_managedblockchain_proposalvotes.py b/tests/test_managedblockchain/test_managedblockchain_proposalvotes.py index a026b496f..eda728398 100644 --- a/tests/test_managedblockchain/test_managedblockchain_proposalvotes.py +++ b/tests/test_managedblockchain/test_managedblockchain_proposalvotes.py @@ -7,7 +7,6 @@ import sure # noqa from freezegun import freeze_time from nose import SkipTest -from moto.managedblockchain.exceptions import BadRequestException from moto import mock_managedblockchain, settings from . import helpers @@ -186,6 +185,18 @@ def test_vote_on_proposal_yes_greater_than(): response["Proposal"]["NetworkId"].should.equal(network_id) response["Proposal"]["Status"].should.equal("IN_PROGRESS") + # Vote no with member 2 + response = conn.vote_on_proposal( + NetworkId=network_id, + ProposalId=proposal_id, + VoterMemberId=member_id2, + Vote="NO", + ) + + # Get proposal details + response = conn.get_proposal(NetworkId=network_id, ProposalId=proposal_id) + response["Proposal"]["Status"].should.equal("REJECTED") + @mock_managedblockchain def test_vote_on_proposal_no_greater_than(): @@ -310,6 +321,47 @@ def test_vote_on_proposal_expiredproposal(): with freeze_time("2015-02-01 12:00:00"): # Vote yes - should set status to expired + response = conn.vote_on_proposal.when.called_with( + NetworkId=network_id, + ProposalId=proposal_id, + VoterMemberId=member_id, + Vote="YES", + ).should.throw( + Exception, + "Proposal {0} is expired and you cannot vote on it.".format(proposal_id), + ) + + # Get proposal details - should be EXPIRED + response = conn.get_proposal(NetworkId=network_id, ProposalId=proposal_id) + response["Proposal"]["Status"].should.equal("EXPIRED") + + +@mock_managedblockchain +def test_vote_on_proposal_status_check(): + conn = boto3.client("managedblockchain", region_name="us-east-1") + + # Create network + response = conn.create_network( + Name="testnetwork1", + Framework="HYPERLEDGER_FABRIC", + FrameworkVersion="1.2", + FrameworkConfiguration=helpers.default_frameworkconfiguration, + VotingPolicy=helpers.default_votingpolicy, + MemberConfiguration=helpers.default_memberconfiguration, + ) + network_id = response["NetworkId"] + member_id = response["MemberId"] + + # Create 2 more members + for counter in range(2, 4): + response = conn.create_proposal( + NetworkId=network_id, + MemberId=member_id, + Actions=helpers.default_policy_actions, + ) + proposal_id = response["ProposalId"] + + # Vote yes response = conn.vote_on_proposal( NetworkId=network_id, ProposalId=proposal_id, @@ -317,9 +369,88 @@ def test_vote_on_proposal_expiredproposal(): Vote="YES", ) - # Get proposal details - should be EXPIRED - response = conn.get_proposal(NetworkId=network_id, ProposalId=proposal_id) - response["Proposal"]["Status"].should.equal("EXPIRED") + memberidlist = [None, None, None] + memberidlist[0] = member_id + for counter in range(2, 4): + # Get the invitation + response = conn.list_invitations() + invitation_id = helpers.select_invitation_id_for_network( + response["Invitations"], network_id, "PENDING" + )[0] + + # Create the member + response = conn.create_member( + InvitationId=invitation_id, + NetworkId=network_id, + MemberConfiguration=helpers.create_member_configuration( + "testmember" + str(counter), + "admin", + "Admin12345", + False, + "Test Member " + str(counter), + ), + ) + member_id = response["MemberId"] + memberidlist[counter - 1] = member_id + + # Should be no more pending invitations + response = conn.list_invitations() + pendinginvs = helpers.select_invitation_id_for_network( + response["Invitations"], network_id, "PENDING" + ) + pendinginvs.should.have.length_of(0) + + # Create another proposal + response = conn.create_proposal( + NetworkId=network_id, + MemberId=member_id, + Actions=helpers.default_policy_actions, + ) + + proposal_id = response["ProposalId"] + + # Vote yes with member 1 + response = conn.vote_on_proposal( + NetworkId=network_id, + ProposalId=proposal_id, + VoterMemberId=memberidlist[0], + Vote="YES", + ) + + # Vote yes with member 2 + response = conn.vote_on_proposal( + NetworkId=network_id, + ProposalId=proposal_id, + VoterMemberId=memberidlist[1], + Vote="YES", + ) + + # Get proposal details - now approved (2 yes, 1 outstanding) + response = conn.get_proposal(NetworkId=network_id, ProposalId=proposal_id) + response["Proposal"]["NetworkId"].should.equal(network_id) + response["Proposal"]["Status"].should.equal("APPROVED") + + # Should be one pending invitation + response = conn.list_invitations() + pendinginvs = helpers.select_invitation_id_for_network( + response["Invitations"], network_id, "PENDING" + ) + pendinginvs.should.have.length_of(1) + + # Vote with member 3 - should throw an exception and not create a new invitation + response = conn.vote_on_proposal.when.called_with( + NetworkId=network_id, + ProposalId=proposal_id, + VoterMemberId=memberidlist[2], + Vote="YES", + ).should.throw(Exception, "and you cannot vote on it") + + # Should still be one pending invitation + response = conn.list_invitations() + pendinginvs = helpers.select_invitation_id_for_network( + response["Invitations"], network_id, "PENDING" + ) + pendinginvs.should.have.length_of(1) @mock_managedblockchain @@ -425,13 +556,21 @@ def test_vote_on_proposal_badvote(): def test_vote_on_proposal_alreadyvoted(): conn = boto3.client("managedblockchain", region_name="us-east-1") + votingpolicy = { + "ApprovalThresholdPolicy": { + "ThresholdPercentage": 50, + "ProposalDurationInHours": 24, + "ThresholdComparator": "GREATER_THAN", + } + } + # Create network - need a good network response = conn.create_network( Name="testnetwork1", Framework="HYPERLEDGER_FABRIC", FrameworkVersion="1.2", FrameworkConfiguration=helpers.default_frameworkconfiguration, - VotingPolicy=helpers.default_votingpolicy, + VotingPolicy=votingpolicy, MemberConfiguration=helpers.default_memberconfiguration, ) network_id = response["NetworkId"] @@ -465,7 +604,6 @@ def test_vote_on_proposal_alreadyvoted(): "testmember2", "admin", "Admin12345", False, "Test Member 2" ), ) - member_id2 = response["MemberId"] # Create another proposal response = conn.create_proposal( @@ -495,7 +633,10 @@ def test_vote_on_proposal_alreadyvoted(): ProposalId=proposal_id, VoterMemberId=member_id, Vote="YES", - ).should.throw(Exception, "Invalid request body") + ).should.throw( + Exception, + "Member {0} has already voted on proposal {1}.".format(member_id, proposal_id), + ) @mock_managedblockchain