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-2022 The Bitmessage developers
# Copyright (c) 2012-2023 The Bitmessage developers
"""
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 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
import six
from six.moves import configparser, http_client, 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 +89,16 @@ 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
try:
from network import BMConnectionPool
from network.threads import StoppableThread
from six.moves import queue
except ImportError:
BMConnectionPool = None
from network import stats, StoppableThread
from version import softwareVersion
try: # TODO: write tests for XML vulnerabilities
@ -156,7 +160,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
@ -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
@ -384,17 +388,21 @@ class BMXMLRPCRequestHandler(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 = []
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 +419,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)))
@ -444,7 +452,8 @@ class BMXMLRPCRequestHandler(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'
@ -460,9 +469,9 @@ class BMXMLRPCRequestHandler(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):
@ -860,7 +869,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 +886,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 +1140,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 +1185,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 +1447,8 @@ class BMRPCDispatcher(object):
or "connectedAndReceivingIncomingConnections".
"""
connections_num = len(network.stats.connectedHostsList())
connections_num = len(stats.connectedHostsList())
if connections_num == 0:
networkStatus = 'notConnected'
elif state.clientHasReceivedIncomingConnections:
@ -1450,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
@ -1462,6 +1472,8 @@ class BMRPCDispatcher(object):
Returns bitmessage connection information as dict with keys *inbound*,
*outbound*.
"""
if BMConnectionPool is None:
raise APIError(21, 'Could not import BMConnectionPool.')
inboundConnections = []
outboundConnections = []
for i in BMConnectionPool().inboundConnections.values():
@ -1493,25 +1505,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):

View File

@ -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

View File

@ -15,12 +15,18 @@ 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:
# 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
cls.state = state

View File

@ -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(

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)