Standalone API test case #2168

Merged
PeterSurda merged 7 commits from gitea-32 into v0.6 2023-12-04 00:59:16 +01:00
5 changed files with 124 additions and 61 deletions

View File

@ -1,5 +1,5 @@
# Copyright (c) 2012-2016 Jonathan Warren # 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. This is not what you run to start the Bitmessage API.
@ -58,27 +58,26 @@ For further examples please reference `.tests.test_api`.
""" """
import base64 import base64
import ConfigParser
import errno import errno
import hashlib import hashlib
import httplib
import json import json
import random import random
import socket import socket
import subprocess # nosec B404 import subprocess # nosec B404
import time import time
import xmlrpclib
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
from struct import pack, unpack from struct import pack, unpack
import six
from six.moves import configparser, http_client, xmlrpc_server
import defaults import defaults
import helper_inbox import helper_inbox
import helper_sent import helper_sent
import network.stats
import proofofwork import proofofwork
import queues import queues
import shared import shared
import shutdown import shutdown
import state import state
from addresses import ( from addresses import (
@ -90,11 +89,16 @@ from addresses import (
) )
from bmconfigparser import config from bmconfigparser import config
from debug import logger 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 inventory import Inventory
try:
from network import BMConnectionPool from network import BMConnectionPool
from network.threads import StoppableThread except ImportError:
from six.moves import queue BMConnectionPool = None
from network import stats, StoppableThread
from version import softwareVersion from version import softwareVersion
try: # TODO: write tests for XML vulnerabilities try: # TODO: write tests for XML vulnerabilities
@ -156,7 +160,7 @@ class ErrorCodes(type):
def __new__(mcs, name, bases, namespace): def __new__(mcs, name, bases, namespace):
result = super(ErrorCodes, mcs).__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 # beware: the formatting is adjusted for list-table
result.__doc__ += """ * - %04i result.__doc__ += """ * - %04i
- %s - %s
@ -164,7 +168,7 @@ class ErrorCodes(type):
return result return result
class APIError(xmlrpclib.Fault): class APIError(xmlrpc_server.Fault):
""" """
APIError exception class APIError exception class
@ -212,7 +216,7 @@ class singleAPI(StoppableThread):
except AttributeError: except AttributeError:
errno.WSAEADDRINUSE = errno.EADDRINUSE errno.WSAEADDRINUSE = errno.EADDRINUSE
RPCServerBase = SimpleXMLRPCServer RPCServerBase = xmlrpc_server.SimpleXMLRPCServer
ct = 'text/xml' ct = 'text/xml'
if config.safeGet( if config.safeGet(
'bitmessagesettings', 'apivariant') == 'json': 'bitmessagesettings', 'apivariant') == 'json':
@ -353,7 +357,7 @@ class command(object): # pylint: disable=too-few-public-methods
# Modified by Jonathan Warren (Atheros). # Modified by Jonathan Warren (Atheros).
# Further modified by the Bitmessage developers # Further modified by the Bitmessage developers
# http://code.activestate.com/recipes/501148 # http://code.activestate.com/recipes/501148
class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler):
"""The main API handler""" """The main API handler"""
# pylint: disable=protected-access # pylint: disable=protected-access
@ -384,17 +388,21 @@ class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
L = [] L = []
while size_remaining: while size_remaining:
chunk_size = min(size_remaining, max_chunk_size) 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]) size_remaining -= len(L[-1])
data = ''.join(L) data = b''.join(L)
# data = self.decode_request_content(data)
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
self.cookies = [] self.cookies = []
validuser = self.APIAuthenticateClient() validuser = self.APIAuthenticateClient()
if not validuser: if not validuser:
time.sleep(2) time.sleep(2)
self.send_response(httplib.UNAUTHORIZED) self.send_response(http_client.UNAUTHORIZED)
self.end_headers() self.end_headers()
return return
# "RPC Username or password incorrect or HTTP header" # "RPC Username or password incorrect or HTTP header"
@ -411,11 +419,11 @@ class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
) )
except Exception: # This should only happen if the module is buggy except Exception: # This should only happen if the module is buggy
# internal error, report as HTTP server error # 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() self.end_headers()
else: else:
# got a valid XML RPC response # 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-type", self.server.content_type)
self.send_header("Content-length", str(len(response))) self.send_header("Content-length", str(len(response)))
@ -444,7 +452,8 @@ class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
if 'Authorization' in self.headers: if 'Authorization' in self.headers:
# handle Basic authentication # handle Basic authentication
encstr = self.headers.get('Authorization').split()[1] encstr = self.headers.get('Authorization').split()[1]
emailid, password = encstr.decode('base64').split(':') emailid, password = base64.b64decode(
encstr).decode('utf-8').split(':')
return ( return (
emailid == config.get( emailid == config.get(
'bitmessagesettings', 'apiusername' 'bitmessagesettings', 'apiusername'
@ -460,9 +469,9 @@ class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
# pylint: disable=no-self-use,no-member,too-many-public-methods # pylint: disable=no-self-use,no-member,too-many-public-methods
@six.add_metaclass(CommandHandler)
class BMRPCDispatcher(object): class BMRPCDispatcher(object):
"""This class is used to dispatch API commands""" """This class is used to dispatch API commands"""
__metaclass__ = CommandHandler
@staticmethod @staticmethod
def _decode(text, decode_type): def _decode(text, decode_type):
@ -860,7 +869,7 @@ class BMRPCDispatcher(object):
' Use deleteAddress API call instead.') ' Use deleteAddress API call instead.')
try: try:
self.config.remove_section(address) self.config.remove_section(address)
except ConfigParser.NoSectionError: except configparser.NoSectionError:
raise APIError( raise APIError(
13, 'Could not find this address in your keys.dat file.') 13, 'Could not find this address in your keys.dat file.')
self.config.save() self.config.save()
@ -877,7 +886,7 @@ class BMRPCDispatcher(object):
address = addBMIfNotPresent(address) address = addBMIfNotPresent(address)
try: try:
self.config.remove_section(address) self.config.remove_section(address)
except ConfigParser.NoSectionError: except configparser.NoSectionError:
raise APIError( raise APIError(
13, 'Could not find this address in your keys.dat file.') 13, 'Could not find this address in your keys.dat file.')
self.config.save() self.config.save()
@ -1131,7 +1140,7 @@ class BMRPCDispatcher(object):
self._verifyAddress(fromAddress) self._verifyAddress(fromAddress)
try: try:
fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled') fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled')
except ConfigParser.NoSectionError: except configparser.NoSectionError:
raise APIError( raise APIError(
13, 'Could not find your fromAddress in the keys.dat file.') 13, 'Could not find your fromAddress in the keys.dat file.')
if not fromAddressEnabled: if not fromAddressEnabled:
@ -1176,7 +1185,7 @@ class BMRPCDispatcher(object):
self._verifyAddress(fromAddress) self._verifyAddress(fromAddress)
try: try:
fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled') fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled')
except ConfigParser.NoSectionError: except configparser.NoSectionError:
raise APIError( raise APIError(
13, 'Could not find your fromAddress in the keys.dat file.') 13, 'Could not find your fromAddress in the keys.dat file.')
if not fromAddressEnabled: if not fromAddressEnabled:
@ -1438,7 +1447,8 @@ class BMRPCDispatcher(object):
or "connectedAndReceivingIncomingConnections". or "connectedAndReceivingIncomingConnections".
""" """
connections_num = len(network.stats.connectedHostsList()) connections_num = len(stats.connectedHostsList())
if connections_num == 0: if connections_num == 0:
networkStatus = 'notConnected' networkStatus = 'notConnected'
elif state.clientHasReceivedIncomingConnections: elif state.clientHasReceivedIncomingConnections:
@ -1450,7 +1460,7 @@ class BMRPCDispatcher(object):
'numberOfMessagesProcessed': state.numberOfMessagesProcessed, 'numberOfMessagesProcessed': state.numberOfMessagesProcessed,
'numberOfBroadcastsProcessed': state.numberOfBroadcastsProcessed, 'numberOfBroadcastsProcessed': state.numberOfBroadcastsProcessed,
'numberOfPubkeysProcessed': state.numberOfPubkeysProcessed, 'numberOfPubkeysProcessed': state.numberOfPubkeysProcessed,
'pendingDownload': network.stats.pendingDownload(), 'pendingDownload': stats.pendingDownload(),
'networkStatus': networkStatus, 'networkStatus': networkStatus,
'softwareName': 'PyBitmessage', 'softwareName': 'PyBitmessage',
'softwareVersion': softwareVersion 'softwareVersion': softwareVersion
@ -1462,6 +1472,8 @@ class BMRPCDispatcher(object):
Returns bitmessage connection information as dict with keys *inbound*, Returns bitmessage connection information as dict with keys *inbound*,
*outbound*. *outbound*.
""" """
if BMConnectionPool is None:
raise APIError(21, 'Could not import BMConnectionPool.')
inboundConnections = [] inboundConnections = []
outboundConnections = [] outboundConnections = []
for i in BMConnectionPool().inboundConnections.values(): for i in BMConnectionPool().inboundConnections.values():
@ -1493,25 +1505,11 @@ class BMRPCDispatcher(object):
"""Test two numeric params""" """Test two numeric params"""
return a + b return a + b
@testmode('clearUISignalQueue')
def HandleclearUISignalQueue(self):
"""clear UISignalQueue"""
queues.UISignalQueue.queue.clear()
return "success"
@command('statusBar') @command('statusBar')
def HandleStatusBar(self, message): def HandleStatusBar(self, message):
"""Update GUI statusbar message""" """Update GUI statusbar message"""
queues.UISignalQueue.put(('updateStatusBar', message)) queues.UISignalQueue.put(('updateStatusBar', message))
return "success"
@testmode('getStatusBar')
def HandleGetStatusBar(self):
"""Get GUI statusbar message"""
try:
_, data = queues.UISignalQueue.get(block=False)
except queue.Empty:
return None
return data
@testmode('undeleteMessage') @testmode('undeleteMessage')
def HandleUndeleteMessage(self, msgid): def HandleUndeleteMessage(self, msgid):

