Standalone API test case #2168
80
src/api.py
80
src/api.py
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
68
src/tests/test_api_thread.py
Normal file
68
src/tests/test_api_thread.py
Normal 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)
|
Reference in New Issue
Block a user