diff --git a/jmclient/jmclient/client_protocol.py b/jmclient/jmclient/client_protocol.py index a0ccc30..155653d 100644 --- a/jmclient/jmclient/client_protocol.py +++ b/jmclient/jmclient/client_protocol.py @@ -58,10 +58,13 @@ class JMTakerClientProtocol(amp.AMP): is considered criticial. """ if 'accepted' not in response or not response['accepted']: - reactor.stop() + #Unintended client shutdown cannot be tested easily in twisted + reactor.stop() #pragma: no cover def defaultErrback(self, failure): - failure.trap(ConnectionAborted, ConnectionClosed, ConnectionDone, ConnectionLost) + #see testing note above + failure.trap(ConnectionAborted, ConnectionClosed, ConnectionDone, + ConnectionLost) #pragma: no cover def defaultCallbacks(self, d): d.addCallback(self.checkClientResponse) diff --git a/jmclient/jmclient/configure.py b/jmclient/jmclient/configure.py index cd84370..7b6ed15 100644 --- a/jmclient/jmclient/configure.py +++ b/jmclient/jmclient/configure.py @@ -306,12 +306,15 @@ def load_program_config(config_path=None, bs=None): configfile.write(defaultconfig) # check for sections - for s in required_options: + #These are left as sanity checks but currently impossible + #since any edits are overlays to the default, these sections/options will + #always exist. + for s in required_options: #pragma: no cover if s not in global_singleton.config.sections(): raise Exception( "Config file does not contain the required section: " + s) # then check for specific options - for k, v in required_options.iteritems(): + for k, v in required_options.iteritems(): #pragma: no cover for o in v: if o not in global_singleton.config.options(k): raise Exception( @@ -320,7 +323,7 @@ def load_program_config(config_path=None, bs=None): try: global_singleton.maker_timeout_sec = global_singleton.config.getint( 'TIMEOUT', 'maker_timeout_sec') - except NoOptionError: + except NoOptionError: #pragma: no cover log.debug('TIMEOUT/maker_timeout_sec not found in .cfg file, ' 'using default value') @@ -332,9 +335,9 @@ def load_program_config(config_path=None, bs=None): try: global_singleton.commit_file_location = global_singleton.config.get( "POLICY", "commit_file_location") - except NoOptionError: + except NoOptionError: #pragma: no cover log.debug("No commitment file location in config, using default " - "location cmttools/commitments.json") + "location cmtdata/commitments.json") set_commitment_file(os.path.join(config_path, global_singleton.commit_file_location)) diff --git a/jmclient/test/.coveragerc b/jmclient/test/.coveragerc index 3da71da..fbe421b 100644 --- a/jmclient/test/.coveragerc +++ b/jmclient/test/.coveragerc @@ -2,4 +2,5 @@ [run] omit = ../jmclient/jsonrpc.py + ../jmclient/slowaes.py ../jmclient/btc.py diff --git a/jmclient/test/test_client_protocol.py b/jmclient/test/test_client_protocol.py new file mode 100644 index 0000000..2abbca6 --- /dev/null +++ b/jmclient/test/test_client_protocol.py @@ -0,0 +1,246 @@ +#! /usr/bin/env python +from __future__ import absolute_import +'''test client-protocol interfacae.''' + +import pytest +from jmclient import (get_schedule, load_program_config, start_reactor, + Taker, get_log, JMTakerClientProtocolFactory, jm_single) +from jmclient.client_protocol import JMProtocolError, JMTakerClientProtocol +import os +from twisted.python.log import startLogging, err +from twisted.python.log import msg as tmsg +from twisted.internet import protocol, reactor +from twisted.internet.error import (ConnectionLost, ConnectionAborted, + ConnectionClosed, ConnectionDone) +from twisted.protocols.amp import UnknownRemoteError +from twisted.python import failure +from twisted.protocols import amp +from jmbase.commands import * +from taker_test_data import t_raw_signed_tx +import json +import time +import jmbitcoin as bitcoin + +test_completed = False + +clientfactory = None +runno = 0 +jlog = get_log() + +class DummyTaker(Taker): + + def set_fail_init(self, val): + self.failinit = val + def set_fail_utxos(self, val): + self.failutxos = val + + def default_taker_info_callback(self, infotype, msg): + jlog.debug(infotype + ":" + msg) + + def initialize(self, orderbook): + """Once the daemon is active and has returned the current orderbook, + select offers, re-initialize variables and prepare a commitment, + then send it to the protocol to fill offers. + """ + if self.failinit==-1: + return (True, -1, "aa"*32, {'dummy':'revelation'},orderbook[:2]) + elif self.failinit: + return (False,) + else: + return (True, 1000000, "aa"*32, {'dummy':'revelation'}, + orderbook[:2]) + + def receive_utxos(self, ioauth_data): + """Triggered when the daemon returns utxo data from + makers who responded; this is the completion of phase 1 + of the protocol + """ + if self.failutxos: + return (False, "dummyreason") + else: + return (True, [x*64 + ":01" for x in ["a", "b", "c"]], t_raw_signed_tx) + + + def on_sig(self, nick, sigb64): + jlog.debug("We got a sig: " + sigb64) + end_test() + return True + + +class JMBaseProtocol(amp.AMP): + def checkClientResponse(self, response): + """A generic check of client acceptance; any failure + is considered criticial. + """ + if 'accepted' not in response or not response['accepted']: + reactor.stop() + + def defaultErrback(self, failure): + failure.trap(ConnectionAborted, ConnectionClosed, ConnectionDone, + ConnectionLost, UnknownRemoteError) + reactor.stop() + + def defaultCallbacks(self, d): + d.addCallback(self.checkClientResponse) + d.addErrback(self.defaultErrback) + +def show_receipt(name, *args): + tmsg("Received msgtype: " + name + ", args: " + ",".join([str(x) for x in args])) + +def end_client(client): + client.shutdown_requested = True + +def end_test(): + #global runno + global test_completed + #runno += 1 + #jlog.info("Updated runno to: " + str(runno)) + #taker = DummyTaker(None, None) + #if runno == 1: + # jlog.info("Run number was less than 2") + # taker.set_fail_init(True) + # taker.set_fail_utxos(False) + # cfactory = JMTakerClientProtocolFactory(taker) + # reactor.connectTCP("localhost", 27184, cfactory) + # return + #elif runno == 2: + # taker.set_fail_init(False) + # taker.set_fail_utxos(True) + # cfactory = JMTakerClientProtocolFactory(taker) + # reactor.connectTCP("localhost", 27184, cfactory) + # return + test_completed = True + client = clientfactory.getClient() + reactor.callLater(2, end_client, client) + +class JMTestServerProtocol(JMBaseProtocol): + + @JMInit.responder + def on_JM_INIT(self, bcsource, network, irc_configs, minmakers, + maker_timeout_sec): + show_receipt("JMINIT", bcsource, network, irc_configs, minmakers, + maker_timeout_sec) + d = self.callRemote(JMInitProto, + nick_hash_length=1, + nick_max_encoded=2, + joinmarket_nick_header="J", + joinmarket_version=5) + self.defaultCallbacks(d) + return {'accepted': True} + + @JMStartMC.responder + def on_JM_START_MC(self, nick): + show_receipt("STARTMC", nick) + d = self.callRemote(JMUp) + self.defaultCallbacks(d) + return {'accepted': True} + + @JMSetup.responder + def on_JM_SETUP(self, role, n_counterparties): + show_receipt("JMSETUP", role,n_counterparties) + d = self.callRemote(JMSetupDone) + self.defaultCallbacks(d) + return {'accepted': True} + + @JMRequestOffers.responder + def on_JM_REQUEST_OFFERS(self): + show_receipt("JMREQUESTOFFERS") + #build a huge orderbook to test BigString Argument + orderbook = ["aaaa" for _ in range(15)] + d = self.callRemote(JMOffers, + orderbook=json.dumps(orderbook)) + self.defaultCallbacks(d) + return {'accepted': True} + + @JMFill.responder + def on_JM_FILL(self, amount, commitment, revelation, filled_offers): + success = False if amount == -1 else True + show_receipt("JMFILL", amount, commitment, revelation, filled_offers) + d = self.callRemote(JMFillResponse, + success=success, + ioauth_data = json.dumps(['dummy', 'list'])) + return {'accepted': True} + + @JMMakeTx.responder + def on_JM_MAKE_TX(self, nick_list, txhex): + show_receipt("JMMAKETX", nick_list, txhex) + d = self.callRemote(JMSigReceived, + nick="dummynick", + sig="xxxsig") + self.defaultCallbacks(d) + #add dummy calls to check message sign and message verify + d2 = self.callRemote(JMRequestMsgSig, + nick="dummynickforsign", + cmd="command1", + msg="msgforsign", + msg_to_be_signed="fullmsgforsign", + hostid="hostid1") + self.defaultCallbacks(d2) + #To test, this must include a valid ecdsa sig + fullmsg = "fullmsgforverify" + priv = "aa"*32 + "01" + pub = bitcoin.privkey_to_pubkey(priv) + sig = bitcoin.ecdsa_sign(fullmsg, priv) + d3 = self.callRemote(JMRequestMsgSigVerify, + msg="msgforverify", + fullmsg=fullmsg, + sig=sig, + pubkey=pub, + nick="dummynickforverify", + hashlen=4, + max_encoded=5, + hostid="hostid2") + self.defaultCallbacks(d3) + d4 = self.callRemote(JMSigReceived, + nick="dummynick", + sig="dummysig") + self.defaultCallbacks(d4) + return {'accepted': True} + + + @JMMsgSignature.responder + def on_JM_MSGSIGNATURE(self, nick, cmd, msg_to_return, hostid): + show_receipt("JMMSGSIGNATURE", nick, cmd, msg_to_return, hostid) + return {'accepted': True} + + @JMMsgSignatureVerify.responder + def on_JM_MSGSIGNATURE_VERIFY(self, verif_result, nick, fullmsg, hostid): + show_receipt("JMMSGSIGVERIFY", verif_result, nick, fullmsg, hostid) + return {'accepted': True} + +class JMTestServerProtocolFactory(protocol.ServerFactory): + protocol = JMTestServerProtocol + +class DummyClientProtocolFactory(JMTakerClientProtocolFactory): + def buildProtocol(self, addr): + return JMTakerClientProtocol(self, self.taker, nick_priv="aa"*32) + +def test_jm_protocol(): + """We cannot use parametrize for different options as + we can't run in sequence; hence, parameters hardcoded as lists here + """ + params = [[False, False], [True, False], [False, True], [-1, False]] + global clientfactory + load_program_config() + jm_single().maker_timeout_sec = 1 + reactor.listenTCP(27184, JMTestServerProtocolFactory()) + clientfactories = [] + takers = [DummyTaker(None, None) for _ in range(len(params))] + for i, p in enumerate(params): + takers[i].set_fail_init(p[0]) + takers[i].set_fail_utxos(p[1]) + if i != 0: + clientfactories.append(JMTakerClientProtocolFactory(takers[i])) + reactor.connectTCP("localhost", 27184, clientfactories[i]) + else: + clientfactories.append(DummyClientProtocolFactory(takers[i])) + clientfactory = clientfactories[0] + start_reactor("localhost", 27184, clientfactories[0]) + print("Got here") + if not test_completed: + raise Exception("Failed test") + + + + + \ No newline at end of file diff --git a/jmclient/test/test_commitment_utils.py b/jmclient/test/test_commitment_utils.py new file mode 100644 index 0000000..d220a04 --- /dev/null +++ b/jmclient/test/test_commitment_utils.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +from __future__ import print_function +from commontest import DummyBlockchainInterface +import pytest + +from jmclient import (load_program_config, jm_single) +from jmclient.commitment_utils import get_utxo_info, validate_utxo_data +from taker_test_data import (t_utxos_by_mixdepth, t_selected_utxos, t_orderbook, + t_maker_response, t_chosen_orders, t_dummy_ext) + +def test_get_utxo_info(): + load_program_config() + jm_single().config.set("BLOCKCHAIN", "network", "mainnet") + dbci = DummyBlockchainInterface() + privkey = "L1RrrnXkcKut5DEMwtDthjwRcTTwED36thyL1DebVrKuwvohjMNi" + #to verify use from_wif_privkey and privkey_to_address + iaddr = "1LDsjB43N2NAQ1Vbc2xyHca4iBBciN8iwC" + fakeutxo = "aa"*32+":08" + + fake_query_results = [{'value': 200000000, + 'address': iaddr, + 'utxo': fakeutxo, + 'confirms': 20}] + dbci.insert_fake_query_results(fake_query_results) + jm_single().bc_interface = dbci + u, priv = get_utxo_info(fakeutxo + "," + privkey) + assert u == fakeutxo + assert priv == privkey + #invalid format + with pytest.raises(Exception) as e_info: + u, priv = get_utxo_info(fakeutxo + privkey) + #invalid index + fu2 = "ab"*32 + ":00004" + with pytest.raises(Exception) as e_info: + u, priv = get_utxo_info(fu2 + "," + privkey) + #invalid privkey + p2 = privkey[:-1] + 'j' + with pytest.raises(Exception) as e_info: + u, priv = get_utxo_info(fakeutxo + "," + p2) + + utxodatas = [(fakeutxo, privkey)] + retval = validate_utxo_data(utxodatas, False) + assert retval + #try to retrieve + retval = validate_utxo_data(utxodatas, True) + assert retval[0] == (fakeutxo, 200000000) + fake_query_results[0]['address'] = "fakeaddress" + dbci.insert_fake_query_results(fake_query_results) + #validate should fail for wrong address + retval = validate_utxo_data(utxodatas, False) + assert not retval + #remove fake query result and trigger not found + dbci.fake_query_results = None + dbci.setQUSFail(True) + retval = validate_utxo_data(utxodatas, False) + assert not retval + dbci.setQUSFail(False) + diff --git a/jmclient/test/test_configure.py b/jmclient/test/test_configure.py index 13c994a..46d544a 100644 --- a/jmclient/test/test_configure.py +++ b/jmclient/test/test_configure.py @@ -1,6 +1,6 @@ #! /usr/bin/env python from __future__ import absolute_import -'''test schedule module.''' +'''test configure module.''' import pytest from jmclient import (load_program_config, jm_single, get_irc_mchannels, @@ -12,6 +12,25 @@ import jmbitcoin as bitcoin import copy import os +def test_attribute_dict(): + from jmclient.configure import AttributeDict + ad = AttributeDict(foo=1, bar=2, baz={"x":3, "y":4}) + assert ad.foo == 1 + assert ad.bar == 2 + assert ad.baz.x == 3 + assert ad["foo"] == 1 + +def test_load_config(): + load_program_config(bs="regtest") + os.makedirs("dummydirforconfig") + ncp = os.path.join(os.getcwd(), "dummydirforconfig") + #need to erase remembered data in global config + jm_single().config_location = "joinmarket.cfg" + load_program_config(config_path=ncp) + os.remove("dummydirforconfig/joinmarket.cfg") + os.removedirs("dummydirforconfig") + jm_single().config_location = "joinmarket.cfg" + load_program_config() def test_config_get_irc_channel(): load_program_config() diff --git a/jmclient/test/test_wallets.py b/jmclient/test/test_wallets.py index 7b44845..5941170 100644 --- a/jmclient/test/test_wallets.py +++ b/jmclient/test/test_wallets.py @@ -54,8 +54,9 @@ def test_query_utxo_set(setup_wallets): ["wallet4utxo.json", "4utxo", [2, 3]]) sync_wallet(wallet) txid = do_tx(wallet, 90000000) - time.sleep(5) + time.sleep(3) txid2 = do_tx(wallet, 20000000) + time.sleep(3) print("Got txs: ", txid, txid2) res1 = jm_single().bc_interface.query_utxo_set(txid + ":0") res2 = jm_single().bc_interface.query_utxo_set(