From 693d4323c2b01dd82bbd8d6ff3b5e8e8c5d63500 Mon Sep 17 00:00:00 2001 From: qstokkink Date: Wed, 20 Nov 2024 14:09:46 +0100 Subject: [PATCH] Update REST tests to use Mocks --- ipv8/test/REST/rest_base.py | 359 ++++------------- ipv8/test/REST/test_identity_endpoint.py | 466 +++++++++++++--------- ipv8/test/REST/test_isolation_endpoint.py | 53 ++- ipv8/test/REST/test_network_endpoint.py | 36 +- ipv8/test/REST/test_overlays_endpoint.py | 139 ++++--- ipv8/test/mocking/community.py | 4 +- ipv8/test/mocking/ipv8.py | 22 + 7 files changed, 505 insertions(+), 574 deletions(-) diff --git a/ipv8/test/REST/rest_base.py b/ipv8/test/REST/rest_base.py index e7a8582b7..126818cbc 100644 --- a/ipv8/test/REST/rest_base.py +++ b/ipv8/test/REST/rest_base.py @@ -1,327 +1,120 @@ from __future__ import annotations -from asyncio import Future, Transport -from threading import RLock -from typing import TYPE_CHECKING, Any, Callable, cast - -from aiohttp import BaseConnector, ClientRequest, ClientSession, ClientTimeout, web -from aiohttp.client_proto import ResponseHandler -from aiohttp.web_protocol import RequestHandler - -from ...community import CommunitySettings -from ...configuration import get_default_configuration -from ...keyvault.crypto import ECCrypto -from ...messaging.anonymization.endpoint import TunnelEndpoint -from ...messaging.interfaces.endpoint import EndpointListener -from ...messaging.interfaces.udp.endpoint import UDPv4Address -from ...peer import Peer -from ...peerdiscovery.network import Network -from ...REST.rest_manager import RESTManager -from ...test.mocking.discovery import MockWalk -from ...test.mocking.endpoint import AutoMockEndpoint, MockEndpoint -from ...util import maybe_coroutine, succeed -from ..base import TestBase +import asyncio +from asyncio import StreamReader, get_running_loop +from io import BytesIO +from json import loads +from typing import TYPE_CHECKING, Any +from unittest.mock import Mock + +from aiohttp import HttpVersion +from aiohttp.abc import AbstractStreamWriter +from aiohttp.http_parser import RawRequestMessage +from aiohttp.typedefs import DEFAULT_JSON_DECODER, JSONDecoder +from aiohttp.web_request import Request +from aiohttp.web_urldispatcher import UrlMappingMatchInfo +from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy +from yarl import URL if TYPE_CHECKING: - from collections.abc import Awaitable - from ssl import SSLContext - - from aiohttp.tracing import Trace - from aiohttp.web_runner import BaseRunner - from aiohttp.web_server import Server + from aiohttp.web import Response - from ...peerdiscovery.discovery import DiscoveryStrategy - from ...types import Address, Community - -class IPv8Transport(Transport, EndpointListener): +class BodyCapture(BytesIO, AbstractStreamWriter): """ - Transport to route over the fake IPv8 internet instead of the actual Internet. + Helper-class to capture RESTResponse bodies. """ - def __init__(self, callback: Callable[[bytes], None], host: str | None = None, port: int | None = None, - server_port: int = 80) -> None: - """ - Either transport to localhost and the server port or to a client with a not-pre-known host and port. - """ - self.callback = callback - Transport.__init__(self) - if host is not None: - self.endpoint = MockEndpoint(UDPv4Address(host, port), UDPv4Address(host, port)) - self.remote = None - else: - self.endpoint = AutoMockEndpoint() - self.remote = UDPv4Address("127.0.0.1", server_port) - EndpointListener.__init__(self, self.endpoint) - self.endpoint.add_listener(self) - self.endpoint.prefixlen = 0 - self.endpoint.open() - - def is_closing(self) -> bool: - """ - Whether we are in the process of shutting down. - """ - return not self.endpoint.is_open() - - def is_reading(self) -> bool: - """ - Whether we are open. - """ - return self.endpoint.is_open() - - def close(self) -> None: - """ - Close our transport. - """ - self.endpoint.close() - - def on_packet(self, packet: tuple[Address, bytes]) -> None: - """ - Callback for when the fake IPv8 internet sends us a message. - - We forward this call to the registered callback. - """ - address, data = packet - self.remote = address - self.callback(data) - - def write(self, data: bytes) -> None: + async def write(self, value: bytes) -> None: """ - Write to the other end of this transport. + Write to our Bytes stream. """ - self.endpoint.send(self.remote, data) + BytesIO.write(self, value) -class MockServer: +class MockRequest(Request): """ - Fake server to hijack TCP sessions and route them over the IPv8 fake internet. + Base class for mocked REST requests. """ - START_PORT = 80 - - def __init__(self, server: Server) -> None: - """ - Rip the information from the real server and hook it into our IPv8 endpoints. - """ - super().__init__() - self.port, MockServer.START_PORT = MockServer.START_PORT, MockServer.START_PORT + 1 - self.transport = IPv8Transport(self.received_data, "127.0.0.1", self.port) - self.handler = RequestHandler(server, loop=server._loop) # noqa: SLF001 - self.handler.connection_made(self.transport) - - def close(self) -> None: + class Transport: """ - Close this fake server (no action required). + A fake transport that does nothing. """ - def shutdown(self, timeout: float) -> Future[bool]: - """ - Shut down this fake server (no action required). - """ - return succeed(True) + def __init__(self) -> None: + """ + Create a new fake Transport. + """ + super().__init__() + self.closing = False - def received_data(self, data: bytes) -> None: - """ - Forward all received data to our handler. - """ - self.handler.data_received(data) + def get_extra_info(self, _: str) -> None: + """ + Get extra info, which is always None. + """ + return + def is_closing(self) -> bool: + """ + Get the closing state. + """ + return self.closing -class MockedSite(web.TCPSite): - """ - Pretend to host a TCP site (actually UDP over fake IPv8 internet). - """ - - async def start(self) -> None: + def __init__(self, path: str = "", method: str = "GET", query: dict | None = None, + match_info: dict[str, str] | None = None, payload_writer: AbstractStreamWriter | None = None) -> None: """ - Start our nefarious rerouting. + Create a new MockRequest, just like if it were returned from aiohttp. """ - self._server = self._runner.server - await web.BaseSite.start(self) + message = RawRequestMessage(method, path, HttpVersion(1, 0), CIMultiDictProxy(CIMultiDict({})), + ((b'', b''),), True, None, False, False, URL(path)) + self._transport = MockRequest.Transport() + super().__init__(message=message, payload=StreamReader(), protocol=self, + payload_writer=payload_writer, task=None, loop=get_running_loop()) + self._query = query or {} + self._match_info = UrlMappingMatchInfo(match_info or {}, Mock()) - async def stop(self) -> None: + @property + def match_info(self) -> UrlMappingMatchInfo: """ - The `wait_for` implementation of our super does not work. - - Instead, we perform our own light-weight shutdown. + Get the match info (the infohash in the url). """ - self._runner._check_site(self) # noqa: SLF001 - await self._runner.shutdown() - self._runner._unreg_site(self) # noqa: SLF001 + return self._match_info - -class MockedRESTManager(RESTManager): - """ - Hook a fake site into the real rest manager. - """ - - async def start_site(self, runner: BaseRunner, host: str, port: int, ssl_context: SSLContext | None) -> None: + @property + def query(self) -> MultiDictProxy[str]: """ - Start accepting connections to the REST API. + Overwrite the query with the query passed in our constructor. """ - runner._server = MockServer(runner.server) # noqa: SLF001 - self.site = MockedSite(runner, host, port, ssl_context=ssl_context) - await self.site.start() + return MultiDictProxy(MultiDict(self._query)) - -class MockRestIPv8: - """ - Manager for IPv8 related objects during REST tests. - - Note that this is not the same as an IPv8 instance, neither is it the same as a MockIPv8 instance! However, many of - the same functionalities are offered. - """ - - def __init__(self, crypto_curve: str, overlay_classes: list[type[Community]], - settings: list[CommunitySettings]) -> None: - """ - Create a new MockRestIPv8 and forward arguments to the created overlay classes. - """ - self.endpoint = AutoMockEndpoint() - self.endpoint.open() - self.configuration = get_default_configuration() - self.configuration["working_directory"] = ":memory:" - self.network = Network() - self.my_peer = Peer(ECCrypto().generate_key(crypto_curve)) - base_settings = CommunitySettings(my_peer=self.my_peer, endpoint=self.endpoint, network=self.network) - for setting in settings: - setting.__dict__.update(base_settings.__dict__) - self.overlays = [overlay_cls(settings[i]) for i, overlay_cls in enumerate(overlay_classes)] - self.strategies: list[tuple[DiscoveryStrategy, int]] = [(MockWalk(overlay), 20) for overlay in self.overlays] - self.rest_manager = None - self.rest_port = 0 - self.overlay_lock = RLock() - - def get_overlay(self, overlay_cls: type[Community]) -> Community | None: + async def json(self, *, loads: JSONDecoder = DEFAULT_JSON_DECODER) -> dict: """ - Get any loaded overlay instance from a given class type, if it exists. + Get the json equivalent of the query (i.e., just the query). """ - return next((o for o in self.overlays if isinstance(o, overlay_cls)), None) + return self._query - def add_strategy(self, overlay: Community, strategy: DiscoveryStrategy, target_peers: int) -> None: + @property + def transport(self) -> asyncio.Transport | None: """ - Register a strategy to call every tick unless a target number of peers has been reached. - If the ``target_peers`` is equal to ``-1``, the strategy is always called. + Overwrite the transport with our fake transport. """ - with self.overlay_lock: - self.overlays.append(overlay) - self.strategies.append((strategy, target_peers)) + return self._transport - def unload_overlay(self, instance: Community) -> Awaitable: - """ - Unregister and unload a given community instance. - """ - self.overlays = [overlay for overlay in self.overlays if overlay != instance] - self.strategies = [(strategy, target_peers) for (strategy, target_peers) in self.strategies - if strategy.overlay != instance] - return maybe_coroutine(instance.unload) - async def produce_anonymized_endpoint(self) -> TunnelEndpoint: - """ - Create an anonymized endpoint. - """ - endpoint = TunnelEndpoint(AutoMockEndpoint()) - endpoint.open() - return endpoint - - async def start_api(self) -> None: - """ - Start the REST API. - """ - self.rest_manager = MockedRESTManager(self) - await self.rest_manager.start(0) - self.rest_port = cast(MockServer, self.rest_manager.site._server).port # noqa: SLF001 - - async def stop(self) -> None: - """ - Stop serving the REST API. - """ - await self.rest_manager.stop() - self.endpoint.close() - for overlay in self.overlays: - await overlay.unload() - - -class MockConnector(BaseConnector): +async def response_to_bytes(response: Response) -> bytes: """ - Connector that routes over the fake IPv8 internet. + Get the bytes of a RESTResponse's body. """ - - async def _create_connection(self, req: ClientRequest, traces: list[Trace], - timeout: ClientTimeout) -> ResponseHandler: - handler = ResponseHandler(self._loop) - handler.connection_made(IPv8Transport(handler.data_received, server_port=req.port)) - return handler + capture = BodyCapture() + if isinstance(response.body, bytes): + return response.body + await response.body.write(capture) + return capture.getvalue() -class RESTTestBase(TestBase): +async def response_to_json(response: Response) -> Any: # noqa: ANN401 """ - HTTP request superclass, which defines the common behavior between the different types of HTTP REST requests. + Get the JSON dict of a RESTResponse's body. """ - - async def initialize(self, overlay_classes: list[type[Community]], node_count: int, - settings: list[CommunitySettings]) -> None: - """ - Initialize a given number of nodes with instances of the given Community classes. - """ - self.overlay_classes = overlay_classes - self.nodes = [await self.create_node(settings) for _ in range(node_count)] - - for node in self.nodes: - for other in self.nodes: - if other == node: - continue - private_peer = other.my_peer - public_peer = Peer(private_peer.public_key, private_peer.address) - for overlay_class in overlay_classes: - node.network.discover_services(public_peer, overlay_class.community_id) - - async def create_node(self, settings: list[CommunitySettings]) -> MockRestIPv8: - """ - Create a new MockRestIPv8 and start its REST API. - """ - ipv8 = MockRestIPv8("curve25519", self.overlay_classes, settings) - await ipv8.start_api() - return ipv8 - - def node(self, i: int) -> MockRestIPv8: - """ - MockRestIPv8 is not actually a MockIPv8. So, we bend the rules here a little bit. - """ - return cast(MockRestIPv8, super().node(i)) - - async def introduce_nodes(self) -> None: - """ - Have each node send an introduction request to each other node. - """ - for node in self.nodes: - for other in self.nodes: - if other != node: - for overlay in node.overlays: - overlay.walk_to(other.endpoint.wan_address) - await self.deliver_messages() - - async def make_request(self, node: MockRestIPv8, endpoint: str, request_type: str, # noqa: PLR0913 - arguments: dict[str, str] | None = None, json_response: bool = True, - json: dict | None = None, expected_status: int = 200) -> Any: # noqa: ANN401 - """ - Forward an HTTP request of the specified type to a url, with the specified set of arguments. - - :param node: the destination node - :param endpoint: the endpoint of this request (i.e. http://:/) - :param request_type: the type of request (GET, POST, PUT, DELETE, etc.) - :param arguments: the arguments to be attached to the request. This should be a dictionary or None - :param json_response: whether the response is expected to be JSON - :param json: a JSON-serializable dictionary that is sent when making the request - :param expected_status: the status code returned in the response, defaults to 200 - :return: a dictionary object with the response - """ - # Using localhost in the URL will cause aiohttp to first try ::1, causing a 1s delay for each request - url = 'http://127.0.0.1:%d/%s' % (node.rest_port, endpoint) - headers = {'User-Agent': 'aiohttp'} - - async with ClientSession(connector=MockConnector()) as session, \ - session.request(request_type, url, json=json, params=arguments, headers=headers) as response: - self.assertEqual(response.status, expected_status, - "Expected HTTP status code %d, got %d" % (expected_status, response.status)) - return await response.json(content_type=None) if json_response else await response.read() + return loads(await response_to_bytes(response)) diff --git a/ipv8/test/REST/test_identity_endpoint.py b/ipv8/test/REST/test_identity_endpoint.py index 17d98b15c..7215a071c 100644 --- a/ipv8/test/REST/test_identity_endpoint.py +++ b/ipv8/test/REST/test_identity_endpoint.py @@ -3,19 +3,22 @@ import base64 import json import urllib.parse -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, cast from ...attestation.communication_manager import CommunicationChannel, CommunicationManager, PseudonymFolderManager from ...attestation.default_identity_formats import FORMATS from ...attestation.identity.community import IdentityCommunity, IdentitySettings from ...attestation.identity.manager import IdentityManager from ...attestation.wallet.community import AttestationCommunity, AttestationSettings +from ...REST.identity_endpoint import IdentityEndpoint +from ..base import TestBase from ..mocking.endpoint import AutoMockEndpoint -from ..REST.rest_base import MockRestIPv8, RESTTestBase +from .rest_base import MockRequest, response_to_json if TYPE_CHECKING: from ...community import CommunitySettings from ...types import PrivateKey + from ..mocking.ipv8 import MockIPv8 class MockPseudonymFolderManager(PseudonymFolderManager): @@ -28,7 +31,7 @@ def __init__(self) -> None: Create a new mocked pseudonym folder manager. """ super().__init__(".") - self.folder_contents: dict[str, bytes] = {} + self.folder_contents: dict[str, PrivateKey] = {} def get_or_create_private_key(self, name: str) -> PrivateKey: """ @@ -53,7 +56,7 @@ def list_pseudonym_files(self) -> list[str]: return list(self.folder_contents) -class TestIdentityEndpoint(RESTTestBase): +class TestIdentityEndpoint(TestBase[IdentityCommunity]): """ Class for testing the REST API of the IdentityEndpoint. """ @@ -64,25 +67,24 @@ async def setUp(self) -> None: """ super().setUp() + self.endpoints = [] self.pseudonym_directories: dict[str, bytes] = {} # Pseudonym to key-bytes mapping + self.initialize(IdentityCommunity, 2, IdentitySettings(identity_manager=IdentityManager(":memory:"), + working_directory=":memory:")) - await self.initialize([IdentityCommunity], 2, self.create_settings()) - - def create_settings(self) -> list[IdentitySettings]: - """ - Create settings for any node. - """ - return [IdentitySettings(identity_manager=IdentityManager(":memory:"), working_directory=':memory:')] - - async def create_node(self, settings: list[CommunitySettings]) -> MockRestIPv8: + def create_node(self, settings: CommunitySettings | None = None, create_dht: bool = False, + enable_statistics: bool = False) -> MockIPv8: """ We load each node i with a pseudonym `my_peer{i}`, which is the default IPv8 `my_peer` key. """ - ipv8 = await super().create_node(settings) - key_file_name = 'my_peer' + str(len(self.pseudonym_directories)) - communication_manager = ipv8.rest_manager.root_endpoint.endpoints['/identity'].communication_manager - communication_manager.working_directory = ':memory:' + self.endpoints.append(IdentityEndpoint()) + ipv8 = super().create_node(settings, create_dht, enable_statistics) + peer_num = len(self.pseudonym_directories) + key_file_name = "my_peer" + str(peer_num) + self.pseudonym_directories[key_file_name] = ipv8.my_peer.key.key_to_bin() + + communication_manager = CommunicationManager(ipv8, working_directory=":memory:") communication_manager.pseudonym_folder_manager = MockPseudonymFolderManager() communication_manager.pseudonym_folder_manager.get_or_create_private_key(key_file_name) @@ -90,19 +92,36 @@ async def create_node(self, settings: list[CommunitySettings]) -> MockRestIPv8: attestation_overlay = AttestationCommunity(AttestationSettings(my_peer=identity_overlay.my_peer, endpoint=identity_overlay.endpoint, network=identity_overlay.network, - working_directory=':memory:')) + working_directory=":memory:")) channel = CommunicationChannel(attestation_overlay, identity_overlay) communication_manager.channels[ipv8.my_peer.public_key.key_to_bin()] = channel communication_manager.name_to_channel[key_file_name] = channel self.pseudonym_directories[key_file_name] = ipv8.my_peer.key.key_to_bin() + self.rest_ep(peer_num).communication_manager = communication_manager + return ipv8 + def rest_ep(self, i: int) -> IdentityEndpoint: + """ + Shortcut to the REST endpoint of node i. + """ + return self.endpoints[i] + def communication_manager(self, i: int) -> CommunicationManager: """ Shortcut to the communication manager of node i. """ - return self.node(i).rest_manager.root_endpoint.endpoints['/identity'].communication_manager + return self.rest_ep(i).communication_manager + + async def clear_pseudonyms(self) -> None: + """ + We set up our tests with one pseudonym per peer by default: get rid of them. + """ + for peer_id in range(2): + await self.rest_ep(peer_id).remove_pseudonym( + MockRequest(f"identity/my_peer{peer_id}/remove", match_info={"pseudonym_name": f"my_peer{peer_id}"}) + ) async def introduce_pseudonyms(self) -> None: """ @@ -120,75 +139,58 @@ async def introduce_pseudonyms(self) -> None: overlay.walk_to(address) await self.deliver_messages() - async def wait_for(self, *args: Any, **kwargs) -> Any: # noqa: ANN401 - """ - Fire a make request and keep repeating this request until a non-empty response is returned. - """ - output = [] - while not output: - output = await self.make_request(*args) - await self.deliver_messages() - return output - - async def wait_for_requests(self, *args: Any, **kwargs) -> Any: # noqa: ANN401 - """ - Fire a make request and keep repeating this request until a response with the key "requests" is returned. - """ - requests = [] - while not requests: - output = await self.wait_for(*args) - if 'requests' in output: - requests = output['requests'] - else: - await self.deliver_messages() - return requests - async def test_list_pseudonyms_empty(self) -> None: """ Check that we do not start with any pseudonyms. """ - await self.make_request(self.node(0), 'identity/my_peer0/remove', 'get') - result = await self.make_request(self.node(0), 'identity', 'get') + await self.clear_pseudonyms() - self.assertDictEqual({'names': []}, result) + result = await response_to_json(await self.rest_ep(0).list_pseudonyms(MockRequest(path="identity"))) + + self.assertDictEqual({"names": []}, result) async def test_list_schemas(self) -> None: """ Check that the endpoint reports the available schemas correctly. """ - schemas = await self.make_request(self.node(0), 'identity/test_pseudonym/schemas', 'get') + result = await self.rest_ep(0).list_schemas(MockRequest("identity/test_pseudonym/schemas", + match_info={"pseudonym_name": "test_pseudonym"})) + schemas = await response_to_json(result) - self.assertSetEqual(set(FORMATS.keys()), set(schemas['schemas'])) + self.assertSetEqual(set(FORMATS.keys()), set(schemas["schemas"])) async def test_list_pseudonyms_one(self) -> None: """ Check that a loaded pseudonym is reported as such. """ - result = await self.make_request(self.node(0), 'identity', 'get') + result = await response_to_json(await self.rest_ep(0).list_pseudonyms(MockRequest(path="identity"))) - self.assertDictEqual({'names': ['my_peer0']}, result) + self.assertDictEqual({"names": ["my_peer0"]}, result) async def test_list_pseudonyms_many(self) -> None: """ Check that all loaded pseudonyms are reported as such. """ - pseudonyms = ['test_pseudonym1', 'test_pseudonym2', 'test_pseudonym3', 'test_pseudonym4'] + pseudonyms = ["test_pseudonym1", "test_pseudonym2", "test_pseudonym3", "test_pseudonym4"] for pseudonym in pseudonyms: - await self.make_request(self.node(0), f'identity/{pseudonym}/schemas', 'get') + await self.rest_ep(0).list_schemas(MockRequest(f"identity/{pseudonym}/schemas", + match_info={"pseudonym_name": pseudonym})) - result = await self.make_request(self.node(0), 'identity', 'get') + result = await response_to_json(await self.rest_ep(0).list_pseudonyms(MockRequest(path="identity"))) - self.assertSetEqual(set(pseudonyms) | {'my_peer0'}, set(result['names'])) + self.assertSetEqual(set(pseudonyms) | {"my_peer0"}, set(result["names"])) async def test_list_public_key_one(self) -> None: """ Check that we retrieve the pseudonym public key correctly. """ - result = await self.make_request(self.node(0), 'identity/test_pseudonym/public_key', 'get') - decoded_public_key = base64.b64decode(result['public_key']) + result = await response_to_json(await self.rest_ep(0).get_pseudonym_public_key( + MockRequest("identity/test_pseudonym/public_key", match_info={"pseudonym_name": "test_pseudonym"}) + )) + decoded_public_key = base64.b64decode(result["public_key"]) # This should have made the `test_pseudonym` private key file (corresponding to the reported public key). - private_key = self.communication_manager(0).pseudonym_folder_manager.folder_contents['test_pseudonym'] + private_key = self.communication_manager(0).pseudonym_folder_manager.folder_contents["test_pseudonym"] self.assertEqual(private_key.pub().key_to_bin(), decoded_public_key) @@ -196,14 +198,17 @@ async def test_list_public_key_many(self) -> None: """ Check that we retrieve the pseudonym public key correctly. """ - pseudonyms = ['test_pseudonym1', 'test_pseudonym2', 'test_pseudonym3', 'test_pseudonym4'] + pseudonyms = ["test_pseudonym1", "test_pseudonym2", "test_pseudonym3", "test_pseudonym4"] # Make sure all pseudonyms exist before querying their keys. # This is not necessary, but has the highest chance of exposing failures. for pseudonym in pseudonyms: - await self.make_request(self.node(0), f'identity/{pseudonym}/schemas', 'get') + await self.rest_ep(0).list_schemas(MockRequest(f"identity/{pseudonym}/schemas", + match_info={"pseudonym_name": pseudonym})) for pseudonym in pseudonyms: - result = await self.make_request(self.node(0), f'identity/{pseudonym}/public_key', 'get') - decoded_public_key = base64.b64decode(result['public_key']) + result = await response_to_json(await self.rest_ep(0).get_pseudonym_public_key( + MockRequest(f"identity/{pseudonym}/public_key", match_info={"pseudonym_name": pseudonym}) + )) + decoded_public_key = base64.b64decode(result["public_key"]) private_key = self.communication_manager(0).pseudonym_folder_manager.folder_contents[pseudonym] @@ -213,26 +218,38 @@ async def test_list_peers(self) -> None: """ Check if peers are correctly listed. """ - result1 = await self.make_request(self.node(0), 'identity/my_peer0/peers', 'get') - result2 = await self.make_request(self.node(1), 'identity/my_peer1/peers', 'get') + await self.clear_pseudonyms() # Start from nothing + + result1 = await response_to_json(await self.rest_ep(0).list_pseudonym_peers( + MockRequest("identity/my_peer0/peers", match_info={"pseudonym_name": "my_peer0"}) + )) + result2 = await response_to_json(await self.rest_ep(1).list_pseudonym_peers( + MockRequest("identity/my_peer1/peers", match_info={"pseudonym_name": "my_peer1"}) + )) - self.assertListEqual([], result1['peers']) - self.assertListEqual([], result2['peers']) + self.assertListEqual([], result1["peers"]) + self.assertListEqual([], result2["peers"]) - await self.introduce_pseudonyms() + await self.introduce_pseudonyms() # After walking, the peers should show up - result1 = await self.make_request(self.node(0), 'identity/my_peer0/peers', 'get') - result2 = await self.make_request(self.node(1), 'identity/my_peer1/peers', 'get') + result1 = await response_to_json(await self.rest_ep(0).list_pseudonym_peers( + MockRequest("identity/my_peer0/peers", match_info={"pseudonym_name": "my_peer0"}) + )) + result2 = await response_to_json(await self.rest_ep(1).list_pseudonym_peers( + MockRequest("identity/my_peer1/peers", match_info={"pseudonym_name": "my_peer1"}) + )) - self.assertEqual(1, len(result1['peers'])) - self.assertEqual(1, len(result2['peers'])) + self.assertEqual(1, len(result1["peers"])) + self.assertEqual(1, len(result2["peers"])) async def test_list_unload(self) -> None: """ Check if a pseudonym stops communicating on unload. """ - await self.make_request(self.node(0), 'identity/my_peer0/unload', 'get') - await self.make_request(self.node(1), 'identity/my_peer1/unload', 'get') + await self.rest_ep(0).unload_pseudonym(MockRequest("identity/my_peer0/unload", + match_info={"pseudonym_name": "my_peer0"})) + await self.rest_ep(1).unload_pseudonym(MockRequest("identity/my_peer1/unload", + match_info={"pseudonym_name": "my_peer1"})) self.assertListEqual([], self.node(0).overlays) self.assertListEqual([], self.node(1).overlays) @@ -241,190 +258,259 @@ async def test_list_credentials_empty(self) -> None: """ Check that we retrieve credentials correctly, if none exist. """ - result = await self.make_request(self.node(0), 'identity/my_peer0/credentials', 'get') + result = await response_to_json(await self.rest_ep(0).list_pseudonym_credentials( + MockRequest("identity/my_peer0/credentials", match_info={"pseudonym_name": "my_peer0"}) + )) - self.assertListEqual([], result['names']) + self.assertListEqual([], result["names"]) async def test_request_attestation(self) -> None: """ Check that requesting an attestation works. """ - b64_subject_key = (await self.make_request(self.node(0), 'identity/my_peer0/public_key', 'get'))['public_key'] - result = await self.make_request(self.node(1), 'identity/my_peer1/public_key', 'get') - qb64_authority_key = urllib.parse.quote(result['public_key'], safe='') - - await self.introduce_pseudonyms() + b64_subject_key = (await response_to_json(await self.rest_ep(0).get_pseudonym_public_key( + MockRequest("identity/my_peer0/public_key", match_info={"pseudonym_name": "my_peer0"}) + )))["public_key"] + b64_authority_key = (await response_to_json(await self.rest_ep(1).get_pseudonym_public_key( + MockRequest("identity/my_peer1/public_key", match_info={"pseudonym_name": "my_peer1"}) + )))["public_key"] + + request = await response_to_json(await self.rest_ep(0).create_pseudonym_credential( + MockRequest(f"identity/my_peer0/request/{urllib.parse.quote(b64_authority_key, safe='')}", "PUT", { + "name": "My attribute", + "schema": "id_metadata", + "metadata": {} + }, {"pseudonym_name": "my_peer0", "authority_key": b64_authority_key}) + )) + self.assertTrue(request["success"]) - request = await self.make_request(self.node(0), f'identity/my_peer0/request/{qb64_authority_key}', 'put', - json={ - "name": "My attribute", - "schema": "id_metadata", - "metadata": {}}) - self.assertTrue(request['success']) + await self.deliver_messages() - outstanding = await self.wait_for(self.node(1), 'identity/my_peer1/outstanding/attestations', 'get') + outstanding = await response_to_json(await self.rest_ep(1).list_pseudonym_outstanding_attestations( + MockRequest("identity/my_peer1/outstanding/attestations", match_info={"pseudonym_name": "my_peer1"}) + )) - self.assertEqual(1, len(outstanding['requests'])) - self.assertEqual(b64_subject_key, outstanding['requests'][0]['peer']) - self.assertEqual("My attribute", outstanding['requests'][0]['attribute_name']) - self.assertDictEqual({}, json.loads(outstanding['requests'][0]['metadata'])) + self.assertEqual(1, len(outstanding["requests"])) + self.assertEqual(b64_subject_key, outstanding["requests"][0]["peer"]) + self.assertEqual("My attribute", outstanding["requests"][0]["attribute_name"]) + self.assertDictEqual({}, json.loads(outstanding["requests"][0]["metadata"])) async def test_request_attestation_metadata(self) -> None: """ Check that requesting an attestation with metadata works. """ - b64_subject_key = (await self.make_request(self.node(0), 'identity/my_peer0/public_key', 'get'))['public_key'] - result = await self.make_request(self.node(1), 'identity/my_peer1/public_key', 'get') - qb64_authority_key = urllib.parse.quote(result['public_key'], safe='') - - await self.introduce_pseudonyms() + b64_subject_key = (await response_to_json(await self.rest_ep(0).get_pseudonym_public_key( + MockRequest("identity/my_peer0/public_key", match_info={"pseudonym_name": "my_peer0"}) + )))["public_key"] + b64_authority_key = (await response_to_json(await self.rest_ep(1).get_pseudonym_public_key( + MockRequest("identity/my_peer1/public_key", match_info={"pseudonym_name": "my_peer1"}) + )))["public_key"] + + request = await response_to_json(await self.rest_ep(0).create_pseudonym_credential( + MockRequest(f"identity/my_peer0/request/{urllib.parse.quote(b64_authority_key, safe='')}", "PUT", { + "name": "My attribute", + "schema": "id_metadata", + "metadata": {"Some key": "Some value"} + }, {"pseudonym_name": "my_peer0", "authority_key": b64_authority_key}) + )) + self.assertTrue(request["success"]) - request = await self.make_request(self.node(0), f'identity/my_peer0/request/{qb64_authority_key}', 'put', - json={ - "name": "My attribute", - "schema": "id_metadata", - "metadata": {"Some key": "Some value"}}) - self.assertTrue(request['success']) + await self.deliver_messages() - outstanding = await self.wait_for(self.node(1), 'identity/my_peer1/outstanding/attestations', 'get') + outstanding = await response_to_json(await self.rest_ep(1).list_pseudonym_outstanding_attestations( + MockRequest("identity/my_peer1/outstanding/attestations", match_info={"pseudonym_name": "my_peer1"}) + )) - self.assertEqual(1, len(outstanding['requests'])) - self.assertEqual(b64_subject_key, outstanding['requests'][0]['peer']) - self.assertEqual("My attribute", outstanding['requests'][0]['attribute_name']) - self.assertDictEqual({"Some key": "Some value"}, json.loads(outstanding['requests'][0]['metadata'])) + self.assertEqual(1, len(outstanding["requests"])) + self.assertEqual(b64_subject_key, outstanding["requests"][0]["peer"]) + self.assertEqual("My attribute", outstanding["requests"][0]["attribute_name"]) + self.assertDictEqual({"Some key": "Some value"}, json.loads(outstanding["requests"][0]["metadata"])) async def test_attest(self) -> None: """ Check that attesting to an attestation request with metadata works. """ - b64_subject_key = (await self.make_request(self.node(0), 'identity/my_peer0/public_key', 'get'))['public_key'] - qb64_subject_key = urllib.parse.quote(b64_subject_key, safe='') - b64_authority_key = (await self.make_request(self.node(1), 'identity/my_peer1/public_key', 'get'))['public_key'] - qb64_authority_key = urllib.parse.quote(b64_authority_key, safe='') + b64_subject_key = (await response_to_json(await self.rest_ep(0).get_pseudonym_public_key( + MockRequest("identity/my_peer0/public_key", match_info={"pseudonym_name": "my_peer0"}) + )))["public_key"] + b64_authority_key = (await response_to_json(await self.rest_ep(1).get_pseudonym_public_key( + MockRequest("identity/my_peer1/public_key", match_info={"pseudonym_name": "my_peer1"}) + )))["public_key"] metadata = {"Some key": "Some value"} request = {"name": "My attribute", "schema": "id_metadata", "metadata": metadata} - await self.introduce_pseudonyms() - await self.make_request(self.node(0), f'identity/my_peer0/request/{qb64_authority_key}', 'put', json=request) - await self.wait_for(self.node(1), 'identity/my_peer1/outstanding/attestations', 'get') + await self.rest_ep(0).create_pseudonym_credential( + MockRequest(f"identity/my_peer0/request/{urllib.parse.quote(b64_authority_key, safe='')}", "PUT", request, + {"pseudonym_name": "my_peer0", "authority_key": b64_authority_key}) + ) + await self.deliver_messages() - result = await self.make_request(self.node(1), f'identity/my_peer1/attest/{qb64_subject_key}', 'put', - json={ + result = await response_to_json(await self.rest_ep(1).attest_pseudonym_credential( + MockRequest(f"identity/my_peer1/attest/{urllib.parse.quote(b64_subject_key, safe='')}", "PUT", { "name": "My attribute", - "value": base64.b64encode(b'Some value').decode()}) - self.assertTrue(result['success']) + "value": base64.b64encode(b'Some value').decode() + }, {"pseudonym_name": "my_peer1", "subject_key": b64_subject_key}) + )) + self.assertTrue(result["success"]) + + await self.deliver_messages() # How node 0 sees itself after receiving the attestation - result = await self.wait_for(self.node(0), 'identity/my_peer0/credentials', 'get') - self.assertEqual(1, len(result['names'])) - self.assertEqual("My attribute", result['names'][0]['name']) - self.assertListEqual([b64_authority_key], result['names'][0]['attesters']) + result = await response_to_json(await self.rest_ep(0).list_pseudonym_credentials( + MockRequest("identity/my_peer0/credentials", match_info={"pseudonym_name": "my_peer0"}) + )) + self.assertEqual(1, len(result["names"])) + self.assertEqual("My attribute", result["names"][0]["name"]) + self.assertListEqual([b64_authority_key], result["names"][0]["attesters"]) for k, v in metadata.items(): - self.assertIn(k, result['names'][0]['metadata']) - self.assertEqual(v, result['names'][0]['metadata'][k]) + self.assertIn(k, result["names"][0]["metadata"]) + self.assertEqual(v, result["names"][0]["metadata"][k]) # How node 1 sees node 0 after making the attestation - result = await self.wait_for(self.node(1), f'identity/my_peer1/credentials/{qb64_subject_key}', 'get') - self.assertEqual(1, len(result['names'])) - self.assertEqual("My attribute", result['names'][0]['name']) - self.assertListEqual([b64_authority_key], result['names'][0]['attesters']) + result = await response_to_json(await self.rest_ep(1).list_subject_credentials( + MockRequest(f"identity/my_peer1/credentials/{urllib.parse.quote(b64_subject_key, safe='')}", + match_info={"pseudonym_name": "my_peer1", "subject_key": b64_subject_key}) + )) + self.assertEqual(1, len(result["names"])) + self.assertEqual("My attribute", result["names"][0]["name"]) + self.assertListEqual([b64_authority_key], result["names"][0]["attesters"]) for k, v in metadata.items(): - self.assertIn(k, result['names'][0]['metadata']) - self.assertEqual(v, result['names'][0]['metadata'][k]) + self.assertIn(k, result["names"][0]["metadata"]) + self.assertEqual(v, result["names"][0]["metadata"][k]) async def test_verify(self) -> None: """ Check that verifying a credential works. """ - self.nodes.append(await self.create_node(self.create_settings())) # We need a third node for this one + self.nodes.append(self.create_node(IdentitySettings(identity_manager=IdentityManager(":memory:"), + working_directory=":memory:"))) + await self.introduce_nodes() - b64_subject_key = (await self.make_request(self.node(0), 'identity/my_peer0/public_key', 'get'))['public_key'] - qb64_subject_key = urllib.parse.quote(b64_subject_key, safe='') - b64_authority_key = (await self.make_request(self.node(1), 'identity/my_peer1/public_key', 'get'))['public_key'] - qb64_authority_key = urllib.parse.quote(b64_authority_key, safe='') - b64_verifier_key = (await self.make_request(self.node(2), 'identity/my_peer2/public_key', 'get'))['public_key'] - qb64_verifier_key = urllib.parse.quote(b64_verifier_key, safe='') + b64_subject_key = (await response_to_json(await self.rest_ep(0).get_pseudonym_public_key( + MockRequest("identity/my_peer0/public_key", match_info={"pseudonym_name": "my_peer0"}) + )))["public_key"] + b64_authority_key = (await response_to_json(await self.rest_ep(1).get_pseudonym_public_key( + MockRequest("identity/my_peer1/public_key", match_info={"pseudonym_name": "my_peer1"}) + )))["public_key"] + b64_verifier_key = (await response_to_json(await self.rest_ep(2).get_pseudonym_public_key( + MockRequest("identity/my_peer2/public_key", match_info={"pseudonym_name": "my_peer2"}) + )))["public_key"] metadata = {"Some key": "Some value"} request = {"name": "My attribute", "schema": "id_metadata", "metadata": metadata} - attest = {"name": "My attribute", "value": base64.b64encode(b'Some value').decode()} + attest = {"name": "My attribute", "value": base64.b64encode(b"Some value").decode()} - await self.introduce_pseudonyms() - await self.make_request(self.node(0), f'identity/my_peer0/request/{qb64_authority_key}', 'put', json=request) - await self.wait_for(self.node(1), 'identity/my_peer1/outstanding/attestations', 'get') - await self.make_request(self.node(1), f'identity/my_peer1/attest/{qb64_subject_key}', 'put', json=attest) + await self.rest_ep(0).create_pseudonym_credential( + MockRequest(f"identity/my_peer0/request/{urllib.parse.quote(b64_authority_key, safe='')}", "PUT", request, + {"pseudonym_name": "my_peer0", "authority_key": b64_authority_key}) + ) + await self.deliver_messages() + await self.rest_ep(1).attest_pseudonym_credential( + MockRequest(f"identity/my_peer1/attest/{urllib.parse.quote(b64_subject_key, safe='')}", "PUT", attest, + {"pseudonym_name": "my_peer1", "subject_key": b64_subject_key}) + ) + await self.deliver_messages() - credentials = await self.wait_for(self.node(0), 'identity/my_peer0/credentials', 'get') + credentials = await response_to_json(await self.rest_ep(0).list_pseudonym_credentials( + MockRequest("identity/my_peer0/credentials", match_info={"pseudonym_name": "my_peer0"}) + )) attribute_hash = credentials["names"][0]["hash"] - result = await self.make_request(self.node(2), f'identity/my_peer2/verify/{qb64_subject_key}', 'put', - json={ - "hash": attribute_hash, - "value": attest["value"], - "schema": request["schema"]}) - self.assertTrue(result['success']) - - verification_requests = await self.wait_for_requests(self.node(0), - 'identity/my_peer0/outstanding/verifications', - 'get') - self.assertEqual(b64_verifier_key, verification_requests[0]['peer']) - self.assertEqual("My attribute", verification_requests[0]['attribute_name']) - - result = await self.make_request(self.node(0), f'identity/my_peer0/allow/{qb64_verifier_key}', 'put', - json={"name": "My attribute"}) - self.assertTrue(result['success']) + result = await response_to_json(await self.rest_ep(2).verify_pseudonym_credential( + MockRequest(f"identity/my_peer2/verify/{urllib.parse.quote(b64_subject_key, safe='')}", "PUT", { + "hash": attribute_hash, + "value": attest["value"], + "schema": request["schema"] + }, {"pseudonym_name": "my_peer2", "subject_key": b64_subject_key}) + )) + self.assertTrue(result["success"]) + await self.deliver_messages() + verification_requests = (await response_to_json(await self.rest_ep(0).list_pseudonym_outstanding_verifications( + MockRequest("identity/my_peer0/outstanding/verifications", match_info={"pseudonym_name": "my_peer0"}) + )))["requests"] + self.assertEqual(b64_verifier_key, verification_requests[0]["peer"]) + self.assertEqual("My attribute", verification_requests[0]["attribute_name"]) + + result = await response_to_json(await self.rest_ep(0).allow_pseudonym_verification( + MockRequest(f"identity/my_peer0/allow/{urllib.parse.quote(b64_verifier_key, safe='')}", "PUT", + {"name": "My attribute"}, {"pseudonym_name": "my_peer0", "verifier_key": b64_verifier_key}) + )) + self.assertTrue(result["success"]) await self.deliver_messages() - output = await self.make_request(self.node(2), 'identity/my_peer2/verifications', 'get') - self.assertEqual(1, len(output['outputs'])) - self.assertEqual(attribute_hash, output['outputs'][0]['hash']) - self.assertEqual(base64.b64encode(b'Some value').decode(), output['outputs'][0]['reference']) - self.assertGreaterEqual(output['outputs'][0]['match'], 0.98) + output = await response_to_json(await self.rest_ep(2).list_pseudonym_verification_output( + MockRequest("identity/my_peer2/verifications", match_info={"pseudonym_name": "my_peer2"}) + )) + + self.assertEqual(1, len(output["outputs"])) + self.assertEqual(attribute_hash, output["outputs"][0]["hash"]) + self.assertEqual(base64.b64encode(b"Some value").decode(), output["outputs"][0]["reference"]) + self.assertGreaterEqual(output["outputs"][0]["match"], 0.98) async def test_disallow_verify(self) -> None: """ Check that no verification is performed, if not allowed. """ - self.nodes.append(await self.create_node(self.create_settings())) # We need a third node for this one + self.nodes.append(self.create_node(IdentitySettings(identity_manager=IdentityManager(":memory:"), + working_directory=":memory:"))) + await self.introduce_nodes() - b64_subject_key = (await self.make_request(self.node(0), 'identity/my_peer0/public_key', 'get'))['public_key'] - qb64_subject_key = urllib.parse.quote(b64_subject_key, safe='') - b64_authority_key = (await self.make_request(self.node(1), 'identity/my_peer1/public_key', 'get'))['public_key'] - qb64_authority_key = urllib.parse.quote(b64_authority_key, safe='') - b64_verifier_key = (await self.make_request(self.node(2), 'identity/my_peer2/public_key', 'get'))['public_key'] - qb64_verifier_key = urllib.parse.quote(b64_verifier_key, safe='') + b64_subject_key = (await response_to_json(await self.rest_ep(0).get_pseudonym_public_key( + MockRequest("identity/my_peer0/public_key", match_info={"pseudonym_name": "my_peer0"}) + )))["public_key"] + b64_authority_key = (await response_to_json(await self.rest_ep(1).get_pseudonym_public_key( + MockRequest("identity/my_peer1/public_key", match_info={"pseudonym_name": "my_peer1"}) + )))["public_key"] + b64_verifier_key = (await response_to_json(await self.rest_ep(2).get_pseudonym_public_key( + MockRequest("identity/my_peer2/public_key", match_info={"pseudonym_name": "my_peer2"}) + )))["public_key"] metadata = {"Some key": "Some value"} request = {"name": "My attribute", "schema": "id_metadata", "metadata": metadata} attest = {"name": "My attribute", "value": base64.b64encode(b'Some value').decode()} - await self.introduce_pseudonyms() - await self.make_request(self.node(0), f'identity/my_peer0/request/{qb64_authority_key}', 'put', json=request) - await self.wait_for(self.node(1), 'identity/my_peer1/outstanding/attestations', 'get') - await self.make_request(self.node(1), f'identity/my_peer1/attest/{qb64_subject_key}', 'put', json=attest) + await self.rest_ep(0).create_pseudonym_credential( + MockRequest(f"identity/my_peer0/request/{urllib.parse.quote(b64_authority_key, safe='')}", "PUT", request, + {"pseudonym_name": "my_peer0", "authority_key": b64_authority_key}) + ) + await self.deliver_messages() + await self.rest_ep(1).attest_pseudonym_credential( + MockRequest(f"identity/my_peer1/attest/{urllib.parse.quote(b64_subject_key, safe='')}", "PUT", attest, + {"pseudonym_name": "my_peer1", "subject_key": b64_subject_key}) + ) + await self.deliver_messages() - credentials = await self.wait_for(self.node(0), 'identity/my_peer0/credentials', 'get') + credentials = await response_to_json(await self.rest_ep(0).list_pseudonym_credentials( + MockRequest("identity/my_peer0/credentials", match_info={"pseudonym_name": "my_peer0"}) + )) attribute_hash = credentials["names"][0]["hash"] - result = await self.make_request(self.node(2), f'identity/my_peer2/verify/{qb64_subject_key}', 'put', - json={ - "hash": attribute_hash, - "value": attest["value"], - "schema": request["schema"]}) - self.assertTrue(result['success']) + result = await response_to_json(await self.rest_ep(2).verify_pseudonym_credential( + MockRequest(f"identity/my_peer2/verify/{urllib.parse.quote(b64_subject_key, safe='')}", "PUT", { + "hash": attribute_hash, + "value": attest["value"], + "schema": request["schema"] + }, {"pseudonym_name": "my_peer2", "subject_key": b64_subject_key}) + )) + self.assertTrue(result["success"]) + await self.deliver_messages() - verification_requests = await self.wait_for_requests(self.node(0), - 'identity/my_peer0/outstanding/verifications', - 'get') - self.assertEqual(b64_verifier_key, verification_requests[0]['peer']) - self.assertEqual("My attribute", verification_requests[0]['attribute_name']) + verification_requests = (await response_to_json(await self.rest_ep(0).list_pseudonym_outstanding_verifications( + MockRequest("identity/my_peer0/outstanding/verifications", match_info={"pseudonym_name": "my_peer0"}) + )))["requests"] + self.assertEqual(b64_verifier_key, verification_requests[0]["peer"]) + self.assertEqual("My attribute", verification_requests[0]["attribute_name"]) - result = await self.make_request(self.node(0), f'identity/my_peer0/disallow/{qb64_verifier_key}', 'put', - json={"name": "My attribute"}) - self.assertTrue(result['success']) + result = await response_to_json(await self.rest_ep(0).disallow_pseudonym_verification( + MockRequest(f"identity/my_peer0/disallow/{urllib.parse.quote(b64_verifier_key, safe='')}", "PUT", + {"name": "My attribute"}, {"pseudonym_name": "my_peer0", "verifier_key": b64_verifier_key}) + )) + self.assertTrue(result["success"]) await self.deliver_messages() - output = await self.make_request(self.node(2), 'identity/my_peer2/verifications', 'get') + output = await response_to_json(await self.rest_ep(2).list_pseudonym_verification_output( + MockRequest("identity/my_peer2/verifications", match_info={"pseudonym_name": "my_peer2"}) + )) - self.assertEqual(0, len(output['outputs'])) + self.assertEqual(0, len(output["outputs"])) diff --git a/ipv8/test/REST/test_isolation_endpoint.py b/ipv8/test/REST/test_isolation_endpoint.py index 70caab8a3..7cf13d680 100644 --- a/ipv8/test/REST/test_isolation_endpoint.py +++ b/ipv8/test/REST/test_isolation_endpoint.py @@ -1,14 +1,17 @@ from __future__ import annotations import unittest +from asyncio import sleep from typing import TYPE_CHECKING, cast from ...bootstrapping.dispersy.bootstrapper import DispersyBootstrapper from ...configuration import DISPERSY_BOOTSTRAPPER -from ...messaging.anonymization.community import TunnelCommunity +from ...messaging.anonymization.community import TunnelCommunity, TunnelSettings +from ...REST.isolation_endpoint import IsolationEndpoint +from ..base import TestBase from ..mocking.community import MockCommunity from ..mocking.endpoint import AutoMockEndpoint, MockEndpoint, MockEndpointListener -from ..REST.rest_base import RESTTestBase +from .rest_base import MockRequest, response_to_json if TYPE_CHECKING: from ...types import Address @@ -31,7 +34,7 @@ class MockTunnelCommunity(TunnelCommunity, MockCommunity): Fake TunnelCommunity just for circuit management. """ - def __init__(self) -> None: + def __init__(self, settings: TunnelSettings) -> None: """ We don't actually initialize the TunnelCommunity, we just want it as a base class. """ @@ -45,7 +48,7 @@ def __init__(self) -> None: self.bootstrappers = [bootstrapper] -class TestIsolationEndpoint(RESTTestBase): +class TestIsolationEndpoint(TestBase[MockTunnelCommunity]): """ Tests for REST requests to the isolation endpoint. """ @@ -63,43 +66,47 @@ async def setUp(self) -> None: self.fake_endpoint_listener = MockEndpointListener(self.fake_endpoint) self.fake_endpoint.add_listener(self.fake_endpoint_listener) - await self.initialize([], 1, []) - self.ipv8 = self.node(0) - self.ipv8.overlay = MockTunnelCommunity() - self.ipv8.overlay.network = self.ipv8.network - self.ipv8.overlays.append(self.ipv8.overlay) + self.initialize(MockTunnelCommunity, 1, TunnelSettings(endpoint=self.fake_endpoint)) + + self.rest_ep = IsolationEndpoint() + self.rest_ep.session = self.node(0) + self.node(0).overlays.append(self.overlay(0)) def bootstrap_addresses(self) -> list[Address]: """ Get the bootstrapper ip addresses. """ - return cast(MockTunnelCommunity, self.ipv8.overlay).bootstrappers[0].ip_addresses + return cast(MockTunnelCommunity, self.node(0).overlay).bootstrappers[0].ip_addresses async def test_no_ip(self) -> None: """ Test if requests that do not specify an IP are rejected. """ - response = await self.make_request(self.ipv8, "isolation", "POST", - json={"port": 5, "exitnode": 1}, expected_status=400) + raw_response = await self.rest_ep.handle_post(MockRequest("isolation", "POST", {"port": 5, "exitnode": 1})) + response = await response_to_json(raw_response) + self.assertEqual(400, raw_response.status) self.assertFalse(response["success"]) async def test_no_port(self) -> None: """ Test if requests that do not specify a port are rejected. """ - response = await self.make_request(self.ipv8, "isolation", "POST", - json={"ip": "127.0.0.1", "exitnode": 1}, expected_status=400) + raw_response = await self.rest_ep.handle_post(MockRequest("isolation", "POST", {"ip": "127.0.0.1", + "exitnode": 1})) + response = await response_to_json(raw_response) + self.assertEqual(400, raw_response.status) self.assertFalse(response["success"]) async def test_no_choice(self) -> None: """ Test if requests that do not specify a to add either an exit node or a bootstrap server. """ - response = await self.make_request(self.ipv8, "isolation", "POST", - json={"ip": "127.0.0.1", "port": 5}, expected_status=400) + raw_response = await self.rest_ep.handle_post(MockRequest("isolation", "POST", {"ip": "127.0.0.1", "port": 5})) + response = await response_to_json(raw_response) + self.assertEqual(400, raw_response.status) self.assertFalse(response["success"]) @unittest.skipIf(AutoMockEndpoint.IPV6_ADDRESSES, "IPv6 not supported") @@ -111,12 +118,14 @@ async def test_add_bootstrap(self) -> None: """ ip, port = TestIsolationEndpoint.FAKE_BOOTSTRAP_ADDRESS - response = await self.make_request(self.ipv8, "isolation", "POST", - json={"ip": ip, "port": port, "bootstrapnode": 1}) + response = await response_to_json(await self.rest_ep.handle_post( + MockRequest("isolation", "POST", {"ip": ip, "port": port, "bootstrapnode": 1}) + )) + await sleep(0) # We only expect one async event, this is ~10x faster than deliver_messages() self.assertTrue(response["success"]) self.assertIn(TestIsolationEndpoint.FAKE_BOOTSTRAP_ADDRESS, self.bootstrap_addresses()) - self.assertIn(TestIsolationEndpoint.FAKE_BOOTSTRAP_ADDRESS, self.ipv8.network.blacklist) + self.assertIn(TestIsolationEndpoint.FAKE_BOOTSTRAP_ADDRESS, self.node(0).network.blacklist) self.assertLessEqual(1, len(self.fake_endpoint_listener.received_packets)) @unittest.skipIf(AutoMockEndpoint.IPV6_ADDRESSES, "IPv6 not supported") @@ -128,8 +137,10 @@ async def test_add_exit(self) -> None: """ ip, port = TestIsolationEndpoint.FAKE_BOOTSTRAP_ADDRESS - response = await self.make_request(self.ipv8, "isolation", "POST", - json={"ip": ip, "port": port, "exitnode": 1}) + response = await response_to_json(await self.rest_ep.handle_post( + MockRequest("isolation", "POST", {"ip": ip, "port": port, "exitnode": 1}) + )) + await sleep(0) # We only expect one async event, this is ~10x faster than deliver_messages() self.assertTrue(response["success"]) self.assertLessEqual(1, len(self.fake_endpoint_listener.received_packets)) diff --git a/ipv8/test/REST/test_network_endpoint.py b/ipv8/test/REST/test_network_endpoint.py index d685ad787..85da64ffb 100644 --- a/ipv8/test/REST/test_network_endpoint.py +++ b/ipv8/test/REST/test_network_endpoint.py @@ -3,10 +3,13 @@ from ...keyvault.crypto import default_eccrypto from ...peer import Peer -from ..REST.rest_base import RESTTestBase +from ...REST.network_endpoint import NetworkEndpoint +from ..base import TestBase +from ..mocking.community import MockCommunity +from .rest_base import MockRequest, response_to_json -class TestNetworkEndpoint(RESTTestBase): +class TestNetworkEndpoint(TestBase[MockCommunity]): """ Tests related to the network REST endpoint. """ @@ -20,14 +23,15 @@ async def setUp(self) -> None: Create a single node. """ super().setUp() - await self.initialize([], 1, []) - self.ipv8 = self.node(0) + self.initialize(MockCommunity, 1) + self.rest_ep = NetworkEndpoint() + self.rest_ep.session = self.node(0) async def test_no_peers(self) -> None: """ Check if the network endpoint returns no peers if it has no peers. """ - peer_response = await self.make_request(self.ipv8, "network", "GET") + peer_response = await response_to_json(await self.rest_ep.retrieve_peers(MockRequest("network"))) self.assertIn("peers", peer_response) self.assertDictEqual({}, peer_response["peers"]) @@ -36,9 +40,9 @@ async def test_one_peer_no_services(self) -> None: """ Check if the network endpoint returns its one known peer with no services. """ - self.ipv8.network.add_verified_peer(self.mock_peer) + self.node(0).network.add_verified_peer(self.mock_peer) - peer_response = await self.make_request(self.ipv8, "network", "GET") + peer_response = await response_to_json(await self.rest_ep.retrieve_peers(MockRequest("network"))) self.assertIn("peers", peer_response) self.assertIn(self.mock_b64mid, peer_response["peers"]) @@ -51,12 +55,12 @@ async def test_one_peer_one_service(self) -> None: """ Check if the network endpoint returns its one known peer with one service. """ - self.ipv8.network.add_verified_peer(self.mock_peer) + self.node(0).network.add_verified_peer(self.mock_peer) mock_service = bytes(list(range(20))) mock_b64service = base64.b64encode(mock_service).decode() - self.ipv8.network.discover_services(self.mock_peer, [mock_service]) + self.node(0).network.discover_services(self.mock_peer, [mock_service]) - peer_response = await self.make_request(self.ipv8, "network", "GET") + peer_response = await response_to_json(await self.rest_ep.retrieve_peers(MockRequest("network"))) self.assertIn("peers", peer_response) self.assertIn(self.mock_b64mid, peer_response["peers"]) @@ -69,12 +73,12 @@ async def test_one_peer_multiple_services(self) -> None: """ Check if the network endpoint returns its one known peer with multiple services. """ - self.ipv8.network.add_verified_peer(self.mock_peer) + self.node(0).network.add_verified_peer(self.mock_peer) mock_services = [bytes([random.randint(0, 255) for _ in range(20)]) for __ in range(30)] mock_b64services = [base64.b64encode(mock_service).decode() for mock_service in mock_services] - self.ipv8.network.discover_services(self.mock_peer, mock_services) + self.node(0).network.discover_services(self.mock_peer, mock_services) - peer_response = await self.make_request(self.ipv8, "network", "GET") + peer_response = await response_to_json(await self.rest_ep.retrieve_peers(MockRequest("network"))) self.assertIn("peers", peer_response) self.assertIn(self.mock_b64mid, peer_response["peers"]) @@ -91,12 +95,12 @@ async def test_multiple_peers_multiple_services(self) -> None: mock_b64services = {} for mock_peer in mock_peers: b64mid = base64.b64encode(mock_peer.mid).decode() - self.ipv8.network.add_verified_peer(mock_peer) + self.node(0).network.add_verified_peer(mock_peer) local_services = [bytes([random.randint(0, 255) for _ in range(20)]) for __ in range(30)] mock_b64services[b64mid] = [base64.b64encode(s).decode() for s in local_services] - self.ipv8.network.discover_services(mock_peer, local_services) + self.node(0).network.discover_services(mock_peer, local_services) - peer_response = await self.make_request(self.ipv8, "network", "GET") + peer_response = await response_to_json(await self.rest_ep.retrieve_peers(MockRequest("network"))) self.assertIn("peers", peer_response) for b64mid, peer_descriptor in peer_response["peers"].items(): diff --git a/ipv8/test/REST/test_overlays_endpoint.py b/ipv8/test/REST/test_overlays_endpoint.py index 3d0779ebf..9614d09c9 100644 --- a/ipv8/test/REST/test_overlays_endpoint.py +++ b/ipv8/test/REST/test_overlays_endpoint.py @@ -5,8 +5,10 @@ from ...keyvault.crypto import default_eccrypto from ...messaging.interfaces.statistics_endpoint import StatisticsEndpoint from ...peer import Peer +from ...REST.overlays_endpoint import OverlaysEndpoint +from ..base import TestBase from ..mocking.community import MockCommunity -from ..REST.rest_base import RESTTestBase +from .rest_base import MockRequest, response_to_json def hexlify(value: str) -> str: @@ -24,7 +26,7 @@ class MockCommunity2(MockCommunity): community_id = b'DifferentCommunityID' -class TestOverlaysEndpoint(RESTTestBase): +class TestOverlaysEndpoint(TestBase[MockCommunity]): """ Tests for REST requests to the overlays endpoint. """ @@ -34,33 +36,34 @@ async def setUp(self) -> None: Set up a single node. """ super().setUp() - await self.initialize([], 1, []) - self.ipv8 = self.node(0) + self.initialize(MockCommunity, 1) + self.rest_ep = OverlaysEndpoint() + self.rest_ep.session = self.node(0) - def mount_statistics(self, i: int, add_mock_community: bool = True) -> None: + def mount_statistics(self, add_mock_community: bool = True) -> None: """ - Add a statistics endpoint to the given node id and possibly load a mock community. + Add a statistics endpoint to the node and possibly load a mock community. """ - self.node(i).endpoint = StatisticsEndpoint(self.node(i).endpoint) - self.node(i).rest_manager.root_endpoint.endpoints['/overlays'].statistics_supported = True + self.node(0).endpoint = StatisticsEndpoint(self.node(0).endpoint) + self.rest_ep.statistics_supported = True if add_mock_community: - self.node(i).overlay = self.add_mock_community(i) + self.node(0).overlay = self.add_mock_community() - def add_mock_community(self, i: int, overlay_class: type[MockCommunity] = MockCommunity) -> MockCommunity: + def add_mock_community(self, overlay_class: type[MockCommunity] = MockCommunity) -> MockCommunity: """ - Add a given overlay class to the given node id. + Add a given overlay class to the node. """ mock_community = overlay_class() - mock_community.endpoint = self.node(i).endpoint - self.node(i).overlays.append(mock_community) + mock_community.endpoint = self.node(0).endpoint + self.node(0).overlays.append(mock_community) return mock_community async def test_no_overlays(self) -> None: """ Check if the overlays endpoint returns no overlays if it has no overlays. """ - response = await self.make_request(self.ipv8, "overlays", "GET") + response = await response_to_json(await self.rest_ep.get_overlays(MockRequest("overlays"))) self.assertIn("overlays", response) self.assertListEqual([], response["overlays"]) @@ -73,9 +76,9 @@ async def test_one_overlay_no_peers(self) -> None: expected_id = hexlify(mock_community.community_id) expected_peer = hexlify(mock_community.my_peer.public_key.key_to_bin()) mock_community.update_global_time(1337) - self.ipv8.overlays.append(mock_community) + self.node(0).overlays.append(mock_community) - response = await self.make_request(self.ipv8, "overlays", "GET") + response = await response_to_json(await self.rest_ep.get_overlays(MockRequest("overlays"))) self.assertIn("overlays", response) self.assertEqual(1, len(response["overlays"])) @@ -94,9 +97,9 @@ async def test_one_overlay_one_peer(self) -> None: expected_peer = Peer(default_eccrypto.generate_key("very-low"), ("1.2.3.4", 5)) mock_community.network.add_verified_peer(expected_peer) mock_community.network.discover_services(expected_peer, [mock_community.community_id]) - self.ipv8.overlays.append(mock_community) + self.node(0).overlays.append(mock_community) - response = await self.make_request(self.ipv8, "overlays", "GET") + response = await response_to_json(await self.rest_ep.get_overlays(MockRequest("overlays"))) self.assertIn("overlays", response) self.assertEqual(1, len(response["overlays"])) @@ -116,9 +119,9 @@ async def test_one_overlay_multiple_peers(self) -> None: expected_peer = Peer(default_eccrypto.generate_key("very-low"), ("1.2.3.4", 5)) mock_community.network.add_verified_peer(expected_peer) mock_community.network.discover_services(expected_peer, [mock_community.community_id]) - self.ipv8.overlays.append(mock_community) + self.node(0).overlays.append(mock_community) - response = await self.make_request(self.ipv8, "overlays", "GET") + response = await response_to_json(await self.rest_ep.get_overlays(MockRequest("overlays"))) self.assertIn("overlays", response) self.assertEqual(1, len(response["overlays"])) @@ -136,7 +139,7 @@ async def test_one_overlay_statistics(self) -> None: """ Check if the overlays endpoint returns overlay statistics correctly for one overlay. """ - self.mount_statistics(0) + self.mount_statistics() self.node(0).endpoint.add_sent_stat(self.overlay(0).get_prefix(), 245, 1337) expected_stats = { @@ -147,7 +150,7 @@ async def test_one_overlay_statistics(self) -> None: 'num_up': 1 } - response = await self.make_request(self.ipv8, "overlays", "GET") + response = await response_to_json(await self.rest_ep.get_overlays(MockRequest("overlays"))) self.assertIn("overlays", response) self.assertEqual(1, len(response["overlays"])) @@ -162,9 +165,9 @@ async def test_multiple_overlays(self) -> None: for i in range(overlay_count): mock_community = MockCommunity() mock_community.update_global_time(100 * i) - self.ipv8.overlays.append(mock_community) + self.node(0).overlays.append(mock_community) - response = await self.make_request(self.ipv8, "overlays", "GET") + response = await response_to_json(await self.rest_ep.get_overlays(MockRequest("overlays"))) self.assertIn("overlays", response) self.assertEqual(3, len(response["overlays"])) @@ -180,7 +183,7 @@ async def test_statistics_no_overlays(self) -> None: """ Check if no statistics are returned if no overlays are loaded. """ - response = await self.make_request(self.ipv8, "overlays/statistics", "GET") + response = await response_to_json(await self.rest_ep.get_statistics(MockRequest("overlays/statistics"))) self.assertIn("statistics", response) self.assertListEqual([], response["statistics"]) @@ -189,22 +192,22 @@ async def test_statistics_one_overlay(self) -> None: """ Check if statistics are returned for one loaded overlay. """ - self.mount_statistics(0) + self.mount_statistics() self.node(0).endpoint.add_sent_stat(self.overlay(0).get_prefix(), 245, 1337, 42.0) expected_stats = { - 'identifier': 245, - 'bytes_down': 0, - 'bytes_up': 1337, - 'num_down': 0, - 'num_up': 1, - 'first_measured_up': 42.0, - 'first_measured_down': 0, - 'last_measured_up': 42.0, - 'last_measured_down': 0 + "identifier": 245, + "bytes_down": 0, + "bytes_up": 1337, + "num_down": 0, + "num_up": 1, + "first_measured_up": 42.0, + "first_measured_down": 0, + "last_measured_up": 42.0, + "last_measured_down": 0 } - response = await self.make_request(self.ipv8, "overlays/statistics", "GET") + response = await response_to_json(await self.rest_ep.get_statistics(MockRequest("overlays/statistics"))) self.assertIn("statistics", response) self.assertEqual(1, len(response["statistics"])) @@ -218,13 +221,13 @@ async def test_statistics_one_overlay_with_unknown(self) -> None: """ Check if statistics are returned for one loaded overlay, with an unknown message. """ - self.mount_statistics(0) + self.mount_statistics() self.node(0).endpoint.add_sent_stat(self.overlay(0).get_prefix(), 245, 1337, 42.0) self.node(0).endpoint.add_sent_stat(self.overlay(0).get_prefix(), 69, 1492, 7.0) # 69 does not exist! self.assertIsNone(self.overlay(0).decode_map[69]) # Test invariant, use a number that does not exist. - response = await self.make_request(self.ipv8, "overlays/statistics", "GET") + response = await response_to_json(await self.rest_ep.get_statistics(MockRequest("overlays/statistics"))) self.assertIn("statistics", response) self.assertEqual(1, len(response["statistics"])) @@ -237,41 +240,47 @@ async def test_enable_stats_not_supported(self) -> None: """ Check if stats cannot be enabled on an endpoint that is not a StatisticsEndpoint. """ - response = await self.make_request(self.ipv8, "overlays/statistics", "POST", json={'enable': 'true'}, - expected_status=412) + raw_response = await self.rest_ep.enable_statistics(MockRequest("overlays/statistics", "POST", + {"enable": "true"})) + response = await response_to_json(raw_response) + self.assertEqual(412, raw_response.status) self.assertFalse(response["success"]) async def test_enable_stats_no_enable_param(self) -> None: """ Check if stats cannot be enabled when the "enable" parameter is missing. """ - self.mount_statistics(0) + self.mount_statistics() - response = await self.make_request(self.ipv8, "overlays/statistics", "POST", json={}, - expected_status=400) + raw_response = await self.rest_ep.enable_statistics(MockRequest("overlays/statistics", "POST", {})) + response = await response_to_json(raw_response) + self.assertEqual(400, raw_response.status) self.assertFalse(response["success"]) async def test_enable_stats_no_target(self) -> None: """ Check if stats cannot be enabled without specifying what overlay(s) to use. """ - self.mount_statistics(0) + self.mount_statistics() - response = await self.make_request(self.ipv8, "overlays/statistics", "POST", json={'enable': 'true'}, - expected_status=412) + raw_response = await self.rest_ep.enable_statistics(MockRequest("overlays/statistics", "POST", + {"enable": "true"})) + response = await response_to_json(raw_response) + self.assertEqual(412, raw_response.status) self.assertFalse(response["success"]) async def test_enable_stats_all(self) -> None: """ Check if stats are correctly returned for one "all" overlays. """ - self.mount_statistics(0) + self.mount_statistics() - response = await self.make_request(self.ipv8, "overlays/statistics", "POST", - json={'enable': 'true', 'all': 'true'}) + response = await response_to_json(await self.rest_ep.enable_statistics( + MockRequest("overlays/statistics", "POST", {"enable": "true", "all": "true"}) + )) self.assertTrue(response["success"]) self.assertIn(self.overlay(0).get_prefix(), self.overlay(0).endpoint.statistics) @@ -280,11 +289,12 @@ async def test_enable_stats_all_many(self) -> None: """ Check if stats are correctly returned for all overlays. """ - self.mount_statistics(0) - mock_community2 = self.add_mock_community(0, MockCommunity2) + self.mount_statistics() + mock_community2 = self.add_mock_community(MockCommunity2) - response = await self.make_request(self.ipv8, "overlays/statistics", "POST", - json={'enable': 'true', 'all': 'true'}) + response = await response_to_json(await self.rest_ep.enable_statistics( + MockRequest("overlays/statistics", "POST", {"enable": "true", "all": "true"}) + )) self.assertTrue(response["success"]) self.assertIn(self.overlay(0).get_prefix(), self.overlay(0).endpoint.statistics) @@ -294,11 +304,12 @@ async def test_enable_stats_one_exclude(self) -> None: """ Check if stats are correctly returned for a specific overlay, excluding another. """ - self.mount_statistics(0) - mock_community2 = self.add_mock_community(0, MockCommunity2) + self.mount_statistics() + mock_community2 = self.add_mock_community(MockCommunity2) - response = await self.make_request(self.ipv8, "overlays/statistics", "POST", - json={'enable': 'true', 'overlay_name': 'MockCommunity'}) + response = await response_to_json(await self.rest_ep.enable_statistics( + MockRequest("overlays/statistics", "POST", {"enable": "true", "overlay_name": "MockCommunity"}) + )) self.assertTrue(response["success"]) self.assertIn(self.overlay(0).get_prefix(), self.overlay(0).endpoint.statistics) @@ -308,13 +319,15 @@ async def test_enable_stats_one_include(self) -> None: """ Check if stats are correctly returned for a specific overlay, including another. """ - self.mount_statistics(0) - mock_community2 = self.add_mock_community(0, MockCommunity2) + self.mount_statistics() + mock_community2 = self.add_mock_community(MockCommunity2) - response = await self.make_request(self.ipv8, "overlays/statistics", "POST", - json={'enable': 'true', 'overlay_name': 'MockCommunity'}) - response2 = await self.make_request(self.ipv8, "overlays/statistics", "POST", - json={'enable': 'true', 'overlay_name': 'MockCommunity2'}) + response = await response_to_json(await self.rest_ep.enable_statistics( + MockRequest("overlays/statistics", "POST", {"enable": "true", "overlay_name": "MockCommunity"}) + )) + response2 = await response_to_json(await self.rest_ep.enable_statistics( + MockRequest("overlays/statistics", "POST", {"enable": "true", "overlay_name": "MockCommunity2"}) + )) self.assertTrue(response["success"]) self.assertTrue(response2["success"]) diff --git a/ipv8/test/mocking/community.py b/ipv8/test/mocking/community.py index cb5c7b194..9e7a57a3e 100644 --- a/ipv8/test/mocking/community.py +++ b/ipv8/test/mocking/community.py @@ -5,13 +5,15 @@ from ...peerdiscovery.network import Network from .endpoint import AutoMockEndpoint +DEFAULT_COMMUNITY_SETTINGS = CommunitySettings() + class MockCommunity(DiscoveryCommunity): """ Semi-inert version of the DiscoveryCommunity for testing. """ - def __init__(self) -> None: + def __init__(self, settings: CommunitySettings = DEFAULT_COMMUNITY_SETTINGS) -> None: """ Create a new MockCommunity. """ diff --git a/ipv8/test/mocking/ipv8.py b/ipv8/test/mocking/ipv8.py index a323b001e..22dec776c 100644 --- a/ipv8/test/mocking/ipv8.py +++ b/ipv8/test/mocking/ipv8.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio from typing import TYPE_CHECKING from ...community import CommunitySettings @@ -15,10 +16,22 @@ if TYPE_CHECKING: from collections.abc import Awaitable, Generator + from ...messaging.anonymization.community import TunnelCommunity from ...peerdiscovery.discovery import DiscoveryStrategy from ...types import Community, Overlay +class MockTunnelEndpoint(AutoMockEndpoint): + """ + An AutoMockEndpoint that serves the TunnelEndpoint API without doing anything. + """ + + def set_tunnel_community(self, tunnel_community: TunnelCommunity | None, hops: int = 1) -> None: + """ + Pretend to adopt a TunnelCommunity. + """ + + class MockIPv8: """ Manager for IPv8 related objects during tests. @@ -111,3 +124,12 @@ async def stop(self) -> None: await self.overlay.unload() if self.dht: await self.dht.unload() + + async def produce_anonymized_endpoint(self) -> MockTunnelEndpoint: + """ + Create a fake anonymized endpoint. + """ + endpoint = MockTunnelEndpoint() + endpoint.open() + await asyncio.sleep(0) + return endpoint