From 81e6de18953b54db039d7fa67fba358b7b177b37 Mon Sep 17 00:00:00 2001 From: Dmitri Bogomolov <4glitch@gmail.com> Date: Sat, 11 Dec 2021 18:24:54 +0200 Subject: [PATCH 1/7] Started a standalone API test case --- src/tests/test_api_thread.py | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/tests/test_api_thread.py diff --git a/src/tests/test_api_thread.py b/src/tests/test_api_thread.py new file mode 100644 index 00000000..c5bd6576 --- /dev/null +++ b/src/tests/test_api_thread.py @@ -0,0 +1,53 @@ +import time +import unittest + +from six.moves import xmlrpc_client + +from pybitmessage import pathmagic + + +class TestAPIThread(unittest.TestCase): + """Test case running the API thread""" + + @classmethod + def setUpClass(cls): + pathmagic.setup() # need this because of import state in network ): + + import helper_sql + import helper_startup + import state + from bmconfigparser import BMConfigParser + + class SqlReadyMock(object): + @staticmethod + def wait(): + return + + helper_sql.sql_ready = SqlReadyMock + cls.state = state + helper_startup.loadConfig() + # helper_startup.fixSocket() + config = BMConfigParser() + + config.set( + 'bitmessagesettings', 'apiusername', 'username') + config.set( + 'bitmessagesettings', 'apipassword', 'password') + config.save() + + import api + cls.thread = api.singleAPI() + cls.thread.daemon = True + cls.thread.start() + time.sleep(3) + cls.api = xmlrpc_client.ServerProxy( + "http://username:password@127.0.0.1:8442/") + + def test_connection(self): + """API command 'helloWorld'""" + self.assertEqual( + self.api.helloWorld('hello', 'world'), 'hello-world') + + @classmethod + def tearDownClass(cls): + cls.state.shutdown = 1 -- 2.45.1 From a16a0cea5c46b6f96ef94f9a090c99b50f840290 Mon Sep 17 00:00:00 2001 From: Dmitri Bogomolov <4glitch@gmail.com> Date: Sat, 11 Dec 2021 19:22:57 +0200 Subject: [PATCH 2/7] Rewrite imports in api for python3 and cut out those looking too tricky --- src/api.py | 49 +++++++++++++++++++++--------------- src/tests/test_api_thread.py | 8 +++--- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/api.py b/src/api.py index 56a23220..495dd980 100644 --- a/src/api.py +++ b/src/api.py @@ -58,27 +58,25 @@ For further examples please reference `.tests.test_api`. """ import base64 -import ConfigParser import errno import hashlib -import httplib import json import random import socket import subprocess # nosec B404 import time -import xmlrpclib from binascii import hexlify, unhexlify -from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer from struct import pack, unpack +from six.moves import configparser, http_client, queue, xmlrpc_server + import defaults import helper_inbox import helper_sent -import network.stats import proofofwork import queues import shared + import shutdown import state from addresses import ( @@ -90,11 +88,17 @@ from addresses import ( ) from bmconfigparser import config from debug import logger -from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery, sqlStoredProcedure, sql_ready +from helper_sql import ( + SqlBulkExecute, sqlExecute, sqlQuery, sqlStoredProcedure, sql_ready) from inventory import Inventory -from network import BMConnectionPool + +try: + import network.stats as network_stats + from network import BMConnectionPool +except ImportError: + network_stats = None + from network.threads import StoppableThread -from six.moves import queue from version import softwareVersion try: # TODO: write tests for XML vulnerabilities @@ -164,7 +168,7 @@ class ErrorCodes(type): return result -class APIError(xmlrpclib.Fault): +class APIError(xmlrpc_server.Fault): """ APIError exception class @@ -212,7 +216,7 @@ class singleAPI(StoppableThread): except AttributeError: errno.WSAEADDRINUSE = errno.EADDRINUSE - RPCServerBase = SimpleXMLRPCServer + RPCServerBase = xmlrpc_server.SimpleXMLRPCServer ct = 'text/xml' if config.safeGet( 'bitmessagesettings', 'apivariant') == 'json': @@ -353,7 +357,7 @@ class command(object): # pylint: disable=too-few-public-methods # Modified by Jonathan Warren (Atheros). # Further modified by the Bitmessage developers # http://code.activestate.com/recipes/501148 -class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): +class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler): """The main API handler""" # pylint: disable=protected-access @@ -394,7 +398,7 @@ class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): validuser = self.APIAuthenticateClient() if not validuser: time.sleep(2) - self.send_response(httplib.UNAUTHORIZED) + self.send_response(http_client.UNAUTHORIZED) self.end_headers() return # "RPC Username or password incorrect or HTTP header" @@ -411,11 +415,11 @@ class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): ) except Exception: # This should only happen if the module is buggy # internal error, report as HTTP server error - self.send_response(httplib.INTERNAL_SERVER_ERROR) + self.send_response(http_client.INTERNAL_SERVER_ERROR) self.end_headers() else: # got a valid XML RPC response - self.send_response(httplib.OK) + self.send_response(http_client.OK) self.send_header("Content-type", self.server.content_type) self.send_header("Content-length", str(len(response))) @@ -860,7 +864,7 @@ class BMRPCDispatcher(object): ' Use deleteAddress API call instead.') try: self.config.remove_section(address) - except ConfigParser.NoSectionError: + except configparser.NoSectionError: raise APIError( 13, 'Could not find this address in your keys.dat file.') self.config.save() @@ -877,7 +881,7 @@ class BMRPCDispatcher(object): address = addBMIfNotPresent(address) try: self.config.remove_section(address) - except ConfigParser.NoSectionError: + except configparser.NoSectionError: raise APIError( 13, 'Could not find this address in your keys.dat file.') self.config.save() @@ -1131,7 +1135,7 @@ class BMRPCDispatcher(object): self._verifyAddress(fromAddress) try: fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled') - except ConfigParser.NoSectionError: + except configparser.NoSectionError: raise APIError( 13, 'Could not find your fromAddress in the keys.dat file.') if not fromAddressEnabled: @@ -1176,7 +1180,7 @@ class BMRPCDispatcher(object): self._verifyAddress(fromAddress) try: fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled') - except ConfigParser.NoSectionError: + except configparser.NoSectionError: raise APIError( 13, 'Could not find your fromAddress in the keys.dat file.') if not fromAddressEnabled: @@ -1438,7 +1442,10 @@ class BMRPCDispatcher(object): or "connectedAndReceivingIncomingConnections". """ - connections_num = len(network.stats.connectedHostsList()) + try: + connections_num = len(network_stats.connectedHostsList()) + except AttributeError: + raise APIError(21, 'Could not import network_stats.') if connections_num == 0: networkStatus = 'notConnected' elif state.clientHasReceivedIncomingConnections: @@ -1450,7 +1457,7 @@ class BMRPCDispatcher(object): 'numberOfMessagesProcessed': state.numberOfMessagesProcessed, 'numberOfBroadcastsProcessed': state.numberOfBroadcastsProcessed, 'numberOfPubkeysProcessed': state.numberOfPubkeysProcessed, - 'pendingDownload': network.stats.pendingDownload(), + 'pendingDownload': network_stats.pendingDownload(), 'networkStatus': networkStatus, 'softwareName': 'PyBitmessage', 'softwareVersion': softwareVersion @@ -1462,6 +1469,8 @@ class BMRPCDispatcher(object): Returns bitmessage connection information as dict with keys *inbound*, *outbound*. """ + if network_stats is None: + raise APIError(21, 'Could not import network_stats.') inboundConnections = [] outboundConnections = [] for i in BMConnectionPool().inboundConnections.values(): diff --git a/src/tests/test_api_thread.py b/src/tests/test_api_thread.py index c5bd6576..147fff5d 100644 --- a/src/tests/test_api_thread.py +++ b/src/tests/test_api_thread.py @@ -29,11 +29,9 @@ class TestAPIThread(unittest.TestCase): # helper_startup.fixSocket() config = BMConfigParser() - config.set( - 'bitmessagesettings', 'apiusername', 'username') - config.set( - 'bitmessagesettings', 'apipassword', 'password') - config.save() + config.set('bitmessagesettings', 'apiusername', 'username') + config.set('bitmessagesettings', 'apipassword', 'password') + config.set('inventory', 'storage', 'filesystem') import api cls.thread = api.singleAPI() -- 2.45.1 From 2c2a41d1054f87601119362416ce84d6dd3b0ba7 Mon Sep 17 00:00:00 2001 From: Dmitri Bogomolov <4glitch@gmail.com> Date: Sat, 11 Dec 2021 20:01:10 +0200 Subject: [PATCH 3/7] Add an obvious test for the 'statusBar' command and remove some junk introduced in 9a194f0. --- src/api.py | 18 ++---------------- src/tests/test_api.py | 14 +------------- src/tests/test_api_thread.py | 23 ++++++++++++++++++++++- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/api.py b/src/api.py index 495dd980..277305b1 100644 --- a/src/api.py +++ b/src/api.py @@ -68,7 +68,7 @@ import time from binascii import hexlify, unhexlify from struct import pack, unpack -from six.moves import configparser, http_client, queue, xmlrpc_server +from six.moves import configparser, http_client, xmlrpc_server import defaults import helper_inbox @@ -1502,25 +1502,11 @@ class BMRPCDispatcher(object): """Test two numeric params""" return a + b - @testmode('clearUISignalQueue') - def HandleclearUISignalQueue(self): - """clear UISignalQueue""" - queues.UISignalQueue.queue.clear() - return "success" - @command('statusBar') def HandleStatusBar(self, message): """Update GUI statusbar message""" queues.UISignalQueue.put(('updateStatusBar', message)) - - @testmode('getStatusBar') - def HandleGetStatusBar(self): - """Get GUI statusbar message""" - try: - _, data = queues.UISignalQueue.get(block=False) - except queue.Empty: - return None - return data + return "success" @testmode('undeleteMessage') def HandleUndeleteMessage(self, msgid): diff --git a/src/tests/test_api.py b/src/tests/test_api.py index 95448947..d599e0a5 100644 --- a/src/tests/test_api.py +++ b/src/tests/test_api.py @@ -13,7 +13,7 @@ import psutil from .samples import ( sample_deterministic_addr3, sample_deterministic_addr4, sample_seed, - sample_inbox_msg_ids, sample_statusbar_msg, + sample_inbox_msg_ids, sample_subscription_addresses, sample_subscription_name ) @@ -88,18 +88,6 @@ class TestAPI(TestAPIProto): 'API Error 0020: Invalid method: test' ) - def test_statusbar_method(self): - """Test statusbar method""" - self.api.clearUISignalQueue() - self.assertEqual( - self.api.statusBar(sample_statusbar_msg), - 'null' - ) - self.assertEqual( - self.api.getStatusBar(), - sample_statusbar_msg - ) - def test_message_inbox(self): """Test message inbox methods""" self.assertEqual( diff --git a/src/tests/test_api_thread.py b/src/tests/test_api_thread.py index 147fff5d..6eff6c77 100644 --- a/src/tests/test_api_thread.py +++ b/src/tests/test_api_thread.py @@ -1,10 +1,12 @@ import time import unittest -from six.moves import xmlrpc_client +from six.moves import queue, xmlrpc_client from pybitmessage import pathmagic +from .samples import sample_statusbar_msg # any + class TestAPIThread(unittest.TestCase): """Test case running the API thread""" @@ -15,16 +17,22 @@ class TestAPIThread(unittest.TestCase): import helper_sql import helper_startup + import queues import state from bmconfigparser import BMConfigParser + # pylint: disable=too-few-public-methods class SqlReadyMock(object): + """Mock helper_sql.sql_ready event with dummy class""" @staticmethod def wait(): + """Don't wait, return immediately""" return helper_sql.sql_ready = SqlReadyMock cls.state = state + cls.queues = queues + helper_startup.loadConfig() # helper_startup.fixSocket() config = BMConfigParser() @@ -46,6 +54,19 @@ class TestAPIThread(unittest.TestCase): self.assertEqual( self.api.helloWorld('hello', 'world'), 'hello-world') + def test_statusbar(self): + """Check UISignalQueue after issuing the 'statusBar' command""" + self.queues.UISignalQueue.queue.clear() + self.assertEqual( + self.api.statusBar(sample_statusbar_msg), 'success') + try: + cmd, data = self.queues.UISignalQueue.get(block=False) + except queue.Empty: + self.fail('UISignalQueue is empty!') + + self.assertEqual(cmd, 'updateStatusBar') + self.assertEqual(data, sample_statusbar_msg) + @classmethod def tearDownClass(cls): cls.state.shutdown = 1 -- 2.45.1 From 1b7a642d6a99f61e154dd097fc8f98357364083c Mon Sep 17 00:00:00 2001 From: Lee Miller Date: Fri, 10 Jun 2022 04:15:49 +0300 Subject: [PATCH 4/7] Rewrite TestAPIThread as subclass of TestPartialRun --- src/tests/test_api_thread.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/tests/test_api_thread.py b/src/tests/test_api_thread.py index 6eff6c77..203a035f 100644 --- a/src/tests/test_api_thread.py +++ b/src/tests/test_api_thread.py @@ -1,25 +1,22 @@ +"""TestAPIThread class definition""" + import time -import unittest from six.moves import queue, xmlrpc_client -from pybitmessage import pathmagic - +from .partial import TestPartialRun from .samples import sample_statusbar_msg # any -class TestAPIThread(unittest.TestCase): +class TestAPIThread(TestPartialRun): """Test case running the API thread""" @classmethod def setUpClass(cls): - pathmagic.setup() # need this because of import state in network ): + super(TestAPIThread, cls).setUpClass() import helper_sql - import helper_startup import queues - import state - from bmconfigparser import BMConfigParser # pylint: disable=too-few-public-methods class SqlReadyMock(object): @@ -30,16 +27,11 @@ class TestAPIThread(unittest.TestCase): return helper_sql.sql_ready = SqlReadyMock - cls.state = state cls.queues = queues - helper_startup.loadConfig() - # helper_startup.fixSocket() - config = BMConfigParser() - - config.set('bitmessagesettings', 'apiusername', 'username') - config.set('bitmessagesettings', 'apipassword', 'password') - config.set('inventory', 'storage', 'filesystem') + cls.config.set('bitmessagesettings', 'apiusername', 'username') + cls.config.set('bitmessagesettings', 'apipassword', 'password') + cls.config.set('inventory', 'storage', 'filesystem') import api cls.thread = api.singleAPI() @@ -66,7 +58,3 @@ class TestAPIThread(unittest.TestCase): self.assertEqual(cmd, 'updateStatusBar') self.assertEqual(data, sample_statusbar_msg) - - @classmethod - def tearDownClass(cls): - cls.state.shutdown = 1 -- 2.45.1 From 2501212a82c58c7d209b3ab54873d4d3f5c34ad7 Mon Sep 17 00:00:00 2001 From: Lee Miller Date: Sun, 12 Jun 2022 02:02:48 +0300 Subject: [PATCH 5/7] Fix py3 incompatibilities in api: - bytes in BMXMLRPCRequestHandler (copied relevant lines from xmlrpc.server once again), - used @six.add_metaclass for the metaclass and six.iteritems() Closes: #1900 --- src/api.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/api.py b/src/api.py index 277305b1..8b256de3 100644 --- a/src/api.py +++ b/src/api.py @@ -1,5 +1,5 @@ # Copyright (c) 2012-2016 Jonathan Warren -# Copyright (c) 2012-2022 The Bitmessage developers +# Copyright (c) 2012-2023 The Bitmessage developers """ This is not what you run to start the Bitmessage API. @@ -68,6 +68,7 @@ import time from binascii import hexlify, unhexlify from struct import pack, unpack +import six from six.moves import configparser, http_client, xmlrpc_server import defaults @@ -98,7 +99,7 @@ try: except ImportError: network_stats = None -from network.threads import StoppableThread +from network import StoppableThread from version import softwareVersion try: # TODO: write tests for XML vulnerabilities @@ -160,7 +161,7 @@ class ErrorCodes(type): def __new__(mcs, name, bases, namespace): result = super(ErrorCodes, mcs).__new__(mcs, name, bases, namespace) - for code in mcs._CODES.iteritems(): + for code in six.iteritems(mcs._CODES): # beware: the formatting is adjusted for list-table result.__doc__ += """ * - %04i - %s @@ -388,10 +389,14 @@ class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler): L = [] while size_remaining: chunk_size = min(size_remaining, max_chunk_size) - L.append(self.rfile.read(chunk_size)) + chunk = self.rfile.read(chunk_size) + if not chunk: + break + L.append(chunk) size_remaining -= len(L[-1]) - data = ''.join(L) + data = b''.join(L) + # data = self.decode_request_content(data) # pylint: disable=attribute-defined-outside-init self.cookies = [] @@ -448,7 +453,8 @@ class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler): if 'Authorization' in self.headers: # handle Basic authentication encstr = self.headers.get('Authorization').split()[1] - emailid, password = encstr.decode('base64').split(':') + emailid, password = base64.b64decode( + encstr).decode('utf-8').split(':') return ( emailid == config.get( 'bitmessagesettings', 'apiusername' @@ -464,9 +470,9 @@ class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler): # pylint: disable=no-self-use,no-member,too-many-public-methods +@six.add_metaclass(CommandHandler) class BMRPCDispatcher(object): """This class is used to dispatch API commands""" - __metaclass__ = CommandHandler @staticmethod def _decode(text, decode_type): -- 2.45.1 From 7c153c0eb8f379b81523d8225be2e45636d34e9d Mon Sep 17 00:00:00 2001 From: Lee Miller Date: Sat, 2 Dec 2023 00:43:42 +0200 Subject: [PATCH 6/7] Mock network.stats for python3 in tests.partial --- src/api.py | 17 +++++++---------- src/mock/network.py | 15 +++++++++------ src/tests/partial.py | 5 +++++ src/tests/test_api_thread.py | 7 +++++++ 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/api.py b/src/api.py index 8b256de3..a4b0aed5 100644 --- a/src/api.py +++ b/src/api.py @@ -94,12 +94,11 @@ from helper_sql import ( from inventory import Inventory try: - import network.stats as network_stats from network import BMConnectionPool except ImportError: - network_stats = None + BMConnectionPool = None -from network import StoppableThread +from network import stats, StoppableThread from version import softwareVersion try: # TODO: write tests for XML vulnerabilities @@ -1448,10 +1447,8 @@ class BMRPCDispatcher(object): or "connectedAndReceivingIncomingConnections". """ - try: - connections_num = len(network_stats.connectedHostsList()) - except AttributeError: - raise APIError(21, 'Could not import network_stats.') + connections_num = len(stats.connectedHostsList()) + if connections_num == 0: networkStatus = 'notConnected' elif state.clientHasReceivedIncomingConnections: @@ -1463,7 +1460,7 @@ class BMRPCDispatcher(object): 'numberOfMessagesProcessed': state.numberOfMessagesProcessed, 'numberOfBroadcastsProcessed': state.numberOfBroadcastsProcessed, 'numberOfPubkeysProcessed': state.numberOfPubkeysProcessed, - 'pendingDownload': network_stats.pendingDownload(), + 'pendingDownload': stats.pendingDownload(), 'networkStatus': networkStatus, 'softwareName': 'PyBitmessage', 'softwareVersion': softwareVersion @@ -1475,8 +1472,8 @@ class BMRPCDispatcher(object): Returns bitmessage connection information as dict with keys *inbound*, *outbound*. """ - if network_stats is None: - raise APIError(21, 'Could not import network_stats.') + if BMConnectionPool is None: + raise APIError(21, 'Could not import BMConnectionPool.') inboundConnections = [] outboundConnections = [] for i in BMConnectionPool().inboundConnections.values(): diff --git a/src/mock/network.py b/src/mock/network.py index 1fb56c30..3f33c91b 100644 --- a/src/mock/network.py +++ b/src/mock/network.py @@ -1,7 +1,7 @@ # pylint: disable=too-few-public-methods """ - Mock Network +Mock Network """ @@ -12,11 +12,14 @@ class objectracker(object): class stats(object): - """Mock network statics""" + """Mock network statistics""" @staticmethod def connectedHostsList(): - """List of all the mock connected hosts""" - return [ - "conn1", "conn2", "conn3", "conn4" - ] + """Mock list of all the connected hosts""" + return ["conn1", "conn2", "conn3", "conn4"] + + @staticmethod + def pendingDownload(): + """Mock pending download count""" + return 0 diff --git a/src/tests/partial.py b/src/tests/partial.py index fd2d3f77..ce39c9df 100644 --- a/src/tests/partial.py +++ b/src/tests/partial.py @@ -15,12 +15,17 @@ class TestPartialRun(unittest.TestCase): @classmethod def setUpClass(cls): + # pylint: disable=import-outside-toplevel,unused-import cls.dirs = (os.path.abspath(os.curdir), pathmagic.setup()) import bmconfigparser import state from debug import logger # noqa:F401 pylint: disable=unused-variable + if sys.hexversion >= 0x3000000: + from mock import network as network_mock + import network + network.stats = network_mock.stats state.shutdown = 0 cls.state = state diff --git a/src/tests/test_api_thread.py b/src/tests/test_api_thread.py index 203a035f..5164ce5d 100644 --- a/src/tests/test_api_thread.py +++ b/src/tests/test_api_thread.py @@ -1,5 +1,6 @@ """TestAPIThread class definition""" +import sys import time from six.moves import queue, xmlrpc_client @@ -58,3 +59,9 @@ class TestAPIThread(TestPartialRun): self.assertEqual(cmd, 'updateStatusBar') self.assertEqual(data, sample_statusbar_msg) + + def test_client_status(self): + status = self.api.clientStatus() + if sys.hexversion >= 0x3000000: + self.assertEqual(status["networkConnections"], 4) + self.assertEqual(status["pendingDownload"], 0) -- 2.45.1 From 0cb80801b15b41cf20c3cde821599456c9fe6495 Mon Sep 17 00:00:00 2001 From: Lee Miller Date: Mon, 4 Dec 2023 01:14:58 +0200 Subject: [PATCH 7/7] Resolve pylint warnings in partial and test_api_thread - mostly suppress --- src/tests/partial.py | 1 + src/tests/test_api_thread.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/tests/partial.py b/src/tests/partial.py index ce39c9df..c88c6031 100644 --- a/src/tests/partial.py +++ b/src/tests/partial.py @@ -23,6 +23,7 @@ class TestPartialRun(unittest.TestCase): from debug import logger # noqa:F401 pylint: disable=unused-variable if sys.hexversion >= 0x3000000: + # pylint: disable=no-name-in-module,relative-import from mock import network as network_mock import network network.stats = network_mock.stats diff --git a/src/tests/test_api_thread.py b/src/tests/test_api_thread.py index 5164ce5d..e3218b85 100644 --- a/src/tests/test_api_thread.py +++ b/src/tests/test_api_thread.py @@ -61,6 +61,7 @@ class TestAPIThread(TestPartialRun): self.assertEqual(data, sample_statusbar_msg) def test_client_status(self): + """Ensure the reply of clientStatus corresponds to mock""" status = self.api.clientStatus() if sys.hexversion >= 0x3000000: self.assertEqual(status["networkConnections"], 4) -- 2.45.1