diff --git a/circle.yml b/circle.yml
index a2248aa27..f21077999 100644
--- a/circle.yml
+++ b/circle.yml
@@ -1,4 +1,24 @@
-general:
- branches:
- only:
- - api
+machine:
+ pre:
+ - sudo -H pip install --upgrade virtualenv
+ - brew install openssl
+dependencies:
+ pre:
+ - ../virtualenvs/venv-system/bin/pip install git+https://github.com/kantai/py-scrypt.git:
+ environment:
+ PYSCRYPT_NO_LINK_FLAGS: "1"
+ LDFLAGS: /usr/local/opt/openssl/lib/libcrypto.a /usr/local/opt/openssl/lib/libssl.a
+ CPPFLAGS: -I/usr/local/opt/openssl/include
+ - ../virtualenvs/venv-system/bin/pip install git+https://github.com/blockstack/virtualchain.git@rc-0.14.2
+compile:
+ post:
+ - ../virtualenvs/venv-system/bin/pip install ./integration_tests
+test:
+ pre:
+ - blockstack setup -y --password PASSWORD
+ - blockstack api start -y --password PASSWORD
+ override:
+ - mkdir $CIRCLE_TEST_REPORTS/django
+ - ../virtualenvs/venv-system/bin/python -m blockstack_integration_tests.live_tests.api_tests --xunit-path "$CIRCLE_TEST_REPORTS/django/" --all
+ post:
+ - blockstack api stop -y
diff --git a/integration_tests/blockstack_integration_tests/live_tests/__init__.py b/integration_tests/blockstack_integration_tests/live_tests/__init__.py
new file mode 100644
index 000000000..68c4ca84a
--- /dev/null
+++ b/integration_tests/blockstack_integration_tests/live_tests/__init__.py
@@ -0,0 +1,21 @@
+"""
+ Blockstack Core
+ ~~~~~
+
+ copyright: (c) 2017 by Blockstack.org
+
+This file is part of Blockstack Core.
+
+ Blockstack Core is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Blockstack Core is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Search. If not, see .
+"""
diff --git a/integration_tests/blockstack_integration_tests/live_tests/api_tests.py b/integration_tests/blockstack_integration_tests/live_tests/api_tests.py
new file mode 100644
index 000000000..15be2b931
--- /dev/null
+++ b/integration_tests/blockstack_integration_tests/live_tests/api_tests.py
@@ -0,0 +1,443 @@
+"""
+ Blockstack Core
+ ~~~~~
+
+ copyright: (c) 2017 by Blockstack.org
+
+This file is part of Blockstack Core.
+
+ Blockstack Core is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Blockstack Core is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Search. If not, see .
+"""
+
+import os, sys, re, json, time
+import unittest
+import requests
+import argparse
+import binascii
+import traceback
+import jsontokens
+
+from test import test_support
+from binascii import hexlify
+
+from blockstack_client import schemas
+import blockstack_client.storage
+import blockstack_client.config as blockstack_config
+import blockstack_client.config as blockstack_constants
+import blockstack_client.keys
+
+BASE_URL = 'http://localhost:5000'
+
+API_PASSWORD = blockstack_config.get_config(
+ blockstack_constants.CONFIG_PATH).get('api_password', None)
+
+DEFAULT_WALLET_ADDRESS = "1QJQxDas5JhdiXhEbNS14iNjr8auFT96GP"
+
+class FakeResponseObj:
+ def __init__(self):
+ self.status_code = 600
+ self.data = ""
+
+class ForwardingClient:
+ def __init__(self, base_url):
+ self.base_url = base_url
+ def get(self, endpoint, headers = {}):
+ resp = requests.get(self.base_url + endpoint, headers = headers)
+ ret_obj = FakeResponseObj()
+ ret_obj.status_code = resp.status_code
+ ret_obj.data = resp.text
+ return ret_obj
+ def post(self, endpoint, data, headers = {}):
+ resp = requests.post(self.base_url + endpoint,
+ data = data, headers = headers)
+ ret_obj = FakeResponseObj()
+ ret_obj.status_code = resp.status_code
+ ret_obj.data = resp.text
+ return ret_obj
+
+class APITestCase(unittest.TestCase):
+ def __init__(self, methodName):
+ super(APITestCase, self).__init__(methodName)
+ self.app = ForwardingClient("http://localhost:6270")
+ def get_request(self, endpoint, headers={}, status_code=200,
+ no_json = False):
+ t_start = time.time()
+ resp = self.app.get(endpoint, headers = headers)
+ t_end = time.time()
+ print("\nget {} time: {}s".format(endpoint, t_end - t_start))
+ if not resp.status_code == status_code:
+ print("Bad status code: {} => {} ".format(endpoint, resp.status_code))
+ print("REQUEST ===> {} + {} <===".format(endpoint, headers))
+ print("RESPONSE ===>\n {} \n<===".format(resp.data))
+
+ self.assertEqual(resp.status_code, status_code)
+ if no_json:
+ return resp.data
+
+ try:
+ data = json.loads(resp.data)
+ return data
+ except Exception as e:
+ if status_code != 200:
+ return {}
+ raise e
+
+ def post_request(self, endpoint, payload, headers={}, status_code=200):
+ t_start = time.time()
+ resp = self.app.post(endpoint, data = json.dumps(payload), headers = headers)
+ t_end = time.time()
+ print("\npost {} time: {}s".format(endpoint, t_end - t_start))
+ if not resp.status_code == status_code:
+ print("{} => {} ".format(endpoint, resp.status_code))
+
+ self.assertEqual(resp.status_code, status_code)
+ try:
+ data = json.loads(resp.data)
+ return data
+ except Exception as e:
+ if status_code != 200:
+ return {}
+ traceback.print_exc()
+ raise e
+
+def get_auth_header(key = None):
+ if key is None:
+ key = API_PASSWORD
+ return {'Authorization' : 'bearer {}'.format(key)}
+
+def check_data(cls, data, required_keys={}):
+ for k in required_keys:
+ cls.assertIn(k, data)
+ if type(required_keys[k]) == dict:
+ check_data(cls, data[k], required_keys = required_keys[k])
+ if type(required_keys[k]) == str:
+ cls.assertRegexpMatches(data[k], required_keys[k])
+ if type(required_keys[k]) == int:
+ cls.assertGreaterEqual(data[k], required_keys[k])
+
+class PingTest(APITestCase):
+ def test_found_user_lookup(self):
+ data = self.get_request("/v1/ping",
+ headers = {} , status_code=200)
+
+ self.assertTrue(data['status'] == 'alive')
+
+class AuthInternal(APITestCase):
+ def test_get_and_use_session_token(self):
+ privkey = ("a28ea1a6f11fb1c755b1d102990d64d6" +
+ "b4468c10705bbcbdfca8bc4497cf8da8")
+
+ auth_header = get_auth_header()
+ request = {
+ 'app_domain': 'test.com',
+ 'app_public_key': blockstack_client.keys.get_pubkey_hex(privkey),
+ 'methods': ['wallet_read'],
+ }
+
+ signer = jsontokens.TokenSigner()
+ package = signer.sign(request, privkey)
+
+ url = "/v1/auth?authRequest={}".format(package)
+ data = self.get_request(url, headers = auth_header, status_code=200)
+ self.assertIn('token', data)
+ session = data['token']
+
+ auth_header = get_auth_header(session)
+ data = self.get_request('/v1/wallet/payment_address',
+ headers = auth_header, status_code=200)
+ data = self.get_request('/v1/users/muneeb.id',
+ headers = auth_header, status_code=403)
+
+class UsersInternal(APITestCase):
+ def test_get_users(self):
+ user = "muneeb.id"
+ data = self.get_request('/v1/users/{}'.format(user),
+ headers = get_auth_header(), status_code=200)
+ to_check = {
+ "@type": True,
+ }
+ check_data(self, data, to_check)
+
+class LookupUsersTest(APITestCase):
+ def test_found_user_lookup(self):
+ base_url = '/v1/names/{}'
+ url = base_url.format('muneeb.id')
+ data = self.get_request(url, headers = {}, status_code=200)
+
+ self.assertTrue(data['status'] == 'registered')
+
+ to_check = {'address': schemas.OP_ADDRESS_PATTERN,
+ 'zonefile_hash' : schemas.OP_ZONEFILE_HASH_PATTERN,
+ 'last_txid' : schemas.OP_TXID_PATTERN}
+ check_data(self, data, to_check)
+
+ url = base_url.format('muneeb')
+ data = self.get_request(url, headers = {}, status_code=500)
+ self.assertTrue(data['error'] == 'Failed to lookup name')
+
+ def test_get_all_names(self):
+ data = self.get_request("/v1/names?page=0",
+ headers = {} , status_code=200)
+ self.assertEqual(len(data), 100, "Paginated name length != 100")
+ data = self.get_request("/v1/names",
+ headers = {} , status_code=401)
+ data = self.get_request("/v1/names?page=10000",
+ headers = {} , status_code=200)
+
+class Zonefiles(APITestCase):
+ def test_get_zonefile(self):
+ zf_url = '/v1/names/{}/zonefile'
+ zf_hash_url = '/v1/names/{}/zonefile/{}'
+ user = 'muneeb.id'
+
+ zf_data = self.get_request(zf_url.format(user),
+ headers = {}, status_code = 200)
+ self.assertIn('zonefile', zf_data)
+
+ zf_hash = blockstack_client.storage.get_zonefile_data_hash(zf_data['zonefile'])
+ zf_data_historic_lookup = self.get_request(zf_hash_url.format(user, zf_hash),
+ headers = {}, status_code = 200)
+ self.assertEqual(zf_data_historic_lookup['zonefile'],
+ zf_data['zonefile'])
+
+class NameHistoryTest(APITestCase):
+ def build_url(self, username):
+ return '/v1/names/{}/history'.format(username)
+
+ def check_history_block(self, blocks):
+ self.assertEqual(len(blocks), 1)
+ block = blocks[0]
+ self.assertRegexpMatches(block['address'], schemas.OP_ADDRESS_PATTERN)
+ self.assertTrue(block['opcode'].startswith('NAME'))
+
+ def test_found_user_lookup(self):
+ usernames = 'muneeb.id'
+ data = self.get_request(self.build_url(usernames),
+ headers = {}, status_code=200)
+
+ self.assertTrue(len(data.keys()) > 2)
+ for block_key, block_data in data.items():
+ self.check_history_block(block_data)
+
+
+class NamesOwnedTest(APITestCase):
+ def build_url(self, addr):
+ return '/v1/addresses/bitcoin/{}'.format(addr)
+ def test_check_names(self):
+ addrs_to_check = ["1QJQxDas5JhdiXhEbNS14iNjr8auFT96GP",
+ "16EMaNw3pkn3v6f2BgnSSs53zAKH4Q8YJg"]
+ names_to_check = ["muneeb.id", "judecn.id"]
+ for addr, name in zip(addrs_to_check, names_to_check):
+ data = self.get_request(self.build_url(addr),
+ headers = {}, status_code = 200)
+ self.assertTrue(len(data["names"]) > 0)
+ self.assertIn(name, data["names"])
+
+class NamespaceTest(APITestCase):
+ def test_id_space(self):
+ data = self.get_request("/v1/namespaces",
+ headers = {} , status_code=200)
+ self.assertIn('id', data)
+
+ data = self.get_request('/v1/namespaces/id', headers = {}, status_code = 200)
+ to_check = {
+ "address": schemas.OP_ADDRESS_PATTERN,
+ "block_number": 0,
+ "history": True,
+ "namespace_id": True,
+ "op": True,
+ "op_fee": 0,
+ "preorder_hash": schemas.OP_HEX_PATTERN,
+ "ready_block": 0,
+ "reveal_block": 0,
+ "sender": schemas.OP_HEX_PATTERN,
+ "sender_pubkey": schemas.OP_PUBKEY_PATTERN,
+ "txid": schemas.OP_TXID_PATTERN
+ }
+
+ check_data(self, data, to_check)
+
+ def test_id_space_names(self):
+ data = self.get_request("/v1/namespaces/id/names?page=0",
+ headers = {} , status_code=200)
+ self.assertEqual(len(data), 100, "Paginated name length != 100")
+ data = self.get_request("/v1/namespaces/id/names",
+ headers = {} , status_code=401)
+
+
+
+class Prices(APITestCase):
+ def test_id_price(self):
+ price_url = "/v1/prices/names/{}"
+ data = self.get_request(price_url.format("muneeb.id"),
+ headers = {} , status_code=200)
+ json_keys = data.keys()
+ self.assertIn('name_price', json_keys)
+ self.assertIn('preorder_tx_fee', json_keys)
+ self.assertIn('register_tx_fee', json_keys)
+ self.assertIn('total_estimated_cost', json_keys)
+ self.assertIn('total_tx_fees', json_keys)
+ self.assertIn('update_tx_fee', json_keys)
+
+ def test_ns_price(self):
+ data = self.get_request("/v1/prices/namespaces/id",
+ headers = {} , status_code=200)
+ check_data(self, data, {'satoshis':0})
+
+class BlockChains(APITestCase):
+ def test_consensus(self):
+ data = self.get_request("/v1/blockchains/bitcoin/consensus",
+ headers = {} , status_code=200)
+ self.assertRegexpMatches(data['consensus_hash'], schemas.OP_CONSENSUS_HASH_PATTERN)
+ def no_test_name_history(self):
+ """ this is currently an unimplemented endpoint """
+ data = self.get_request("/v1/blockchains/bitcoin/names/muneeb.id/history",
+ headers = {} , status_code=405)
+ def test_names_pending(self):
+ data = self.get_request("/v1/blockchains/bitcoin/pending",
+ headers = {} , status_code=200)
+ self.assertIn("queues", data)
+ def test_operations(self):
+ data = self.get_request("/v1/blockchains/bitcoin/operations/456383",
+ headers = {} , status_code=200)
+
+ to_check = {"address" : schemas.OP_ADDRESS_PATTERN,
+ "block_number" : 0,
+ "consensus_hash": schemas.OP_CONSENSUS_HASH_PATTERN,
+ "first_registered": 0,
+ "history" : True,
+ "op" : True,
+ "txid": schemas.OP_HEX_PATTERN,
+ "value_hash": schemas.OP_HEX_PATTERN}
+ check_data(self, data[0], to_check)
+
+class BlockChainsInternal(APITestCase):
+ def test_unspents(self):
+ url = "/v1/blockchains/bitcoin/{}/unspent".format(DEFAULT_WALLET_ADDRESS)
+ self.get_request(url, headers = {}, status_code = 403)
+ data = self.get_request(url, headers = get_auth_header(), status_code = 200)
+
+ self.assertTrue(len(data) >= 1)
+ data = data[0]
+
+ self.assertTrue(data['confirmations'] >= 0)
+ self.assertRegexpMatches(data['out_script'], schemas.OP_HEX_PATTERN)
+ self.assertRegexpMatches(data['outpoint']['hash'], schemas.OP_HEX_PATTERN)
+ self.assertRegexpMatches(data['transaction_hash'], schemas.OP_HEX_PATTERN)
+ self.assertTrue(data['value'] >= 0)
+ def test_txs(self):
+ url = "/v1/blockchains/bitcoin/txs".format(DEFAULT_WALLET_ADDRESS)
+ self.post_request(url, payload = {}, headers = {}, status_code = 403)
+ self.post_request(url, payload = {}, headers = get_auth_header(), status_code = 401)
+
+class WalletInternal(APITestCase):
+ def test_addresses(self):
+ for endpoint in ['payment_address', 'owner_address']:
+ data = self.get_request("/v1/wallet/{}".format(endpoint),
+ headers = get_auth_header(), status_code = 200)
+ self.assertRegexpMatches(data['address'], schemas.OP_ADDRESS_PATTERN)
+ data = self.get_request("/v1/wallet/data_pubkey",
+ headers = get_auth_header(), status_code = 200)
+ self.assertRegexpMatches(data['public_key'], schemas.OP_PUBKEY_PATTERN)
+ def test_balance(self):
+ data = self.get_request("/v1/wallet/balance",
+ headers = get_auth_header(), status_code = 200)
+ to_check = {'balance' : { 'bitcoin' : 0, 'satoshis' : 0 } }
+ check_data(self, data, to_check)
+ def test_keys(self):
+ data = self.get_request("/v1/wallet/keys",
+ headers = get_auth_header(), status_code = 200)
+ to_check = {
+ "data_privkey": schemas.OP_HEX_PATTERN,
+ "data_pubkey": schemas.OP_PUBKEY_PATTERN,
+ "owner_address": schemas.OP_ADDRESS_PATTERN,
+ "owner_privkey": True,
+ "payment_address": schemas.OP_ADDRESS_PATTERN,
+ "payment_privkey": True,
+ }
+
+ check_data(self, data, to_check)
+
+class NodeInternal(APITestCase):
+ def test_registrar(self):
+ self.get_request("/v1/node/registrar/state", headers = get_auth_header(),
+ status_code = 200)
+ def test_get_log(self):
+ self.get_request("/v1/node/log", headers = get_auth_header(),
+ status_code = 200, no_json = True)
+ def test_config(self):
+ data = self.get_request("/v1/node/config", headers = get_auth_header(),
+ status_code = 200)
+ to_check = { "bitcoind": True,
+ "blockchain-reader": True,
+ "blockchain-writer": True,
+ "blockstack-client": True }
+
+ check_data(self, data, to_check)
+
+def test_main(args = []):
+ test_classes = [PingTest, LookupUsersTest, NamespaceTest, BlockChains,
+ Prices, NamesOwnedTest, NameHistoryTest,
+ AuthInternal, BlockChainsInternal, Zonefiles, WalletInternal, NodeInternal]
+
+ test_map = {}
+ for t in test_classes:
+ test_map[t.__name__] = t
+
+
+ with test_support.captured_stdout() as out:
+ try:
+ test_support.run_unittest(PingTest)
+ except Exception as e:
+ traceback.print_exc(file=sys.stdout)
+ out = out.getvalue()
+ if out[-3:-1] != "OK":
+ print(out)
+ print("Failure of the ping test means the rest of the unit tests will " +
+ "fail. Is the blockstack api daemon running? (did you run " +
+ "`blockstack api start`)")
+ return
+
+ if len(args) == 1 and args[0] == "--list":
+ print("Tests supported: ")
+ for testname in test_map.keys():
+ print(testname)
+ return
+
+ test_runner = test_support.run_unittest
+
+ if "--xunit-path" in args:
+ ainx = args.index("--xunit-path")
+ del args[ainx]
+ from xmlrunner import XMLTestRunner
+ test_runner = XMLTestRunner(output=args[ainx]).run
+ del args[ainx]
+
+ if "--api_password" in args:
+ ainx = args.index("--api_password")
+ del args[ainx]
+ global API_PASSWORD
+ API_PASSWORD = args[ainx]
+ del args[ainx]
+
+ if len(args) == 0 or args[0] == "--all":
+ args = [ testname for testname in test_map.keys() ]
+
+ test_suite = unittest.TestSuite()
+ for test_name in args:
+ test_suite.addTest( unittest.TestLoader().loadTestsFromTestCase(test_map[test_name]) )
+ test_runner( test_suite )
+
+if __name__ == '__main__':
+ test_main(sys.argv[1:])
diff --git a/integration_tests/setup.py b/integration_tests/setup.py
index d6674be58..0dd3ec6de 100755
--- a/integration_tests/setup.py
+++ b/integration_tests/setup.py
@@ -28,6 +28,7 @@ setup(
include_package_data=True,
install_requires=[
'blockstack>=0.14.2',
+ 'xmlrunner>=1.7.7'
],
classifiers=[
'Intended Audience :: Developers',