From 987a295a4358653cad0666932f016448d9d27860 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/3] 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 a44ce07949627269c87c89239941b19cd66b2559 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/3] Rewrite imports in api for python3 and cut out those looking too tricky --- src/api.py | 54 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/src/api.py b/src/api.py index de220cc4..7eff52ef 100644 --- a/src/api.py +++ b/src/api.py @@ -57,28 +57,25 @@ For further examples please reference `.tests.test_api`. """ import base64 -import ConfigParser import errno import hashlib -import httplib import json import random # nosec import socket import subprocess import time -import xmlrpclib from binascii import hexlify, unhexlify -from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer from struct import pack +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 ( addBMIfNotPresent, @@ -90,9 +87,21 @@ from addresses import ( from bmconfigparser import BMConfigParser from debug import logger from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery, sqlStoredProcedure, sql_ready -from inventory import Inventory + +# TODO: solve the issue with next two imports and remove the cutout +# FIXME: perhaps this module shouldn't use the Inventory at all +try: + import shutdown + from inventory import Inventory +except ImportError: + Inventory = None + +try: + import network.stats as network_stats +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 @@ -162,7 +171,7 @@ class ErrorCodes(type): return result -class APIError(xmlrpclib.Fault): +class APIError(xmlrpc_server.Fault): """ APIError exception class @@ -210,7 +219,7 @@ class singleAPI(StoppableThread): except AttributeError: errno.WSAEADDRINUSE = errno.EADDRINUSE - RPCServerBase = SimpleXMLRPCServer + RPCServerBase = xmlrpc_server.SimpleXMLRPCServer ct = 'text/xml' if BMConfigParser().safeGet( 'bitmessagesettings', 'apivariant') == 'json': @@ -351,7 +360,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 @@ -392,7 +401,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" @@ -409,11 +418,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))) @@ -858,7 +867,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() @@ -875,7 +884,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() @@ -1266,6 +1275,8 @@ class BMRPCDispatcher(object): requiredPayloadLengthExtraBytes): """Handle a request to disseminate an encrypted message""" + if Inventory is None: + raise APIError(21, 'Could not import Inventory.') # The device issuing this command to PyBitmessage supplies a msg # object that has already been encrypted but which still needs the POW # to be done. PyBitmessage accepts this msg object and sends it out @@ -1322,6 +1333,8 @@ class BMRPCDispatcher(object): def HandleDissimatePubKey(self, payload): """Handle a request to disseminate a public key""" + if Inventory is None: + raise APIError(21, 'Could not import Inventory.') # The device issuing this command to PyBitmessage supplies a pubkey # object to be disseminated to the rest of the Bitmessage network. # PyBitmessage accepts this pubkey object and sends it out to the rest @@ -1411,7 +1424,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: @@ -1423,7 +1439,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 @@ -1475,6 +1491,8 @@ class BMRPCDispatcher(object): @command('shutdown') def HandleShutdown(self): """Shutdown the bitmessage. Returns 'done'.""" + if Inventory is None: + raise APIError(21, 'Could not import shutdown.') # backward compatible trick because False == 0 is True state.shutdown = False return 'done' -- 2.45.1 From 22d93879430eba5a3e26c2e35c0312501d88ffc8 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/3] 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 7eff52ef..3f128adc 100644 --- a/src/api.py +++ b/src/api.py @@ -67,7 +67,7 @@ import time from binascii import hexlify, unhexlify from struct import pack -from six.moves import configparser, http_client, queue, xmlrpc_server +from six.moves import configparser, http_client, xmlrpc_server import defaults import helper_inbox @@ -1455,25 +1455,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 835b4afb..220a1c1a 100644 --- a/src/tests/test_api.py +++ b/src/tests/test_api.py @@ -12,7 +12,7 @@ from six.moves import xmlrpc_client # nosec import psutil from .samples import ( - sample_seed, sample_deterministic_addr3, sample_deterministic_addr4, sample_statusbar_msg, + sample_seed, sample_deterministic_addr3, sample_deterministic_addr4, sample_inbox_msg_ids, sample_test_subscription_address, sample_subscription_name) from .test_process import TestProcessProto @@ -86,18 +86,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 c5bd6576..f118330f 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() @@ -48,6 +56,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