Update test_teller.py

This commit is contained in:
2026-06-01 20:59:59 -04:00
parent d09c739288
commit 88cf1253fd
+66 -14
View File
@@ -31,7 +31,7 @@ import argparse
import json import json
import os import os
import sys import sys
from datetime import date, timedelta from datetime import date, datetime, timedelta
# ── Load .env from project root ─────────────────────────────────────────────── # ── Load .env from project root ───────────────────────────────────────────────
@@ -52,6 +52,25 @@ if os.path.exists(_env):
TELLER_BASE = 'https://api.teller.io' TELLER_BASE = 'https://api.teller.io'
TELLER_VERSION = '2020-10-12' TELLER_VERSION = '2020-10-12'
# ── Logger ────────────────────────────────────────────────────────────────────
_log_file = None # set in main() when --log-file is given
def _log(label, data, status_code=None, url=None):
"""Append a structured JSON entry to the log file if one is configured."""
if _log_file is None:
return
entry = {
'timestamp': datetime.now().isoformat(timespec='seconds'),
'label': label,
'url': url,
'status': status_code,
'response': data,
}
with open(_log_file, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, default=str) + '\n')
# ── Console helpers ─────────────────────────────────────────────────────────── # ── Console helpers ───────────────────────────────────────────────────────────
def _ok(msg): print(f' \033[32m✓\033[0m {msg}') def _ok(msg): print(f' \033[32m✓\033[0m {msg}')
@@ -164,7 +183,7 @@ function startConnect() {{
""" """
def connect_bank(cert_path, key_path, port=8765, days=30): def connect_bank(cert_path, key_path, port=8765, days=30, log_path=None):
""" """
Spin up a local HTTP server, open Teller Connect in the browser, Spin up a local HTTP server, open Teller Connect in the browser,
capture the access token, then run the full test suite. capture the access token, then run the full test suite.
@@ -256,6 +275,11 @@ def connect_bank(cert_path, key_path, port=8765, days=30):
_ok(f'Bank connected: {institution}') _ok(f'Bank connected: {institution}')
_info(f'Enrollment ID : {enrollment_id}') _info(f'Enrollment ID : {enrollment_id}')
_info(f'Access token : {access_token[:8]}{"*" * (len(access_token) - 8)}') _info(f'Access token : {access_token[:8]}{"*" * (len(access_token) - 8)}')
_log('teller_connect_enrollment', {
'enrollment_id': enrollment_id,
'institution': institution,
'access_token': f'{access_token[:8]}…(redacted)',
})
# Save token to a local file for reuse # Save token to a local file for reuse
token_file = os.path.join(_root, '.teller_token') token_file = os.path.join(_root, '.teller_token')
@@ -269,6 +293,10 @@ def connect_bank(cert_path, key_path, port=8765, days=30):
_info(f'Token saved to .teller_token (gitignored) for reuse with --token') _info(f'Token saved to .teller_token (gitignored) for reuse with --token')
# Run full test suite with the captured token # Run full test suite with the captured token
global _log_file
if log_path:
_log_file = log_path
print() print()
try: try:
session = _build_session(access_token, cert_path, key_path) session = _build_session(access_token, cert_path, key_path)
@@ -294,38 +322,47 @@ def connect_bank(cert_path, key_path, port=8765, days=30):
def test_connectivity(session): def test_connectivity(session):
_head('1. Connectivity — GET /accounts') _head('1. Connectivity — GET /accounts')
url = f'{TELLER_BASE}/accounts'
try: try:
resp = session.get(f'{TELLER_BASE}/accounts', timeout=15) resp = session.get(url, timeout=15)
if resp.status_code == 200: if resp.status_code == 200:
accounts = resp.json() accounts = resp.json()
_log('get_accounts', accounts, resp.status_code, url)
_ok(f'Connected — {len(accounts)} account(s) returned') _ok(f'Connected — {len(accounts)} account(s) returned')
for a in accounts: for a in accounts:
_info(f'{a["id"]} {a.get("institution", {}).get("name", "?")} ' _info(f'{a["id"]} {a.get("institution", {}).get("name", "?")} '
f'{a.get("name", "")} ({a.get("type", "")} / {a.get("subtype", "")})') f'{a.get("name", "")} ({a.get("type", "")} / {a.get("subtype", "")})')
return accounts return accounts
elif resp.status_code == 401: elif resp.status_code == 401:
_log('get_accounts', resp.text, resp.status_code, url)
_fail('Unauthorized (401) — check access token and mTLS certs') _fail('Unauthorized (401) — check access token and mTLS certs')
_info(f'Response: {resp.text[:500]}') _info(f'Response: {resp.text[:500]}')
else: else:
_log('get_accounts', resp.text, resp.status_code, url)
_fail(f'HTTP {resp.status_code}') _fail(f'HTTP {resp.status_code}')
_info(f'Response: {resp.text[:500]}') _info(f'Response: {resp.text[:500]}')
except Exception as e: except Exception as e:
_log('get_accounts', str(e), None, url)
_fail(f'Request failed: {e}') _fail(f'Request failed: {e}')
return [] return []
def test_balances(session, account_id): def test_balances(session, account_id):
_head(f'2. Balance — GET /accounts/{account_id}/balances') _head(f'2. Balance — GET /accounts/{account_id}/balances')
url = f'{TELLER_BASE}/accounts/{account_id}/balances'
try: try:
resp = session.get(f'{TELLER_BASE}/accounts/{account_id}/balances', timeout=15) resp = session.get(url, timeout=15)
if resp.status_code == 200: if resp.status_code == 200:
data = resp.json() data = resp.json()
_log('get_balances', data, resp.status_code, url)
_ok(f'Ledger: {data.get("ledger", "n/a")} | Available: {data.get("available", "n/a")}') _ok(f'Ledger: {data.get("ledger", "n/a")} | Available: {data.get("available", "n/a")}')
_info(_pretty(data)) _info(_pretty(data))
return data return data
else: else:
_log('get_balances', resp.text, resp.status_code, url)
_fail(f'HTTP {resp.status_code}{resp.text[:300]}') _fail(f'HTTP {resp.status_code}{resp.text[:300]}')
except Exception as e: except Exception as e:
_log('get_balances', str(e), None, url)
_fail(f'Request failed: {e}') _fail(f'Request failed: {e}')
return None return None
@@ -334,14 +371,12 @@ def test_transactions(session, account_id, days=30):
_head(f'3. Transactions — GET /accounts/{account_id}/transactions (last {days} days)') _head(f'3. Transactions — GET /accounts/{account_id}/transactions (last {days} days)')
start = (date.today() - timedelta(days=days)).isoformat() start = (date.today() - timedelta(days=days)).isoformat()
end = date.today().isoformat() end = date.today().isoformat()
url = f'{TELLER_BASE}/accounts/{account_id}/transactions'
try: try:
resp = session.get( resp = session.get(url, params={'start_date': start, 'end_date': end}, timeout=30)
f'{TELLER_BASE}/accounts/{account_id}/transactions',
params={'start_date': start, 'end_date': end},
timeout=30,
)
if resp.status_code == 200: if resp.status_code == 200:
txns = resp.json() txns = resp.json()
_log('get_transactions', txns, resp.status_code, url)
_ok(f'{len(txns)} transaction(s) from {start} to {end}') _ok(f'{len(txns)} transaction(s) from {start} to {end}')
for txn in txns[:5]: for txn in txns[:5]:
amount = float(txn.get('amount', 0)) amount = float(txn.get('amount', 0))
@@ -352,27 +387,30 @@ def test_transactions(session, account_id, days=30):
_info(f'… and {len(txns) - 5} more') _info(f'… and {len(txns) - 5} more')
return txns return txns
else: else:
_log('get_transactions', resp.text, resp.status_code, url)
_fail(f'HTTP {resp.status_code}{resp.text[:300]}') _fail(f'HTTP {resp.status_code}{resp.text[:300]}')
except Exception as e: except Exception as e:
_log('get_transactions', str(e), None, url)
_fail(f'Request failed: {e}') _fail(f'Request failed: {e}')
return [] return []
def test_transaction_detail(session, account_id, txn_id): def test_transaction_detail(session, account_id, txn_id):
_head(f'4. Transaction detail — GET /accounts/{account_id}/transactions/{txn_id}') _head(f'4. Transaction detail — GET /accounts/{account_id}/transactions/{txn_id}')
url = f'{TELLER_BASE}/accounts/{account_id}/transactions/{txn_id}'
try: try:
resp = session.get( resp = session.get(url, timeout=15)
f'{TELLER_BASE}/accounts/{account_id}/transactions/{txn_id}',
timeout=15,
)
if resp.status_code == 200: if resp.status_code == 200:
data = resp.json() data = resp.json()
_log('get_transaction_detail', data, resp.status_code, url)
_ok('Retrieved successfully') _ok('Retrieved successfully')
_info(_pretty(data)) _info(_pretty(data))
return data return data
else: else:
_log('get_transaction_detail', resp.text, resp.status_code, url)
_fail(f'HTTP {resp.status_code}{resp.text[:300]}') _fail(f'HTTP {resp.status_code}{resp.text[:300]}')
except Exception as e: except Exception as e:
_log('get_transaction_detail', str(e), None, url)
_fail(f'Request failed: {e}') _fail(f'Request failed: {e}')
return None return None
@@ -433,11 +471,25 @@ def main():
help='Days of transaction history to fetch (default: 30)') help='Days of transaction history to fetch (default: 30)')
parser.add_argument('--port', type=int, default=8765, parser.add_argument('--port', type=int, default=8765,
help='Local server port for --connect mode (default: 8765)') help='Local server port for --connect mode (default: 8765)')
parser.add_argument('--log-file', metavar='PATH', default=None,
help='Append all API responses as JSON lines to this file '
'(default: logs/teller_test_YYYYMMDD_HHMMSS.log)')
args = parser.parse_args() args = parser.parse_args()
cert = args.cert cert = args.cert
key = args.key key = args.key
# ── Set up log file ───────────────────────────────────────────────────────
global _log_file
log_path = args.log_file
if log_path is None:
log_dir = os.path.join(_root, 'logs')
os.makedirs(log_dir, exist_ok=True)
stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
log_path = os.path.join(log_dir, f'teller_test_{stamp}.log')
_log_file = log_path
_info(f'Logging responses to: {_log_file}')
if not cert or not key: if not cert or not key:
_fail('Cert/key paths not found.\n' _fail('Cert/key paths not found.\n'
' Set TELLER_CERT_PATH / TELLER_KEY_PATH in .env, ' ' Set TELLER_CERT_PATH / TELLER_KEY_PATH in .env, '
@@ -445,7 +497,7 @@ def main():
sys.exit(1) sys.exit(1)
if args.connect: if args.connect:
connect_bank(cert, key, port=args.port, days=args.days) connect_bank(cert, key, port=args.port, days=args.days, log_path=log_path)
print('\n\033[1mDone.\033[0m\n') print('\n\033[1mDone.\033[0m\n')
return return