View File

@ -12,11 +12,14 @@ class objectracker(object):
class stats(object): class stats(object):
"""Mock network statics""" """Mock network statistics"""
@staticmethod @staticmethod
def connectedHostsList(): def connectedHostsList():
"""List of all the mock connected hosts""" """Mock list of all the connected hosts"""
return [ return ["conn1", "conn2", "conn3", "conn4"]
"conn1", "conn2", "conn3", "conn4"
] @staticmethod
def pendingDownload():
"""Mock pending download count"""
return 0

View File

@ -15,12 +15,18 @@ class TestPartialRun(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
# pylint: disable=import-outside-toplevel,unused-import
cls.dirs = (os.path.abspath(os.curdir), pathmagic.setup()) cls.dirs = (os.path.abspath(os.curdir), pathmagic.setup())
import bmconfigparser import bmconfigparser
import state import state
from debug import logger # noqa:F401 pylint: disable=unused-variable 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
state.shutdown = 0 state.shutdown = 0
cls.state = state cls.state = state

View File

@ -13,7 +13,7 @@ import psutil
from .samples import ( from .samples import (
sample_deterministic_addr3, sample_deterministic_addr4, sample_seed, 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 sample_subscription_addresses, sample_subscription_name
) )
@ -88,18 +88,6 @@ class TestAPI(TestAPIProto):
'API Error 0020: Invalid method: test' '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): def test_message_inbox(self):
"""Test message inbox methods""" """Test message inbox methods"""
self.assertEqual( self.assertEqual(

View File

@ -0,0 +1,68 @@
"""TestAPIThread class definition"""
import sys
import time
from six.moves import queue, xmlrpc_client
from .partial import TestPartialRun
from .samples import sample_statusbar_msg # any
class TestAPIThread(TestPartialRun):
"""Test case running the API thread"""
@classmethod
def setUpClass(cls):
super(TestAPIThread, cls).setUpClass()
import helper_sql
import queues
# 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.queues = queues
cls.config.set('bitmessagesettings', 'apiusername', 'username')
cls.config.set('bitmessagesettings', 'apipassword', 'password')
cls.config.set('inventory', 'storage', 'filesystem')
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')
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)
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)
self.assertEqual(status["pendingDownload"], 0)